Skip to content

Rust API

GitHub Actions edited this page Jul 7, 2026 · 12 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.7.7" }

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.7.7" }
journal_log_writer = { package = "systemd-journal-sdk-log-writer", version = "0.7.7" }

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

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.

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.

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