class RSpec::Trace::Consumer

Public Class Methods

new(input, traceparent_string = nil) click to toggle source
# File lib/rspec/trace/consumer.rb, line 10
def initialize(input, traceparent_string = nil)
  @input = input
  @tracer_provider = OpenTelemetry.tracer_provider
  @tracer_provider.sampler = OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON
  @tracer = @tracer_provider.tracer("rspec-trace-formatter", RSpec::Trace::VERSION)
  @spans = []
  @contexts = [load_context_from_traceparent(traceparent_string)]
  @tokens = []
end

Public Instance Methods

run() click to toggle source
# File lib/rspec/trace/consumer.rb, line 20
def run
  @input.each_line do |line|
    next if line.strip.empty?

    begin
      event = parse_event(line)
    rescue
      warn "invalid line: #{line}"
      next
    end

    case event[:event].to_sym
    when :initiated
      root_span_name = ENV.fetch("RSPEC_TRACE_FORMATTER_ROOT_SPAN_NAME", "rspec")
      create_span(name: root_span_name, timestamp: event[:timestamp])
      create_span(name: "examples loading", timestamp: event[:timestamp]) do |span|
        span.add_attributes("rspec.type" => "loading")
      end
    when :start
      complete_span(timestamp: event[:timestamp])
      create_span(name: "examples running", timestamp: event[:timestamp]) do |span|
        add_attributes_to_span(
          span: span,
          attributes: {count: event[:count], type: "suite"},
          attribute_prefix: "rspec"
        )
      end
    when :example_group_started
      create_span(name: event.dig(:group, :description), timestamp: event[:timestamp]) do |span|
        add_attributes_to_span(
          span: span,
          attributes: event[:group].merge(type: "example_group"),
          attribute_prefix: "rspec",
          exclude_attributes: [:description]
        )
      end
    when :example_group_finished
      complete_span(timestamp: event[:timestamp])
    when :example_started
      create_span(name: event.dig(:example, :description), timestamp: event[:timestamp]) do |span|
        add_attributes_to_span(
          span: span,
          attributes: event[:example].merge(type: "example"),
          attribute_prefix: "rspec",
          exclude_attributes: [:description]
        )
      end
    when :example_passed
      complete_span(timestamp: event[:timestamp]) do |span|
        add_attributes_to_span(
          span: span,
          attributes: event.dig(:example, :result),
          attribute_prefix: "rspec.result"
        )
      end
    when :example_pending
      complete_span(timestamp: event[:timestamp]) do |span|
        add_attributes_to_span(
          span: span,
          attributes: event.dig(:example, :result),
          attribute_prefix: "rspec.result"
        )
      end
    when :example_failed
      complete_span(timestamp: event[:timestamp]) do |span|
        add_attributes_to_span(
          span: span,
          attributes: event.dig(:example, :result),
          attribute_prefix: "rspec.result"
        )
        event_attributes = {
          "exception.type" => event.dig(:exception, :type),
          "exception.message" => event.dig(:exception, :message),
          "exception.stacktrace" => event.dig(:exception, :backtrace)
        }
        span.add_event("exception", attributes: event_attributes, timestamp: event[:timestamp])
        span.status = OpenTelemetry::Trace::Status.error
      end
    when :stop
      complete_span(timestamp: event[:timestamp]) until @spans.empty?
      @tracer_provider.force_flush
      exit
    end
  end
end

Private Instance Methods

add_attributes_to_span(span:, attributes:, attribute_prefix: nil, exclude_attributes: []) click to toggle source
# File lib/rspec/trace/consumer.rb, line 141
def add_attributes_to_span(span:, attributes:, attribute_prefix: nil, exclude_attributes: [])
  attributes_for_span = attributes.map do |key, value|
    next if value.nil? || exclude_attributes.include?(key)

    full_key = attribute_prefix ? "#{attribute_prefix}.#{key}" : key.to_s
    [full_key, value]
  end.compact.to_h

  span.add_attributes(attributes_for_span)
end
complete_span(timestamp:) { |span| ... } click to toggle source
# File lib/rspec/trace/consumer.rb, line 132
def complete_span(timestamp:)
  @spans.pop.tap do |span|
    yield span if block_given?
    span.finish(end_timestamp: timestamp)
    @contexts.pop
    OpenTelemetry::Context.detach(@tokens.pop)
  end
end
create_span(name:, timestamp:) { |span| ... } click to toggle source
# File lib/rspec/trace/consumer.rb, line 122
def create_span(name:, timestamp:)
  @tracer.start_span(name, start_timestamp: timestamp, with_parent: @contexts.last).tap do |span|
    yield span if block_given?
    @spans.push(span)
    new_context = OpenTelemetry::Trace.context_with_span(span, parent_context: @contexts.last)
    @contexts.push(new_context)
    @tokens.push(OpenTelemetry::Context.attach(new_context))
  end
end
load_context_from_traceparent(traceparent_string) click to toggle source
# File lib/rspec/trace/consumer.rb, line 108
def load_context_from_traceparent(traceparent_string)
  return OpenTelemetry::Context.current unless traceparent_string

  OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator.extract(
    {"traceparent" => traceparent_string}
  )
end
parse_event(line) click to toggle source
# File lib/rspec/trace/consumer.rb, line 116
def parse_event(line)
  event = JSON.parse(line, symbolize_names: true)
  event[:timestamp] = Time.parse(event[:timestamp])
  event
end