fluent-plugin-windows-exporter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,431 @@
1
+ #
2
+ # Copyright 2021- Fujimoto Seiji, Fukuda Daijiro
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require "fiddle/import"
17
+ require_relative "hkey_perf_data_raw_type"
18
+ require_relative "hkey_perf_data_converted_type"
19
+
20
+ # A reader for Windows registry key: HKeyPerfData.
21
+ # This provides Windows performance counter data.
22
+ # ref: https://docs.microsoft.com/en-us/windows/win32/perfctrs/using-the-registry-functions-to-consume-counter-data
23
+ # This provide the raw counter value, which has not been calculated according to the counter type.
24
+ # ref: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.performancecountertype
25
+ # You can use this as follows:
26
+ #
27
+ # Usage:
28
+ # require_relative "hkey_perf_data_reader"
29
+ # reader = HKeyPerfDataReader::Reader.new
30
+ # data = reader.read
31
+ # data.keys
32
+ # => ["RAS", "WSMan Quota Statistics", "Event Log", ...]
33
+ # data["Memory"].instances[0].counters.keys
34
+ # => ["Page Faults/sec", "Available Bytes", ...]
35
+ # data["Memory"].instances[0].counters["Available Bytes"]
36
+ # => #<...PerfCounter... @name="Available Bytes", @value=2852536320, @base_value=0>
37
+ # data["Memory"].instances[0].counters["% Committed Bytes In Use"]
38
+ # => #<...PerfCounter... @name="% Committed Bytes In Use", @value=3583260914, @base_value=4294967295>
39
+ # Note: some counters have `base_value` in separate from `value` depending on the counter type.
40
+ # data["Processor"].instance_names
41
+ # => ["0", "1", "2", "3", ... , "_Total"]
42
+ #
43
+ # Public API:
44
+ # * HKeyPerfDataReader::Reader#new(object_name_whitelist: [], logger: nil)
45
+ # * object_name_whitelist
46
+ # * you can use this in order to speed up the `read` process.
47
+ # * if this is an empty list, then this reader trys to read all data.
48
+ #
49
+ # * HKeyPerfDataReader::Reader#read()
50
+ # return hash of PerfObject
51
+
52
+ module HKeyPerfDataReader
53
+ module Constants
54
+ HKEY_PERFORMANCE_DATA = 0x80000004
55
+ HKEY_PERFORMANCE_TEXT = 0x80000050
56
+ PERF_NO_INSTANCES = -1
57
+ # https://docs.microsoft.com/ja-jp/windows/win32/debug/system-error-codes
58
+ ERROR_SUCCESS = 0
59
+ ERROR_MORE_DATA = 234
60
+ end
61
+
62
+ class Reader
63
+ include Constants
64
+
65
+ def initialize(object_name_whitelist: [], logger: nil)
66
+ @raw_data = nil
67
+ @is_little_endian = true
68
+ @binary_parser = nil
69
+ @counter_name_reader = CounterNameTableReader.new
70
+ @object_name_whitelist = Set.new(object_name_whitelist)
71
+ @logger = logger.nil? ? NullLogger.new : logger
72
+ end
73
+
74
+ def read
75
+ @raw_data = RawReader.read(@logger)
76
+ # `littleEndian` flag in PerfDataBlock: https://docs.microsoft.com/en-us/windows/win32/api/winperf/ns-winperf-perf_data_block
77
+ # Although we use this flag value, it will probably never be BigEndian, and we probably don't need to use this value.
78
+ @is_little_endian = @raw_data[8..11].unpack("L")[0] == 1
79
+ if @binary_parser.nil?
80
+ @logger.trace("HKeyPerfData LittlEndian: #{@is_little_endian}")
81
+ @binary_parser = BinaryParser.new(is_little_endian: @is_little_endian)
82
+ end
83
+
84
+ header = read_header
85
+ @logger.trace("HKeyPerfData numObjectTypes: #{header.numObjectTypes}")
86
+
87
+ unless header.signature == "PERF"
88
+ @logger.error("Could not read HKeyPerfData. The header is invalid.")
89
+ return {}
90
+ end
91
+
92
+ perf_objects = {}
93
+
94
+ offset = header.headerLength
95
+ header.numObjectTypes.times do
96
+ perf_object, total_byte_length, success = read_perf_object(offset)
97
+ unless success
98
+ if total_byte_length.nil?
99
+ @logger.trace("Can not continue. Stop reading.")
100
+ break
101
+ else
102
+ @logger.trace("Skip this object and continue reading.")
103
+ offset += total_byte_length
104
+ next
105
+ end
106
+ end
107
+
108
+ @logger.trace("Duplicate object name: #{perf_object.name}") if perf_objects.key?(perf_object.name)
109
+ perf_objects[perf_object.name] = perf_object
110
+ offset += total_byte_length
111
+ end
112
+
113
+ perf_objects
114
+ rescue => e
115
+ @logger.error("Could not read HKeyPerfData. Message: #{e.message}")
116
+ {}
117
+ end
118
+
119
+ private
120
+
121
+ def read_header
122
+ raw_perf_data_block = @binary_parser.parse_data_block(@raw_data)
123
+ ConvertedType::PerfDataBlock.new(raw_perf_data_block)
124
+ end
125
+
126
+ def read_perf_object(object_start_offset)
127
+ cur_offset = object_start_offset
128
+
129
+ object_type = @binary_parser.parse_object_type(@raw_data[cur_offset..])
130
+ cur_offset += object_type.headerLength
131
+
132
+ name = @counter_name_reader.read(object_type.objectNameTitleIndex)
133
+ if name.to_s.empty?
134
+ @logger.trace("Can not get object name. Skip. ObjectNameTitleIndex: #{object_type.objectNameTitleIndex}")
135
+ return nil, object_type.totalByteLength, false
136
+ end
137
+
138
+ perf_object = ConvertedType::PerfObject.new(name, object_type)
139
+
140
+ @logger.trace("object name: #{perf_object.name}")
141
+
142
+ unless @object_name_whitelist.empty?
143
+ unless @object_name_whitelist.include?(perf_object.name)
144
+ @logger.trace("Object name #{perf_object.name} is not in the whitelist. Skip. ")
145
+ return nil, object_type.totalByteLength, false
146
+ end
147
+ end
148
+
149
+ cur_offset = set_couner_defs_to_object(
150
+ perf_object, object_type.numCounters, cur_offset
151
+ )
152
+
153
+ if object_type.numInstances == PERF_NO_INSTANCES || object_type.numInstances == 0
154
+ set_counters_to_no_instance_object(
155
+ perf_object,
156
+ object_start_offset + object_type.definitionLength,
157
+ )
158
+ else
159
+ set_counters_to_multiple_instance_object(
160
+ perf_object,
161
+ object_type.numInstances,
162
+ object_start_offset + object_type.definitionLength,
163
+ )
164
+ end
165
+
166
+ return perf_object, object_type.totalByteLength, true
167
+ rescue => e
168
+ @logger.warn("error occurred: objectname: #{perf_object&.name}, message: #{e.message}")
169
+ return nil, object_type&.totalByteLength, false
170
+ end
171
+
172
+ def set_couner_defs_to_object(perf_object, num_of_counters, counter_def_start_offset)
173
+ cur_offset = counter_def_start_offset
174
+
175
+ num_of_counters.times do
176
+ counter_def = @binary_parser.parse_counter_definition(@raw_data[cur_offset..])
177
+
178
+ name = @counter_name_reader.read(counter_def.counterNameTitleIndex)
179
+ unless name.to_s.empty?
180
+ perf_object.add_counter_def(
181
+ ConvertedType::PerfCounterDef.new(name, counter_def)
182
+ )
183
+ else
184
+ @logger.trace("Can not get counter name. Skip. CounterNameTitleIndex: #{counter_def.counterNameTitleIndex}")
185
+ end
186
+
187
+ cur_offset += counter_def.byteLength
188
+ end
189
+
190
+ cur_offset
191
+ end
192
+
193
+ def set_counters_to_no_instance_object(perf_object, counter_block_offset)
194
+ # to unify data format, use no name instance for a container for the counters
195
+ instance = ConvertedType::PerfInstance.new("")
196
+
197
+ perf_object.counter_defs.each do |counter_def|
198
+ instance.add_counter(
199
+ counter_def,
200
+ read_counter_value(
201
+ counter_def,
202
+ counter_block_offset + counter_def.counter_offset,
203
+ )
204
+ )
205
+ end
206
+
207
+ perf_object.add_instance(instance)
208
+ end
209
+
210
+ def set_counters_to_multiple_instance_object(
211
+ perf_object, num_of_instances, first_instance_offset
212
+ )
213
+ cur_instance_offset = first_instance_offset
214
+
215
+ num_of_instances.times do
216
+ instance_def = @binary_parser.parse_instance_definition(
217
+ @raw_data[cur_instance_offset..]
218
+ )
219
+
220
+ name_offset = cur_instance_offset + instance_def.nameOffset
221
+ instance_name = @raw_data[
222
+ name_offset..name_offset+instance_def.nameLength-1
223
+ ].encode("UTF-8", "UTF-16LE").strip
224
+
225
+ instance = ConvertedType::PerfInstance.new(instance_name)
226
+
227
+ counter_block_offset = cur_instance_offset + instance_def.byteLength
228
+
229
+ counter_block = @binary_parser.parse_counter_block(
230
+ @raw_data[counter_block_offset..]
231
+ )
232
+
233
+ perf_object.counter_defs.each do |counter_def|
234
+ instance.add_counter(
235
+ counter_def,
236
+ read_counter_value(
237
+ counter_def,
238
+ counter_block_offset + counter_def.counter_offset,
239
+ )
240
+ )
241
+ end
242
+
243
+ perf_object.add_instance(instance)
244
+ cur_instance_offset = counter_block_offset + counter_block.byteLength
245
+ end
246
+ end
247
+
248
+ def read_counter_value(counter_def, offset)
249
+ # Currently counter data is limited to DWORD and ULONGLONG data types
250
+ # ref: https://docs.microsoft.com/en-us/windows/win32/perfctrs/retrieving-counter-data
251
+ # We don't need to consider `counterType` unless we need to format the value for output.
252
+ endian_mark = @is_little_endian ? "<" : ">"
253
+ case counter_def.counter_size
254
+ when 4
255
+ return @raw_data[offset..offset+3].unpack("L#{endian_mark}")[0]
256
+ when 8
257
+ return @raw_data[offset..offset+7].unpack("Q#{endian_mark}")[0]
258
+ else
259
+ return @raw_data[offset..offset+3].unpack("L#{endian_mark}")[0]
260
+ end
261
+ end
262
+
263
+ class NullLogger
264
+ def trace(*args, &block)
265
+ end
266
+
267
+ def debug(*args, &block)
268
+ end
269
+
270
+ def info(*args, &block)
271
+ end
272
+
273
+ def warn(*args, &block)
274
+ end
275
+
276
+ def error(*args, &block)
277
+ end
278
+
279
+ def fatal(*args, &block)
280
+ end
281
+ end
282
+ end
283
+
284
+ class CounterNameTableReader
285
+ def initialize
286
+ @counter_name_table = nil
287
+ end
288
+
289
+ def read(index)
290
+ # In order to reduce the process in the initialization phase.
291
+ if @counter_name_table.nil?
292
+ @counter_name_table = CounterNameTableReader.build_table
293
+ end
294
+
295
+ @counter_name_table[index]
296
+ end
297
+
298
+ private
299
+
300
+ def self.build_table
301
+ # https://docs.microsoft.com/en-us/windows/win32/perfctrs/retrieving-counter-names-and-help-text
302
+ # https://github.com/leoluk/perflib_exporter
303
+
304
+ table = {}
305
+
306
+ raw_data = RawReader.read_counter_name_table
307
+ # I'm not sure if this endian should be the same as PerfDataBlock's.
308
+ converted_data = raw_data.encode("UTF-8", "UTF-16LE").split("\u0000")
309
+
310
+ loop do
311
+ index = converted_data.shift
312
+ value = converted_data.shift
313
+ break if index.nil? || value.nil?
314
+ table[index.to_i] = value
315
+ end
316
+
317
+ table
318
+ end
319
+ end
320
+
321
+ module API
322
+ extend Fiddle::Importer
323
+ dlload "advapi32.dll"
324
+ [
325
+ "long RegQueryValueExW(void *, void *, void *, void *, void *, void *)",
326
+ "long RegCloseKey(void *)",
327
+ ].each do |fn|
328
+ extern fn, :stdcall
329
+ end
330
+ end
331
+
332
+ module RawReader
333
+ include Constants
334
+ BUFFER_SIZE = 128*1024*1024 # 128kb
335
+
336
+ def self.read(logger = nil)
337
+ # https://docs.microsoft.com/en-us/windows/win32/perfctrs/using-the-registry-functions-to-consume-counter-data
338
+ # https://docs.microsoft.com/en-us/windows/win32/perfctrs/retrieving-counter-data
339
+
340
+ hkey = convert_handle(HKEY_PERFORMANCE_DATA)
341
+ type = packdw(0)
342
+ source = make_wstr("Global")
343
+ size = packdw(BUFFER_SIZE)
344
+
345
+ # NOTE: By Stoping allocating every time and starting reusing, we might be able to speed up the process.
346
+ data = "\0".force_encoding("ASCII-8BIT") * unpackdw(size)
347
+
348
+ begin
349
+ ret = API.RegQueryValueExW(hkey, source, 0, type, data, size)
350
+
351
+ while ret == ERROR_MORE_DATA
352
+ size = packdw(unpackdw(size) + BUFFER_SIZE)
353
+ data = "\0".force_encoding("ASCII-8BIT") * unpackdw(size)
354
+ ret = API.RegQueryValueExW(hkey, source, 0, type, data, size)
355
+ end
356
+
357
+ unless ret == ERROR_SUCCESS
358
+ raise IOError, "RegQueryValueEx failed with #{ret}."
359
+ end
360
+
361
+ return data[0..unpackdw(size)]
362
+ ensure
363
+ ret = API.RegCloseKey(hkey)
364
+ unless ret == ERROR_SUCCESS
365
+ logger&.warn("RegCloseKey failed with #{ret}.")
366
+ end
367
+ end
368
+ end
369
+
370
+ def self.read_counter_name_table
371
+ # This process can be replaced by:
372
+ # require "win32/registry"
373
+ # Win32::Registry::HKEY_PERFORMANCE_TEXT.read("Counter")
374
+
375
+ # There is a problem with getting some name data in ruby.
376
+ # This is caused by `SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)` called from `rubygems\defaults\operating_system.rb`.
377
+ # https://github.com/fluent-plugins-nursery/fluent-plugin-windows-exporter/issues/1#issuecomment-994168635
378
+
379
+ # https://docs.microsoft.com/en-us/windows/win32/perfctrs/retrieving-counter-names-and-help-text
380
+ hkey = convert_handle(HKEY_PERFORMANCE_TEXT)
381
+ source = make_wstr("Counter")
382
+ size = packdw(0)
383
+
384
+ ret = API.RegQueryValueExW(hkey, source, nil, nil, nil, size)
385
+
386
+ unless ret == ERROR_SUCCESS
387
+ raise IOError, "RegQueryValueEx failed getting required buffer size. Error is #{ret}."
388
+ end
389
+
390
+ data = "\0".force_encoding("ASCII-8BIT") * unpackdw(size)
391
+
392
+ ret = API.RegQueryValueExW(hkey, source, nil, nil, data, size)
393
+
394
+ unless ret == ERROR_SUCCESS
395
+ raise IOError, "RegQueryValueEx failed with #{ret}."
396
+ end
397
+
398
+ # NOTE: no need to call `RegCloseKey` when just taking counter table data
399
+
400
+ data
401
+ end
402
+
403
+ private
404
+
405
+ def self.packdw(dw)
406
+ [dw].pack("V")
407
+ end
408
+
409
+ def self.unpackdw(dw)
410
+ dw += [0].pack("V")
411
+ dw.unpack("V")[0]
412
+ end
413
+
414
+ def self.make_wstr(str)
415
+ str.encode(Encoding::UTF_16LE)
416
+ end
417
+
418
+ def self.win64?
419
+ /^(?:x64|x86_64)/ =~ RUBY_PLATFORM
420
+ end
421
+
422
+ def self.convert_handle(h)
423
+ # In winreg.h, HKEY values are ((HKEY)(ULONG_PTR)((LONG)0x8000...))
424
+ # So, in a 64bit environment, original 4-byte values are casted to 8-byte values for `LONG` cast.
425
+ # Since `LONG` is a signed type, the upper 4-bytes must be `FFFFFFFF`. (2's complement)
426
+ # NOTE: The implementation of `win32::Registry` uses `RegOpenKeyExW` to take proper HKEY values, but we can't use this way because we have to handle `HKEY_PERFORMANCE_DATA`, which can't be opened by `RegOpenKeyExW`.
427
+ return h unless win64?
428
+ 0xFFFFFFFF00000000 | h
429
+ end
430
+ end
431
+ end