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.
- checksums.yaml +5 -5
- data/.gitignore +14 -14
- data/CHANGELOG.md +22 -0
- data/Gemfile +4 -4
- data/LICENSE.txt +203 -203
- data/README.md +285 -66
- data/Rakefile +10 -10
- data/appveyor.yml +24 -0
- data/fluent-plugin-winevtlog.gemspec +28 -25
- data/lib/fluent/plugin/in_windows_eventlog.rb +234 -320
- data/lib/fluent/plugin/in_windows_eventlog2.rb +216 -0
- data/test/generate-windows-event.rb +47 -0
- data/test/helper.rb +33 -29
- data/test/plugin/test_in_windows_eventlog2.rb +214 -0
- data/test/plugin/test_in_winevtlog.rb +48 -51
- metadata +56 -7
@@ -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
|
-
|
12
|
-
|
13
|
-
"
|
14
|
-
"
|
15
|
-
"
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"
|
19
|
-
"
|
20
|
-
"
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
config_param :
|
25
|
-
config_param :
|
26
|
-
config_param :
|
27
|
-
|
28
|
-
config_param :
|
29
|
-
config_param :
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
end
|
102
|
-
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
@
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|