fluent-plugin-windows-eventlog 0.1.0 → 0.4.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,320 +1,234 @@
1
- require 'win32/eventlog'
2
- require 'fluent/plugin/input'
3
- require 'fluent/plugin'
4
-
5
- module Fluent::Plugin
6
- class WindowsEventLogInput < Input
7
- Fluent::Plugin.register_input('windows_eventlog', self)
8
-
9
- helpers :timer
10
-
11
- KEY_MAP = {"record_number" => :record_number,
12
- "time_generated" => :time_generated,
13
- "time_written" => :time_written,
14
- "event_id" => :event_id,
15
- "event_type" => :event_type,
16
- "event_category" => :category,
17
- "source_name" => :source,
18
- "computer_name" => :computer,
19
- "user" => :user,
20
- "description" => :description}
21
-
22
- config_param :tag, :string
23
- config_param :read_interval, :time, default: 2
24
- config_param :pos_file, :string, default: nil
25
- config_param :channels, :array, default: ['Application']
26
- config_param :keys, :string, default: []
27
- config_param :read_from_head, :bool, default: false
28
- config_param :from_encoding, :string, default: nil
29
- config_param :encoding, :string, default: nil
30
-
31
- attr_reader :chs
32
-
33
- def initialize
34
- super
35
- @chs = []
36
- @keynames = []
37
- @tails = {}
38
- end
39
-
40
- def configure(conf)
41
- super
42
- @chs = @channels.map {|ch| ch.strip.downcase }.uniq
43
- if @chs.empty?
44
- raise Fluent::ConfigError, "windows_eventlog: 'channels' parameter is required on windows_eventlog input"
45
- end
46
- @keynames = @keys.map {|k| k.strip }.uniq
47
- if @keynames.empty?
48
- @keynames = KEY_MAP.keys
49
- end
50
- @tag = tag
51
- @stop = false
52
- configure_encoding
53
- @receive_handlers = if @encoding
54
- method(:encode_record)
55
- else
56
- method(:no_encode_record)
57
- end
58
- end
59
-
60
- def configure_encoding
61
- unless @encoding
62
- if @from_encoding
63
- raise Fluent::ConfigError, "windows_eventlog: 'from_encoding' parameter must be specied with 'encoding' parameter."
64
- end
65
- end
66
-
67
- @encoding = parse_encoding_param(@encoding) if @encoding
68
- @from_encoding = parse_encoding_param(@from_encoding) if @from_encoding
69
- end
70
-
71
- def parse_encoding_param(encoding_name)
72
- begin
73
- Encoding.find(encoding_name) if encoding_name
74
- rescue ArgumentError => e
75
- raise Fluent::ConfigError, e.message
76
- end
77
- end
78
-
79
- def encode_record(record)
80
- if @encoding
81
- if @from_encoding
82
- record.encode!(@encoding, @from_encoding)
83
- else
84
- record.force_encoding(@encoding)
85
- end
86
- end
87
- end
88
-
89
- def no_encode_record(record)
90
- record
91
- end
92
-
93
- def start
94
- super
95
- if @pos_file
96
- @pf_file = File.open(@pos_file, File::RDWR|File::CREAT|File::BINARY)
97
- @pf_file.sync = true
98
- @pf = PositionFile.parse(@pf_file)
99
- end
100
- start_watchers(@chs)
101
- end
102
-
103
- def shutdown
104
- stop_watchers(@tails.keys, true)
105
- @pf_file.close if @pf_file
106
- super
107
- end
108
-
109
- def setup_wacther(ch, pe)
110
- wlw = WindowsLogWatcher.new(ch, pe, &method(:receive_lines))
111
- wlw.attach do |watcher|
112
- wlw.timer_trigger = timer_execute(:in_winevtlog, @read_interval, &watcher.method(:on_notify))
113
- end
114
- wlw
115
- end
116
-
117
- def start_watchers(chs)
118
- chs.each { |ch|
119
- pe = nil
120
- if @pf
121
- pe = @pf[ch]
122
- if @read_from_head && pe.read_num.zero?
123
- el = Win32::EventLog.open(ch)
124
- pe.update(el.oldest_record_number-1,1)
125
- el.close
126
- end
127
- end
128
- @tails[ch] = setup_wacther(ch, pe)
129
- }
130
- end
131
-
132
- def stop_watchers(chs, unwatched = false)
133
- chs.each { |ch|
134
- wlw = @tails.delete(ch)
135
- if wlw
136
- wlw.unwatched = unwatched
137
- close_watcher(wlw)
138
- end
139
- }
140
- end
141
-
142
- def close_watcher(wlw)
143
- wlw.close
144
- # flush_buffer(wlw)
145
- end
146
-
147
- def receive_lines(ch, lines, pe)
148
- return if lines.empty?
149
- begin
150
- for r in lines
151
- h = {"channel" => ch}
152
- @keynames.each {|k| h[k]=@receive_handlers.call(r.send(KEY_MAP[k]).to_s)}
153
- #h = Hash[@keynames.map {|k| [k, r.send(KEY_MAP[k]).to_s]}]
154
- router.emit(@tag, Fluent::Engine.now, h)
155
- pe[1] +=1
156
- end
157
- rescue
158
- $log.error "unexpected error", error: $!.to_s
159
- $log.error_backtrace
160
- end
161
- end
162
-
163
-
164
- class WindowsLogWatcher
165
- def initialize(ch, pe, &receive_lines)
166
- @ch = ch
167
- @pe = pe || MemoryPositionEntry.new
168
- @receive_lines = receive_lines
169
- @timer_trigger = nil
170
- end
171
-
172
- attr_reader :ch
173
- attr_accessor :unwatched
174
- attr_accessor :pe
175
- attr_accessor :timer_trigger
176
-
177
- def attach
178
- yield self
179
- on_notify
180
- end
181
-
182
- def detach
183
- @timer_trigger.detach if @timer_trigger.attached?
184
- end
185
-
186
- def close
187
- detach
188
- end
189
-
190
- def on_notify
191
- el = Win32::EventLog.open(@ch)
192
- rl_sn = [el.oldest_record_number, el.total_records]
193
- pe_sn = [@pe.read_start, @pe.read_num]
194
- # if total_records is zero, oldest_record_number has no meaning.
195
- if rl_sn[1] == 0
196
- return
197
- end
198
-
199
- if pe_sn[0] == 0 && pe_sn[1] == 0
200
- @pe.update(rl_sn[0], rl_sn[1])
201
- return
202
- end
203
-
204
- cur_end = rl_sn[0] + rl_sn[1] -1
205
- old_end = pe_sn[0] + pe_sn[1] -1
206
-
207
- if (rl_sn[0] < pe_sn[0])
208
- # may be a record number rotated.
209
- cur_end += 0xFFFFFFFF
210
- end
211
-
212
- if (cur_end < old_end)
213
- # something occured.
214
- @pe.update(rl_sn[0], rl_sn[1])
215
- return
216
- end
217
-
218
- read_more = false
219
- begin
220
- numlines = cur_end - old_end
221
-
222
- winlogs = el.read(Win32::EventLog::SEEK_READ | Win32::EventLog::FORWARDS_READ, old_end + 1)
223
- @receive_lines.call(@ch, winlogs, pe_sn)
224
-
225
- @pe.update(pe_sn[0], pe_sn[1])
226
- old_end = pe_sn[0] + pe_sn[1] -1
227
- end while read_more
228
- el.close
229
- end
230
- end
231
-
232
- class PositionFile
233
- def initialize(file, map, last_pos)
234
- @file = file
235
- @map = map
236
- @last_pos = last_pos
237
- end
238
-
239
- def [](ch)
240
- if m = @map[ch]
241
- return m
242
- end
243
- @file.pos = @last_pos
244
- @file.write ch
245
- @file.write "\t"
246
- seek = @file.pos
247
- @file.write "00000000\t00000000\n"
248
- @last_pos = @file.pos
249
- @map[ch] = FilePositionEntry.new(@file, seek)
250
- end
251
-
252
- # parsing file and rebuild mysself
253
- def self.parse(file)
254
- map = {}
255
- file.pos = 0
256
- file.each_line {|line|
257
- # check and get a matched line as m
258
- m = /^([^\t]+)\t([0-9a-fA-F]+)\t([0-9a-fA-F]+)/.match(line)
259
- next unless m
260
- ch = m[1]
261
- pos = m[2].to_i(16)
262
- seek = file.pos - line.bytesize + ch.bytesize + 1
263
- map[ch] = FilePositionEntry.new(file, seek)
264
- }
265
- new(file, map, file.pos)
266
- end
267
- end
268
-
269
- class FilePositionEntry
270
- START_SIZE = 8
271
- NUM_OFFSET = 9
272
- NUM_SIZE = 8
273
- LN_OFFSET = 17
274
- SIZE = 18
275
-
276
- def initialize(file, seek)
277
- @file = file
278
- @seek = seek
279
- end
280
-
281
- def update(start, num)
282
- @file.pos = @seek
283
- @file.write "%08x\t%08x" % [start, num]
284
- end
285
-
286
- def read_start
287
- @file.pos = @seek
288
- raw = @file.read(START_SIZE)
289
- raw ? raw.to_i(16) : 0
290
- end
291
-
292
- def read_num
293
- @file.pos = @seek + NUM_OFFSET
294
- raw = @file.read(NUM_SIZE)
295
- raw ? raw.to_i(16) : 0
296
- end
297
- end
298
-
299
- class MemoryPositionEntry
300
- def initialize
301
- @start = 0
302
- @num = 0
303
- end
304
-
305
- def update(start, num)
306
- @start = start
307
- @num = num
308
- end
309
-
310
- def read_start
311
- @start
312
- end
313
-
314
- def read_num
315
- @num
316
- end
317
- end
318
-
319
- end
320
- end
1
+ require 'win32/eventlog'
2
+ require 'fluent/plugin/input'
3
+ require 'fluent/plugin'
4
+
5
+ module Fluent::Plugin
6
+ class WindowsEventLogInput < Input
7
+ Fluent::Plugin.register_input('windows_eventlog', self)
8
+
9
+ helpers :timer, :storage
10
+
11
+ DEFAULT_STORAGE_TYPE = 'local'
12
+ KEY_MAP = {"record_number" => [:record_number, :string],
13
+ "time_generated" => [:time_generated, :string],
14
+ "time_written" => [:time_written, :string],
15
+ "event_id" => [:event_id, :string],
16
+ "event_type" => [:event_type, :string],
17
+ "event_category" => [:category, :string],
18
+ "source_name" => [:source, :string],
19
+ "computer_name" => [:computer, :string],
20
+ "user" => [:user, :string],
21
+ "description" => [:description, :string],
22
+ "string_inserts" => [:string_inserts, :array]}
23
+
24
+ config_param :tag, :string
25
+ config_param :read_interval, :time, default: 2
26
+ config_param :pos_file, :string, default: nil,
27
+ obsoleted: "This section is not used anymore. Use 'store_pos' instead."
28
+ config_param :channels, :array, default: ['application']
29
+ config_param :keys, :array, default: []
30
+ config_param :read_from_head, :bool, default: false
31
+ config_param :from_encoding, :string, default: nil
32
+ config_param :encoding, :string, default: nil
33
+ desc "Parse 'description' field and set parsed result into event record. 'description' and 'string_inserts' fields are removed from the record"
34
+ config_param :parse_description, :bool, default: false
35
+
36
+ config_section :storage do
37
+ config_set_default :usage, "positions"
38
+ config_set_default :@type, DEFAULT_STORAGE_TYPE
39
+ config_set_default :persistent, true
40
+ end
41
+
42
+ attr_reader :chs
43
+
44
+ def initialize
45
+ super
46
+ @chs = []
47
+ @keynames = []
48
+ @tails = {}
49
+ end
50
+
51
+ def configure(conf)
52
+ super
53
+ @chs = @channels.map {|ch| ch.strip.downcase }.uniq
54
+ if @chs.empty?
55
+ raise Fluent::ConfigError, "windows_eventlog: 'channels' parameter is required on windows_eventlog input"
56
+ end
57
+ @keynames = @keys.map {|k| k.strip }.uniq
58
+ if @keynames.empty?
59
+ @keynames = KEY_MAP.keys
60
+ end
61
+ @keynames.delete('string_inserts') if @parse_description
62
+
63
+ @tag = tag
64
+ @stop = false
65
+ configure_encoding
66
+ @receive_handlers = if @encoding
67
+ method(:encode_record)
68
+ else
69
+ method(:no_encode_record)
70
+ end
71
+ @pos_storage = storage_create(usage: "positions")
72
+ end
73
+
74
+ def configure_encoding
75
+ unless @encoding
76
+ if @from_encoding
77
+ raise Fluent::ConfigError, "windows_eventlog: 'from_encoding' parameter must be specied with 'encoding' parameter."
78
+ end
79
+ end
80
+
81
+ @encoding = parse_encoding_param(@encoding) if @encoding
82
+ @from_encoding = parse_encoding_param(@from_encoding) if @from_encoding
83
+ end
84
+
85
+ def parse_encoding_param(encoding_name)
86
+ begin
87
+ Encoding.find(encoding_name) if encoding_name
88
+ rescue ArgumentError => e
89
+ raise Fluent::ConfigError, e.message
90
+ end
91
+ end
92
+
93
+ def encode_record(record)
94
+ if @encoding
95
+ if @from_encoding
96
+ record.encode!(@encoding, @from_encoding)
97
+ else
98
+ record.force_encoding(@encoding)
99
+ end
100
+ end
101
+ end
102
+
103
+ def no_encode_record(record)
104
+ record
105
+ end
106
+
107
+ def start
108
+ super
109
+ @chs.each do |ch|
110
+ start, num = @pos_storage.get(ch)
111
+ if @read_from_head || (!num || num.zero?)
112
+ el = Win32::EventLog.open(ch)
113
+ @pos_storage.put(ch, [el.oldest_record_number - 1, 1])
114
+ el.close
115
+ end
116
+ timer_execute("in_windows_eventlog_#{escape_channel(ch)}".to_sym, @read_interval) do
117
+ on_notify(ch)
118
+ end
119
+ end
120
+ end
121
+
122
+ def escape_channel(ch)
123
+ ch.gsub(/[^a-zA-Z0-9]/, '_')
124
+ end
125
+
126
+ def receive_lines(ch, lines)
127
+ return if lines.empty?
128
+ begin
129
+ for r in lines
130
+ h = {"channel" => ch}
131
+ @keynames.each do |k|
132
+ type = KEY_MAP[k][1]
133
+ value = r.send(KEY_MAP[k][0])
134
+ h[k]=case type
135
+ when :string
136
+ @receive_handlers.call(value.to_s)
137
+ when :array
138
+ value.map {|v| @receive_handlers.call(v.to_s)}
139
+ else
140
+ raise "Unknown value type: #{type}"
141
+ end
142
+ end
143
+ parse_desc(h) if @parse_description
144
+ #h = Hash[@keynames.map {|k| [k, r.send(KEY_MAP[k][0]).to_s]}]
145
+ router.emit(@tag, Fluent::Engine.now, h)
146
+ end
147
+ rescue => e
148
+ log.error "unexpected error", error: e
149
+ log.error_backtrace
150
+ end
151
+ end
152
+
153
+ def on_notify(ch)
154
+ el = Win32::EventLog.open(ch)
155
+
156
+ current_oldest_record_number = el.oldest_record_number
157
+ current_total_records = el.total_records
158
+
159
+ read_start, read_num = @pos_storage.get(ch)
160
+
161
+ # if total_records is zero, oldest_record_number has no meaning.
162
+ if current_total_records == 0
163
+ return
164
+ end
165
+
166
+ if read_start == 0 && read_num == 0
167
+ @pos_storage.put(ch, [current_oldest_record_number, current_total_records])
168
+ return
169
+ end
170
+
171
+ current_end = current_oldest_record_number + current_total_records - 1
172
+ old_end = read_start + read_num - 1
173
+
174
+ if current_oldest_record_number < read_start
175
+ # may be a record number rotated.
176
+ current_end += 0xFFFFFFFF
177
+ end
178
+
179
+ if current_end < old_end
180
+ # something occured.
181
+ @pos_storage.put(ch, [current_oldest_record_number, current_total_records])
182
+ return
183
+ end
184
+
185
+ winlogs = el.read(Win32::EventLog::SEEK_READ | Win32::EventLog::FORWARDS_READ, old_end + 1)
186
+ receive_lines(ch, winlogs)
187
+ @pos_storage.put(ch, [read_start, read_num + winlogs.size])
188
+ ensure
189
+ el.close
190
+ end
191
+
192
+ GROUP_DELIMITER = "\r\n\r\n".freeze
193
+ RECORD_DELIMITER = "\r\n\t".freeze
194
+ FIELD_DELIMITER = "\t\t".freeze
195
+ NONE_FIELD_DELIMITER = "\t".freeze
196
+
197
+ def parse_desc(record)
198
+ desc = record.delete('description'.freeze)
199
+ return if desc.nil?
200
+
201
+ elems = desc.split(GROUP_DELIMITER)
202
+ record['description_title'] = elems.shift
203
+ elems.each { |elem|
204
+ parent_key = nil
205
+ elem.split(RECORD_DELIMITER).each { |r|
206
+ key, value = if r.index(FIELD_DELIMITER)
207
+ r.split(FIELD_DELIMITER)
208
+ else
209
+ r.split(NONE_FIELD_DELIMITER)
210
+ end
211
+ key.chop! # remove ':' from key
212
+ if value.nil?
213
+ parent_key = to_key(key)
214
+ else
215
+ # parsed value sometimes contain unexpected "\t". So remove it.
216
+ value.strip!
217
+ if parent_key.nil?
218
+ record[to_key(key)] = value
219
+ else
220
+ k = "#{parent_key}.#{to_key(key)}"
221
+ record[k] = value
222
+ end
223
+ end
224
+ }
225
+ }
226
+ end
227
+
228
+ def to_key(key)
229
+ key.downcase!
230
+ key.gsub!(' '.freeze, '_'.freeze)
231
+ key
232
+ end
233
+ end
234
+ end