Python systemd journal reader and writer SDK. No native journal bindings, no system journal library linkage.
- Read
.journal,.journal~,.journal.zst,.journal~.zstfiles - Read regular and compact journal object layouts
- Zstd compression support via Python standard library
compression.zstd - XZ DATA object support via Python standard library
lzma - LZ4 DATA object support via
lz4block compression - Forward/backward iteration, cursors, timestamps
- Binary field values as
bytes - mmap-backed file reads for
.journal/.journal~inputs and for.journal.zstinputs after repository-local decompression - Open
.journal/.journal~readers refresh header and entry-array state at tail/end so published appends become visible during the same reader session - Raw current-entry
FIELD=valuepayload enumeration without materializing the full entry first - Byte-preserving RAW field-name access via full payloads,
raw_fields,raw_field_values,FileReader.get_raw(), andFileReader.get_raw_values() - Field enumeration and unique value queries
- Export, JSON, and text output formatting
- libsystemd-compatible
SdJournalfacade - Directory iteration across root journal files plus one machine-id subdirectory level, with interleaved multi-file ordering, including mixed regular/compact, compressed/uncompressed, and sealed/unsealed files
- Create regular keyed-hash journal files by default, or compact journal files
with
compact: True/format: 'compact' - Byte-safe field values via
bytes/bytearray/memoryview - Direct raw full-payload appends via
Writer.append_raw()andLog.append_raw()for systemd-compatibleKEY=valuebyte payloads, including binary values after the first= - Optional zstd, xz, and lz4-compressed DATA object writing via
compression: 'zstd',compression: 'xz', orcompression: 'lz4', using systemd's 512-byte default threshold and 8-byte minimum clamp - Forward Secure Sealing TAG writing through
SealOptions, including stockjournalctl --verify --verify-keycoverage for sealed files generated by this writer - Append entries with integer timestamps and sequence numbers
- Configurable explicit live-reader publication cadence through
'live_publish_every_entries'/'livePublishEveryEntries', defaulting to systemd-compatible publication after every entry - Directory writer with chain active naming by default, opt-in strict systemd active naming, entry-count/file-size/duration rotation, and file-count/byte/age retention
- Directory writer entries include indexed
_BOOT_ID=<boot-id>metadata, and include_SOURCE_REALTIME_TIMESTAMP=<usec>whensource_realtime_usec/sourceRealtimeUsecis provided - Whole-file mapped arena writes for the direct-file writer hot path, with positional file-I/O fallback when mmap is unavailable
- Shared field-name policy layers for direct-file and directory writers:
default
FIELD_NAME_POLICY_JOURNALD, app-facingFIELD_NAME_POLICY_JOURNAL_APP, and structure-levelFIELD_NAME_POLICY_RAW - Optional pure cross-SDK cooperative lockfile with stale-owner detection, plus
a secondary platform file lock on the lock file, when callers explicitly
acquire
journal.lock.WriterLock.acquire(path) - Native systemd writers do not participate in the SDK lock protocol and remain an operational exclusion
--fileand--directoryoptions--output=default|json|export--list-bootsand--fields--since,--until,--boot, and--followfor file-backed query/follow behavior--headand--tail--verifyand--verify-keyfor file-backed structural and sealed TAG/HMAC verification- Repeated same-field OR matching and
+disjunction - Daemon-only commands (sync, flush, rotate) return errors
Directory mode follows stock file-backed traversal for .journal and
.journal~ files, skips namespace-suffix subdirectories by default, and also
accepts whole-file .journal.zst / .journal~.zst as an SDK extension. Mixed
directories may contain regular, compact, DATA-compressed, sealed, and unsealed
files together.
Python 3.14+ provides the compression.zstd standard library module needed for
zstd DATA objects and whole-file .journal.zst inputs. Non-zstd reads do not
use that module. LZ4 DATA object compression/decompression requires
lz4==4.4.5.
The package import path is safe on Linux, FreeBSD, macOS, and Windows. POSIX
targets load fcntl only when the caller enables the optional writer lock.
Windows uses Python's standard-library msvcrt byte-range lock API for that
optional helper.
The writer uses os.pread / os.pwrite where Python provides them and falls
back to seek-preserving os.read / os.write positional I/O otherwise. If a
writable mmap cannot be created or resized, the direct writer falls back to the
same positional file-I/O arena.
Directory rotation and retention fsync journal files on every target. POSIX targets also fsync parent directories through directory file descriptors where available. Windows skips parent-directory fsync because Python exposes file fsync there, not a portable directory-handle fsync.
Optional lock stale-owner detection uses Linux procfs boot/process start evidence when available. On other targets it uses conservative process-liveness checks, so a live PID is not treated as stale merely because procfs start-time evidence is unavailable.
from journal import SdJournalOpen
journal = SdJournalOpen('/path/to/journal', 0)
journal.seek_head()
while journal.next() != 0:
entry = journal.get_entry()
msg = entry['fields'].get(b'MESSAGE')
if msg:
print(msg.decode('utf-8'))
journal.close()from journal import SdJournalOpen
journal = SdJournalOpen('/path/to/journal', 0)
journal.add_match(b'BINARY_PAYLOAD=\xff\x00')
journal.seek_head()
while journal.next() != 0:
entry = journal.get_entry()
binary = entry['fields'].get(b'BINARY_PAYLOAD')
if binary:
print(bytes(binary))from journal import Writer
w = Writer.create('/path/to/plugin.journal')
w.append([
{'name': 'MESSAGE', 'value': b'plugin started'},
{'name': 'PRIORITY', 'value': b'6'},
{'name': 'SYSLOG_IDENTIFIER', 'value': b'example-plugin'},
])
w.close()Raw full-payload append is available when the caller already has
systemd-style KEY=value byte payloads:
w = Writer.create('/path/to/plugin.journal')
w.append_raw([
b'MESSAGE=plugin started',
b'PRIORITY=6',
b'BINARY_PAYLOAD=\xff\x00=value',
])
w.close()append_raw() validates the bytes before the first = as the field name. RAW
field-name policy still requires a non-empty name and forbids = inside the
name because the journal DATA payload format uses the first = as the split.
writer.close() matches systemd's plain close path and leaves the file in
ONLINE state. Use writer.close_offline() to finalize a single file as
OFFLINE; directory rotation uses writer.archive_to() to produce ARCHIVED
files.
Live-reader publication can be tuned when the consumer does not need immediate stock follow-reader wakeups:
w = Writer.create('/path/to/plugin.journal', {
'live_publish_every_entries': 64,
})1 is the default and publishes after every entry. 0 disables explicit SDK
live publication for poll/snapshot consumers. N > 1 publishes after every
N entries. This is not an fsync or durability setting.
Journal files are created with systemd journald's 0640 default permissions.
Use file_mode when a consumer needs a different mode:
w = Writer.create('/path/to/private.journal', {
'file_mode': 0o600,
})The same option is accepted by Log for newly-created active files. The
override applies only to newly-created files; existing files keep their current
filesystem permissions. POSIX modes remain subject to the process umask,
matching systemd/open semantics. Non-POSIX platforms may ignore POSIX mode
bits.
from journal import Log
journal = Log('/path/to/journal-dir', {
'source': 'system',
'max_entries': 100000,
'max_bytes': 128 * 1024 * 1024,
'max_duration_usec': 3_600_000_000,
'max_files': 10,
'max_retention_bytes': 1024 * 1024 * 1024,
'max_retention_age_usec': 7 * 24 * 3_600_000_000,
})
journal.append([
{'name': 'MESSAGE', 'value': b'plugin started'},
{'name': 'PRIORITY', 'value': b'6'},
])
journal.append_raw([
b'MESSAGE=raw plugin event',
b'PRIORITY=6',
])
journal.close()Log stores files below <directory>/<machine-id>/. By default it uses the
chain filename form for the active file:
<source>@<seqnum-id>-<head-seqnum>-<head-realtime>.journal. Set
'strict_systemd_naming': True to use <source>.journal as the active file.
If strict naming opens a directory with a stale chain-named ONLINE active
file, it archives that file before creating <source>.journal, so the directory
does not keep parallel active files.
If an existing active file is rejected by the low-level append-open path as
unsupported, Log follows journald's reliable-open behavior: it uses readable
header metadata to continue sequence identity where possible, moves the old
active file to a collision-safe *.journal~ disposed name, and creates a fresh
active file. Direct low-level append-open still returns an unsupported error.
Rotation and retention limits are disabled when omitted or set to 0; the
example above opts into explicit limits. Duration rotation is checked before
append using the incoming entry realtime and the active file head realtime.
Retention counts the tracked active/current file in file-count and
committed-byte limits, but deletion only selects older unprotected files owned
by the configured source; the tracked active/current file is never deleted to
satisfy a retention limit. Call journal.enforce_retention() to apply
age/count/byte retention without waiting for another append-triggered rotation
or close.
Retention also runs once when a writer opens or creates the active file:
existing-active reopen and 'open_mode': 'eager' enforce it during
construction, while lazy archived-only construction defers enforcement until
the first append opens the active file, before the first entry is written.
Use 'open_mode': 'eager' to create/open the active file during construction,
and 'identity_mode': 'strict' with 'machine_id' and 'boot_id' when callers
must reject missing identity instead of generating SDK-local IDs.
configured_directory(), journal_directory(), active_file_path(),
machine_id(), boot_id(), and source_name() expose the configured root,
effective journalctl --directory path, active path, and identity.
lifecycle callbacks receive created, rotated, and deleted events;
artifact_sizer includes consumer-owned sidecar bytes in size-based retention.
append() accepts source_realtime_usec / sourceRealtimeUsec and clamps
non-progressing realtime and monotonic overrides forward, including explicit
zero monotonic overrides. append() and append_raw() prepend the effective
_BOOT_ID as an indexed DATA payload, and prepend _SOURCE_REALTIME_TIMESTAMP
when a source realtime option is supplied.
The low-level Writer.append() path preserves explicit caller-provided
realtime and monotonic timestamps without clamping or rejecting them; callers
using that raw API are responsible for not producing same-boot backward
monotonic entries unless they are intentionally creating invalid fixtures.
Timestamp option key presence distinguishes explicit zero from an omitted
default. On reopen, Log seeds the monotonic clamp floor from a persisted chain
tail only when the tail entry boot ID matches the current writer boot ID.
Log is a single-writer object; callers must serialize method calls on one
instance. The journal file contract is one writer per file. Acquire
journal.lock.WriterLock.acquire(path) when the caller wants the optional
cooperating-writer lock helper to reject another SDK writer for the same file.
field_name_policy / fieldNamePolicy selects the writer field-name layer.
The default FIELD_NAME_POLICY_JOURNALD preserves trusted systemd fields such
as _HOSTNAME and _TRANSPORT. FIELD_NAME_POLICY_JOURNAL_APP drops caller
fields that journald would reject from untrusted applications and fails only
when no caller fields remain. FIELD_NAME_POLICY_RAW accepts any non-empty
field name that does not contain =, but RAW-mode files are not guaranteed to
be accepted by stock systemd tooling. Producer-specific field transformations
belong outside the SDK.
Structured 'rotation_policy' / 'rotationPolicy' and 'retention_policy' /
'retentionPolicy' option dictionaries are also accepted for the Go-style
optional policy contract. Omitted policy fields are disabled; explicitly
setting a structured policy field to 0 is rejected.
python3 cmd/journalctl.py --file fixtures/systemd/test-data/no-rtc/system.journal.zst --head 1 --output=json
python3 cmd/journalctl.py --directory /var/log/journal --list-boots
python3 cmd/journalctl.py --file ./sample.journal PRIORITY=3 PRIORITY=4 + MESSAGE=boot
python3 cmd/journalctl.py --directory ./journals --boot=all --since @1700000000 --until @1700003600
python3 cmd/journalctl.py --file ./active.journal --follow --no-tail --boot=allSdJournalOpen(path, flags)- Open journal file or directorySdJournalOpenFile(path, flags)/SdJournalOpenDirectory(path, flags)/SdJournalOpenFiles(paths, flags)- Open explicit file, directory, or file setSdJournalClose(journal)- Close the readerSdJournalNext(journal)- Advance to next entry (returns 1 on success, 0 on EOF)SdJournalNextSkip(journal, skip)- Advance up toskipmatching entriesSdJournalPrevious(journal)- Go to previous entrySdJournalPreviousSkip(journal, skip)- Move backward up toskipmatching entriesSdJournalSeekHead(journal)- Seek to first entrySdJournalSeekTail(journal)- Seek to last entrySdJournalSeekRealtimeUsec(journal, usec)- Seek by realtime timestampSdJournalSeekCursor(journal, cursor)- Seek to a cursor location; a syntactically valid cursor is accepted even when no exact entry existsSdJournalGetEntry(journal)- Get current entry objectSdJournalGetData(journal, field_name)- Get firstFIELD=valuepayload for a fieldSdJournalRestartData(journal)/SdJournalEnumerateAvailableData(journal)- enumerate current-entryFIELD=valuepayloads, preserving repeated/binary valuesSdJournalGetCursor(journal)- Get current cursor stringSdJournalTestCursor(journal, cursor)- Test if cursor matches current positionSdJournalGetRealtimeUsec(journal)- Get entry realtime timestampSdJournalGetMonotonicUsec(journal)- Get entry monotonic timestamp and boot IDSdJournalGetSeqnum(journal)- Get entry sequence number and sequence IDSdJournalEnumerateFields(journal)- List all field namesSdJournalRestartFields(journal)/SdJournalEnumerateField(journal)- stateful field-name enumerationSdJournalQueryUnique(journal, fieldName)- Get unique[fieldName, bytes]values for a fieldSdJournalQueryUniqueState(journal, field_name)/SdJournalRestartUnique(journal)/SdJournalEnumerateAvailableUnique(journal)- stateful unique-value enumeration as fullFIELD=valuepayloadsSdJournalListBoots(journal)- List boot entries
Current-entry data payloads returned by SdJournalEnumerateAvailableData() stay
valid after end-of-row enumeration and until the reader advances, seeks,
clears/restarts DATA enumeration, refreshes/remaps the file, or closes. Python
returns bytes objects from the facade, so callers can retain those objects
normally.
SdJournalAddMatch(journal, data)- Add match filter (AND)SdJournalAddDisjunction(journal)- Add OR group for subsequent matchesSdJournalAddConjunction(journal)- Start an explicit AND groupSdJournalFlushMatches(journal)- Clear match filtersSdJournalSetOutputMode(journal, mode)- Set output formatverify_file(path)- Verify journal structureverify_file_with_key(path, verification_key)- Verify sealed TAG/HMAC integrity and then journal structureFileReader.visit_entry_payloads(callback)- Visit current-entryFIELD=valuepayloads without materializing the full entry objectFileReader.collect_entry_payloads()- Collect current-entry full payloadsFileReader.get_entry_payload(field_name)- Get the first full payload for a UTF-8 or byte field nameFileReader.get_raw(field_name)/FileReader.get_raw_values(field_name)- Return byte-name field values from the current entryFileReader.refresh()- Refresh header and entry-array state for active.journal/.journal~inputsFileReader,DirectoryReader,SdJournal, andWritersupport Python context-manager cleanup withwith ...:
Writer.create(path, options)- Create new journal file (options['live_publish_every_entries']controls explicit live-reader publication)writer.append(fields, options)- Append entrywriter.append_raw(payloads, options)- Append fullKEY=valuebyte payloads after field-name policy validation/filteringwriter.sync()- Sync to diskwriter.close()- Close while preservingONLINEstatewriter.close_offline()- Close withOFFLINEstatewriter.archive_to(path)- Rename and close withARCHIVEDstateLog(directory, options)- Create a high-level directory writer (options['live_publish_every_entries']is passed to active writers)log.append(fields, options)- Append through the directory writerlog.append_raw(payloads, options)- Append fullKEY=valuebyte payloads through the directory writer with the same high-level metadata injection aslog.append()log.sync()- Sync the active journal filelog.enforce_retention()- Apply retention without rotating or closinglog.close()- Archive the active file and apply retentionlog.active_file()- Return the current active file pathlog.active_file_path()- Return the created/opened active file path, or''before lazy creationlog.configured_directory()- Return the configured root before machine-id expansionlog.journal_directory()- Return the machine-id journal directorylog.machine_id(),log.boot_id(),log.source_name()- Return the writer identity and source prefix
- Full systemd object-graph verification parity is tracked separately
- Daemon-only operations not supported
Uses Python standard library modules and one compression dependency:
os,struct,tempfile,time,json- Core I/O and utilitiescompression.zstd- Zstd compression and decompression (Python 3.14+)lzma- XZ compression and decompressionlz4==4.4.5- LZ4 block compression and decompression
The adapter passes the shared conformance test manifest:
python3 -m compileall python
python3 adapter.py list | python3 -c "import sys,json; tests=json.load(sys.stdin); print(f'{len(tests)} tests supported')"