Skip to content

Rust API

GitHub Actions edited this page Oct 10, 2026 · 18 revisions

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" }

Read One File

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.

Scan Payloads With Minimal Work

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.

Enumerate Current-Row DATA With Row Lifetime

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.

Read A Directory

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.

Use Snapshot Bounds For Query Workloads

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.

Query Unique Values Through Indexes

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 Query

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.

Compare Explorer Strategies

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.

Write A Directory With Rotation And Retention

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.

Dedicated Root Retention

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.

Write Structured Fields

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.

Field-Name Policy

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::Journald for trusted journald-like producers;
  • FieldNamePolicy::Raw only for file-format-level tools and tests.
  • FieldNamePolicy::JournalApp for untrusted application-facing rules.

Raw files are journal files, but stock systemd tooling is not guaranteed to accept invalid systemd field names.

Netdata Function Boundary

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.

Verify A File

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.

Clone this wiki locally