fluent-plugin-windows-exporter 1.0.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.
@@ -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