class

Logarithm::Systemd::JournalReader

Inherits Reference < Object

High-level interface for reading systemd journal entries.

This class provides a safe, idiomatic Crystal interface for accessing systemd journal entries. It encapsulates all low-level C API calls and provides high-level methods for seeking, reading, and iterating through journal entries.

Core Concepts

Journal Navigation

The journal maintains a read position that advances with each next_entry call. Use seek methods to position the reader at different points in the journal.

Cursors

Cursors are opaque strings that uniquely identify journal entry positions. They enable resumable processing and can be persisted across application restarts.

Timestamps

The journal uses microsecond-precision timestamps. The seek_realtime method accepts Crystal Time objects and converts them automatically.

Basic Usage Patterns

Real-time Monitoring

reader = JournalReader.new
return unless reader.open

# Start from current entries
reader.seek_tail

# Monitor new entries
loop do
  if entry = reader.next_entry
    process_entry(entry)
  else
    sleep 0.1.seconds
  end
end

reader.close

Historical Data Processing

reader = JournalReader.new
return unless reader.open

# Read from last 24 hours
cutoff = Time.utc - 24.hours
reader.seek_realtime(cutoff)

while entry = reader.next_entry
  # Process historical entries
  analyze_entry(entry)
end

reader.close

Resumable Processing

reader = JournalReader.new
return unless reader.open

# Resume from saved position
if cursor = load_saved_cursor()
  reader.seek_cursor(cursor)
else
  reader.seek_head # Start from beginning
end

while entry = reader.next_entry
  process_entry(entry)

  # Save progress periodically
  if rand < 0.01 # 1% chance
    save_cursor(entry.cursor)
  end
end

reader.close

Advanced Usage

Batch Processing

reader = JournalReader.new
reader.open
reader.seek_realtime(Time.utc - 1.hour)

batch = [] of JournalEntry
batch_size = 100

while entry = reader.next_entry
  batch << entry

  if batch.size >= batch_size
    process_batch(batch)
    batch.clear
  end
end

# Process remaining entries
process_batch(batch) unless batch.empty?

Filtered Reading

reader = JournalReader.new
reader.open
reader.seek_tail

# Only process error messages
while entry = reader.next_entry
  next unless entry.priority
  next unless entry.priority.to_i <= 3 # Error level and below

  handle_error(entry)
end

Error Handling

Connection Errors

reader = JournalReader.new
unless reader.open
  Log.error { "Failed to open journal - check permissions" }
  return
end

Seek Failures

unless reader.seek_realtime(some_time)
  Log.warn { "Could not seek to #{some_time}, using tail" }
  reader.seek_tail
end

Read Errors

while entry = reader.next_entry
  begin
    process_entry(entry)
  rescue ex
    Log.error { "Failed to process entry #{entry.cursor}: #{ex.message}" }
    # Continue processing other entries
  end
end

Performance Considerations

Memory Usage

  • Each JournalEntry holds field data in memory
  • Large journals may require streaming processing
  • Consider field filtering to reduce memory footprint

Seek Performance

  • seek_head/seek_tail: O(1) - immediate positioning
  • seek_cursor: O(1) - direct position lookup
  • seek_realtime: O(log n) - binary search on timestamps

Read Performance

  • next_entry: O(1) - sequential access
  • Field access: O(1) - hash lookup
  • Large field sets may impact processing speed

Optimization Tips

# Pre-allocate readers for reuse
@reader = JournalReader.new

# Use cursors for resumable processing
# Batch entries to reduce function call overhead
# Filter early to avoid processing unwanted entries

Thread Safety

JournalReader instances are not thread-safe. Each thread should use its own reader instance. Concurrent access to the same journal can be achieved by coordinating read positions between threads.

Resource Management

Always call close when finished to free system resources:

reader = JournalReader.new
begin
  reader.open
  # Use reader...
ensure
  reader.close
end

Common Issues

Permission Denied

  • Ensure user has access to journal files
  • Check systemd-journal group membership
  • Verify journal file permissions

Empty Journal

  • New systems may have minimal journal content
  • Check journalctl --list-boots for available data

Cursor Invalid

  • Cursors can become invalid after journal rotation
  • Implement fallback to seek_tail for invalid cursors

Constructors

Instance methods

close

Closes the journal connection and frees resources.

Should be called when finished reading to clean up properly.

Source
cursor

Gets the cursor for the current journal position.

The cursor is an opaque string that uniquely identifies the current reading position in the journal. It can be saved and later used with seek_cursor to resume reading from the same position.

Returns: Cursor string, or nil if not available

Source
next_entry

Reads the next journal entry in sequence.

Returns the next JournalEntry after the current position, or nil if there are no more entries or an error occurs. Call this method repeatedly to iterate through all entries.

Returns: Next journal entry, or nil if none available

Example:

while entry = reader.next_entry
  puts "#{entry.timestamp}: #{entry.message}"
end
Source
open

Opens the systemd journal for reading.

This method establishes a connection to the systemd journal daemon and prepares for reading entries. Must be called before any other operations.

Returns: true if successful, false if journal cannot be opened

Source
seek_cursor(cursor : String) : Bool

Seeks to a specific cursor position in the journal.

Cursors are opaque strings that uniquely identify a journal entry's position. They can be obtained from JournalEntry#cursor and used to resume reading from a specific point.

Parameters:

  • cursor: Cursor string identifying the position to seek to

Returns: true if successful, false on error

Source
seek_head

Seeks to the beginning of the journal (oldest entries).

After calling this, next_entry will return entries starting from the oldest available in the journal.

Returns: true if successful, false on error

Source
seek_realtime(time : Time) : Bool

Seeks to entries from a specific point in time.

This allows reading journal entries starting from a particular timestamp, which is useful for time-based filtering.

Parameters:

  • time: Time to seek to (entries from this time forward)

Returns: true if successful, false on error

Example:

# Read entries from last hour
reader.seek_realtime(Time.utc - 1.hour)
Source
seek_tail

Seeks to the end of the journal (newest entries).

After calling this, next_entry will return entries starting from the most recent entries. Useful for monitoring new logs.

Returns: true if successful, false on error

Source