Module: RailsErrorDashboard::Integrations::Tracer

Defined in:
lib/rails_error_dashboard/integrations/tracer.rb

Overview

OpenTelemetry tracer façade for the outbound direction — emits spans from the gem’s capture path so host operators can audit error tracking latency from their existing Datadog/Honeycomb/Jaeger pipeline.

Symmetric counterpart to LlmSpanProcessor (which is INBOUND — pulls OTel spans INTO RED breadcrumbs). This module pushes OUTBOUND: gem operations OUT to the host’s tracer provider.

Designed to be called from hot paths unconditionally. When OTel is absent or the feature is off, ‘in_span` runs the block with a no-op span object — call sites do NOT branch on availability.

HOST APP SAFETY (HOST_APP_SAFETY.md):

  • No-op when ‘enable_otel_export = false` OR OTel API not loaded

  • Per-span-kind opt-in/out via config.otel_spans

  • Tracer instance memoized per-process (rebuild on ‘reset!`)

  • Every public method hard-rescues — never raises into host code

  • Block return value is preserved even when tracer errors

  • Exceptions raised by the block re-raise after being recorded

Configuration:

config.enable_otel_export = true     # master switch (default false)
config.otel_service_name = "my-app"  # falls back to application_name
config.otel_spans = [:capture, :breadcrumbs, :health, :notifications]

Usage from capture-path code:

Tracer.in_span("capture_error", kind: :capture,
               attributes: { error_type: exception.class.name }) do |span|
  # ... do the work ...
  span&.set_attribute("rails_error_dashboard.error_id", error.id)
end

The span object yielded may be the real OTel span or a NoopSpan. Always use safe-nav (‘span&.`) or guard with `span.respond_to?(:…)`.

Defined Under Namespace

Classes: NoopSpan

Constant Summary collapse

INSTRUMENTATION_NAME =
"rails_error_dashboard"
ALL_SPAN_KINDS =
%i[capture breadcrumbs health notifications].freeze
NOOP_SPAN =
NoopSpan.new.freeze

Class Method Summary collapse

Class Method Details

.emit?(kind) ⇒ Boolean

Returns true when the OTel API is loaded AND the master switch is on AND the given span kind is in the enabled set. Cheap — called on every in_span invocation, including in the hot path.

Parameters:

  • kind (Symbol)

Returns:

  • (Boolean)


97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rails_error_dashboard/integrations/tracer.rb', line 97

def emit?(kind)
  config = RailsErrorDashboard.configuration
  return false unless config.enable_otel_export
  return false unless otel_api_loaded?

  enabled_kinds = config.otel_spans
  return false if enabled_kinds.nil? || enabled_kinds.empty?
  enabled_kinds.include?(kind)
rescue StandardError
  false
end

.in_span(name, kind: :capture, attributes: {}) {|span| ... } ⇒ Object

Yields a span object to the block. Returns the block’s return value. Records exceptions raised by the block as span events and re-raises.

Parameters:

  • name (String)

    short span name (will be namespaced with INSTRUMENTATION_NAME)

  • kind (Symbol) (defaults to: :capture)

    one of ALL_SPAN_KINDS — checked against config.otel_spans

  • attributes (Hash<String,Object>) (defaults to: {})

    attached to the span at creation

Yield Parameters:

  • span (NoopSpan, ::OpenTelemetry::Trace::Span)

    real or no-op

Returns:

  • (Object)

    whatever the block returns



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rails_error_dashboard/integrations/tracer.rb', line 67

def in_span(name, kind: :capture, attributes: {})
  return yield(NOOP_SPAN) unless emit?(kind)

  tr = tracer
  return yield(NOOP_SPAN) unless tr

  full_name = "#{INSTRUMENTATION_NAME}.#{name}"
  merged = base_attributes.merge(safe_stringify(attributes))

  tr.in_span(full_name, attributes: merged) do |span|
    begin
      yield span
    rescue StandardError => e
      record_block_exception(span, e)
      raise
    end
  end
rescue StandardError => e
  # Tracer internals failed (e.g. OTel SDK threw on add_span). Fall back
  # to running the block with a no-op so the host app never sees a crash
  # caused by the tracer.
  Logger.debug("[RailsErrorDashboard] Tracer.in_span(#{name.inspect}) failed: #{e.class}: #{e.message}")
  yield NOOP_SPAN
end

.otel_api_loaded?Boolean

Returns true if the OTel API gem is loaded (NOT the SDK). The API alone is enough — it ships a ProxyTracerProvider that’s a no-op when no SDK is configured, which is the behavior we want.

Returns:

  • (Boolean)


119
120
121
122
123
124
125
# File 'lib/rails_error_dashboard/integrations/tracer.rb', line 119

def otel_api_loaded?
  return @otel_api_loaded unless @otel_api_loaded.nil?
  @otel_api_loaded = !!(defined?(::OpenTelemetry) &&
                        ::OpenTelemetry.respond_to?(:tracer_provider))
rescue StandardError
  @otel_api_loaded = false
end

.reset!Object

Reset memoized tracer + availability — for spec isolation only.



110
111
112
113
# File 'lib/rails_error_dashboard/integrations/tracer.rb', line 110

def reset!
  @tracer = nil
  @otel_api_loaded = nil
end