Skip to content

Latest commit

 

History

History

README.md

Python Journal SDK

Python systemd journal reader and writer SDK. No native journal bindings, no system journal library linkage.

Current Features

Reader

  • Read .journal, .journal~, .journal.zst, .journal~.zst files
  • 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 lz4 block compression
  • Forward/backward iteration, cursors, timestamps
  • Binary field values as bytes
  • mmap-backed file reads for .journal / .journal~ inputs and for .journal.zst inputs 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=value payload enumeration without materializing the full entry first
  • Byte-preserving RAW field-name access via full payloads, raw_fields, raw_field_values, FileReader.get_raw(), and FileReader.get_raw_values()
  • Field enumeration and unique value queries
  • Export, JSON, and text output formatting
  • libsystemd-compatible SdJournal facade
  • 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

Writer

  • 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() and Log.append_raw() for systemd-compatible KEY=value byte payloads, including binary values after the first =
  • Optional zstd, xz, and lz4-compressed DATA object writing via compression: 'zstd', compression: 'xz', or compression: 'lz4', using systemd's 512-byte default threshold and 8-byte minimum clamp
  • Forward Secure Sealing TAG writing through SealOptions, including stock journalctl --verify --verify-key coverage 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> when source_realtime_usec / sourceRealtimeUsec is 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-facing FIELD_NAME_POLICY_JOURNAL_APP, and structure-level FIELD_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

journalctl

  • --file and --directory options
  • --output=default|json|export
  • --list-boots and --fields
  • --since, --until, --boot, and --follow for file-backed query/follow behavior
  • --head and --tail
  • --verify and --verify-key for 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.

Requirements

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.

Platform Behavior

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.

Basic Reader Usage

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()

Binary Field Values

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))

Writer Usage

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.

Directory Writer Usage

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.

journalctl CLI

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=all

API

Reader API

  • SdJournalOpen(path, flags) - Open journal file or directory
  • SdJournalOpenFile(path, flags) / SdJournalOpenDirectory(path, flags) / SdJournalOpenFiles(paths, flags) - Open explicit file, directory, or file set
  • SdJournalClose(journal) - Close the reader
  • SdJournalNext(journal) - Advance to next entry (returns 1 on success, 0 on EOF)
  • SdJournalNextSkip(journal, skip) - Advance up to skip matching entries
  • SdJournalPrevious(journal) - Go to previous entry
  • SdJournalPreviousSkip(journal, skip) - Move backward up to skip matching entries
  • SdJournalSeekHead(journal) - Seek to first entry
  • SdJournalSeekTail(journal) - Seek to last entry
  • SdJournalSeekRealtimeUsec(journal, usec) - Seek by realtime timestamp
  • SdJournalSeekCursor(journal, cursor) - Seek to a cursor location; a syntactically valid cursor is accepted even when no exact entry exists
  • SdJournalGetEntry(journal) - Get current entry object
  • SdJournalGetData(journal, field_name) - Get first FIELD=value payload for a field
  • SdJournalRestartData(journal) / SdJournalEnumerateAvailableData(journal) - enumerate current-entry FIELD=value payloads, preserving repeated/binary values
  • SdJournalGetCursor(journal) - Get current cursor string
  • SdJournalTestCursor(journal, cursor) - Test if cursor matches current position
  • SdJournalGetRealtimeUsec(journal) - Get entry realtime timestamp
  • SdJournalGetMonotonicUsec(journal) - Get entry monotonic timestamp and boot ID
  • SdJournalGetSeqnum(journal) - Get entry sequence number and sequence ID
  • SdJournalEnumerateFields(journal) - List all field names
  • SdJournalRestartFields(journal) / SdJournalEnumerateField(journal) - stateful field-name enumeration
  • SdJournalQueryUnique(journal, fieldName) - Get unique [fieldName, bytes] values for a field
  • SdJournalQueryUniqueState(journal, field_name) / SdJournalRestartUnique(journal) / SdJournalEnumerateAvailableUnique(journal) - stateful unique-value enumeration as full FIELD=value payloads
  • SdJournalListBoots(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 matches
  • SdJournalAddConjunction(journal) - Start an explicit AND group
  • SdJournalFlushMatches(journal) - Clear match filters
  • SdJournalSetOutputMode(journal, mode) - Set output format
  • verify_file(path) - Verify journal structure
  • verify_file_with_key(path, verification_key) - Verify sealed TAG/HMAC integrity and then journal structure
  • FileReader.visit_entry_payloads(callback) - Visit current-entry FIELD=value payloads without materializing the full entry object
  • FileReader.collect_entry_payloads() - Collect current-entry full payloads
  • FileReader.get_entry_payload(field_name) - Get the first full payload for a UTF-8 or byte field name
  • FileReader.get_raw(field_name) / FileReader.get_raw_values(field_name) - Return byte-name field values from the current entry
  • FileReader.refresh() - Refresh header and entry-array state for active .journal / .journal~ inputs
  • FileReader, DirectoryReader, SdJournal, and Writer support Python context-manager cleanup with with ...:

Writer API

  • Writer.create(path, options) - Create new journal file (options['live_publish_every_entries'] controls explicit live-reader publication)
  • writer.append(fields, options) - Append entry
  • writer.append_raw(payloads, options) - Append full KEY=value byte payloads after field-name policy validation/filtering
  • writer.sync() - Sync to disk
  • writer.close() - Close while preserving ONLINE state
  • writer.close_offline() - Close with OFFLINE state
  • writer.archive_to(path) - Rename and close with ARCHIVED state
  • Log(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 writer
  • log.append_raw(payloads, options) - Append full KEY=value byte payloads through the directory writer with the same high-level metadata injection as log.append()
  • log.sync() - Sync the active journal file
  • log.enforce_retention() - Apply retention without rotating or closing
  • log.close() - Archive the active file and apply retention
  • log.active_file() - Return the current active file path
  • log.active_file_path() - Return the created/opened active file path, or '' before lazy creation
  • log.configured_directory() - Return the configured root before machine-id expansion
  • log.journal_directory() - Return the machine-id journal directory
  • log.machine_id(), log.boot_id(), log.source_name() - Return the writer identity and source prefix

Limitations

  • Full systemd object-graph verification parity is tracked separately
  • Daemon-only operations not supported

Dependencies

Uses Python standard library modules and one compression dependency:

  • os, struct, tempfile, time, json - Core I/O and utilities
  • compression.zstd - Zstd compression and decompression (Python 3.14+)
  • lzma - XZ compression and decompression
  • lz4==4.4.5 - LZ4 block compression and decompression

Conformance

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')"