Module: Winlog

Defined in:
lib/winlog.rb,
lib/winlog/version.rb,
ext/winlog/winlog.cpp

Overview

winlog โ€” structured, registration-free ETW TraceLogging telemetry for Ruby: events that cost nothing when nobody is listening.

Register a provider by name (auto name-hashed GUID โ€” no manifest, no message DLL, no registry write, no elevation) and emit runtime-dynamic, self-describing events decodable by WPA, PerfView, and the inbox logman/tracerpt with zero setup. When no ETW session has enabled the provider, a log call is gated in C before the fields are even looked at (~one Ruby method call). Emit-only: events go to ETW sessions, NOT to Event Viewer.

PROV = Winlog.open("MyCompany.MyApp")
PROV.log(:info, "Startup", version: "1.4.2", pid: Process.pid)  # => false (no session)
Winlog.open("Tool") { |p| p.log(:info, "Ran", args: ARGV.join(" ")) }

Defined Under Namespace

Classes: Closed, Error, Provider

Constant Summary collapse

LEVELS =

Symbolic levels -> winmeta values (tracelogging.md ยง5, verified table).

{
  critical: 1,   # WINEVENT_LEVEL_CRITICAL
  error:    2,   # WINEVENT_LEVEL_ERROR
  warn:     3,   # WINEVENT_LEVEL_WARNING
  info:     4,   # WINEVENT_LEVEL_INFO
  debug:    5,   # WINEVENT_LEVEL_VERBOSE
  verbose:  5    # alias of :debug
}.freeze
OPCODES =

Symbolic opcodes -> winmeta values. START/STOP bracket activities.

{ info: 0, start: 1, stop: 2 }.freeze
KEYWORD_RESERVED_MASK =

Keyword bits 48..63 are reserved by Microsoft; winlog REJECTS them.

0xFFFF_0000_0000_0000
VERSION =
"0.1.0"

Class Method Summary collapse

Class Method Details

.guid_for(name) ⇒ Object

The ETW name-hashed GUID for name WITHOUT registering anything. Pure Ruby (SHA-1 over the documented signature + UTF-16BE upcased name, .NET byte order). Same name rules as Winlog.open. Case-insensitive.

Winlog.guid_for("MyCompany.MyComponent")
# => "ce5fa4ea-ab00-5402-8b76-9f76ac858fb5"


81
82
83
84
85
86
87
88
89
90
91
# File 'lib/winlog.rb', line 81

def guid_for(name)
  name = validate_name!(name)
  bytes = Digest::SHA1.digest(
    GUID_SIGNATURE.pack("C*") + name.upcase.encode("UTF-16BE").b
  ).bytes[0, 16]
  bytes[7] = (bytes[7] & 0x0F) | 0x50
  [bytes[0, 4].reverse, bytes[4, 2].reverse, bytes[6, 2].reverse,
   bytes[8, 2], bytes[10, 6]]
    .map { |part| part.map { |b| format("%02x", b) }.join }
    .join("-")
end

.level_for(level) ⇒ Object

Map a level (Symbol in LEVELS or Integer 1..255) to its winmeta Integer. Raises ArgumentError on an unknown Symbol or out-of-range Integer.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/winlog.rb', line 95

def level_for(level)
  case level
  when Symbol
    LEVELS.fetch(level) do
      raise ArgumentError, "unknown level #{level.inspect} " \
                           "(known: #{LEVELS.keys.inspect})"
    end
  when Integer
    unless level.between?(1, 255)
      raise ArgumentError, "level must be 1..255, got #{level.inspect}"
    end
    level
  else
    raise ArgumentError,
          "level must be a Symbol or Integer, got #{level.inspect}"
  end
end

.new_activity_idObject

Winlog.new_activity_id -> 36-char lowercase hyphenated GUID String. Wraps EventActivityIdControl(CREATE_ID), which only GENERATES an id and never reads or writes the calling thread's implicit activity id (E17 fiber-safety). Raises Winlog::Error only if the OS call fails (practically never).



744
745
746
747
748
749
750
751
752
753
754
755
756
757
# File 'ext/winlog/winlog.cpp', line 744

static VALUE
winlog_new_activity_id(VALUE self)
{
    GUID g;
    ULONG status;

    memset(&g, 0, sizeof g);
    status = EventActivityIdControl(EVENT_ACTIVITY_CTRL_CREATE_ID, &g);
    if (status != ERROR_SUCCESS)
        rb_raise(eError,
                 "winlog: EventActivityIdControl(CREATE_ID) failed (error %lu)",
                 (unsigned long)status);
    return guid_to_rb(g);
}

.opcode_for(opcode) ⇒ Object

Map an opcode (Symbol in OPCODES or Integer 0..255) to its Integer value.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/winlog.rb', line 114

def opcode_for(opcode)
  case opcode
  when Symbol
    OPCODES.fetch(opcode) do
      raise ArgumentError, "unknown opcode #{opcode.inspect} " \
                           "(known: #{OPCODES.keys.inspect})"
    end
  when Integer
    unless opcode.between?(0, 255)
      raise ArgumentError, "opcode must be 0..255, got #{opcode.inspect}"
    end
    opcode
  else
    raise ArgumentError,
          "opcode must be a Symbol or Integer, got #{opcode.inspect}"
  end
end

.open(name) ⇒ Object

Register a TraceLogging provider under name and return a Winlog::Provider. Block form yields the provider and ensure-closes it, returning the block value. Registration is system-wide registration-FREE (no manifest, registry, or elevation). On a rare EventRegister failure NO exception is raised โ€” the provider is a benign no-op; check #registered? (MS guidance).

Winlog.open("X")                     # => #<Winlog::Provider X {guid}>
Winlog.open("X") { |p| p.log(...) }  # => block value; provider closed after


64
65
66
67
68
69
70
71
72
73
# File 'lib/winlog.rb', line 64

def open(name)
  prov = Provider.new(name)
  return prov unless block_given?

  begin
    yield prov
  ensure
    prov.close
  end
end