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 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.