Logarithm::Systemd::JournalReader
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 positioningseek_cursor: O(1) - direct position lookupseek_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-bootsfor available data
Cursor Invalid
- Cursors can become invalid after journal rotation
- Implement fallback to
seek_tailfor invalid cursors
Constructors
Instance methods
Closes the journal connection and frees resources.
Should be called when finished reading to clean up properly.
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
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
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
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
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
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)
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