sdjournal 0.1.22

Pure Rust systemd journal reader and query engine
Documentation
use crate::config::JournalConfig;
use crate::cursor::{Cursor, SdJournalEntryKey};
use crate::entry::EntryRef;
use crate::error::Result;
use crate::journal::Journal;

use super::filter::{CompiledFilter, MatchTerm};
use super::{
    cursor_from_key, is_skippable_live_file_error, key_from_entry_ref, warn_live_file_error,
};

pub(super) struct JournalSnapshot {
    pub(super) journal: Option<Journal>,
    pub(super) last_seen: Option<SdJournalEntryKey>,
}

pub(super) struct ReplayState {
    pub(super) journal: Option<Journal>,
    pub(super) cursor: Option<Cursor>,
    pub(super) since_realtime: Option<u64>,
    pub(super) last_key: Option<SdJournalEntryKey>,
    pub(super) remaining: Option<usize>,
}

impl ReplayState {
    pub(super) fn new(
        snapshot: JournalSnapshot,
        after_cursor: Option<Cursor>,
        since_realtime: Option<u64>,
        config: &JournalConfig,
    ) -> Self {
        let (cursor, last_key) = match after_cursor {
            Some(cursor) => match cursor.sdjournal_entry_key() {
                Some(key) => (None, Some(key)),
                None => (Some(cursor), None),
            },
            None => (None, None),
        };

        Self {
            journal: snapshot.journal,
            cursor,
            since_realtime,
            last_key,
            remaining: config.max_live_replay_entries,
        }
    }
}

pub(super) struct EntryBatch {
    pub(super) entries: Vec<EntryRef>,
    pub(super) last_key: Option<SdJournalEntryKey>,
    pub(super) exhausted: bool,
}

pub(super) fn collect_replay_batch(
    replay: &ReplayState,
    filter: &CompiledFilter,
    limit: usize,
) -> Result<EntryBatch> {
    let Some(journal) = replay.journal.as_ref() else {
        return Ok(EntryBatch {
            entries: Vec::new(),
            last_key: None,
            exhausted: true,
        });
    };
    let mut q = journal.query();
    apply_compiled_filter(&mut q, filter);
    if let Some(since) = replay.since_realtime {
        q.since_realtime(since);
    }
    if let Some(key) = replay.last_key {
        q.after_cursor(cursor_from_key(key));
    } else if let Some(cursor) = replay.cursor.clone() {
        q.after_cursor(cursor);
    }
    q.limit(limit.saturating_add(1));

    collect_entry_batch(q, limit, "skipping live replay entry")
}

fn collect_entry_batch(
    query: crate::JournalQuery,
    limit: usize,
    skip_message: &'static str,
) -> Result<EntryBatch> {
    let mut entries = Vec::with_capacity(limit.min(64));
    let mut exhausted = true;
    for item in query.iter()? {
        match item {
            Ok(entry) => {
                if entries.len() >= limit {
                    exhausted = false;
                    break;
                }
                entries.push(entry);
            }
            Err(err) if is_skippable_live_file_error(&err) => {
                warn_live_file_error(skip_message, &err);
            }
            Err(err) => return Err(err),
        }
    }

    let last_key = entries.last().map(key_from_entry_ref);
    Ok(EntryBatch {
        entries,
        last_key,
        exhausted,
    })
}

fn apply_compiled_filter(query: &mut crate::JournalQuery, filter: &CompiledFilter) {
    for term in &filter.global_terms {
        apply_term_to_query(query, term);
    }
    for branch in &filter.or_groups {
        query.or_group(|group| {
            for term in branch {
                apply_term_to_or_group(group, term);
            }
        });
    }
    if filter.global_terms.is_empty() && filter.or_groups.is_empty() {
        for branch in &filter.branches {
            for term in branch {
                apply_term_to_query(query, term);
            }
        }
    }
}

fn apply_term_to_query(query: &mut crate::JournalQuery, term: &MatchTerm) {
    match term {
        MatchTerm::Exact { field, value } => {
            query.match_exact(field, value);
        }
        MatchTerm::Present { field } => {
            query.match_present(field);
        }
    }
}

fn apply_term_to_or_group(group: &mut crate::query::OrGroupBuilder, term: &MatchTerm) {
    match term {
        MatchTerm::Exact { field, value } => {
            group.match_exact(field, value);
        }
        MatchTerm::Present { field } => {
            group.match_present(field);
        }
    }
}