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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9a6df367ea2d0404effb7f44cb924e4f6fe67dbd5371b6038e056423054d3507
|
|
4
|
+
data.tar.gz: 74481ee0141c167adebff54a3e9ceaf2b17122f89899aa681b06214fc486596f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3f48d2c5ae916db9f2fcdf76072464b069d5cc25983c534515fb4455b14f9de3a777dd6de61afe425ee77fb9095d7c8bec8289010d8a9dbbb8b645bef7b15e7d
|
|
7
|
+
data.tar.gz: b31ae1e789399d0781e44160c8638b469ad4674e598229fa2823993e68acc6b8390cdb0434fd7656fd5b906cc63deb38677c3c9b1c27cbce4de60d6e17ec87b9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-27
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `Winlog.open(name)` — register a TraceLogging provider by name (standard ETW
|
|
8
|
+
name-hashed GUID; no manifest, message DLL, registry write, or elevation),
|
|
9
|
+
returning a `Winlog::Provider`. Block form yields the provider and
|
|
10
|
+
ensure-closes it.
|
|
11
|
+
- `Winlog::Provider#log(level, event, **fields)` — emit one runtime-dynamic,
|
|
12
|
+
self-describing event with typed fields (UTF-8 text, binary, int64, double,
|
|
13
|
+
boolean) plus `keyword:`, `opcode:`, and explicit `activity:`/`related:` GUIDs
|
|
14
|
+
for correlation. Gated in native code before any field is read when no session
|
|
15
|
+
is listening (~one Ruby method call); never raises on delivery problems.
|
|
16
|
+
- `Winlog::Provider#enabled?(level:, keyword:)` — in-process check (no system
|
|
17
|
+
call) for skipping expensive argument building.
|
|
18
|
+
- `Winlog::Provider#name` / `#guid` / `#registered?` / `#registration_result` /
|
|
19
|
+
`#close` / `#closed?` / `#inspect`.
|
|
20
|
+
- `Winlog.guid_for(name)` — the pure-Ruby ETW name-hash, for handing the GUID to
|
|
21
|
+
logman (which does not name-hash).
|
|
22
|
+
- `Winlog.new_activity_id` — a fresh 128-bit activity id
|
|
23
|
+
(`EventActivityIdControl(CREATE_ID)`), fiber-safe (never touches the
|
|
24
|
+
thread-ambient activity id).
|
|
25
|
+
- Constants `Winlog::LEVELS`, `Winlog::OPCODES`, `Winlog::KEYWORD_RESERVED_MASK`.
|
|
26
|
+
- Errors `Winlog::Error` and `Winlog::Closed`; plain argument misuse raises
|
|
27
|
+
Ruby's own `ArgumentError` / `TypeError` / `RangeError`.
|
|
28
|
+
|
|
29
|
+
Built on Microsoft's MIT-licensed `TraceLoggingDynamic.h` (vendored into
|
|
30
|
+
`ext/winlog/`); winlog implements no ETW encoding of its own. Emit-only: events
|
|
31
|
+
go to ETW sessions, not the Windows Event Log. Windows MSVC (mswin) Ruby only.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This product vendors Microsoft's TraceLoggingDynamic.h
|
|
26
|
+
(ext/winlog/TraceLoggingDynamic.h), which is Copyright (c) Microsoft
|
|
27
|
+
Corporation and licensed under the MIT license (microsoft/tracelogging,
|
|
28
|
+
etw/cpp/traceloggingdynamic). Its copyright header is preserved in the file.
|
data/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# winlog
|
|
2
|
+
|
|
3
|
+
**Structured, registration-free ETW telemetry for Ruby — TraceLogging events that cost nothing when nobody is listening.**
|
|
4
|
+
|
|
5
|
+
`winlog` registers a [TraceLogging](https://learn.microsoft.com/windows/win32/tracelogging/trace-logging-portal)
|
|
6
|
+
provider by name (auto name-hashed GUID — **no manifest, no message DLL, no
|
|
7
|
+
registry write, no elevation**) and emits runtime-dynamic, self-describing
|
|
8
|
+
events: an event name and typed fields decided at call time, decodable by WPA,
|
|
9
|
+
PerfView, and the inbox `logman`/`tracerpt` with zero setup.
|
|
10
|
+
|
|
11
|
+
The pain it removes: you cannot leave `puts` debugging or verbose file logging on
|
|
12
|
+
in production, and a log file is an unstructured island disconnected from the
|
|
13
|
+
rest of the system's diagnostics. ETW is the structured, OS-native tracing
|
|
14
|
+
pipeline that Windows itself and the .NET runtime emit into — but reaching it
|
|
15
|
+
from a scripting language has historically meant authoring a manifest, compiling
|
|
16
|
+
a message DLL, and registering it with admin rights. TraceLogging is the
|
|
17
|
+
manifest-free encoding that fixes this: every event carries its own schema, so it
|
|
18
|
+
decodes **without any registration**. `winlog` is the thin Ruby binding to it.
|
|
19
|
+
|
|
20
|
+
- **No setup, anywhere.** `gem install winlog`, `Winlog.open`, `log`. There is no
|
|
21
|
+
registration step the gem performs on the system, and no other Ruby gem emits
|
|
22
|
+
native ETW. `Logger` writes files; this feeds WPA/PerfView timelines right next
|
|
23
|
+
to kernel and .NET events.
|
|
24
|
+
- **A disabled `log` costs about one Ruby method call.** When no ETW session has
|
|
25
|
+
enabled the provider, the call is gated in native code *before the fields are
|
|
26
|
+
even looked at* — no allocation, no transcoding, no iteration. Leave it in
|
|
27
|
+
production; that is the entire point of ETW.
|
|
28
|
+
- **Hard to misuse.** Reserved keyword bits, level 0, malformed activity GUIDs,
|
|
29
|
+
NUL bytes in names, and unknown level symbols all raise loudly. Delivery
|
|
30
|
+
problems never raise — ETW is lossy by design, so `log` returns a boolean.
|
|
31
|
+
|
|
32
|
+
| What | API |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Register a provider | `Winlog.open("MyCompany.MyApp")` (block form auto-closes) |
|
|
35
|
+
| Emit an event | `provider.log(:info, "Event", field: value, ...)` |
|
|
36
|
+
| Cheap "is anyone listening?" | `provider.enabled?(level: :debug, keyword: 0x1)` |
|
|
37
|
+
| Name-hashed GUID (no register) | `Winlog.guid_for("MyCompany.MyApp")` |
|
|
38
|
+
| Fresh correlation id | `Winlog.new_activity_id` |
|
|
39
|
+
| Stop / free | `provider.close` |
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- **Windows 10 or later** with a native **MSVC (mswin)** Ruby. Not supported on
|
|
44
|
+
MinGW/UCRT Ruby (the extension is built with `cl.exe`).
|
|
45
|
+
- Visual Studio 2017+ / Build Tools with the **Desktop development with C++**
|
|
46
|
+
workload to build from source. The [`vcvars`](https://github.com/main-path/vcvars)
|
|
47
|
+
dev dependency loads the MSVC environment for `rake compile` automatically — no
|
|
48
|
+
Developer Command Prompt needed; point build failures at `vcvars doctor`.
|
|
49
|
+
- **x64.** arm64-mswin is expected to work but is untested and unsupported until
|
|
50
|
+
an arm64-mswin Ruby distribution exists (the code is arch-neutral).
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
gem install winlog
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
require "winlog"
|
|
62
|
+
|
|
63
|
+
# EXE-lifetime provider: open once at startup, never close (the kernel cleans
|
|
64
|
+
# up registration at process exit; the GC free hook is a safety net).
|
|
65
|
+
PROV = Winlog.open("MyCompany.MyApp")
|
|
66
|
+
PROV.guid # => "ce5fa4ea-..."; hand to logman as "{...}"
|
|
67
|
+
|
|
68
|
+
PROV.log(:info, "Startup", version: "1.4.2", pid: Process.pid) # => false (no session)
|
|
69
|
+
|
|
70
|
+
# Skip building expensive arguments when nobody is listening:
|
|
71
|
+
if PROV.enabled?(level: :debug)
|
|
72
|
+
PROV.log(:debug, "CacheDump", entries: cache.size, hot: cache.hot_keys.join(","))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Activity correlation (fiber-safe: explicit IDs, never the thread-ambient one):
|
|
76
|
+
aid = Winlog.new_activity_id
|
|
77
|
+
PROV.log(:info, "JobStart", opcode: :start, activity: aid, job: "reindex")
|
|
78
|
+
PROV.log(:info, "JobStop", opcode: :stop, activity: aid, ok: true)
|
|
79
|
+
|
|
80
|
+
# Misuse raises loudly:
|
|
81
|
+
PROV.log(:fatal, "X") # ArgumentError: unknown level :fatal
|
|
82
|
+
PROV.log(:info, "X", keyword: 1 << 50) # ArgumentError: reserved keyword bits
|
|
83
|
+
Winlog.open("päivä") # ArgumentError: name must be printable ASCII
|
|
84
|
+
|
|
85
|
+
# Scoped provider for a short-lived tool:
|
|
86
|
+
Winlog.open("MyCompany.Tool") { |p| p.log(:info, "Ran", args: ARGV.join(" ")) }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`log` returns `true` only when a session had the provider enabled at that
|
|
90
|
+
level+keyword **and** the write succeeded; otherwise `false` (no listener, the
|
|
91
|
+
provider never registered, or ETW dropped the event). It never raises on
|
|
92
|
+
delivery.
|
|
93
|
+
|
|
94
|
+
## Seeing your events
|
|
95
|
+
|
|
96
|
+
Collecting a trace requires an **elevated prompt** (or membership in the
|
|
97
|
+
*Performance Log Users* group). This is inherent to ETW for *every* provider on
|
|
98
|
+
Windows, including Microsoft's own — it is not a `winlog` property and concerns
|
|
99
|
+
*collection*, never *emit*. Ranked, copy-pasteable:
|
|
100
|
+
|
|
101
|
+
**1. Inbox only — `logman` + `tracerpt` (nothing to install).** `logman` does
|
|
102
|
+
not name-hash, so give it `provider.guid`:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
logman start rb -p "{ce5fa4ea-ab00-5402-8b76-9f76ac858fb5}" 0xffffffffffffffff 5 -o rb.etl -ets
|
|
106
|
+
ruby app.rb
|
|
107
|
+
logman stop rb -ets
|
|
108
|
+
tracerpt rb.etl -o rb.xml # TDH auto-decodes TraceLogging on Win10+
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**2. WPR (inbox) + WPA (free).** A `.wprp` profile can name the provider with the
|
|
112
|
+
star (`*`) syntax, which name-hashes for you:
|
|
113
|
+
|
|
114
|
+
```xml
|
|
115
|
+
<EventProvider Id="MyApp" Name="*MyCompany.MyApp" />
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`wpr -start profile.wprp -filemode` → `wpr -stop out.etl` → open in WPA → **System
|
|
119
|
+
Activity ▸ Generic Events**.
|
|
120
|
+
|
|
121
|
+
**3. PerfView** (single-exe download): `PerfView /onlyProviders=*MyCompany.MyApp collect`.
|
|
122
|
+
The `*` converts the name to its GUID the EventSource way.
|
|
123
|
+
|
|
124
|
+
> **Events do NOT appear in Event Viewer / `wevtutil`.** That requires registering
|
|
125
|
+
> the provider with an Event Log channel (a manifest + `wevtutil im`, i.e.
|
|
126
|
+
> registration and admin) — exactly what `winlog` deliberately does not do.
|
|
127
|
+
> TraceLogging events go to **ETW sessions**, decoded by the tools above.
|
|
128
|
+
|
|
129
|
+
## Levels, keywords, opcodes
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
Winlog::LEVELS # critical:1 error:2 warn:3 info:4 debug:5 (verbose: alias of debug)
|
|
133
|
+
Winlog::OPCODES # info:0 start:1 stop:2
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- **Level** is a Symbol from `LEVELS` or an Integer `1..255` (1..5 are the
|
|
137
|
+
standard winmeta levels). Level **0 is rejected** — always assign a meaningful
|
|
138
|
+
non-zero level.
|
|
139
|
+
- **Keyword** is a 64-bit bitmask; bits **0..47 are yours**, bits **48..63 are
|
|
140
|
+
reserved by Microsoft** (`Winlog::KEYWORD_RESERVED_MASK`) and setting any of
|
|
141
|
+
them raises `ArgumentError`. The default is `0`, which **bypasses keyword
|
|
142
|
+
filtering** (ETW semantics) — fine for getting started, but assign meaningful
|
|
143
|
+
keywords so sessions can filter.
|
|
144
|
+
- **Opcode** `:start`/`:stop` bracket an activity for decoders.
|
|
145
|
+
|
|
146
|
+
## Activities and fibers
|
|
147
|
+
|
|
148
|
+
Correlate related events by passing the *same* explicit activity id, generated
|
|
149
|
+
with `Winlog.new_activity_id`:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
aid = Winlog.new_activity_id
|
|
153
|
+
prov.log(:info, "RequestStart", opcode: :start, activity: aid, related: parent_aid)
|
|
154
|
+
# ... work ...
|
|
155
|
+
prov.log(:info, "RequestStop", opcode: :stop, activity: aid, status: 200)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`winlog` **never uses or mutates ETW's thread-ambient activity id.** Under a
|
|
159
|
+
fiber scheduler such as [`winloop`](https://github.com/main-path/winloop), one
|
|
160
|
+
thread hosts many fibers, and the thread-local id would smear one fiber's
|
|
161
|
+
activity across all of them. Correlation is therefore always per-call and
|
|
162
|
+
explicit, which is fiber-correct by construction. If you want an ambient pattern,
|
|
163
|
+
store the id in `Thread.current[:winlog_activity]` (fiber-local in Ruby) and pass
|
|
164
|
+
it yourself.
|
|
165
|
+
|
|
166
|
+
`winlog` never blocks, so it never offloads to a worker thread and integrates
|
|
167
|
+
with a scheduler trivially: `log`/`enabled?`/`open`/`close` run inline, occupying
|
|
168
|
+
the loop thread for at most one ~1–3 µs `EventWriteTransfer` per enabled event.
|
|
169
|
+
|
|
170
|
+
## Costs and validation
|
|
171
|
+
|
|
172
|
+
| Operation | Cost (spike-measured, x64) |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `enabled?` native check (`IsEnabled`) | ~0.4 ns |
|
|
175
|
+
| Disabled `log` (gated, fields untouched) | ~one Ruby method call (~0.1 µs) |
|
|
176
|
+
| Enabled `EventWriteTransfer` | ~1–3 µs/event (expected; measure under a live session) |
|
|
177
|
+
|
|
178
|
+
**The deliberate validation asymmetry (E2).** To keep the disabled path free, a
|
|
179
|
+
`log` call validates its *control* arguments on every call — level, event-name
|
|
180
|
+
type/content/NUL, `keyword:`/`opcode:`/`activity:`/`related:` — but does **not**
|
|
181
|
+
look at field *values* until the event is actually enabled. Consequently a bad
|
|
182
|
+
field value (e.g. `nil`, an `Array`) raises `TypeError` **only when a session is
|
|
183
|
+
attached**. Use the `enabled?` guard idiom above if you need expensive arguments
|
|
184
|
+
built only when watched, and rely on the control-arg checks to keep the common
|
|
185
|
+
bugs deterministic regardless.
|
|
186
|
+
|
|
187
|
+
Field-value → ETW type mapping:
|
|
188
|
+
|
|
189
|
+
| Ruby value | ETW type | notes |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `String` (text encoding) | UTF-8 string | transcoded; embedded NUL or invalid bytes → `ArgumentError` |
|
|
192
|
+
| `String` (`Encoding::BINARY`) | binary | UINT16-counted; > 65535 bytes → `ArgumentError` |
|
|
193
|
+
| `Integer` | int64 | outside INT64 → `RangeError` |
|
|
194
|
+
| `Float` | double | |
|
|
195
|
+
| `true` / `false` | bool32 | |
|
|
196
|
+
| anything else (incl. `nil`) | — | `TypeError` |
|
|
197
|
+
|
|
198
|
+
## Errors
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
Winlog::Error < StandardError # base for what winlog raises itself
|
|
202
|
+
└── Winlog::Closed # any op (except close/closed?/inspect) on a closed provider
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Plain argument misuse raises Ruby's own `ArgumentError` / `TypeError` /
|
|
206
|
+
`RangeError`. There is deliberately **no `OSError`**: registration failure is a
|
|
207
|
+
queryable status (`registered?` is `false`, `registration_result` is the
|
|
208
|
+
HRESULT), and write failure is a `false` return — ETW is lossy by design and
|
|
209
|
+
Microsoft's guidance is to ignore both. The single OS call with no status channel,
|
|
210
|
+
`EventActivityIdControl` inside `Winlog.new_activity_id`, raises `Winlog::Error`
|
|
211
|
+
with the Win32 code (it cannot fail in practice).
|
|
212
|
+
|
|
213
|
+
## How it works
|
|
214
|
+
|
|
215
|
+
`winlog` is built on Microsoft's **`TraceLoggingDynamic.h`** (vendored into
|
|
216
|
+
`ext/winlog/`, © Microsoft, MIT). `TraceLoggingProvider.h` — the usual header —
|
|
217
|
+
requires compile-time-constant provider/event/field names, which a runtime Ruby
|
|
218
|
+
`log(level, event, **fields)` API cannot supply; `TraceLoggingDynamic.h` is
|
|
219
|
+
Microsoft's supported answer for runtime-dynamic events, and (being C++:
|
|
220
|
+
templates + `std::vector`) makes `winlog` the suite's first C++ extension.
|
|
221
|
+
|
|
222
|
+
Under the hood: `EventRegister` opens a `REGHANDLE` and `EventSetInformation`
|
|
223
|
+
attaches the provider traits; the provider GUID is the standard ETW name-hash of
|
|
224
|
+
the name (SHA-1 over a fixed signature + the UTF-16BE upper-invariant name, .NET
|
|
225
|
+
byte order — the same GUID EventSource, WPR `*name`, PerfView, and
|
|
226
|
+
`Winlog.guid_for` compute). Each `log` builds one self-describing event (metadata
|
|
227
|
+
+ packed field data) and writes it with `EventWriteTransfer`. `enabled?` reads
|
|
228
|
+
in-process state maintained by ETW's enable callback — no system call.
|
|
229
|
+
|
|
230
|
+
Honest limitations:
|
|
231
|
+
|
|
232
|
+
- **Lossy transport.** Even a `true` return does not guarantee a session retained
|
|
233
|
+
the event (buffers can be full); a `false` may mean disabled, unregistered, or
|
|
234
|
+
dropped.
|
|
235
|
+
- **64 KB event ceiling** (ETW), **65535-byte** binary fields (UINT16-counted),
|
|
236
|
+
and a ~32 KB metadata cap. Oversized events return `false`, never raise.
|
|
237
|
+
- **Emit-only.** No session control, no consumption/decoding, no Event Log
|
|
238
|
+
delivery. The five field types above are the whole v1 surface.
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
[MIT](LICENSE.txt). The vendored `ext/winlog/TraceLoggingDynamic.h` is
|
|
243
|
+
© Microsoft Corporation, also MIT.
|