fluent-plugin-systemd 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 07f12e01a1d81c731f44477a051807042230adfa
4
- data.tar.gz: 8717ea703cd39fb947af86b5773a68de5b55c040
3
+ metadata.gz: 14abb1b9c6b985bbaee61aae73791108578c738e
4
+ data.tar.gz: ab54b3167ad1832efd73d15eff17d0b4ed2d6e0a
5
5
  SHA512:
6
- metadata.gz: 92b3275665979af1a9d691f05acdc65a88fd88b062c6224673cde0c691c9c7a4376a1c97c96c7e1dc9cc54f224fe27e7c39600bc0b6307c88ad182e442971520
7
- data.tar.gz: 110c2d6ff7ebeac0eb96ceaf0d470d5bdfdf5da2e4556f8b2fc98b213e08b87553d231073f2828a5050dc88437f307e2846cea0ebfd39c131465a34b83123e5e
6
+ metadata.gz: 26dca0c3b93b49899fbb903ccfc66f79d9f2ded5416ce1bdd28b920b83583a4de29c812a426aabe9aad4b5718055b532f2fcaa6c2b15d77e4ea8bcb46fcebe23
7
+ data.tar.gz: a715f762f61481b93c1179af10c30f978cbafad7c01ffdd136a79e6d21757d5ce449f59640887d875fc74327c1c1c51d2d09a08eee1a0f1760054ecfc111c9e7
data/README.md CHANGED
@@ -1,45 +1,57 @@
1
- # systemd input plugin for [Fluentd](http://github.com/fluent/fluentd)
1
+ # systemd plugin for [Fluentd](http://github.com/fluent/fluentd)
2
2
 
3
3
  [![Build Status](https://travis-ci.org/reevoo/fluent-plugin-systemd.svg?branch=master)](https://travis-ci.org/reevoo/fluent-plugin-systemd) [![Code Climate GPA](https://codeclimate.com/github/reevoo/fluent-plugin-systemd/badges/gpa.svg)](https://codeclimate.com/github/reevoo/fluent-plugin-systemd) [![Gem Version](https://badge.fury.io/rb/fluent-plugin-systemd.svg)](https://rubygems.org/gems/fluent-plugin-systemd)
4
4
 
5
- # Requirements <a name="requirements"></a>
5
+ ## Overview
6
+
7
+ **systemd** input plugin reads logs from the systemd journal
8
+ **systemd** filter plugin allows for basic manipulation of systemd journal entries
9
+
10
+ ## Support
11
+
12
+ [![Fluentd Slack](http://slack.fluentd.org/badge.svg)](http://slack.fluentd.org/)
13
+
14
+ Join the #plugin-systemd channel on the [Fluentd Slack](http://slack.fluentd.org/)
6
15
 
7
16
 
17
+ ## Requirements
18
+
8
19
  |fluent-plugin-systemd|fluentd|td-agent|ruby|
9
20
  |----|----|----|----|
10
- | 0.2.x | >= 0.14.11, < 2 | 3 | >= 2.1 |
21
+ | > 0.1.0 | >= 0.14.11, < 2 | 3 | >= 2.1 |
11
22
  | 0.0.x | ~> 0.12.0 | 2 | >= 1.9 |
12
23
 
13
- * The 0.1.x series is developed from this branch (master)
24
+ * The 0.x.x series is developed from this branch (master)
14
25
  * The 0.0.x series (compatible with fluentd v0.12, and td-agent 2) is developed on the [0.0.x branch](https://github.com/reevoo/fluent-plugin-systemd/tree/0.0.x)
15
26
 
16
- ## Overview
17
-
18
- **systemd** input plugin reads logs from the systemd journal
19
-
20
27
  ## Installation
21
28
 
22
29
  Simply use RubyGems:
23
30
 
24
- gem install fluent-plugin-systemd -v 0.2.0
31
+ gem install fluent-plugin-systemd -v 0.3.0
25
32
 
26
33
  or
27
34
 
28
- td-agent-gem install fluent-plugin-systemd -v 0.2.0
35
+ td-agent-gem install fluent-plugin-systemd -v 0.3.0
29
36
 
30
- ## Configuration
37
+ ## Input Plugin Configuration
31
38
 
32
39
  <source>
33
40
  @type systemd
41
+ tag kube-proxy
34
42
  path /var/log/journal
35
43
  filters [{ "_SYSTEMD_UNIT": "kube-proxy.service" }]
44
+ read_from_head true
36
45
  <storage>
37
46
  @type local
38
47
  persistent true
39
48
  path kube-proxy.pos
40
49
  </storage>
41
- tag kube-proxy
42
- read_from_head true
50
+ <entry>
51
+ field_map {"MESSAGE": "log", "_PID": ["process", "pid"], "_CMDLINE": "process", "_COMM": "cmd"}
52
+ fields_strip_underscores true
53
+ fields_lowercase true
54
+ </entry>
43
55
  </source>
44
56
 
45
57
  <match kube-proxy>
@@ -78,19 +90,83 @@ If true reads all available journal from head, otherwise starts reading from tai
78
90
 
79
91
  **`strip_underscores`**
80
92
 
93
+ _This parameter is deprecated and will be removed in favour of entry in v1.0._
94
+
81
95
  If true strips underscores from the beginning of systemd field names.
82
96
  May be useful if outputting to kibana, as underscore prefixed fields are unindexed there.
83
97
 
98
+ **`entry`**
99
+
100
+ Optional configuration for an embeded systemd entry filter. See the [Filter Plugin Configuration](#filter-plugin-configuration) for config reference.
101
+
84
102
  **`tag`**
85
103
 
86
- _Required_
104
+ _Required_
87
105
 
88
106
  A tag that will be added to events generated by this input.
89
107
 
90
- ## Example
108
+ ### Example
91
109
 
92
110
  For an example of a full working setup including the plugin, [take a look at](https://github.com/assemblyline/fluentd)
93
111
 
112
+ ## Filter Plugin Configuration
113
+
114
+ <filter kube-proxy>
115
+ @type systemd_entry
116
+ field_map {"MESSAGE": "log", "_PID": ["process", "pid"], "_CMDLINE": "process", "_COMM": "cmd"}
117
+ field_map_strict false
118
+ fields_lowercase true
119
+ fields_strip_underscores true
120
+ </filter>
121
+
122
+ **`field_map`**
123
+
124
+ Object / hash defining a mapping of source fields to destination fields. Destination fields may be existing or new user-defined fields. If multiple source fields are mapped to the same destination field, the contents of the fields will be appended to the destination field in the order defined in the mapping. A field map declaration takes the form of:
125
+
126
+ {
127
+ "<src_field1>": "<dst_field1>",
128
+ "<src_field2>": ["<dst_field1>", "<dst_field2>"],
129
+ ...
130
+ }
131
+ Defaults to an empty map.
132
+
133
+ **`field_map_strict`**
134
+
135
+ If true, only destination fields from `field_map` are included in the result. Defaults to false.
136
+
137
+ **`fields_lowercase`**
138
+
139
+ If true, lowercase all non-mapped fields. Defaults to false.
140
+
141
+ **`fields_strip_underscores`**
142
+
143
+ If true, strip leading underscores from all non-mapped fields. Defaults to false.
144
+
145
+ ### Example
146
+
147
+ Given a systemd journal source entry:
148
+ ```
149
+ {
150
+ "_MACHINE_ID": "bb9d0a52a41243829ecd729b40ac0bce"
151
+ "_HOSTNAME": "arch"
152
+ "MESSAGE": "this is a log message",
153
+ "_PID": "123"
154
+ "_CMDLINE": "login -- root"
155
+ "_COMM": "login"
156
+ }
157
+ ```
158
+ The resulting entry using the above sample configuration:
159
+ ```
160
+ {
161
+ "machine_id": "bb9d0a52a41243829ecd729b40ac0bce"
162
+ "hostname": "arch",
163
+ "msg": "this is a log message",
164
+ "pid": "123"
165
+ "cmd": "login"
166
+ "process": "123 login -- root"
167
+ }
168
+ ```
169
+
94
170
  ## Dependencies
95
171
 
96
172
  This plugin depends on libsystemd
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require "fluent/plugin/filter"
3
+ require "fluent/plugin/systemd/entry_mutator"
4
+
5
+ module Fluent
6
+ module Plugin
7
+ # Fluentd systemd/journal filter plugin
8
+ class SystemdEntryFilter < Filter
9
+ Fluent::Plugin.register_filter("systemd_entry", self)
10
+
11
+ config_param :field_map, :hash, default: {}
12
+ config_param :field_map_strict, :bool, default: false
13
+ config_param :fields_strip_underscores, :bool, default: false
14
+ config_param :fields_lowercase, :bool, default: false
15
+
16
+ def configure(conf)
17
+ super
18
+ @mutator = SystemdEntryMutator.new(**@config_root_section.to_h)
19
+ if @mutator.field_map_strict && @mutator.field_map.empty?
20
+ log.warn("`field_map_strict` set to true with empty `field_map`, expect no fields")
21
+ end
22
+ end
23
+
24
+ def filter(_tag, _time, entry)
25
+ @mutator.run(entry)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  require "systemd/journal"
2
3
  require "fluent/plugin/input"
3
4
  require "fluent/plugin/systemd/pos_writer"
5
+ require "fluent/plugin/systemd/entry_mutator"
4
6
 
5
7
  module Fluent
6
8
  module Plugin
@@ -9,13 +11,14 @@ module Fluent
9
11
 
10
12
  helpers :timer, :storage
11
13
 
12
- DEFAULT_STORAGE_TYPE = "local".freeze
14
+ DEFAULT_STORAGE_TYPE = "local"
13
15
 
14
16
  config_param :path, :string, default: "/var/log/journal"
15
17
  config_param :filters, :array, default: []
16
18
  config_param :pos_file, :string, default: nil, deprecated: "Use <storage> section with `persistent: true' instead"
17
19
  config_param :read_from_head, :bool, default: false
18
- config_param :strip_underscores, :bool, default: false
20
+ config_param :strip_underscores, :bool, default: false, deprecated: "Use <entry> section or `systemd_entry` " \
21
+ "filter plugin instead"
19
22
  config_param :tag, :string
20
23
 
21
24
  config_section :storage do
@@ -24,10 +27,24 @@ module Fluent
24
27
  config_set_default :persistent, false
25
28
  end
26
29
 
30
+ config_section :entry, param_name: "entry_opts", required: false, multi: false do
31
+ config_param :field_map, :hash, default: {}
32
+ config_param :field_map_strict, :bool, default: false
33
+ config_param :fields_strip_underscores, :bool, default: false
34
+ config_param :fields_lowercase, :bool, default: false
35
+ end
36
+
27
37
  def configure(conf)
28
38
  super
39
+ @journal = nil
29
40
  @pos_storage = PosWriter.new(@pos_file, storage_create(usage: "positions"))
30
- @journal = nil
41
+ # legacy strip_underscores backwards compatibility (legacy takes
42
+ # precedence and is mutually exclusive with the entry block)
43
+ mut_opts = @strip_underscores ? { fields_strip_underscores: true } : @entry_opts.to_h
44
+ @mutator = SystemdEntryMutator.new(**mut_opts)
45
+ if @mutator.field_map_strict && @mutator.field_map.empty?
46
+ log.warn("`field_map_strict` set to true with empty `field_map`, expect no fields")
47
+ end
31
48
  end
32
49
 
33
50
  def start
@@ -89,17 +106,24 @@ module Fluent
89
106
  return unless @journal || init_journal
90
107
  init_journal if @journal.wait(0) == :invalidate
91
108
  watch do |entry|
92
- begin
93
- router.emit(@tag, Fluent::EventTime.from_time(entry.realtime_timestamp), formatted(entry))
94
- rescue => e
95
- log.error("Exception emitting record: #{e}")
96
- end
109
+ emit(entry)
97
110
  end
98
111
  end
99
112
 
113
+ def emit(entry)
114
+ router.emit(@tag, Fluent::EventTime.from_time(entry.realtime_timestamp), formatted(entry))
115
+ rescue Fluent::Plugin::Buffer::BufferOverflowError => e
116
+ retries ||= 0
117
+ raise e if retries > 10
118
+ retries += 1
119
+ sleep 1.5**retries + rand(0..3)
120
+ retry
121
+ rescue => e
122
+ log.error("Exception emitting record: #{e}")
123
+ end
124
+
100
125
  def formatted(entry)
101
- return entry.to_h unless @strip_underscores
102
- Hash[entry.to_h.map { |k, v| [k.gsub(/\A_+/, ""), v] }]
126
+ @mutator.run(entry)
103
127
  end
104
128
 
105
129
  def watch
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+ require "fluent/config/error"
3
+
4
+ module Fluent
5
+ module Plugin
6
+ # A simple stand-alone configurable mutator for systemd journal entries.
7
+ #
8
+ # Note regarding field mapping:
9
+ # The input `field_map` option is meant to have a structure that is
10
+ # intuative or logical for humans when declaring a field map.
11
+ # {
12
+ # "<source_field1>" => "<new_field1>",
13
+ # "<source_field2>" => ["<new_field1>", "<new_field2>"]
14
+ # }
15
+ # Internally the inverse of the human-friendly field_map is
16
+ # computed (and cached) upon object creation and used as a "mapped model"
17
+ # {
18
+ # "<new_field1>" => ["<source_field1>", "<source_field2>"],
19
+ # "<new_field2>" => ["<source_field2>"]
20
+ # }
21
+ class SystemdEntryMutator
22
+
23
+ Options = Struct.new(
24
+ :field_map,
25
+ :field_map_strict,
26
+ :fields_lowercase,
27
+ :fields_strip_underscores,
28
+ )
29
+
30
+ def self.default_opts
31
+ Options.new({}, false, false, false)
32
+ end
33
+
34
+ # Constructor keyword options (all other kwargs are ignored):
35
+ # field_map - hash describing the desired field mapping in the form:
36
+ # {"<source_field>" => "<new_field>", ...}
37
+ # where `new_field` is a string or array of strings
38
+ # field_map_strict - boolean if true will only include new fields
39
+ # defined in `field_map`
40
+ # fields_strip_underscores - boolean if true will strip all leading
41
+ # underscores from non-mapped fields
42
+ # fields_lowercase - boolean if true lowercase all non-mapped fields
43
+ #
44
+ # raises `Fluent::ConfigError` for invalid options
45
+ def initialize(**options)
46
+ @opts = options_from_hash(options)
47
+ validate_options(@opts)
48
+ @map = invert_field_map(@opts.field_map)
49
+ @map_src_fields = @opts.field_map.keys
50
+ @no_transform = @opts == self.class.default_opts
51
+ end
52
+
53
+ # Expose config state as read-only instance properties of the mutator.
54
+ def method_missing(sym, *args)
55
+ return @opts[sym] if @opts.members.include?(sym)
56
+ super
57
+ end
58
+
59
+ # The main run method that performs all configured mutations, if any,
60
+ # against a single journal entry. Returns the mutated entry hash.
61
+ # entry - hash or `Systemd::Journal:Entry`
62
+ def run(entry)
63
+ return entry.to_h if @no_transform
64
+ return map_fields(entry) if @opts.field_map_strict
65
+ format_fields(entry, map_fields(entry))
66
+ end
67
+
68
+ # Run field mapping against a single journal entry. Returns the mutated
69
+ # entry hash.
70
+ # entry - hash or `Systemd::Journal:Entry`
71
+ def map_fields(entry)
72
+ mapped = {}
73
+ @map.each do |cstm, sysds|
74
+ vals = sysds.collect { |fld| entry[fld] }.compact
75
+ next if vals.empty? # systemd field does not exist in source entry
76
+ mapped[cstm] = vals.length == 1 ? vals[0] : vals.join(" ")
77
+ end
78
+ mapped
79
+ end
80
+
81
+ # Run field formatting (mutations applied to all non-mapped fields)
82
+ # against a single journal entry. Returns the mutated entry hash.
83
+ # entry - hash or `Systemd::Journal:Entry`
84
+ # mapped - Optional hash that represents a previously mapped entry to
85
+ # which the formatted fields will be added
86
+ def format_fields(entry, mapped = nil)
87
+ mapped ||= {}
88
+ entry.each do |fld, val|
89
+ # don't mess with explicitly mapped fields
90
+ next if @map_src_fields.include?(fld)
91
+ fld = fld.gsub(/\A_+/, "") if @opts.fields_strip_underscores
92
+ fld = fld.downcase if @opts.fields_lowercase
93
+ # account for mapping (appending) to an existing systemd field
94
+ mapped[fld] = mapped.key?(fld) ? [val, mapped[fld]].join(" ") : val
95
+ end
96
+ mapped
97
+ end
98
+
99
+ private
100
+
101
+ # Returns a `SystemdEntryMutator::Options` struct derived from the
102
+ # elements in the supplied hash merged with the option defaults
103
+ def options_from_hash(opts)
104
+ merged = self.class.default_opts
105
+ merged.each_pair do |k, _|
106
+ merged[k] = opts[k] if opts.key?(k)
107
+ end
108
+ merged
109
+ end
110
+
111
+ def validate_options(opts)
112
+ unless validate_strings_or_empty(opts[:field_map].keys)
113
+ err = "`field_map` keys must be strings"
114
+ end
115
+ unless validate_strings_or_empty(opts[:field_map].values, true)
116
+ err = "`field_map` values must be strings or array of strings"
117
+ end
118
+ %i[field_map_strict fields_strip_underscores fields_lowercase].each do |opt|
119
+ err = "`#{opt}` must be boolean" unless [true, false].include?(opts[opt])
120
+ end
121
+ fail Fluent::ConfigError, err unless err.nil?
122
+ end
123
+
124
+ # Validates that values in array `arr` are strings. If `nested` is true
125
+ # also allow and validate that `arr` values can be an array of strings
126
+ def validate_strings_or_empty(arr, nested = false)
127
+ return true if arr.empty?
128
+ arr.each do |v|
129
+ return true if v.is_a?(String)
130
+ if v.is_a?(Array) && nested
131
+ v.each { |nstd| return false unless nstd.is_a?(String) }
132
+ end
133
+ end
134
+ false
135
+ end
136
+
137
+ # Compute the inverse of a human friendly field map `fm` which is what
138
+ # the mutator uses for the actual mapping. The resulting structure for
139
+ # the inverse field map hash is:
140
+ # {"<new_field_name>" => ["<source_field_name>", ...], ...}
141
+ def invert_field_map(fm)
142
+ invs = {}
143
+ fm.values.flatten.uniq.each do |cstm|
144
+ sysds = fm.select { |_, v| (v == cstm || v.include?(cstm)) }
145
+ invs[cstm] = sysds.keys
146
+ end
147
+ invs
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "fluent/plugin/input"
2
3
 
3
4
  module Fluent
@@ -66,7 +67,7 @@ module Fluent
66
67
  def write_pos
67
68
  @lock.synchronize do
68
69
  if @written_cursor != @cursor
69
- file = File.open(@path, "w+", 0644)
70
+ file = File.open(@path, "w+", 0o644)
70
71
  file.print @cursor
71
72
  file.close
72
73
  @written_cursor = @cursor
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-systemd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ed Robinson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-20 00:00:00.000000000 Z
11
+ date: 2017-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -109,7 +109,9 @@ extra_rdoc_files: []
109
109
  files:
110
110
  - LICENCE
111
111
  - README.md
112
+ - lib/fluent/plugin/filter_systemd_entry.rb
112
113
  - lib/fluent/plugin/in_systemd.rb
114
+ - lib/fluent/plugin/systemd/entry_mutator.rb
113
115
  - lib/fluent/plugin/systemd/pos_writer.rb
114
116
  homepage: https://github.com/reevoo/fluent-plugin-systemd
115
117
  licenses:
@@ -131,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
133
  version: '0'
132
134
  requirements: []
133
135
  rubyforge_project:
134
- rubygems_version: 2.6.9
136
+ rubygems_version: 2.6.11
135
137
  signing_key:
136
138
  specification_version: 4
137
139
  summary: Input plugin to read from systemd journal.