# frozen_string_literal: true # Copyright The OpenTelemetry Authors # # SPDX-License-Identifier: Apache-2.0 require 'opentelemetry-api' require_relative 'version' module OpenTelemetry module Instrumentation module RSpec # An RSpec Formatter that outputs Otel spans class Formatter attr_reader :output ::RSpec::Core::Formatters.register self, :example_started, :example_finished, :example_group_started, :example_group_finished, :start, :stop @clock = Time.method(:now) def self.current_timestamp @clock.call end def initialize(output = StringIO.new, tracer_provider = OpenTelemetry.tracer_provider) @spans_and_tokens = [] @output = output @tracer_provider = tracer_provider end def tracer @tracer ||= @tracer_provider.tracer('OpenTelemetry::Instrumentation::RSpec', OpenTelemetry::Instrumentation::RSpec::VERSION) end def current_timestamp self.class.current_timestamp end def start(notification) span = tracer.start_span('RSpec suite', start_timestamp: current_timestamp) track_span(span) end def stop(notification) pop_and_finalize_span end def example_group_started(notification) description = notification.group.description span = tracer.start_span(description, start_timestamp: current_timestamp) track_span(span) end def example_group_finished(notification) pop_and_finalize_span end def example_started(notification) example = notification.example attributes = { 'rspec.example.location' => example.location.to_s, 'rspec.example.full_description' => example.full_description.to_s, 'rspec.example.described_class' => example.metadata[:described_class].to_s } span = tracer.start_span(example.description, attributes: attributes, start_timestamp: current_timestamp) track_span(span) end def example_finished(notification) pop_and_finalize_span do |span| result = notification.example.execution_result span.set_attribute('rspec.example.result', result.status.to_s) add_exception_and_failures(span, result.exception) end end def add_exception_and_failures(span, exception) return if exception.nil? exception_message = strip_console_codes(exception.message) span.status = OpenTelemetry::Trace::Status.error(exception_message) span.set_attribute('rspec.example.failure_message', exception_message) if exception.is_a? ::RSpec::Expectations::ExpectationNotMetError if exception.is_a? ::RSpec::Core::MultipleExceptionError exception.all_exceptions.each do |error| record_stripped_exception(span, error) end span.set_attribute('rspec.example.failure_message', multiple_failure_message(exception)) if exception.failures.any? else span.record_exception(exception, attributes: { 'exception.message' => exception_message }) end end def track_span(span) token = OpenTelemetry::Context.attach( OpenTelemetry::Trace.context_with_span(span) ) @spans_and_tokens.unshift([span, token]) end def pop_and_finalize_span span, token = *@spans_and_tokens.shift return unless span.recording? yield span if block_given? span.finish(end_timestamp: current_timestamp) OpenTelemetry::Context.detach(token) end def record_stripped_exception(span, error) error_message = strip_console_codes(error.message) span.record_exception(error, attributes: { 'exception.message' => error_message }) end def strip_console_codes(string) string.gsub(/\e\[([;\d]+)?m/, '') end def multiple_failure_message(exception) exception.failures.map(&:message).map { |x| strip_console_codes(x) }.join("\n\n") end end end end end