Rust Journal SDK
This workspace contains pure-Rust systemd journal reader and writer components. It does not link to libsystemd or other system journal libraries for SDK behavior.
crates.io Usage
The public Rust SDK package is systemd-journal-sdk. Use a Cargo dependency
alias if existing code should import it as journal:
[]
= { = "systemd-journal-sdk", = "0.9.0" }
The workspace also publishes project-prefixed lower-level packages for consumers that need direct access to the same internal layers used by the SDK:
systemd-journal-sdk-commonsystemd-journal-sdk-coresystemd-journal-sdk-registrysystemd-journal-sdk-log-writersystemd-journal-sdk-host(journal_host), the optional local-host identity and monotonic helper cratesystemd-journal-sdk-indexsystemd-journal-sdk-engine
Current writer scope:
- regular journal files by default and compact journal files with
JournalFileOptions::with_compact(true)orConfig::with_compact(true); - uncompressed DATA objects by default;
- optional zstd, xz, and lz4-compressed DATA object writing through
JournalFileOptionsandjournal::Config, using systemd's 512-byte default threshold and 8-byte minimum clamp; - keyed hash tables using the journal file ID;
- byte-safe field values through
&[u8]field payloads; - direct-file writing through
journal_core; - high-level directory writing through
journal::Log; - systemd-compatible
0640journal file permissions by default, configurable for newly-created files throughJournalFileOptions::with_file_mode()andConfig::with_file_mode(); - chain active naming by default, with
Config::with_strict_systemd_naming(true)available for strict systemd<source>.journalactive naming; - shared field-name policy layers for direct-file and directory writers:
default
FieldNamePolicy::Journald, app-facingFieldNamePolicy::JournalApp, and structure-levelFieldNamePolicy::Raw; - entry-count, file-size, and duration rotation;
- tracked journal-file-count, byte-size, and age retention;
- optional pure cross-SDK cooperative lockfile with stale-owner detection when
callers explicitly acquire
journal_core::file::lock::WriterLock; - Forward Secure Sealing TAG writing through
SealOptions, including stockjournalctl --verify --verify-keycoverage for sealed files generated by this writer; - FSS
SealOptions::start_usecnormalization to systemd's verification-key epoch boundary, so unaligned source timestamps still produce sealed files that stockjournalctl --verify --verify-keycan validate; - low-level
EntryWriteOptions::seqnum(...)andEntryWriteOptions::boot_id(...)exact-regeneration support for preserving ENTRY sequence gaps and per-entry boot IDs when rewriting existing journal files. Leave them unset for normal auto-incrementing sequence numbers and the writer-wide boot ID; - native systemd writers do not participate in the SDK lock protocol and remain an operational exclusion;
- live stock-reader validation for the current writer slice with
journalctl --file,journalctl --file --follow --no-tail --boot=all --merge, and libsystemd reader APIs, including live sequence-order checks; - configurable explicit live-reader publication cadence through
JournalWriter::set_live_publish_every_entries()andConfig::with_live_publish_every_entries(), defaulting to systemd-compatible publication after every entry.
Deferred scope:
- appending to arbitrary historical or systemd-created journal variants. In particular, append-open on historical unkeyed-hash files is unsupported and returns a controlled error before entry mutation;
- the imported legacy
jfjournal_file::JournalWriterremains available for compatibility with that crate's public surface, but it is not the supported production writer path. It also returns a controlled unsupported-file error for unkeyed append targets instead of panicking. New writer integrations should usejournal_core::file::JournalWriteror the high-leveljournal::Logdirectory writer; - full systemd object-graph verification parity beyond the current repository verification API.
Current reader scope:
- regular and compact journal files;
.journal,.journal~,.journal.zst, and.journal~.zstfiles;- zstd-compressed fixture files;
- zstd, lz4, and xz-compressed DATA objects through pure-Rust dependencies;
- directory reading across active and archived files with bounded recursive
traversal, symlink-cycle protection, and interleaved multi-file ordering,
including mixed regular/compact, compressed/uncompressed,
sealed/unsealed, and whole-file
.journal.zstfiles in one directory; - forward/backward iteration, cursors, realtime and monotonic timestamps, seqnum metadata, field enumeration, binary field values, repeated field values, stateful current-entry data enumeration, unique value enumeration, and export/json/text formatting;
- byte-preserving RAW field-name access through
Entry::raw_fields(),Entry::get_raw(), andEntry::get_raw_values();Entry.fieldsandEntry.field_valuesare UTF-8 string-keyed convenience maps and do not synthesize lossy names for non-UTF8 RAW field names; - export byte output preserves non-UTF8 RAW field names; JSON output, field
enumeration, unique queries, and
get_datafacade helpers remain UTF-8 field-name surfaces; - libsystemd-compatible facade functions for open file/directory/files, close, seek head/tail/realtime/cursor, next/previous/skip, match groups, current-entry data enumeration, field enumeration, unique value enumeration, realtime/monotonic/seqnum/cursor metadata, and boot listing;
- facade cursor seeking follows libsystemd semantics: valid missing cursors are
accepted as seek locations, while
test_cursorchecks exact current position; - cursor metadata is emitted in the official systemd cursor shape; cursor seek and test also accept the older SDK cursor shape for compatibility;
- current-entry facade data enumeration returns borrowed
FIELD=valuebytes for the current DATA object, matching libsystemd-style validity until the current row is reset or the reader advances; uncompressed DATA is returned directly from the mmap-backed journal payload, while compressed DATA is copied into row-owned stable storage so later compressed DATA reads cannot invalidate earlier pointers from the same row; - direct facade unique queries return language-native
(field, value)pairs; stateful unique enumeration returns full binary-safeFIELD=valuepayloads; FileReader::visit_unique_values()andDirectoryReader::visit_unique_values()stream indexed unique values without first materializing the full result set;FileReader::explore()provides an optimized single-file query surface for log-explorer workloads: exact indexed filters, selected facet counters, optional histogram, optional FTS, optional returned rows, and query counters. It lazily classifies reusable DATA objects by DATA offset during candidate-row traversal, groups facets that share the same effective filter set into one traversal pass, and expands all fields only for returned rows.ExplorerAnchor::Autois the default: forward queries start from the lower time bound or file head, while backward queries start from the upper time bound or file tail.ExplorerFieldMode::FirstValueis the default explorer accounting mode: one selected facet/histogram/source field contributes at most one value per row, so traversal may stop after all required fields are found.ExplorerFieldMode::AllValuesis available when a caller needs exact duplicate-value accounting and accepts the slower full-row scan.explore()owns the reader position and replaces the reader match state while it runs; callers should explicitly seek and reapply any manual matches before continuing normal iteration after an explorer query;FileReader::explore_with_strategy()exposes explicit strategy selection.ExplorerStrategy::Traversalis the default behavior used byexplore().ExplorerStrategy::Indexderives all-values facet and histogram counts from FIELD/DATA indexes and DATA entry posting lists. It rejects default first-value semantics, FTS, and source-realtime-bounded queries instead of returning approximate results.ExplorerStrategy::Compareruns traversal and index, fails if their logical outputs differ, and returns timing/counter diagnostics inExplorerResult::comparison. There is no automatic planner because index aggregation is faster only for some query shapes;journal::netdataprovides the Netdata-specific Rust function boundary over the explorer. It is the SDK API intended to replace Netdata's genericsystemd-journal.pluginlogs function.NetdataJournalFunction::systemd_journal()runs asystemd-journalrequest JSON against a journal directory and returns Netdata-shaped function JSON. This layer owns Netdata request parsing, default facets, default display columns, histogram defaults, field presentation transforms, row options, and zero-count vocabulary padding for filtered requests. The default profile keeps UID/GID values as raw journal data and does not resolve host user or group names. The separateNetdataJournalFunction::systemd_journal_plugin_compatible()constructor opts into host user/group name presentation to emulate Netdata's installed plugin, with per-query UID/GID display caching so repeated values do not repeatedly call host name-service lookups. This layer is intentionally separate from the core journal file-format reader. Consumers that need Netdata function control can userun_directory_request_json_with_options()orrun_directory_request_bytes_with_options()withNetdataFunctionRunOptionsto supply a timeout, progress callback, cancellation callback, and optional caller-ownedNetdataFunctionState. Progress is reported against the files selected for the query after source and time-window preselection, including file-end progress for small or fast files. Cancellation is checked before each selected file, during active Explorer scans, and after file-end progress callbacks. The optional state hook lets Netdata pass registry-provided source type/name metadata and persist per-file learned journal-vs-source-realtime drift. Without state, the wrapper falls back to journal headers and plugin-compatible filename classification for built-in__logs_sourcesgroups.NetdataFunctionConfig::source_selector_nameandsource_selector_helpcustomize only the displayed selector label/help while preserving the__logs_sourceswire id. Sampling uses plugin-compatible sampled, unsampled, and estimated counters for full-analysis sliced requests and is disabled for data-only requests. Thequeryrequest member uses NetdataSIMPLE_PATTERNbehavior: ordered|terms, leading!negative terms, escaped separators, substring*parts, and case-insensitive matching. The SDK Netdata boundary always executes indexed slice semantics. Theslicerequest member is retained in the normalized echo because it is part of the plugin request shape; it does not select a slower non-slice fallback path. Cancellation and no-change responses use Netdata's compact function error envelope; timeout returns a partial table response;src/internal/testcmd/netdata_function_wrapperis a thin offline test adapter over the SDK Netdata boundary. It exposes the same CLI shape as Netdata's plugin test path:netdata_function_wrapper --test systemd-journal --dir <journal-dir> --timeout <seconds> < <request.json>. The request JSON is read from stdin to avoid privileged file reads in test binaries. The comparison tools under../tests/netdata_function/compare semantic function output against an externalsystemd-journal.pluginbinary. The wrapper has diagnostic-only--progress-jsonl,--cancel-immediately, and--cancel-after-progressswitches to validate the SDK run-control API; production consumers should calljournal::netdatadirectly and wire callbacks to their own function framework;- default reader options use live/windowed mmap with a 32 MiB window. Smaller windows are available for constrained environments, but high-cardinality indexed queries can become remap-bound with very small windows;
- file-backed
journalctloutput covers the stock v260.1 short family,verbose,with-unit,cat,export,json,json-pretty,json-sse, andjson-seqmodes;--output-fieldsprojects requested fields while JSON/export retain stock metadata fields; - short-style
journalctllabels include hostname, identifier/unit, and PID components from journal fields, and--no-hostnamesuppresses the hostname component; --output exportuses systemd's size-prefixed binary field encoding and blank-line entry separator; JSON output includes realtime and monotonic timestamps, preserves valid UTF-8 strings, and encodes binary values as arrays of unsigned bytes;- libsystemd-style match behavior: AND between different fields, OR between
values for the same field,
SdJournalAddDisjunction()for+, andSdJournalAddConjunction()for explicit AND groups; - a file-backed
journalctlcommand undersrc/cmd/journalctlwith--since,--until,--boot,--invocation,-I,--list-invocations,--header, and--followsupport for repository-backed files and directories; - verification APIs:
journal::verify_file()for structural verification andjournal::verify_file_with_key()for sealed TAG/HMAC verification; - a conformance adapter under
src/adapter.
Platform behavior:
- Linux is the validated reference runtime and keeps mmap-backed hot paths, monotonic timestamps, Unix directory sync, and SIGBUS handling.
- FreeBSD and macOS builds use monotonic timestamps and the same pure file reader/writer paths. Optional identity and lock helpers are separate from the core file-format writer.
- Windows builds use unbiased interrupt time for automatic writer timestamps and no-op directory fsync/SIGBUS hooks. Optional identity and lock helpers are separate from the core file-format writer.
- Non-Linux build checks are compilation evidence only unless runtime evidence
from that OS is recorded separately. Files written on non-Linux targets must
still pass Linux stock
journalctl --verify --fileand repository interoperability checks before production compatibility is claimed.
Reader limitations:
list_bootsuses file-level boot metadata in this slice;- full systemd object-graph verification parity is tracked separately;
- daemon-only journalctl operations are not implemented.
Basic directory writer usage:
use ;
let origin = Origin ;
let config = new
.with_boot_id;
let mut log = new?;
let timestamps = default
.with_entry_realtime_usec
.with_entry_monotonic_usec;
log.write_entry_with_timestamps?;
log.sync?;
log.close?;
# Ok::
Log stores files below <directory>/<machine-id>/. By default the active file
uses the chain filename form
<source>@<seqnum-id>-<head-seqnum>-<head-realtime>.journal; call
Config::with_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.
Unset rotation and retention limits are disabled. 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. Duration
rotation is checked before append using the incoming entry realtime and the
active file head realtime.
Call Log::enforce_retention() to apply age/count/byte retention without
waiting for another append-triggered rotation or close. Call Log::close() to
archive the current file and enforce retention; Drop only performs best-effort
state persistence.
Rust 0.8.2 adds Log::close_without_retention() for closing before reopening
with a changed retention policy. It follows the same archive and sync path as
close() and skips retention. Both methods consume the writer, discard an
empty strict-named active file, and leave an unopened lazy writer unopened.
The caller owns retention enforcement on subsequent writers.
Retention also runs once when a writer opens or creates the active file:
existing-active reopen and LogOpenMode::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 Config::with_open_mode(LogOpenMode::Eager) to create/open the active file
during construction. LogIdentityMode::Strict is the default and only
supported identity mode; callers must provide Origin.machine_id,
Config::with_boot_id(), and explicit per-entry monotonic timestamps.
Log::configured_directory(), Log::journal_directory(),
Log::active_path(), Log::machine_id(), Log::boot_id(), and
Log::source() expose the same directory/identity contract as the other SDKs.
Lifecycle observers receive Created, Rotated, and RetainedDeleted events;
Log::with_artifact_sizer() includes per-journal sidecar bytes in retained-size
decisions. write_entry_with_timestamps() accepts
EntryTimestamps::source_realtime_usec for _SOURCE_REALTIME_TIMESTAMP
injection and clamps non-progressing realtime and monotonic overrides forward.
The low-level JournalWriter::add_entry() 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. 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_core::file::lock::WriterLock when the caller wants the optional
cooperating-writer lock helper to reject another SDK writer for the same file.
Config::with_field_name_policy() selects the high-level writer field-name
layer. The default FieldNamePolicy::Journald preserves trusted systemd fields
such as _HOSTNAME and _TRANSPORT. FieldNamePolicy::JournalApp drops caller
fields that journald would reject from untrusted applications and fails only
when no caller fields remain. FieldNamePolicy::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.
Journal files are created with systemd journald's 0640 default permissions.
Use JournalFileOptions::with_file_mode() for direct-file writers or
Config::with_file_mode() for directory writers when a consumer needs another
mode. 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.
Live-reader publication can be tuned when the consumer does not need immediate stock follow-reader wakeups:
let config = config.with_live_publish_every_entries;
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.
Binary-safe values:
let timestamps = default
.with_entry_realtime_usec
.with_entry_monotonic_usec;
log.write_entry_with_timestamps?;
# Ok::
Basic reader usage:
use FileReader;
let mut reader = open?;
reader.add_match;
reader.seek_head;
while reader.next?
# Ok::
Optimized single-file explorer usage:
use ;
let mut reader = open?;
let result = reader.explore?;
if let Some = result.facets.get
# Ok::
The default first-value mode counts at most one value per selected field per
row. Use ExplorerFieldMode::AllValues when a row may contain repeated values
for a selected facet or histogram field and every duplicate value must count.
Explorer column catalogs are built from FIELD indexes. Do not use row traversal
to discover columns in production; a comparison that needs
debug_collect_column_fields_by_row_traversal has found a bug in the explorer
or its column-catalog setup, not a valid operating mode.
Specialized callers can select an execution strategy:
use ;
let mut reader = open?;
let result = reader.explore_with_strategy?;
# Ok::
The index strategy is exact for its supported subset, but it is not a universal
speedup. It can be much faster for narrow unfiltered all-values facets and
histograms, and slower for many facets or selective filters. Use
ExplorerStrategy::Compare when validating a query shape before relying on the
index strategy; successful compare results include traversal and index timings
and stats in ExplorerResult::comparison.
The default ExplorerAnchor::Auto chooses the natural scan start for the query
direction. Use explicit Head, Tail, or Realtime(usec) anchors only for
manual paging or when the caller intentionally wants a non-default start point.
For RAW-mode files, use the byte-keyed entry surface when field names are not guaranteed to be UTF-8:
if let Some = entry.get_raw
for field in entry.raw_fields
File-backed journalctl:
--file/-i may be repeated and accepts glob patterns, matching stock
file-backed journalctl. --file=- is intentionally unsupported because
portable stdin journals would need seekable mmap-capable descriptors.
Repeated matches for the same field are OR alternatives. Matches for different
fields are ANDed. A separate + argument creates an explicit disjunction:
Syslog identifier, priority, facility, grep, dmesg, cursor, system unit, user
unit, and invocation filters are supported for file-backed inputs. Unit filters
support exact units and glob expansion over journal unit fields.
--list-invocations and --header operate on explicit file/directory inputs.
--new-id128 is a portable standalone utility action, and --disk-usage
reports allocated filesystem usage for explicit --file or --directory
inputs.
--vacuum-size, --vacuum-files, and --vacuum-time operate on explicit
--directory inputs, deleting only stock-recognized archived .journal and
.journal~ files while protecting active/current and unknown files.
The CLI implements the stock v260.1 output-mode family, --output-fields
projection for verbose, export, JSON modes, and cat, and stock output
controls such as --all, --full, --no-full, empty-result messages, and
--pager-end implicit 1000-line tail behavior. --output=help prints the
official mode list without opening a journal.
Realtime ranges, boot filters, and follow mode are supported for file-backed inputs:
Indexed snapshots
Rust 0.9.0 adds bounded IndexedSnapshot traversal and strict offline index
verification for uncertain files. See Indexed snapshots
for API examples, caller exclusion, borrowed payloads and recovery costs. Failures
after a possible file or publication-state mutation prevent further mutation and
preserve uncertain files during cleanup. Validation and capacity failures known
to precede mutation leave the writer reusable.
Upgrading from Rust 0.8.x: 0.9.0 adds the SdkError::Cancelled and
JournalError::WriterPoisoned variants. Neither enum is #[non_exhaustive],
so code that matches every variant without a wildcard arm must handle the new
variants.