fluent-plugin-windows-eventlog 0.1.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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