Skip to content

Adding custom SpanProcessors considered impossible without private method callsΒ #1933

@0robustus1

Description

@0robustus1

We've recently looked into adding a custom SpanProcessor to most of our applications. We need integrate with a particular vendor that doesn't fully/truly support OpenTelemetry specifications and relies on setting custom attributes (e.g. operation.name, resource.name and even adjusting the Span Kind (to avoid "internal")).
The SpanProcessor itself is easily written, but adding it to an otherwise default setup of OpenTelemetry in Ruby is not:

require 'rake'
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

class RakeTaskSpanProcessor < ::OpenTelemetry::SDK::Trace::SpanProcessor
  def on_start(span, _parent_context)
    return unless span.name =~ /^rake\.(invoke|execute)/

    task = span.attributes['rake.task']

    span.set_attribute('operation.name', span.name)
    span.set_attribute('resource.name', task || 'unknown')
  end
end

::OpenTelemetry::SDK.configure do |c|
  c.add_span_processor(RakeTaskSpanProcessor.new)
  c.use_all
end

desc 'Dummy task'
task :dummy do
  puts 'foo'
end

# write to a file: Rakefile.custom
# run: env OTEL_RESOURCE_ATTRIBUTES="service.namespace=rake-tester,service.name=test" rake -f Rakefile.custom dummy

Adding a SpanProcessor in this way will result in no exporting SpanProcessors being added. This seems to be due to https://github.com/open-telemetry/opentelemetry-ruby/blob/opentelemetry-sdk/v1.9.0/sdk/lib/opentelemetry/sdk/configurator.rb#L180-L183 which only adds the default config from the environment if no other SpanProcessors are configured. That seems to be due to Exporters being implemented as SpanProcessors themselves with no other clearly identifiable markers (e.g. a shared parent class or a specific attribute). So while it is unfortunate that just "adding" a SpanProcessor breaks default behaviour it does seem consistent.

However, there is no built in way for somebody who adds a SpanProcessor to opt in to also getting the default SpanProcessors (exporters), because wrapped_exporters_from_env is a private method.

In our testing the following setup works as intended:

require 'rake'
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

class RakeTaskSpanProcessor < ::OpenTelemetry::SDK::Trace::SpanProcessor
  def on_start(span, _parent_context)
    return unless span.name =~ /^rake\.(invoke|execute)/

    task = span.attributes['rake.task']

    span.set_attribute('operation.name', span.name)
    span.set_attribute('resource.name', task || 'unknown')
  end
end

::OpenTelemetry::SDK.configure do |c|
  c.add_span_processor(RakeTaskSpanProcessor.new)
  c.send(:wrapped_exporters_from_env).compact.each { |p| c.add_span_processor(p) }
  c.use_all
end

desc 'Dummy task'
task :dummy do
  puts 'foo'
end

But it requires calling a private method (c.send(:wrapped_exporters_from_env).compact.each { |p| c.add_span_processor(p) }), which is inherently undesirable.

Instead it would be preferable if obtaining the environment based default exporters would be a documented method on the public interface or if there would be a method to add span processors without deactivating the default behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions