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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +77 -0
- data/lib/fluent/plugin/hkey_perf_data_converted_type.rb +148 -0
- data/lib/fluent/plugin/hkey_perf_data_raw_type.rb +143 -0
- data/lib/fluent/plugin/hkey_perf_data_reader.rb +431 -0
- data/lib/fluent/plugin/in_windows_exporter.rb +882 -0
- data/lib/fluent/plugin/test_hpd_reader.rb +124 -0
- data/lib/fluent/plugin/winffi.rb +164 -0
- metadata +129 -0
@@ -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
|