Class: LittleGhost::Tracing::OpenTelemetry

Inherits:
Instrumentation::Subscriber show all
Defined in:
lib/little_ghost/tracing/open_telemetry.rb

Overview

OpenTelemetry turns LittleGhost lifecycle notifications into GenAI spans and events. It brings agents, model calls, tools, workflows, and token usage into the same traces as the rest of an application.

LittleGhost.configure do |config|
config.instrument LittleGhost::Tracing::OpenTelemetry.new
end

LittleGhost depends only on opentelemetry-api. Install and configure the desired SDK, processors, and exporters before registering this subscriber.

Content and trust

Prompts, responses, messages, tool arguments, and exception content are omitted by default. They appear only when Instrumentation has an explicit, scrubbed Support::ContentCapture policy.

Constant Summary collapse

ATTRIBUTE_NAMES =
{
  agent_id: "gen_ai.agent.id",
  agent_name: "gen_ai.agent.name",
  model_id: "gen_ai.request.model",
  model_provider: "gen_ai.provider.name",
  response_id: "gen_ai.response.id",
  response_model: "gen_ai.response.model",
  finish_reasons: "gen_ai.response.finish_reasons",
  input_tokens: "gen_ai.usage.input_tokens",
  output_tokens: "gen_ai.usage.output_tokens",
  cache_read_tokens: "gen_ai.usage.cache_read.input_tokens",
  cache_write_tokens: "gen_ai.usage.cache_creation.input_tokens",
  reasoning_tokens: "gen_ai.usage.reasoning.output_tokens",
  time_to_first_token: "gen_ai.response.time_to_first_chunk",
  session_id: "gen_ai.conversation.id",
  tool_name: "gen_ai.tool.name",
  tool_type: "gen_ai.tool.type",
  tool_call_id: "gen_ai.tool.call.id",
  workflow_name: "gen_ai.workflow.name",
  assembly_id: "little_ghost.assembly.id",
  assembly_kind: "little_ghost.assembly.kind",
  http_response_status_code: "http.response.status_code",
  error_class: "error.type",
  error_type: "error.type"
}.freeze
OPERATIONS =

:nodoc:

{
  agent: "invoke_agent",
  agent_turn: "agent_turn",
  model: "chat",
  run: "invoke_agent",
  runtime: "runtime_startup",
  session_store: "session_store",
  subagent: "invoke_agent",
  assembly_step: "execute_assembly_step",
  tool: "execute_tool",
  workflow: "invoke_workflow",
  swarm: "invoke_swarm",
  graph: "invoke_graph",
  assembly: "invoke_assembly"
}.freeze
REQUEST_SETTING_ATTRIBUTES =

:nodoc:

{
  frequency_penalty: "gen_ai.request.frequency_penalty",
  max_tokens: "gen_ai.request.max_tokens",
  max_output_tokens: "gen_ai.request.max_tokens",
  presence_penalty: "gen_ai.request.presence_penalty",
  seed: "gen_ai.request.seed",
  temperature: "gen_ai.request.temperature",
  top_k: "gen_ai.request.top_k",
  top_p: "gen_ai.request.top_p"
}.freeze
CONTENT_KEYS =

:nodoc:

%w[arguments content input input_text message output output_text prompt response text].freeze
SENSITIVE_KEY =

:nodoc:

/(authorization|api[_-]?key|credential|password|secret|(?:^|[_-])token(?:$|[_-])|cookie|private[_-]?key)/i
MAX_ATTRIBUTE_LENGTH =

:nodoc:

1_024

Instance Method Summary collapse

Constructor Details

#initialize(tracer: nil) ⇒ OpenTelemetry

Uses tracer or the global tracer provider.



81
82
83
84
85
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 81

def initialize(tracer: nil)
  @tracer = tracer
  @entries = {}
  @mutex = Mutex.new
end

Instance Method Details

#emit(name, attributes) ⇒ Object

Adds a structured event to the correlated active span.



98
99
100
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 98

def emit(name, attributes)
  add_event(name.to_sym, prepare_attributes(:emit, name.to_sym, attributes))
end

#finish(name, attributes) ⇒ Object

Finishes the span correlated by operation ID.



93
94
95
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 93

def finish(name, attributes)
  finish_span(name.to_sym, prepare_attributes(:finish, name.to_sym, attributes))
end

#flush(timeout: nil) ⇒ Object

Flushes through the configured tracer provider when supported.



135
136
137
138
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 135

def flush(timeout: nil)
  provider = ::OpenTelemetry.tracer_provider
  provider.force_flush(timeout:) if provider.respond_to?(:force_flush)
end

#shutdown(timeout: nil) ⇒ Object

Finishes spans still owned by this subscriber.



141
142
143
144
145
146
147
148
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 141

def shutdown(timeout: nil)
  spans = @mutex.synchronize do
    current = @entries.values.map { |entry| entry.fetch(:span) }.uniq
    @entries = {}
    current
  end
  spans.each(&:finish)
end

#start(name, attributes) ⇒ Object

Starts a span for a lifecycle operation.



88
89
90
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 88

def start(name, attributes)
  start_span(name.to_sym, prepare_attributes(:start, name.to_sym, attributes))
end

#trace_context(operation_id: nil) ⇒ Object

Supplies W3C propagation fields for an active operation when known.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 103

def trace_context(operation_id: nil, **)
  span = @mutex.synchronize { @entries.dig(operation_id, :span) }
  context = span&.context
  return {} unless context&.valid?

  carrier = {}
  trace_context_propagator.inject(
    carrier,
    context: ::OpenTelemetry::Trace.context_with_span(span)
  )
  carrier.slice("traceparent", "tracestate").merge(trace_id: context.hex_trace_id)
rescue
  context&.valid? ? {trace_id: context.hex_trace_id} : {}
end

#with_span(name, attributes:, parent_operation_id: nil) ⇒ Object

Wraps a block in a standalone internal span. Prefer lifecycle Instrumentation methods for normal framework operations.



120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 120

def with_span(name, attributes:, parent_operation_id: nil)
  parent = @mutex.synchronize { @entries[parent_operation_id] }
  parent_context = parent && ::OpenTelemetry::Trace.context_with_span(parent.fetch(:span))
  options = {kind: :internal, attributes:}
  options[:with_parent] = parent_context if parent_context
  span = tracer.start_span(name, **options)
  yield span
rescue => error
  span.status = ::OpenTelemetry::Trace::Status.error(error.class.name) if span
  raise
ensure
  span&.finish
end