Repository navigation
Rust API
Every example on this page is compiled and executed by repository CI against synthetic fixtures, except blocks marked illustrative-only.
The normal Rust dependency is the public SDK package:
[dependencies]
journal = { package = "systemd-journal-sdk", version = "0.9.0" }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.
Use the lower-level packages only when the public package does not expose the
type you need. For example, structured directory writes currently use
StructuredField from systemd-journal-sdk-log-writer:
[dependencies]
journal = { package = "systemd-journal-sdk", version = "0.9.0" }
journal_log_writer = { package = "systemd-journal-sdk-log-writer", version = "0.9.0" }Callers that intentionally want local-host identity can also depend on
systemd-journal-sdk-host and import its journal_host lib. Core writers never
import or call that helper automatically; callers pass helper-returned values to
the writer explicitly.
[dependencies]
journal_host = { package = "systemd-journal-sdk-host", version = "0.9.0" }Use FileReader when the caller owns ordering and reads one journal file.
use journal::FileReader;
let mut reader = FileReader::open("/var/log/journal/example/system.journal")?;
reader.add_match(b"PRIORITY=6");
reader.seek_head();
while reader.next()? {
let entry = reader.get_entry()?;
if let Some(message) = entry.get_str("MESSAGE") {
println!("{message}");
}
}
# Ok::<(), Box<dyn std::error::Error>>(())get_entry() materializes maps and owned payloads. It is convenient, but it is
not the lowest-cost scan path.
Use visit_entry_payloads() when the consumer can work with FIELD=value
bytes directly.
use journal::FileReader;
let mut reader = FileReader::open("/var/log/journal/example/system.journal")?;
reader.seek_head();
while reader.next()? {
reader.visit_entry_payloads(|payload| {
if payload.starts_with(b"MESSAGE=") {
let value = &payload[b"MESSAGE=".len()..];
println!("{}", String::from_utf8_lossy(value));
}
Ok(())
})?;
}
# Ok::<(), Box<dyn std::error::Error>>(())Uncompressed payloads are borrowed from mmap-backed journal data. Compressed payloads are decompressed into row-owned storage. The payload is valid only inside the callback for this visitor shape.
Use entry_data_restart() and enumerate_entry_payload() when a facade-like
caller needs current-row payloads that stay valid until the row changes.
use journal::FileReader;
let mut reader = FileReader::open("/var/log/journal/example/system.journal")?;
reader.seek_head();
if reader.next()? {
reader.entry_data_restart()?;
while let Some(payload) = reader.enumerate_entry_payload()? {
println!("{}", String::from_utf8_lossy(payload));
}
}
# Ok::<(), Box<dyn std::error::Error>>(())Do not keep the returned slice after advancing, seeking, restarting DATA enumeration, remapping, or closing the reader. Copy if longer ownership is required.
Use DirectoryReader for stock-like ordering across active and archived files.
use journal::DirectoryReader;
let mut reader = DirectoryReader::open("/var/log/journal")?;
reader.seek_tail();
while reader.previous()? {
let realtime = reader.get_realtime_usec()?;
let entry = reader.get_entry()?;
println!("{realtime} {:?}", entry.get_str("MESSAGE"));
}
# Ok::<(), Box<dyn std::error::Error>>(())Directory reading merges journal files in journal order. It is the right API
for journalctl --directory style behavior.
The default reader is live. Use snapshot bounds when a query may ignore entries appended after it starts.
use journal::{FileReader, ReaderOptions};
let options = ReaderOptions::snapshot();
let mut reader = FileReader::open_with_options("/var/log/journal/example/system.journal", options)?;
reader.seek_head();
# Ok::<(), Box<dyn std::error::Error>>(())Snapshot bounds avoid live-file refresh work during long scans.
Unique values for one field should use the FIELD object's DATA chain, not a row scan.
use journal::FileReader;
let mut reader = FileReader::open("/var/log/journal/example/system.journal")?;
reader.visit_unique_values("SYSLOG_IDENTIFIER", |value| {
println!("{}", String::from_utf8_lossy(value));
Ok(())
})?;
# Ok::<(), Box<dyn std::error::Error>>(())Use query_unique() only when the caller needs an owned vector of all values.
For facade-style stateful enumeration, FileReader exposes
query_unique_state(), restart_unique_state(), clear_unique_state(), and
enumerate_unique_payload(field_name). DirectoryReader exposes the same
state controls and enumerate_unique_payload() for multi-file de-duplicated
payloads. These methods keep the same indexed FIELD/DATA-chain contract, so
query setup and each enumeration step can fail independently if the journal
contains damaged or undecompressible DATA. Directory-level unique enumeration
builds an exact 8-entry per-open-reader LRU cache from per-file FIELD/DATA
chains and reuses it for repeated queries or restarts while the already-open
file set's header signatures stay unchanged. The entry count is bounded, but
each cache entry keeps the full exact unique set for one field.
Explorer is the API for filters, facets, histogram, FTS, and selected returned rows.
use journal::{ExplorerQuery, FileReader};
let mut reader = FileReader::open("/var/log/journal/example/system.journal")?;
let query = ExplorerQuery::default()
.with_filter("PRIORITY", ["3", "4"])
.with_facet("SYSLOG_IDENTIFIER")
.with_histogram("PRIORITY");
let result = reader.explore(&query)?;
println!("matched rows: {}", result.stats.rows_matched);
# Ok::<(), Box<dyn std::error::Error>>(())Default Explorer behavior:
-
ExplorerStrategy::Traversal; -
ExplorerFieldMode::FirstValue; - source realtime enabled;
- indexed filters;
- all-field expansion only for returned rows.
Do not enable debug_collect_column_fields_by_row_traversal in production.
Use ExplorerStrategy::Compare to validate a query shape before using the
index strategy.
use journal::{ExplorerFieldMode, ExplorerQuery, ExplorerStrategy, FileReader};
let mut reader = FileReader::open("/var/log/journal/example/system.journal")?;
let query = ExplorerQuery {
facets: vec![b"PRIORITY".to_vec()],
field_mode: ExplorerFieldMode::AllValues,
use_source_realtime: false,
limit: 0,
..ExplorerQuery::default()
};
let result = reader.explore_with_strategy(&query, ExplorerStrategy::Compare)?;
if let Some(comparison) = result.comparison {
println!("traversal: {:?}", comparison.traversal_duration);
println!("index: {:?}", comparison.index_duration);
}
# Ok::<(), Box<dyn std::error::Error>>(())The index strategy is exact only for its supported subset. It is not a universal faster mode.
Use Log for production ingestion directories.
use journal::{Config, EntryTimestamps, Log, Origin, RetentionPolicy, RotationPolicy, Source};
use std::path::Path;
use std::time::Duration;
let origin = Origin {
machine_id: Some("00112233445566778899aabbccddeeff".parse()?),
namespace: None,
source: Source::System,
};
let config = Config::new(
origin,
RotationPolicy::default()
.with_number_of_entries(100_000)
.with_duration_of_journal_file(Duration::from_secs(3600)),
RetentionPolicy::default()
.with_number_of_journal_files(8)
.with_duration_of_journal_files(Duration::from_secs(7 * 24 * 3600)),
)
.with_boot_id("ffeeddccbbaa99887766554433221100".parse()?)
.with_compact(true)
.with_live_publish_every_entries(64);
let mut log = Log::new(Path::new("/var/log/journal-sdk"), config)?;
let timestamps = EntryTimestamps::default()
.with_entry_realtime_usec(1_700_000_000_000_000)
.with_entry_monotonic_usec(1);
log.write_entry_with_timestamps(
&[
b"MESSAGE=plugin started".as_slice(),
b"PRIORITY=6".as_slice(),
b"SYSLOG_IDENTIFIER=example-plugin".as_slice(),
],
timestamps,
)?;
log.sync()?;
log.close()?;
# Ok::<(), Box<dyn std::error::Error>>(())Log stores files below <directory>/<machine-id>/. By default it uses
Netdata-compatible chain active names. Use
Config::with_strict_systemd_naming(true) only when the consumer needs
<source>.journal active naming.
By default, Log syncs each archived journal file on the caller path during
rotation, explicit close, and stale-active startup archive. Latency-sensitive
callers may set Config::with_sync_on_archive(false) to skip that archive-file
sync. With the opt-out, the caller owns archived-file durability before relying
on side indexes or allowing retention to delete archived files.
Rust 0.8.2 adds Log::close_without_retention() for closing before a
retention-policy change. It follows close()'s archive and sync path while
skipping retention. Both methods consume the writer; construct a new writer
with the new policy to reopen. An unopened lazy writer stays unopened, and an
empty strict-named active file is discarded. Normal close() continues to
apply its policy when archiving a file, except when discarding that empty file.
The caller owns subsequent retention enforcement.
The optional journal_host helper can load local-host identity for callers that
then pass those values into Origin and entry timestamps explicitly. On Linux,
containerized callers can set LoadOptions::with_host_filesystem_prefix("/host")
when they intentionally want host machine identity instead of container-local
identity. Missing host files fall back to container-local files; present invalid
host files return an error so collectors do not silently switch identity.
This feature is unreleased. It adds the public Config.root_retention field,
LogLifecycleEvent::Archived variant and WriterError::RootRetention variant.
Callers using exhaustive struct literals or enum matches must handle these
additions when upgrading; Config::new keeps root retention disabled.
Use Config::with_root_retention(true) when one caller owns a dedicated root
and source across machine identity changes. This has the same retention contract
as the Go API. It requires strict systemd naming, explicit machine/boot identity,
and no namespace or artifact sizer. Default Log behavior remains machine-local,
with committed-byte accounting and archive-head-age retention.
The caller MUST serialize access, exclude other writers and directory changes,
and verify recovered active files' provenance and indexes before opening the
log. inspect_root_retention(root, &source) supplies header-only inventory
before open; log.inspect_root_retention() also validates the live file's
filesystem and journal identities. Header checks also reject inconsistent
object counts and cached offset/count pairs. Missing or replaced files are errors,
and
inspection remains available after writer poisoning. Neither API verifies
payloads/indexes or protects against concurrent external modification. See
Writer-APIs for the shared ownership and accounting contract.
Root rotation, close and drop check live-file ownership before mutation. Archive and lazy successor destinations must be unoccupied. Creation or rotation preflight errors leave the writer healthy for retry after restoring the expected paths. Failed close/drop preflight preserves both the held file and the named replacement. Checks occur at lifecycle boundaries, not on every append.
use journal::{Config, EntryTimestamps, Log, Origin, RetentionPolicy, RotationPolicy, Source};
use std::path::Path;
use std::time::{Duration, SystemTime};
let day = Duration::from_secs(24 * 3600);
let config = Config::new(
Origin {
machine_id: Some("00112233445566778899aabbccddeeff".parse()?),
namespace: None,
source: Source::Unknown("example-history".into()),
},
RotationPolicy::default().with_duration_of_journal_file(day),
RetentionPolicy::default()
.with_duration_of_journal_files(30 * day)
.with_size_of_journal_files(1024 * 1024 * 1024),
)
.with_boot_id("ffeeddccbbaa99887766554433221100".parse()?)
.with_strict_systemd_naming(true)
.with_root_retention(true);
let mut log = Log::new(Path::new("/var/log/journal-sdk"), config)?;
log.write_entry_with_timestamps(
&[b"MESSAGE=saved"],
EntryTimestamps::default().with_entry_monotonic_usec(1),
)?;
// Install the replacement before maintenance; invalid policies change nothing.
log.set_root_retention_policy(
RetentionPolicy::default()
.with_duration_of_journal_files(30 * day)
.with_size_of_journal_files(2 * 1024 * 1024 * 1024),
)?;
let result = log.maintain_root_retention(SystemTime::now())?;
let inventory = log.inspect_root_retention()?;
println!("{} bytes, {} files, {:?}", inventory.bytes, inventory.files.len(), result.last_successful_at);
log.close()?;
# Ok::<(), Box<dyn std::error::Error>>(())set_root_retention_policy() installs a validated value without enforcing
limits. root_retention_policy() returns a copy. Explicit rotation limits stay
in effect; derived allocation geometry is recalculated. Maintenance archives a
live file only for idle age expiry or changed allocation geometry, discards an
empty file when needed, and leaves its successor lazy. Archived lifecycle
events describe finalization without a successor; ordinary rotations retain
their existing Rotated event. Successful finalization emits Archived before
any deletion, including retired files and close; empty-file disposal is not an
archive event. Even eager construction may return without an active file when
startup maintenance finalizes a recovered file. New root files use the current
configuration, including compact layout, rather than inheriting the recovered
file's layout.
Root maintenance runs on startup (including lazy archived-only histories),
active-file creation, rotation, close() of a nonempty active file, and explicit
calls. Closing an unopened or empty Log does not sweep history.
close_without_retention() finalizes without pruning. New SDK files allocate
at least 8 MiB; existing files are counted at their actual length and smaller allowances
are accepted. Once finalized, a file loses live protection, so a small allowance
or policy shrink can delete even the newest file in the same pass. There is no
newest-file grace period. The SDK does not schedule maintenance.
maintain_root_retention(now) and enforce_retention() report failures;
automatic safe inventory/unlink/directory-sync failures are recorded without
failing a healthy append. Failure to open or validate a retired file before
mutation also leaves the current writer healthy. Uncertain archive mutation
still poisons the writer and stops pruning. last_root_retention_result() exposes the latest attempt,
last successful time, deletion count, typed shared error and a post-attempt
inventory only when inventory_valid is true. Read this retained result after
an explicit maintenance error as well; an invalid inventory is unknown, not
zero storage. is_poisoned() separately reports writer health.
Root mode rejects artifact sizing in new_with_hooks. Attaching an artifact
sizer through the infallible with_artifact_sizer builder makes subsequent
mutating operations fail configuration validation; close/drop preserve the
files instead of applying that invalid combination.
Use structured fields when the producer already has field names and values split.
use journal::{Config, EntryTimestamps, Log, Origin, RetentionPolicy, RotationPolicy, Source};
use journal_log_writer::StructuredField;
use std::path::Path;
let origin = Origin {
machine_id: Some("00112233445566778899aabbccddeeff".parse()?),
namespace: None,
source: Source::System,
};
let config = Config::new(
origin,
RotationPolicy::default(),
RetentionPolicy::default(),
)
.with_boot_id("ffeeddccbbaa99887766554433221100".parse()?);
let mut log = Log::new(Path::new("/var/log/journal-sdk"), config)?;
let fields = [
StructuredField::new(b"MESSAGE", b"binary-safe structured entry"),
StructuredField::new(b"PRIORITY", b"6"),
StructuredField::new(b"BINARY_PAYLOAD", b"\x00\x01\x02\xff"),
];
let timestamps = EntryTimestamps::default()
.with_entry_realtime_usec(1_700_000_000_000_001)
.with_entry_monotonic_usec(2);
log.write_fields_with_timestamps(&fields, timestamps)?;
# Ok::<(), Box<dyn std::error::Error>>(())This avoids constructing KEY=value bytes only to split them again.
use journal::{Config, FieldNamePolicy, Origin, RetentionPolicy, RotationPolicy, Source};
let origin = Origin {
machine_id: Some("00112233445566778899aabbccddeeff".parse()?),
namespace: None,
source: Source::System,
};
let config = Config::new(
origin,
RotationPolicy::default(),
RetentionPolicy::default(),
)
.with_boot_id("ffeeddccbbaa99887766554433221100".parse()?)
.with_field_name_policy(FieldNamePolicy::Journald);
# Ok::<(), Box<dyn std::error::Error>>(())Use:
-
FieldNamePolicy::Journaldfor trusted journald-like producers; -
FieldNamePolicy::Rawonly for file-format-level tools and tests. -
FieldNamePolicy::JournalAppfor untrusted application-facing rules.
Raw files are journal files, but stock systemd tooling is not guaranteed to
accept invalid systemd field names.
Use journal::netdata when the consumer needs Netdata-shaped function output.
use journal::netdata::{
NetdataFunctionConfig, NetdataFunctionRunOptions, NetdataJournalFunction,
SystemdJournalProfile,
};
use serde_json::json;
use std::path::Path;
let function = NetdataJournalFunction::systemd_journal();
let request = json!({
"after": 0,
"before": 0,
"last": 200,
"facets": ["PRIORITY", "SYSLOG_IDENTIFIER"],
"histogram": "PRIORITY"
});
let response = function.run_directory_request_json_with_options(
Path::new("/var/log/journal"),
&request,
NetdataFunctionRunOptions::from_timeout_seconds(30),
)?;
println!("{}", serde_json::to_string(&response)?);
# Ok::<(), Box<dyn std::error::Error>>(())Customize NetdataFunctionConfig::source_selector_name and
source_selector_help when the same function shape serves a domain-specific
journal backend. The wire id remains __logs_sources; only the label and help
shown by Netdata change.
let mut config = NetdataFunctionConfig::systemd_journal();
config.source_selector_name = "Trap Jobs".to_string();
config.source_selector_help = "Select the trap job to query".to_string();
let function = NetdataJournalFunction::new(config, SystemdJournalProfile);This layer is Netdata-specific. Generic log explorers should use Explorer directly unless they need the Netdata request and response shape.
use journal::verify_file;
verify_file("/var/log/journal/example/system.journal")?;
# Ok::<(), Box<dyn std::error::Error>>(())Use verify_file_with_key() for sealed files when a verification key is
available. Verification is for integrity checks, not normal query serving.
File-path verification uses the same bounded reader access architecture as
normal file reads, so it avoids whole-file resident buffers while still walking
the object graph and sealed HMAC ranges.