Class: RailsErrorDashboard::Integrations::LlmSpanProcessor
- Inherits:
-
Object
- Object
- RailsErrorDashboard::Integrations::LlmSpanProcessor
- Defined in:
- lib/rails_error_dashboard/integrations/llm_span_processor.rb
Overview
OpenTelemetry SpanProcessor that maps GenAI semantic-convention spans into LLM breadcrumbs. Registered with ‘OpenTelemetry.tracer_provider` when the host app already runs OTel (ruby_llm, thoughtbot/instrumentation, etc. all emit GenAI spans automatically).
IMPORTANT — does NOT subclass ::OpenTelemetry::SDK::Trace::SpanProcessor. That would NameError at file-load time on hosts without the SDK. Ruby’s OTel SDK accepts any duck-typed processor — name + arity is the contract.
Reads attribute keys per the GenAI semconv (current + deprecated aliases). Spec: opentelemetry.io/docs/specs/semconv/gen-ai/
HOST APP SAFETY:
-
on_finish wraps the entire body in rescue StandardError => nil
-
No work happens unless enable_llm_observability AND enable_breadcrumbs
-
Non-GenAI spans return immediately (cheapest possible path)
-
Never raises, never blocks the tracer pipeline
Constant Summary collapse
- PROVIDER_KEYS =
Attribute keys — current GenAI semconv, with deprecated aliases.
[ "gen_ai.provider.name", "gen_ai.system" ].freeze
- MODEL_KEYS =
[ "gen_ai.response.model", "gen_ai.request.model" ].freeze
- INPUT_TOKEN_KEYS =
[ "gen_ai.usage.input_tokens", "gen_ai.usage.prompt_tokens" ].freeze
- OUTPUT_TOKEN_KEYS =
[ "gen_ai.usage.output_tokens", "gen_ai.usage.completion_tokens" ].freeze
- TOOL_NAME_KEY =
"gen_ai.tool.name"- OPERATION_KEY =
"gen_ai.operation.name"- ERROR_TYPE_KEY =
"error.type"
Class Method Summary collapse
-
.register! ⇒ Boolean
Idempotently register a single shared LlmSpanProcessor instance with the host’s OpenTelemetry tracer provider.
- .registered? ⇒ Boolean
-
.reset! ⇒ Object
Test hook — clear the registered flag so re-registration is possible in a fresh spec example.
Instance Method Summary collapse
-
#force_flush(timeout: nil) ⇒ Object
OTel SDK Export::SUCCESS == 0.
-
#on_finish(span) ⇒ Object
Required SpanProcessor interface.
-
#on_start(_span, _parent_context) ⇒ Object
Required SpanProcessor interface — no-op.
- #shutdown(timeout: nil) ⇒ Object
Class Method Details
.register! ⇒ Boolean
Idempotently register a single shared LlmSpanProcessor instance with the host’s OpenTelemetry tracer provider. Called from Engine ‘after_initialize` when `enable_llm_observability` is on.
Returns false (and does nothing) when:
-
OTel SDK isn’t loaded (‘Integrations::OTel.available?` is false)
-
‘enable_llm_observability` is off
-
The active tracer provider is the default ‘ProxyTracerProvider` (SDK loaded but `OpenTelemetry::SDK.configure` never called) —detected by absence of `add_span_processor`
-
Already registered in this process (Spring reload safety)
-
‘add_span_processor` raises (host app safety — never crash boot)
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 38 def register! return false if @registered return false unless RailsErrorDashboard.configuration.enable_llm_observability return false unless OTel.available? provider = ::OpenTelemetry.tracer_provider return false unless provider.respond_to?(:add_span_processor) provider.add_span_processor(new) @registered = true true rescue StandardError => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] LlmSpanProcessor.register! failed: #{e.}") false end |
.registered? ⇒ Boolean
62 63 64 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 62 def registered? @registered == true end |
.reset! ⇒ Object
Test hook — clear the registered flag so re-registration is possible in a fresh spec example. Does NOT remove the processor from the tracer provider (OTel SDK offers no symmetric ‘remove_span_processor`).
57 58 59 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 57 def reset! @registered = false end |
Instance Method Details
#force_flush(timeout: nil) ⇒ Object
OTel SDK Export::SUCCESS == 0. Hardcoded so this file loads without OTel.
106 107 108 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 106 def force_flush(timeout: nil) 0 end |
#on_finish(span) ⇒ Object
Required SpanProcessor interface. Must never raise.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 83 def on_finish(span) return unless RailsErrorDashboard.configuration.enable_llm_observability return unless RailsErrorDashboard.configuration. attrs = safe_attributes(span) return if attrs.empty? return unless gen_ai_span?(attrs) event = build_event(span, attrs) category = event.tool_call? ? "llm_tool" : "llm" Services::BreadcrumbCollector.add( category, event., duration_ms: event.duration_ms, metadata: event. ) rescue StandardError => e RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] LlmSpanProcessor.on_finish failed: #{e.}") nil end |
#on_start(_span, _parent_context) ⇒ Object
Required SpanProcessor interface — no-op. We only act when the span is fully populated (attributes/timestamps/status), which is on_finish.
78 79 80 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 78 def on_start(_span, _parent_context) nil end |
#shutdown(timeout: nil) ⇒ Object
110 111 112 |
# File 'lib/rails_error_dashboard/integrations/llm_span_processor.rb', line 110 def shutdown(timeout: nil) 0 end |