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",
  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",
  tool: "execute_tool",
  workflow: "invoke_workflow"
}.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.



75
76
77
78
79
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 75

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.



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

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.



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

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.



129
130
131
132
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 129

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.



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

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.



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

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.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 97

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.



114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/little_ghost/tracing/open_telemetry.rb', line 114

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