systemd-journal 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/README.md +25 -2
- data/Rakefile +6 -6
- data/lib/systemd/id128.rb +9 -0
- data/lib/systemd/journal.rb +33 -246
- data/lib/systemd/journal/filterable.rb +102 -0
- data/lib/systemd/journal/native.rb +9 -6
- data/lib/systemd/journal/navigable.rb +112 -0
- data/lib/systemd/journal/version.rb +1 -1
- data/lib/systemd/journal/waitable.rb +74 -0
- data/lib/systemd/journal/{compat.rb → writable.rb} +4 -3
- data/lib/systemd/journal_entry.rb +27 -2
- data/spec/no_ffi.rb +1 -1
- data/spec/spec_helper.rb +2 -2
- data/spec/systemd/id128_spec.rb +5 -5
- data/spec/systemd/journal_entry_spec.rb +69 -15
- data/spec/systemd/journal_spec.rb +206 -125
- metadata +7 -4
@@ -1,7 +1,9 @@
|
|
1
|
+
require 'systemd/id128'
|
2
|
+
|
1
3
|
module Systemd
|
2
4
|
class Journal
|
3
5
|
# Provides the FFI bindings to the native `libsystemd-journal` shared
|
4
|
-
#
|
6
|
+
# library.
|
5
7
|
module Native
|
6
8
|
# rubocop:disable LineLength
|
7
9
|
require 'ffi'
|
@@ -31,6 +33,8 @@ module Systemd
|
|
31
33
|
attach_function :sd_journal_get_data, [:pointer, :string, :pointer, :pointer], :int
|
32
34
|
attach_function :sd_journal_restart_data, [:pointer], :void
|
33
35
|
attach_function :sd_journal_enumerate_data, [:pointer, :pointer, :pointer], :int
|
36
|
+
attach_function :sd_journal_get_catalog, [:pointer, :pointer], :int
|
37
|
+
attach_function :sd_journal_get_catalog_for_message_id, [Systemd::Id128::Native::Id128.by_value, :pointer], :int
|
34
38
|
|
35
39
|
attach_function :sd_journal_get_data_threshold, [:pointer, :pointer], :int
|
36
40
|
attach_function :sd_journal_set_data_threshold, [:pointer, :size_t], :int
|
@@ -41,12 +45,11 @@ module Systemd
|
|
41
45
|
attach_function :sd_journal_restart_unique, [:pointer], :void
|
42
46
|
|
43
47
|
# event notification
|
44
|
-
enum
|
45
|
-
:nop,
|
46
|
-
:append,
|
47
|
-
:invalidate
|
48
|
-
]
|
48
|
+
enum :wake_reason, [:nop, :append, :invalidate]
|
49
49
|
attach_function :sd_journal_wait, [:pointer, :uint64], :wake_reason, blocking: true
|
50
|
+
attach_function :sd_journal_get_fd, [:pointer], :int
|
51
|
+
attach_function :sd_journal_process, [:pointer], :wake_reason
|
52
|
+
attach_function :sd_journal_reliable_fd, [:pointer], :int
|
50
53
|
|
51
54
|
# filtering
|
52
55
|
attach_function :sd_journal_add_match, [:pointer, :string, :size_t], :int
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Systemd
|
2
|
+
class Journal
|
3
|
+
module Navigable
|
4
|
+
# returns a string representing the current read position.
|
5
|
+
# This string can be passed to {#seek} or {#cursor?}.
|
6
|
+
# @return [String] a cursor token.
|
7
|
+
def cursor
|
8
|
+
out_ptr = FFI::MemoryPointer.new(:pointer, 1)
|
9
|
+
if (rc = Native.sd_journal_get_cursor(@ptr, out_ptr)) < 0
|
10
|
+
raise JournalError.new(rc)
|
11
|
+
end
|
12
|
+
|
13
|
+
Journal.read_and_free_outstr(out_ptr.read_pointer)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Check if the read position is currently at the entry represented by the
|
17
|
+
# provided cursor value.
|
18
|
+
# @param c [String] a cursor token returned from {#cursor}.
|
19
|
+
# @return [Boolean] True if current entry is the one represented by the
|
20
|
+
# provided cursor, False otherwise.
|
21
|
+
def cursor?(c)
|
22
|
+
if (rc = Native.sd_journal_test_cursor(@ptr, c)) < 0
|
23
|
+
raise JournalError.new(rc)
|
24
|
+
end
|
25
|
+
|
26
|
+
rc > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# Move the read pointer by `offset` entries.
|
30
|
+
# @param [Integer] offset how many entries to move the read pointer by.
|
31
|
+
# If this value is positive, the read pointer moves forward. Otherwise,
|
32
|
+
# it moves backwards.
|
33
|
+
# @return [Integer] number of entries the read pointer actually moved.
|
34
|
+
def move(offset)
|
35
|
+
offset > 0 ? move_next_skip(offset) : move_previous_skip(-offset)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Move the read pointer to the next entry in the journal.
|
39
|
+
# @return [Boolean] True if moving to the next entry was successful.
|
40
|
+
# @return [Boolean] False if unable to move to the next entry, indicating
|
41
|
+
# that the pointer has reached the end of the journal.
|
42
|
+
def move_next
|
43
|
+
rc = Native.sd_journal_next(@ptr)
|
44
|
+
raise JournalError.new(rc) if rc < 0
|
45
|
+
rc > 0
|
46
|
+
end
|
47
|
+
|
48
|
+
# Move the read pointer forward by `amount` entries.
|
49
|
+
# @return [Integer] actual number of entries by which the read pointer
|
50
|
+
# moved. If this number is less than the requested amount, the read
|
51
|
+
# pointer has reached the end of the journal.
|
52
|
+
def move_next_skip(amount)
|
53
|
+
rc = Native.sd_journal_next_skip(@ptr, amount)
|
54
|
+
raise JournalError.new(rc) if rc < 0
|
55
|
+
rc
|
56
|
+
end
|
57
|
+
|
58
|
+
# Move the read pointer to the previous entry in the journal.
|
59
|
+
# @return [Boolean] True if moving to the previous entry was successful.
|
60
|
+
# @return [Boolean] False if unable to move to the previous entry,
|
61
|
+
# indicating that the pointer has reached the beginning of the journal.
|
62
|
+
def move_previous
|
63
|
+
rc = Native.sd_journal_previous(@ptr)
|
64
|
+
raise JournalError.new(rc) if rc < 0
|
65
|
+
rc > 0
|
66
|
+
end
|
67
|
+
|
68
|
+
# Move the read pointer backwards by `amount` entries.
|
69
|
+
# @return [Integer] actual number of entries by which the read pointer
|
70
|
+
# was moved. If this number is less than the requested amount, the
|
71
|
+
# read pointer has reached the beginning of the journal.
|
72
|
+
def move_previous_skip(amount)
|
73
|
+
rc = Native.sd_journal_previous_skip(@ptr, amount)
|
74
|
+
raise JournalError.new(rc) if rc < 0
|
75
|
+
rc
|
76
|
+
end
|
77
|
+
|
78
|
+
# Seek to a position in the journal.
|
79
|
+
# Note: after seeking, you must call {#move_next} or {#move_previous}
|
80
|
+
# before you can call {#read_field} or {#current_entry}.
|
81
|
+
#
|
82
|
+
# @param [Symbol, Time] whence one of :head, :tail, or a Time instance.
|
83
|
+
# `:head` (or `:start`) will seek to the beginning of the journal.
|
84
|
+
# `:tail` (or `:end`) will seek to the end of the journal. When a
|
85
|
+
# `Time` is provided, seek to the journal entry logged closest to that
|
86
|
+
# time. When a String is provided, assume it is a cursor from {#cursor}
|
87
|
+
# and seek to that entry.
|
88
|
+
# @return [True]
|
89
|
+
def seek(where)
|
90
|
+
rc = case
|
91
|
+
when [:head, :start].include?(where)
|
92
|
+
Native.sd_journal_seek_head(@ptr)
|
93
|
+
when [:tail, :end].include?(where)
|
94
|
+
Native.sd_journal_seek_tail(@ptr)
|
95
|
+
when where.is_a?(Time)
|
96
|
+
Native.sd_journal_seek_realtime_usec(
|
97
|
+
@ptr,
|
98
|
+
where.to_i * 1_000_000
|
99
|
+
)
|
100
|
+
when where.is_a?(String)
|
101
|
+
Native.sd_journal_seek_cursor(@ptr, where)
|
102
|
+
else
|
103
|
+
raise ArgumentError.new("Unknown seek type: #{where.class}")
|
104
|
+
end
|
105
|
+
|
106
|
+
raise JournalErrornew(rc) if rc < 0
|
107
|
+
|
108
|
+
true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Systemd
|
2
|
+
class Journal
|
3
|
+
module Waitable
|
4
|
+
# Block until the journal is changed.
|
5
|
+
# @param timeout_usec [Integer] maximum number of microseconds to wait
|
6
|
+
# or `-1` to wait indefinitely.
|
7
|
+
# @example Wait for an event for a maximum of 3 seconds
|
8
|
+
# j = Systemd::Journal.new
|
9
|
+
# j.seek(:tail)
|
10
|
+
# if j.wait(3 * 1_000_000)
|
11
|
+
# # event occurred
|
12
|
+
# end
|
13
|
+
# @return [Nil] the wait time was reached (no events occured).
|
14
|
+
# @return [Symbol] :append new entries were appened to the journal.
|
15
|
+
# @return [Symbol] :invalidate journal files were added/removed/rotated.
|
16
|
+
def wait(timeout_usec = -1, opts = {})
|
17
|
+
if opts[:select]
|
18
|
+
wait_select(timeout_usec)
|
19
|
+
else
|
20
|
+
rc = Native.sd_journal_wait(@ptr, timeout_usec)
|
21
|
+
raise JournalError.new(rc) if rc.is_a?(Fixnum) && rc < 0
|
22
|
+
rc == :nop ? nil : rc
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Determine if calls to {#wait} with `select: true` will reliably wake
|
27
|
+
# when a change occurs. If false, there might be some (unknown) latency
|
28
|
+
# involved between when an change occurs and when {#wait} returns.
|
29
|
+
# @return [Boolean]
|
30
|
+
def wait_select_reliable?
|
31
|
+
Native.sd_journal_reliable_fd(@ptr) > 0
|
32
|
+
end
|
33
|
+
|
34
|
+
# Block and wait for new entries to be appended to the journal. When new
|
35
|
+
# entries are written, yields them in turn. Note that this function does
|
36
|
+
# not automatically seek to the end of the journal prior to waiting.
|
37
|
+
# This method Does not return.
|
38
|
+
# @example Print out events as they happen
|
39
|
+
# j = Systemd::Journal.new
|
40
|
+
# j.seek(:tail)
|
41
|
+
# j.watch do |event|
|
42
|
+
# puts event.message
|
43
|
+
# end
|
44
|
+
def watch
|
45
|
+
loop { (yield current_entry while move_next) if wait }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def wait_select(timeout_usec)
|
51
|
+
timeout_sec = (timeout_usec == -1 ? nil : timeout_usec / 1e6)
|
52
|
+
r, *_ = IO.select([io_object], [], [], timeout_sec)
|
53
|
+
r ? reason_for_wakeup : nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def io_object
|
57
|
+
@io ||= IO.new(file_descriptor, autoclose: false)
|
58
|
+
end
|
59
|
+
|
60
|
+
def file_descriptor
|
61
|
+
fd = Native.sd_journal_get_fd(@ptr)
|
62
|
+
raise JournalError.new(rc) if fd < 0
|
63
|
+
fd
|
64
|
+
end
|
65
|
+
|
66
|
+
def reason_for_wakeup
|
67
|
+
rc = Native.sd_journal_process(@ptr)
|
68
|
+
raise JournalError.new(rc) if rc.is_a?(Fixnum) && rc < 0
|
69
|
+
rc == :nop ? nil : rc
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -3,9 +3,10 @@ require 'systemd/journal_error'
|
|
3
3
|
|
4
4
|
module Systemd
|
5
5
|
class Journal
|
6
|
-
# This module provides
|
7
|
-
#
|
8
|
-
|
6
|
+
# This module provides write access to the systemd Journal, compatible with
|
7
|
+
# the systemd-journal.gem by Daniel Mack
|
8
|
+
# (https://github.com/zonque/systemd-journal.gem).
|
9
|
+
module Writable
|
9
10
|
# system is unusable
|
10
11
|
LOG_EMERG = 0
|
11
12
|
# action must be taken immediately
|
@@ -14,10 +14,14 @@ module Systemd
|
|
14
14
|
@entry = entry
|
15
15
|
@fields = entry.map do |key, value|
|
16
16
|
name = key.downcase.to_sym
|
17
|
-
define_singleton_method(name){ value } unless respond_to?(name)
|
17
|
+
define_singleton_method(name) { value } unless respond_to?(name)
|
18
18
|
name
|
19
19
|
end
|
20
|
+
end
|
20
21
|
|
22
|
+
# not all journal entries will have all fields. don't raise an error.
|
23
|
+
def method_missing(m)
|
24
|
+
nil
|
21
25
|
end
|
22
26
|
|
23
27
|
# Get the value of a given field in the entry, or nil if it doesn't exist
|
@@ -27,7 +31,28 @@ module Systemd
|
|
27
31
|
|
28
32
|
def each
|
29
33
|
return to_enum(:each) unless block_given?
|
30
|
-
@entry.each{ |key, value| yield [key, value] }
|
34
|
+
@entry.each { |key, value| yield [key, value] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def catalog(opts = {})
|
38
|
+
return nil unless catalog?
|
39
|
+
|
40
|
+
opts[:replace] = true unless opts.key?(:replace)
|
41
|
+
|
42
|
+
cat = Systemd::Journal.catalog_for(self[:message_id])
|
43
|
+
# catalog_for does not do field substitution for us, so we do it here
|
44
|
+
# if requested
|
45
|
+
opts[:replace] ? field_substitute(cat) : cat
|
46
|
+
end
|
47
|
+
|
48
|
+
def catalog?
|
49
|
+
!self[:message_id].nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def field_substitute(msg)
|
55
|
+
msg.gsub(/@[A-Z_0-9]+@/) { |field| self[field[1..-2]] || field }
|
31
56
|
end
|
32
57
|
end
|
33
58
|
end
|
data/spec/no_ffi.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -8,7 +8,7 @@ RSpec.configure do |config|
|
|
8
8
|
config.before(:each) do
|
9
9
|
|
10
10
|
# Stub open and close calls
|
11
|
-
dummy_open = ->(ptr, flags, path=nil) do
|
11
|
+
dummy_open = ->(ptr, flags, path = nil) do
|
12
12
|
ptr.write_pointer(nil)
|
13
13
|
0
|
14
14
|
end
|
@@ -19,7 +19,7 @@ RSpec.configure do |config|
|
|
19
19
|
|
20
20
|
# Raise an exception if any native calls are actually called
|
21
21
|
native_calls = Systemd::Journal::Native.methods.select do |m|
|
22
|
-
m.to_s.start_with?(
|
22
|
+
m.to_s.start_with?('sd_')
|
23
23
|
end
|
24
24
|
|
25
25
|
native_calls -= [
|
data/spec/systemd/id128_spec.rb
CHANGED
@@ -9,7 +9,7 @@ describe Systemd::Id128 do
|
|
9
9
|
out.write_array_of_uint8(dummy)
|
10
10
|
0
|
11
11
|
end
|
12
|
-
Systemd::Id128.boot_id.
|
12
|
+
expect(Systemd::Id128.boot_id).to eq('a10c' * 8)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -20,7 +20,7 @@ describe Systemd::Id128 do
|
|
20
20
|
out.write_array_of_uint8(dummy)
|
21
21
|
0
|
22
22
|
end
|
23
|
-
Systemd::Id128.machine_id.
|
23
|
+
expect(Systemd::Id128.machine_id).to eq('a10c' * 8)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -31,9 +31,9 @@ describe Systemd::Id128 do
|
|
31
31
|
out.write_array_of_uint8(dummy)
|
32
32
|
0
|
33
33
|
end
|
34
|
-
Systemd::Id128.random.
|
35
|
-
end
|
34
|
+
expect(Systemd::Id128.random).to eq('a10c' * 8)
|
35
|
+
end
|
36
36
|
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
end
|
@@ -3,41 +3,95 @@ require 'spec_helper'
|
|
3
3
|
describe Systemd::JournalEntry do
|
4
4
|
subject do
|
5
5
|
Systemd::JournalEntry.new(
|
6
|
-
'_PID'
|
7
|
-
'_EXE'
|
8
|
-
'PRIORITY'
|
9
|
-
'OBJECT_ID'=> ':)'
|
6
|
+
'_PID' => '125',
|
7
|
+
'_EXE' => '/usr/bin/sshd',
|
8
|
+
'PRIORITY' => '4',
|
9
|
+
'OBJECT_ID' => ':)'
|
10
10
|
)
|
11
11
|
end
|
12
12
|
|
13
13
|
it 'allows enumerating entries' do
|
14
|
-
expect{ |b| subject.each(&b) }.to yield_successive_args(
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
expect { |b| subject.each(&b) }.to yield_successive_args(
|
15
|
+
%w{_PID 125},
|
16
|
+
%w{_EXE /usr/bin/sshd},
|
17
|
+
%w{PRIORITY 4},
|
18
|
+
%w{OBJECT_ID :)}
|
19
19
|
)
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'responds to field names as methods' do
|
23
|
-
subject._pid.
|
24
|
-
subject.priority.
|
23
|
+
expect(subject._pid).to eq('125')
|
24
|
+
expect(subject.priority).to eq('4')
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'doesnt overwrite existing methods' do
|
28
|
-
subject.object_id.
|
28
|
+
expect(subject.object_id).to_not eq(':)')
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'allows accessing via [string]' do
|
32
|
-
subject['OBJECT_ID'].
|
32
|
+
expect(subject['OBJECT_ID']).to eq(':)')
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'allows accessing via [symbol]' do
|
36
|
-
subject[:object_id].
|
36
|
+
expect(subject[:object_id]).to eq(':)')
|
37
37
|
end
|
38
38
|
|
39
39
|
it 'lists all fields it contains' do
|
40
|
-
subject.fields.
|
40
|
+
expect(subject.fields).to eq([:_pid, :_exe, :priority, :object_id])
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should not have a catalog' do
|
44
|
+
expect(subject.catalog?).to be_false
|
45
|
+
end
|
46
|
+
|
47
|
+
it "doesn't throw NoMethod errors" do
|
48
|
+
expect { subject.froobaz }.not_to raise_error
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with catalogs' do
|
52
|
+
subject do
|
53
|
+
Systemd::JournalEntry.new(
|
54
|
+
'_PID' => '125',
|
55
|
+
'_EXE' => '/usr/bin/sshd',
|
56
|
+
'PRIORITY' => '4',
|
57
|
+
'MESSAGE_ID' => 'ab1fced28a0'
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#catalog?' do
|
62
|
+
it 'returns true if the entry has a message ID' do
|
63
|
+
expect(subject.catalog?).to be_true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#catalog' do
|
68
|
+
it 'asks the journal for the message with our ID' do
|
69
|
+
Systemd::Journal
|
70
|
+
.should_receive(:catalog_for)
|
71
|
+
.with('ab1fced28a0')
|
72
|
+
.and_return('catalog')
|
73
|
+
|
74
|
+
expect(subject.catalog).to eq('catalog')
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'does field replacement by default' do
|
78
|
+
Systemd::Journal
|
79
|
+
.should_receive(:catalog_for)
|
80
|
+
.with('ab1fced28a0')
|
81
|
+
.and_return('catalog @_PID@ @PRIORITY@')
|
82
|
+
|
83
|
+
expect(subject.catalog).to eq('catalog 125 4')
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'skips field replacement if requested' do
|
87
|
+
Systemd::Journal
|
88
|
+
.should_receive(:catalog_for)
|
89
|
+
.with('ab1fced28a0')
|
90
|
+
.and_return('cat @_PID@ @PRIORITY@')
|
91
|
+
|
92
|
+
expect(subject.catalog(replace: false)).to eq('cat @_PID@ @PRIORITY@')
|
93
|
+
end
|
94
|
+
end
|
41
95
|
end
|
42
96
|
|
43
97
|
end
|