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.
@@ -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
- # library.
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 :wake_reason, [
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
@@ -1,6 +1,6 @@
1
1
  module Systemd
2
2
  class Journal
3
3
  # The version of the systemd-journal gem.
4
- VERSION = '1.0.2'
4
+ VERSION = '1.1.0'
5
5
  end
6
6
  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 compatibility with the systemd-journal.gem
7
- # by Daniel Mack (https://github.com/zonque/systemd-journal.gem)
8
- module Compat
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
@@ -1,4 +1,4 @@
1
1
  $NO_FFI_SPEC = true
2
2
  module Systemd::Journal::Native ; end
3
3
 
4
- puts "Warning: running specs without libsystemd-journal and libsystemd-id128"
4
+ puts 'Warning: running specs without libsystemd-journal and libsystemd-id128'
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?("sd_")
22
+ m.to_s.start_with?('sd_')
23
23
  end
24
24
 
25
25
  native_calls -= [
@@ -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.should eq("a10c" * 8)
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.should eq("a10c" * 8)
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.should eq("a10c" * 8)
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' => '125',
7
- '_EXE' => '/usr/bin/sshd',
8
- 'PRIORITY' => '4',
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
- ['_PID', '125'],
16
- ['_EXE', '/usr/bin/sshd'],
17
- ['PRIORITY', '4'],
18
- ['OBJECT_ID', ':)']
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.should eq('125')
24
- subject.priority.should eq('4')
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.should_not eq(':)')
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'].should eq(':)')
32
+ expect(subject['OBJECT_ID']).to eq(':)')
33
33
  end
34
34
 
35
35
  it 'allows accessing via [symbol]' do
36
- subject[:object_id].should eq(':)')
36
+ expect(subject[:object_id]).to eq(':)')
37
37
  end
38
38
 
39
39
  it 'lists all fields it contains' do
40
- subject.fields.should eq([:_pid, :_exe, :priority, :object_id])
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