Journal Reader¶
systemd-client gives you full access to the systemd journal -- both through the high-level client and through a dedicated low-level JournalReader.
Let's explore both approaches.
Note
The journal always uses subprocess (journalctl --user --output=json) regardless of the backend setting. This is because systemd's journal has no D-Bus API. See Backends for more details.
Through the Client (The Easy Way)¶
The simplest way to read journal entries is right from your client instance:
from systemd_client import SystemdClient, JournalPriority
client = SystemdClient()
entries = client.journal("my-app.service", lines=50) # (1)!
# With all filters
entries = client.journal(
unit="my-app.service",
lines=100,
since="1h ago", # (2)!
until="30m ago",
priority=JournalPriority.WARNING, # (3)!
grep="error|timeout", # (4)!
)
- Fetches the last 50 journal entries for this unit.
- Accepts human-readable time expressions like
"1h ago","today", or absolute dates like"2025-01-15". - Filters to WARNING level and below (WARNING, ERR, CRIT, ALERT, EMERG). Lower number = higher severity.
- Regex pattern applied to the message content, just like
journalctl --grep.
Tip
For most use cases, the client-level journal() method is all you need. Only reach for JournalReader when you need fine-grained control.
Low-Level JournalReader¶
When you need more control -- like specifying syslog identifiers or reverse ordering -- use JournalReader and JournalQuery directly:
from systemd_client import JournalReader, JournalPriority
from systemd_client.journal import JournalQuery
reader = JournalReader()
query = JournalQuery(
unit="my-app.service",
priority=JournalPriority.ERR,
lines=50,
since="2h ago",
reverse=True, # (1)!
identifiers=["my-app"], # (2)!
)
entries = reader.query(query)
- Newest entries first -- handy when you want to see the most recent errors.
- Filter by syslog identifier (the program name that produced the log line).
JournalQuery Parameters¶
Here's every parameter you can use to build a query:
| Parameter | Type | Description |
|---|---|---|
unit |
str | None |
Filter by unit name |
lines |
int | None |
Number of recent entries to return |
since |
str | None |
Start time ("1h ago", "2025-01-01", "today") |
until |
str | None |
End time |
priority |
JournalPriority | None |
Minimum priority level |
grep |
str | None |
Regex filter on message content |
boot |
str | None |
Filter by boot ID |
reverse |
bool |
Newest entries first (default False) |
follow |
bool |
Follow new entries in real-time (used internally) |
identifiers |
list[str] |
Filter by syslog identifiers |
Inspect the Generated Command¶
Curious what journalctl command will actually run? You can peek at the arguments:
query = JournalQuery(
unit="my-app.service", lines=50, priority=JournalPriority.WARNING
)
print(query.to_args())
$ python inspect_query.py
['--user', '--output=json', '--no-pager', '--unit', 'my-app.service', '--lines', '50', '--priority', '4']
Technical Details
The to_args() method returns the exact argument list passed to journalctl. This is useful for debugging when your query isn't returning the results you expect.
Following the Journal¶
Real-time log tailing is supported in both sync and async flavors.
Sync (Blocking Iterator)¶
for entry in client.journal_follow("my-app.service"): # (1)!
ts = entry.timestamp.strftime("%H:%M:%S") if entry.timestamp else ""
print(f"{ts} [{entry.priority.name}] {entry.message}")
- This blocks forever, yielding new entries as they appear. Press Ctrl+C to stop.
Warning
The sync journal_follow is a blocking call. It will hold up your entire thread. For non-blocking follow, use the async client.
Async (Non-Blocking Generator)¶
- Non-blocking -- your event loop stays free to handle other work while waiting for new log lines.
Low-Level Follow¶
You can also follow using the JournalReader directly:
reader = JournalReader()
query = JournalQuery(unit="my-app.service", lines=10)
for entry in reader.follow(query): # (1)!
print(entry.message)
- Starts by replaying the last 10 entries, then follows new ones in real-time.
JournalEntry Fields¶
Each entry is a frozen dataclass with these fields:
entry.message # str: the log message # (1)!
entry.priority # JournalPriority: emerg(0)..debug(7) # (2)!
entry.timestamp # datetime | None: wall-clock UTC
entry.monotonic_timestamp # int | None: microseconds since boot
entry.unit # str | None: originating unit
entry.syslog_identifier # str | None: program name
entry.pid # int | None: process ID
entry.uid # int | None: user ID
entry.boot_id # str | None: boot identifier
entry.hostname # str | None: machine hostname
entry.cursor # str | None: journal cursor # (3)!
entry.fields # dict[str, str]: all extra fields
- The main log line content -- this is what you'll use most often.
JournalPriorityis aStrEnum, so you can compare it as a string or use.name/.value.- The cursor is a unique, opaque string that identifies this exact position in the journal. Useful for bookmarking.
Accessing Extra Fields¶
The fields dict contains every journal field, including internal ones that don't have named attributes:
entries = client.journal("my-app.service", lines=10)
for entry in entries:
print(entry.message, entry.pid)
# Extra journal fields not on the dataclass
cgroup = entry.fields.get("_SYSTEMD_CGROUP", "") # (1)!
exe = entry.fields.get("_EXE", "")
- Journal fields prefixed with
_are trusted fields set by the journal itself (not the logging process).
Priority Levels¶
The JournalPriority enum follows standard syslog priority levels:
from systemd_client import JournalPriority
JournalPriority.EMERG # "0" -- system is unusable
JournalPriority.ALERT # "1" -- immediate action needed
JournalPriority.CRIT # "2" -- critical conditions
JournalPriority.ERR # "3" -- error conditions # (1)!
JournalPriority.WARNING # "4" -- warning conditions
JournalPriority.NOTICE # "5" -- normal but significant
JournalPriority.INFO # "6" -- informational
JournalPriority.DEBUG # "7" -- debug messages
ERR(notERROR) follows the syslog convention.
Info
When you pass priority=JournalPriority.WARNING to a query, you get entries at priority 4 and below -- that means WARNING, ERR, CRIT, ALERT, and EMERG. Lower number = higher severity. This matches how journalctl --priority works.