plogr is a cross-distro/platform package-activity logger that records every install and removal event on your system, then writes an append-only history in both JSON and TOML. It can also monitor your downloads(configurable) folder for new files.
4. Configuration
5. Usage
1. Setup
2. Check Status
3. Start Monitoring
4. Query Logs
5. Export Logs
6. Manual Logging
- Zero-maintenance Hooks - hooks directly into DNF4 and DNF5; no polling required.
- Native DNF5 Plugin - C++ plugin for DNF5 with automatic transaction logging.
- Dual Log Formats - automatically mirrors entries to
packages.jsonandpackages.tomlfiles. - Modular Backends - easily extendable with backends for APT, Pacman, Homebrew, etc.
- Append-Only History - entries are never deleted; removals are flagged as removed.
- Scope-Aware Logging - choose between user-only (
--scope user) or system-wide (--scope system) package tracking. - Download Monitoring - automatically log files downloaded to your Downloads folder.
- Log Querying - search and filter your package history directly from the CLI.
Package builds will soon be available from my Copr repository. Until then:
- Fedora / RHEL – build & install the RPM as shown below (automatic DNF integration).
- All other platforms – install the pure-Python package with
pipx(orpip) and, if you like, add a small hook so your package manager calls the CLI automatically.
# system Python → isolated environment in ~/.local/pipx:
pipx install plogr==0.7.0
# verify
plogr --helpSupported Python versions: 3.12–3.13 (3.14 is blocked until upstream PyO3/pydantic-core add support). If your system default is newer, set UV_PYTHON=python3.13 when building or installing with uv/pipx.
You can now enable the download-monitoring service:
systemctl --user daemon-reload # first time only
systemctl --user enable --now plogr.serviceThe RPM package includes a native C++ plugin for DNF5 that provides automatic package logging via the DNF5 Actions Plugin system.
When you install the RPM package, the DNF5 plugin is automatically:
- Built using CMake with libdnf5 dependencies
- Installed to
/usr/lib64/dnf5/plugins/plogr.so - Configured with Actions Plugin hooks at
/usr/share/libdnf5/plugins/actions.d/plogr.actions - Enabled by default for automatic transaction logging
If you're building from source or need to install manually:
# Install build dependencies
sudo dnf install cmake gcc-c++ libdnf5-devel pkgconfig
# Build and install the plugin
cd libdnf5-plugin/dnf5-plugin
mkdir build && cd build
cmake ..
make
sudo make installVerify the plugin is working:
# Check DNF5 version
dnf5 --version
# Test plugin loading (should show no errors)
dnf5 list installed | head -5
# Check if transactions are being logged
plogr statusAPT / Debian & Ubuntu
- Create
/etc/apt/apt.conf.d/99plogr(system-wide) or~/.config/apt/apt.conf.d/99plogr(user):
DPkg::Post-Invoke { "plogr install-apt-hook || true"; };
- Put a tiny helper on your PATH, e.g.
/usr/local/bin/plogr-install-apt-hook:
#!/usr/bin/env bash
# Log every package touched in the most recent dpkg transaction
awk '{print $4}' /var/log/dpkg.log | tail -n +2 | while read -r pkg; do
plogr install "$pkg" apt
doneMake it executable (chmod +x …).
Pacman / Arch
Create /etc/pacman.d/hooks/plogr.hook:
[Trigger]
Operation = Install
Operation = Upgrade
Operation = Remove
Type = Package
Target = *
[Action]
Description = Log package transaction with plogr
When = PostTransaction
Exec = /usr/bin/plogr pacman-hook %t%t is the transaction database path; you can parse it inside pacman-hook to emit individual plogr install/remove calls.
Other managers
For Homebrew, Chocolatey and Winget, use manual logging or adapt the examples above. Read Package-manager integration and CONTRIBUTING for what’s still needed.
Linux
| Package manager | Status | Setup |
|---|---|---|
| DNF4 (Fedora/RHEL) | Automatic logging via Python plugin | See DNF Plugin Configuration. The RPM installs /usr/lib/python3.*/site-packages/dnf-plugins/plogr.py and a sample config; enable it with enabled=1. |
| DNF5 (Fedora/RHEL) | Automatic logging via native C++ plugin | See DNF Plugin Configuration. The RPM installs /usr/lib64/dnf5/plugins/plogr.so and is enabled by default. |
| APT (Debian/Ubuntu) | Planned | Parser exists (apt.py), but no hook yet. A DPkg::Post-Invoke script is needed. Help welcome → Contributing |
| Pacman (Arch) | Planned | Parser exists (pacman.py). Needs an alpm hook (/etc/pacman.d/hooks/plogr.hook). See Contributing |
macOS
| Package manager | Status | Notes |
|---|---|---|
| Homebrew | Planned | Parsing implemented (brew.py). Requires a post-install wrapper or Homebrew tap to invoke the CLI automatically. See Contributing. |
Windows
| Package manager | Status | Notes |
|---|---|---|
| Chocolatey | Planned | Parser implemented (chocolatey.py). Needs a PowerShell extension/post-install script. |
| Winget | Planned | Parser implemented (winget.py). Requires a winget source extension or scheduled task to call the CLI. |
See the Contributing guide if you’d like to help wire up these back-ends.
User scope logs packages for the current user only. Files are stored in ~/.local/share/plogr/.
Configuration file: ~/.config/plogr/plogr.conf
{
"scope": "user",
"enable_dnf_hooks": true,
"enable_download_monitoring": true,
"downloads_dir": "~/Downloads",
"monitored_extensions": ".rpm, .deb, .pkg, .exe, .msi, .dmg",
"log_format": "both"
}System scope logs packages system-wide. Files are stored in /var/log/plogr/.
Configuration file: /etc/plogr/plogr.conf
{
"scope": "system",
"enable_dnf_hooks": true,
"enable_download_monitoring": false,
"log_format": "both"
}plogr includes both a Python plugin for DNF4 and a native C++ plugin for DNF5.
User scope: ~/.config/dnf/plugins/plogr.conf
[main]
enabled = 1
scope = userSystem scope: /etc/dnf/plugins/plogr.conf
[main]
enabled = 1
scope = systemThe DNF5 plugin is automatically installed to /usr/lib64/dnf5/plugins/plogr.so and is enabled by default.
User scope: ~/.config/dnf5/plugins/plogr.conf
[main]
enabled = 1
scope = userSystem scope: /etc/dnf5/plugins/plogr.conf
[main]
enabled = 1
scope = system- DNF5 plugin not loading: Ensure you're using DNF5 (
dnf5 --version) and the plugin is installed to/usr/lib64/dnf5/plugins/ - DNF4 plugin not loading: Ensure you're using DNF4 (
dnf --version) and the plugin is installed to/usr/lib/python3.*/site-packages/dnf-plugins/ - Both plugins installed: The system will use the appropriate plugin based on which DNF version you're running
- Plugin conflicts: Only one plugin should be active at a time; the Python plugin is for DNF4, the C++ plugin is for DNF5
- DNF5 transactions not logged: Check that the Actions Plugin file at
/usr/share/libdnf5/plugins/actions.d/plogr.actionsexists and has no trailing characters - Scope detection issues: Run
sudo plogr setupto properly configure system scope, or useplogr setupfor user scope - Configuration conflicts: Ensure only one config file exists - either system (
/etc/plogr/) or user (~/.config/plogr/)
All commands can be run with --scope user (default) or --scope system (requires sudo).
Initializes configuration and directories.
plogr setupShow current status and statistics.
plogr statusStarts the monitoring daemon. For users, this monitors the downloads directory.
# Start download monitoring (user scope only, foreground)
plogr daemon
# Run in background (non-systemd, POSIX only)
plogr daemon --background
# Run continuously via systemd-user service
# (the unit file is shipped in /usr/lib/systemd/user by the RPM)
#
# 1. Reload user units to ensure systemd sees the new file
systemctl --user daemon-reload
# 2. Enable + start the logger – this will automatically create
# ~/.config/systemd/user/ if it does not already exist.
systemctl --user enable --now plogr.service
# Note: The systemd service includes security hardening that restricts
# access to only the log directory and Downloads folder. If you use a
# custom downloads directory, you may need to modify the service file.
# Optional: keep the service running even after logging out
sudo loginctl enable-linger "$USER"System scope requires sudo. Without it, commands fall back to user scope and emit a warning.
Search the package logs.
# Find all packages with 'nginx' in the name
plogr query --name nginx
# Find packages installed with dnf in the last 30 days
plogr query --manager dnf --days 30Export the full log to stdout.
# Export user logs as JSON
plogr export --format jsonManually log a package installation or removal.
# Log a package installation
plogr install package-name dnf
# Log a package removal
plogr remove package-name dnf
# Log a downloaded file
plogr install downloaded-file.zip downloadfunction pkglog_preexec() {
[[ $1 == git\ clone* ]] || return
local repo=${${1#git clone }##*/}
repo=${repo%.git}
plogr install $repo git
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec pkglog_preexecfunction _pkglog_bash_preexec() {
[[ $BASH_COMMAND == git\ clone* ]] || return
local repo=${BASH_COMMAND#git clone}
repo=${repo##*/}
repo=${repo%.git}
plogr install "$repo" git
}function fish_preexec --on-event fish_preexec
if string match -rq '^git clone ' -- $argv[1]
set repo (basename (string replace -r '^git clone +' '' $argv[1]) .git)
plogr install $repo git
end
end[
{
"name": "neovim",
"manager": "dnf",
"action": "install",
"date": "2025-06-21T17:02:41-05:00",
"removed": false,
"scope": "user",
"version": "0.9.5-1.fc42",
"metadata": {
"arch": "x86_64",
"repo": "fedora"
}
},
{
"name": "firefox-120.0.tar.bz2",
"manager": "download",
"action": "install",
"date": "2025-06-21T18:30:15-05:00",
"removed": false,
"scope": "user",
"metadata": {
"file_path": "/home/user/Downloads/firefox-120.0.tar.bz2",
"file_size": 52428800,
"file_type": ".tar.bz2"
}
}
][[package]]
name = "neovim"
manager = "dnf"
action = "install"
date = "2025-06-21T17:02:41-05:00"
removed = false
scope = "user"
version = "0.9.5-1.fc42"
[package.metadata]
arch = "x86_64"
repo = "fedora"
[[package]]
name = "firefox-120.0.tar.bz2"
manager = "download"
action = "install"
date = "2025-06-21T18:30:15-05:00"
removed = false
scope = "user"
[package.metadata]
file_path = "/home/user/Downloads/firefox-120.0.tar.bz2"
file_size = 52428800
file_type = ".tar.bz2"- Log files:
~/.local/share/plogr/packages.jsonpackages.toml
- Configuration:
~/.config/plogr/plogr.conf - DNF plugin config:
~/.config/dnf/plugins/plogr.conf
- Log files:
/var/log/plogr/packages.jsonpackages.toml
- Configuration:
/etc/plogr/plogr.conf - DNF plugin config:
/etc/dnf/plugins/plogr.conf
With the provided Makefile, building and installing a local version of the RPM is simple.
# Create the SRPM and RPM packages
make rpm
# Install the newly built package
make install
# Supported Python: 3.12–3.13. Python 3.14 is currently blocked because PyO3/pydantic-core
# do not yet support it. If your system Python is newer, set build envs to 3.13:
# UV_PYTHON=python3.13 PYO3_PYTHON=python3.13 make rpm
# UV_PYTHON=python3.13 PYO3_PYTHON=python3.13 make installThe Makefile handles placing the source tarball in the correct rpmbuild directory for you.
Please read CONTRIBUTING.md for details on how to contribute to this project.
MIT License (see LICENSE)
I currently use AI to help me with scaffolding, research, debugging, and documentation. All other aspects of the code is done by me, keep in mind I do this for fun so keep your expectations low... lol.