Module: Brute::Contrib::Otel

Defined in:
lib/brute/contrib/otel.rb

Overview

OpenTelemetry for a turn, as hooks rather than middleware.

Brute::Contrib::Otel.subscribe(agent)
agent.start("what changed?")

These are pure observers: they read the turn and write spans, and never touch what the agent does. That is why they are subscribers and not layers — a middleware earns its place in the stack by being able to alter or skip what is below it, and telemetry never should.

Every event it needs already exists:

turn_start / turn_end   the span itself
after_llm               token usage, from env[:metadata][:last_llm_usage]
before_tool / after_tool a span event per tool call and result

The tracer is injectable; without one it asks OpenTelemetry, and when the SDK is not loaded subscribe does nothing at all.

Note the span is opened and finished by hand rather than through tracer.in_span, which wants a block around the work. OpenTelemetry's current context is fiber-local, so this does not attach the span as current: under Async a turn's LLM call and its tools may run in other fibers, and an attached-but-never-detached context leaks across them.

Constant Summary collapse

SPAN_NAME =
"brute.turn"

Class Method Summary collapse

Class Method Details

.default_tracerObject



53
54
55
56
57
# File 'lib/brute/contrib/otel.rb', line 53

def default_tracer
  return nil unless defined?(::OpenTelemetry)

  ::OpenTelemetry.tracer_provider.tracer("brute", Brute::VERSION)
end

.subscribe(agent, tracer: default_tracer) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/brute/contrib/otel.rb', line 37

def subscribe(agent, tracer: default_tracer)
  return agent if tracer.nil?

  agent
    .on(Brute::Hooks::TURN_START_EVENT) { |env| start(env, tracer) }
    .on(Brute::Hooks::TURN_END_EVENT) { |env| finish(env) }
    .on(Brute::Hooks::AFTER_LLM_EVENT) { |env| record_usage(env) }
    .on(Brute::Hooks::BEFORE_TOOL_EVENT) { |env, call| tool_called(env, call) }
    .on(Brute::Hooks::AFTER_TOOL_EVENT) { |env, call| tool_returned(env, call) }
    .on(Brute::Hooks::LLM_FAILURE_EVENT) { |env| failed(env) }
    .on(Brute::Hooks::DURATION_EVENT) { |env, started, finished, layer| layer_finished(env, started, finished, layer) }
    .on(Brute::Hooks::TURN_DURATION_EVENT) { |env, started, finished| timed(env, "turn", finished - started) }
    .on(Brute::Hooks::LLM_DURATION_EVENT) { |env, started, finished| timed(env, "llm", finished - started) }
    .on(Brute::Hooks::TOOL_DURATION_EVENT) { |env, started, finished, call| timed(env, "tool", finished - started, "tool.name" => call[:name].to_s) }
end