winlog 0.1.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/CHANGELOG.md +31 -0
- data/LICENSE.txt +28 -0
- data/README.md +243 -0
- data/ext/winlog/TraceLoggingDynamic.h +3482 -0
- data/ext/winlog/extconf.rb +30 -0
- data/ext/winlog/winlog.cpp +788 -0
- data/lib/winlog/version.rb +5 -0
- data/lib/winlog.rb +216 -0
- metadata +125 -0
data/lib/winlog.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "winlog/version"
|
|
4
|
+
require "winlog/winlog" # native extension: Winlog::Provider + Error/Closed + new_activity_id
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
# winlog — structured, registration-free ETW TraceLogging telemetry for Ruby:
|
|
8
|
+
# events that cost nothing when nobody is listening.
|
|
9
|
+
#
|
|
10
|
+
# Register a provider by name (auto name-hashed GUID — no manifest, no message
|
|
11
|
+
# DLL, no registry write, no elevation) and emit runtime-dynamic, self-describing
|
|
12
|
+
# events decodable by WPA, PerfView, and the inbox logman/tracerpt with zero
|
|
13
|
+
# setup. When no ETW session has enabled the provider, a `log` call is gated in
|
|
14
|
+
# C before the fields are even looked at (~one Ruby method call). Emit-only:
|
|
15
|
+
# events go to ETW sessions, NOT to Event Viewer.
|
|
16
|
+
#
|
|
17
|
+
# PROV = Winlog.open("MyCompany.MyApp")
|
|
18
|
+
# PROV.log(:info, "Startup", version: "1.4.2", pid: Process.pid) # => false (no session)
|
|
19
|
+
# Winlog.open("Tool") { |p| p.log(:info, "Ran", args: ARGV.join(" ")) }
|
|
20
|
+
module Winlog
|
|
21
|
+
# Symbolic levels -> winmeta values (tracelogging.md §5, verified table).
|
|
22
|
+
LEVELS = {
|
|
23
|
+
critical: 1, # WINEVENT_LEVEL_CRITICAL
|
|
24
|
+
error: 2, # WINEVENT_LEVEL_ERROR
|
|
25
|
+
warn: 3, # WINEVENT_LEVEL_WARNING
|
|
26
|
+
info: 4, # WINEVENT_LEVEL_INFO
|
|
27
|
+
debug: 5, # WINEVENT_LEVEL_VERBOSE
|
|
28
|
+
verbose: 5 # alias of :debug
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# Symbolic opcodes -> winmeta values. START/STOP bracket activities.
|
|
32
|
+
OPCODES = { info: 0, start: 1, stop: 2 }.freeze
|
|
33
|
+
|
|
34
|
+
# Keyword bits 48..63 are reserved by Microsoft; winlog REJECTS them.
|
|
35
|
+
KEYWORD_RESERVED_MASK = 0xFFFF_0000_0000_0000
|
|
36
|
+
|
|
37
|
+
# 16 signature bytes prepended to the name before SHA-1, per the documented
|
|
38
|
+
# ETW name-hash algorithm (tracelogging.md §4).
|
|
39
|
+
GUID_SIGNATURE = [0x48, 0x2C, 0x2D, 0xB2, 0xC3, 0x90, 0x47, 0xC8,
|
|
40
|
+
0x87, 0xF8, 0x1A, 0x15, 0xBF, 0xC1, 0x30, 0xFB].freeze
|
|
41
|
+
private_constant :GUID_SIGNATURE
|
|
42
|
+
|
|
43
|
+
# Printable ASCII (bytes 0x21..0x7E): no spaces, no control chars, no NUL.
|
|
44
|
+
NAME_PATTERN = /\A[\x21-\x7E]{1,255}\z/
|
|
45
|
+
private_constant :NAME_PATTERN
|
|
46
|
+
|
|
47
|
+
# Base class for everything winlog raises itself. (Error and Closed are
|
|
48
|
+
# actually defined in C so they exist before this file's reopen; documented
|
|
49
|
+
# here for the reader.)
|
|
50
|
+
#
|
|
51
|
+
# class Winlog::Error < StandardError; end
|
|
52
|
+
# class Winlog::Closed < Winlog::Error; end
|
|
53
|
+
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
# Register a TraceLogging provider under +name+ and return a Winlog::Provider.
|
|
57
|
+
# Block form yields the provider and ensure-closes it, returning the block
|
|
58
|
+
# value. Registration is system-wide registration-FREE (no manifest, registry,
|
|
59
|
+
# or elevation). On a rare EventRegister failure NO exception is raised — the
|
|
60
|
+
# provider is a benign no-op; check #registered? (MS guidance).
|
|
61
|
+
#
|
|
62
|
+
# Winlog.open("X") # => #<Winlog::Provider X {guid}>
|
|
63
|
+
# Winlog.open("X") { |p| p.log(...) } # => block value; provider closed after
|
|
64
|
+
def open(name)
|
|
65
|
+
prov = Provider.new(name)
|
|
66
|
+
return prov unless block_given?
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
yield prov
|
|
70
|
+
ensure
|
|
71
|
+
prov.close
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The ETW name-hashed GUID for +name+ WITHOUT registering anything. Pure Ruby
|
|
76
|
+
# (SHA-1 over the documented signature + UTF-16BE upcased name, .NET byte
|
|
77
|
+
# order). Same +name+ rules as Winlog.open. Case-insensitive.
|
|
78
|
+
#
|
|
79
|
+
# Winlog.guid_for("MyCompany.MyComponent")
|
|
80
|
+
# # => "ce5fa4ea-ab00-5402-8b76-9f76ac858fb5"
|
|
81
|
+
def guid_for(name)
|
|
82
|
+
name = validate_name!(name)
|
|
83
|
+
bytes = Digest::SHA1.digest(
|
|
84
|
+
GUID_SIGNATURE.pack("C*") + name.upcase.encode("UTF-16BE").b
|
|
85
|
+
).bytes[0, 16]
|
|
86
|
+
bytes[7] = (bytes[7] & 0x0F) | 0x50
|
|
87
|
+
[bytes[0, 4].reverse, bytes[4, 2].reverse, bytes[6, 2].reverse,
|
|
88
|
+
bytes[8, 2], bytes[10, 6]]
|
|
89
|
+
.map { |part| part.map { |b| format("%02x", b) }.join }
|
|
90
|
+
.join("-")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Map a level (Symbol in LEVELS or Integer 1..255) to its winmeta Integer.
|
|
94
|
+
# Raises ArgumentError on an unknown Symbol or out-of-range Integer.
|
|
95
|
+
def level_for(level)
|
|
96
|
+
case level
|
|
97
|
+
when Symbol
|
|
98
|
+
LEVELS.fetch(level) do
|
|
99
|
+
raise ArgumentError, "unknown level #{level.inspect} " \
|
|
100
|
+
"(known: #{LEVELS.keys.inspect})"
|
|
101
|
+
end
|
|
102
|
+
when Integer
|
|
103
|
+
unless level.between?(1, 255)
|
|
104
|
+
raise ArgumentError, "level must be 1..255, got #{level.inspect}"
|
|
105
|
+
end
|
|
106
|
+
level
|
|
107
|
+
else
|
|
108
|
+
raise ArgumentError,
|
|
109
|
+
"level must be a Symbol or Integer, got #{level.inspect}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Map an opcode (Symbol in OPCODES or Integer 0..255) to its Integer value.
|
|
114
|
+
def opcode_for(opcode)
|
|
115
|
+
case opcode
|
|
116
|
+
when Symbol
|
|
117
|
+
OPCODES.fetch(opcode) do
|
|
118
|
+
raise ArgumentError, "unknown opcode #{opcode.inspect} " \
|
|
119
|
+
"(known: #{OPCODES.keys.inspect})"
|
|
120
|
+
end
|
|
121
|
+
when Integer
|
|
122
|
+
unless opcode.between?(0, 255)
|
|
123
|
+
raise ArgumentError, "opcode must be 0..255, got #{opcode.inspect}"
|
|
124
|
+
end
|
|
125
|
+
opcode
|
|
126
|
+
else
|
|
127
|
+
raise ArgumentError,
|
|
128
|
+
"opcode must be a Symbol or Integer, got #{opcode.inspect}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Validate a provider name: printable ASCII (0x21..0x7E), 1..255 bytes.
|
|
133
|
+
# Returns the name as a String. Raises ArgumentError on anything else.
|
|
134
|
+
def validate_name!(name)
|
|
135
|
+
str = String(name)
|
|
136
|
+
# Compare raw bytes so a broken/foreign encoding can never make the regex
|
|
137
|
+
# match raise (it would on an invalid byte sequence); printable ASCII is a
|
|
138
|
+
# pure byte predicate. Reuse the same 0x21..0x7E, 1..255 contract as
|
|
139
|
+
# NAME_PATTERN, then return a clean UTF-8 String.
|
|
140
|
+
bytes = str.bytes
|
|
141
|
+
ok = bytes.length.between?(1, 255) && bytes.all? { |b| b >= 0x21 && b <= 0x7E }
|
|
142
|
+
unless ok
|
|
143
|
+
raise ArgumentError,
|
|
144
|
+
"name must be 1..255 printable-ASCII bytes (0x21..0x7E: no spaces, " \
|
|
145
|
+
"control chars, or NUL), got #{name.inspect}"
|
|
146
|
+
end
|
|
147
|
+
str.b.force_encoding(Encoding::UTF_8)
|
|
148
|
+
end
|
|
149
|
+
private_class_method :validate_name!
|
|
150
|
+
|
|
151
|
+
# Native (TypedData) provider class; the C side defines the bridges and the
|
|
152
|
+
# read-only methods. The Ruby layer here adds the validated, ergonomic API.
|
|
153
|
+
class Provider
|
|
154
|
+
# The provider name as given (frozen UTF-8 String). Stored at initialize and
|
|
155
|
+
# never read back from provider metadata (tld swaps in empty NullMetadata on
|
|
156
|
+
# a failed registration). Correct regardless of registration outcome.
|
|
157
|
+
def name
|
|
158
|
+
raise Winlog::Closed, "winlog: provider is closed" if closed?
|
|
159
|
+
|
|
160
|
+
@name
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Same contract as Winlog.open (no block form here; open delegates to new).
|
|
164
|
+
# Raises ArgumentError on a bad name; never raises on EventRegister failure
|
|
165
|
+
# (see #registered?).
|
|
166
|
+
def initialize(name)
|
|
167
|
+
@name = Winlog.send(:validate_name!, name).dup.freeze
|
|
168
|
+
_register(@name)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Emit one TraceLogging event. THE call of the gem.
|
|
172
|
+
#
|
|
173
|
+
# level : Symbol in LEVELS, or Integer 1..255.
|
|
174
|
+
# event : event name (Symbol or String; valid UTF-8, non-empty, no NUL).
|
|
175
|
+
# fields : keyword splat of field-name => value pairs, plus the reserved
|
|
176
|
+
# control kwargs keyword:/opcode:/activity:/related: (extracted by
|
|
177
|
+
# exact Symbol key; String keys with the same text stay fields).
|
|
178
|
+
#
|
|
179
|
+
# Returns true if enabled and written, false if skipped/dropped. Never
|
|
180
|
+
# raises on delivery problems (ETW is lossy by design). Raises Winlog::Closed
|
|
181
|
+
# after close. Field-value type errors raise only when the event is enabled
|
|
182
|
+
# (the deliberate E2 asymmetry; control args validate on every call).
|
|
183
|
+
def log(level, event, **fields)
|
|
184
|
+
# Map opcode symbol -> integer here (Ruby owns the OPCODES table) without
|
|
185
|
+
# disturbing the String-key escape hatch: only a :opcode Symbol key is a
|
|
186
|
+
# control arg, and only when its value is a Symbol does it need mapping.
|
|
187
|
+
if fields.key?(:opcode) && fields[:opcode].is_a?(Symbol)
|
|
188
|
+
fields = fields.dup
|
|
189
|
+
fields[:opcode] = Winlog.opcode_for(fields[:opcode])
|
|
190
|
+
end
|
|
191
|
+
_log(Winlog.level_for(level), event, fields)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Is any ETW session listening (with the given level/keyword filter)?
|
|
195
|
+
# Reads in-process enable state — no system call. Raises Winlog::Closed
|
|
196
|
+
# after close.
|
|
197
|
+
#
|
|
198
|
+
# level: nil (default; "any level") | Symbol in LEVELS | Integer 1..255
|
|
199
|
+
# keyword: Integer 0..0x0000_FFFF_FFFF_FFFF (reserved bits raise)
|
|
200
|
+
def enabled?(level: nil, keyword: 0)
|
|
201
|
+
lvl = level.nil? ? nil : Winlog.level_for(level)
|
|
202
|
+
_enabled(lvl, keyword)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Never raises, even closed.
|
|
206
|
+
def inspect
|
|
207
|
+
if closed?
|
|
208
|
+
"#<Winlog::Provider (closed)>"
|
|
209
|
+
else
|
|
210
|
+
"#<Winlog::Provider #{@name} {#{guid}}>"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private :_register, :_log, :_write, :_enabled
|
|
215
|
+
end
|
|
216
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: winlog
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ned
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake-compiler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: vcvars
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 0.1.1
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0.1'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.1.1
|
|
74
|
+
description: |
|
|
75
|
+
winlog emits Windows ETW TraceLogging events from Ruby with no manifest, no
|
|
76
|
+
message DLL, no registry writes, and no elevation: register a provider by
|
|
77
|
+
name (standard ETW name-hashed GUID), then log runtime-dynamic events with
|
|
78
|
+
typed fields (UTF-8 text, binary, int64, double, boolean) plus levels,
|
|
79
|
+
keywords, opcodes, and explicit activity IDs for correlation. Events are
|
|
80
|
+
self-describing and decode in WPA, PerfView, and the inbox logman/tracerpt
|
|
81
|
+
with zero setup; when no session is listening, a log call is gated in native
|
|
82
|
+
code before field processing and costs about one Ruby method call. Built on
|
|
83
|
+
Microsoft's MIT-licensed TraceLoggingDynamic.h (vendored). Emit-only: events
|
|
84
|
+
go to ETW sessions, not the Windows Event Log. Windows MSVC (mswin) Ruby only.
|
|
85
|
+
executables: []
|
|
86
|
+
extensions:
|
|
87
|
+
- ext/winlog/extconf.rb
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- CHANGELOG.md
|
|
91
|
+
- LICENSE.txt
|
|
92
|
+
- README.md
|
|
93
|
+
- ext/winlog/TraceLoggingDynamic.h
|
|
94
|
+
- ext/winlog/extconf.rb
|
|
95
|
+
- ext/winlog/winlog.cpp
|
|
96
|
+
- lib/winlog.rb
|
|
97
|
+
- lib/winlog/version.rb
|
|
98
|
+
homepage: https://github.com/main-path/winlog
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
homepage_uri: https://github.com/main-path/winlog
|
|
103
|
+
source_code_uri: https://github.com/main-path/winlog
|
|
104
|
+
changelog_uri: https://github.com/main-path/winlog/blob/main/CHANGELOG.md
|
|
105
|
+
bug_tracker_uri: https://github.com/main-path/winlog/issues
|
|
106
|
+
rubygems_mfa_required: 'true'
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.0'
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 3.6.9
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: Structured, registration-free ETW TraceLogging telemetry for Ruby — events
|
|
124
|
+
that cost nothing when nobody is listening.
|
|
125
|
+
test_files: []
|