Module: Braintrust::Trace::SpanOrigin

Included in:
SpanExporter
Defined in:
lib/braintrust/trace/span_origin.rb

Overview

Span origin provenance decoration.

This is a behavior, not a type. Prepend it onto any exporter whose export(span_data, timeout:) it can super into, and every exported SpanData gains a braintrust.context_json attribute carrying span origin (SDK name/version, instrumentation scope, environment).

Because it only ever touches the SpanData copies handed to this exporter, the enrichment is invisible to any other exporter sharing the same tracer provider - there is no global patch and nothing leaks onto a customer's other OTel traces.

Constant Summary collapse

CONTEXT_JSON_ATTR_KEY =
"braintrust.context_json"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.attributes_with_origin(attributes, instrumentation_name:, environment:) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/braintrust/trace/span_origin.rb', line 55

def self.attributes_with_origin(attributes, instrumentation_name:, environment:)
  context = parse_context_json(attributes[CONTEXT_JSON_ATTR_KEY])
  span_origin = context["span_origin"].is_a?(Hash) ? context["span_origin"] : {}

  span_origin_changed = false
  unless span_origin.key?("name")
    span_origin["name"] = "braintrust.sdk.ruby"
    span_origin_changed = true
  end
  unless span_origin.key?("version")
    span_origin["version"] = Braintrust::VERSION
    span_origin_changed = true
  end
  unless span_origin.key?("instrumentation")
    span_origin["instrumentation"] = {"name" => instrumentation_name}
    span_origin_changed = true
  end
  if environment && !span_origin.key?("environment")
    span_origin["environment"] = environment
    span_origin_changed = true
  end

  context_changed = context["span_origin"] != span_origin || span_origin_changed
  return attributes unless context_changed

  context["span_origin"] = span_origin
  attributes.merge(CONTEXT_JSON_ATTR_KEY => JSON.generate(context))
end

.enrich(span_data, environment:) ⇒ OpenTelemetry::SDK::Trace::SpanData

Enrich a single SpanData with span origin provenance. Mutates the SpanData in place (replacing its frozen attributes hash with a new frozen hash - it never mutates the shared hash) and returns it.

Parameters:

  • span_data (OpenTelemetry::SDK::Trace::SpanData)
  • environment (Hash, nil)

    process environment (name:) or nil

Returns:

  • (OpenTelemetry::SDK::Trace::SpanData)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/braintrust/trace/span_origin.rb', line 40

def self.enrich(span_data, environment:)
  attributes = span_data.attributes || {}
  enriched_attributes = attributes_with_origin(
    attributes,
    instrumentation_name: instrumentation_name(span_data),
    environment: environment
  )

  return span_data if enriched_attributes.equal?(attributes)

  span_data.attributes = enriched_attributes.freeze
  span_data.total_recorded_attributes = enriched_attributes.length
  span_data
end

.instrumentation_name(span) ⇒ Object



93
94
95
96
97
98
99
100
101
102
# File 'lib/braintrust/trace/span_origin.rb', line 93

def self.instrumentation_name(span)
  if span.respond_to?(:instrumentation_scope) && span.instrumentation_scope&.respond_to?(:name)
    return span.instrumentation_scope.name
  end
  if span.respond_to?(:instrumentation_library) && span.instrumentation_library&.respond_to?(:name)
    return span.instrumentation_library.name
  end

  "braintrust-ruby"
end

.parse_context_json(raw) ⇒ Object



84
85
86
87
88
89
90
91
# File 'lib/braintrust/trace/span_origin.rb', line 84

def self.parse_context_json(raw)
  return {} unless raw.is_a?(String) && !raw.strip.empty?

  parsed = JSON.parse(raw)
  parsed.is_a?(Hash) ? parsed : {}
rescue JSON::ParserError
  {}
end

Instance Method Details

#export(span_data, timeout: nil) ⇒ Integer

Exporter behavior: enrich each SpanData with span origin before export.

Parameters:

  • span_data (Array<OpenTelemetry::SDK::Trace::SpanData>)

Returns:

  • (Integer)

    export result from the wrapped exporter



26
27
28
29
30
31
32
# File 'lib/braintrust/trace/span_origin.rb', line 26

def export(span_data, timeout: nil)
  # Environment is process-global and stable; read it once per batch
  # rather than once per span. It is cheap (ENV reads only).
  environment = Internal::Env.detect_environment
  enriched = span_data.map { |sd| SpanOrigin.enrich(sd, environment: environment) }
  super(enriched, timeout: timeout)
end