Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions api/lib/opentelemetry/internal/proxy_tracer_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,22 @@ module Internal
# It delegates to a "real" TracerProvider after the global tracer provider is registered.
# It returns {ProxyTracer} instances until the delegate is installed.
class ProxyTracerProvider < Trace::TracerProvider
Key = Struct.new(:name, :version)
Key = Struct.new(:name, :version, :attributes)
private_constant(:Key)

# Wraps a legacy TracerProvider whose `#tracer` method does not accept
# the `attributes:` keyword argument, dropping the argument on delegation.
class LegacyProviderWrapper
def initialize(legacy_provider)
@legacy_provider = legacy_provider
end

def tracer(name = nil, version = nil, attributes: nil) # rubocop:disable Lint/UnusedMethodArgument
@legacy_provider.tracer(name, version)
end
end
private_constant(:LegacyProviderWrapper)

# Returns a new {ProxyTracerProvider} instance.
#
# @return [ProxyTracerProvider]
Expand All @@ -35,25 +48,45 @@ def delegate=(provider)
return
end

provider = LegacyProviderWrapper.new(provider) unless supports_attributes?(provider)

@mutex.synchronize do
@delegate = provider
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than have conditional code executed on every provider.tracer(...) call, could we conditionally wrap the provider before the mutex with a legacy provider that just drops the attributes: arg? I.e. (roughly)

class LegacyProviderWrapper
  def initialize(legacy_provider)
    @legacy_provider = legacy_provider
  end

  def tracer(name, version, attributes:)
    @legacy_provider.tracer(name, version)
  end
end

# then above

provider = LegacyProviderWrapper.new(provider) unless provider.respond_to?(:tracer) && provider.method(:tracer).parameters.any? { |_, n| n == :attributes }

This would avoid penalizing providers implementing the new signature (which should be the common case, assuming most people use the SDK provider) and avoids the conditional dispatch in the legacy case.

@registry.each { |key, tracer| tracer.delegate = provider.tracer(key.name, key.version) }
@registry.each { |key, tracer| tracer.delegate = @delegate.tracer(key.name, key.version, attributes: key.attributes) }
end
end

# Returns a {Tracer} instance.
#
# @param [optional String] name Instrumentation package name
# @param [optional String] version Instrumentation package version
# Supports both positional arguments (legacy) and keyword arguments:
# tracer('name', '1.0') # legacy positional
# tracer(name: 'name', version: '1.0', attributes: {...}) # keyword
#
# When both positional and keyword arguments are provided for the same
# parameter, the keyword argument takes precedence.
#
# @param [String] name Instrumentation scope name
# @param [String] version Instrumentation scope version
# @param [Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}] attributes
# Instrumentation scope attributes
#
# @return [Tracer]
def tracer(name = nil, version = nil)
def tracer(deprecated_name = nil, deprecated_version = nil, name: nil, version: nil, attributes: nil)
name ||= deprecated_name
version ||= deprecated_version
@mutex.synchronize do
return @delegate.tracer(name, version) unless @delegate.nil?
return @delegate.tracer(name, version, attributes: attributes) unless @delegate.nil?

@registry[Key.new(name, version)] ||= ProxyTracer.new
@registry[Key.new(name, version, attributes)] ||= ProxyTracer.new
end
end

private

def supports_attributes?(provider)
provider.respond_to?(:tracer) &&
provider.method(:tracer).parameters.any? { |_, n| n == :attributes }
end
end
end
end
15 changes: 12 additions & 3 deletions api/lib/opentelemetry/trace/tracer_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ module Trace
class TracerProvider
# Returns a {Tracer} instance.
#
# @param [optional String] name Instrumentation package name
# @param [optional String] version Instrumentation package version
# Supports both positional arguments (legacy) and keyword arguments:
# tracer('name', '1.0') # legacy positional
# tracer(name: 'name', version: '1.0', attributes: {...}) # keyword
#
# When both positional and keyword arguments are provided for the same
# parameter, the keyword argument takes precedence.
#
# @param [String] name Instrumentation scope name
# @param [String] version Instrumentation scope version
# @param [Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}] attributes
# Instrumentation scope attributes
#
# @return [Tracer]
def tracer(name = nil, version = nil)
def tracer(deprecated_name = nil, deprecated_version = nil, name: nil, version: nil, attributes: nil)
@tracer ||= Tracer.new
end
end
Expand Down
37 changes: 36 additions & 1 deletion api/test/opentelemetry/trace/tracer_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,45 @@
let(:tracer_provider) { OpenTelemetry::Trace::TracerProvider.new }

describe '.tracer' do
it 'returns the same tracer for the same arguments' do
# Legacy positional calling conventions
it 'returns a tracer with no arguments' do
tracer = tracer_provider.tracer
_(tracer).must_be_instance_of(OpenTelemetry::Trace::Tracer)
end

it 'returns a tracer with name only' do
tracer = tracer_provider.tracer('component')
_(tracer).must_be_instance_of(OpenTelemetry::Trace::Tracer)
end

it 'returns the same tracer for the same positional arguments' do
tracer1 = tracer_provider.tracer('component', '1.0')
tracer2 = tracer_provider.tracer('component', '1.0')
_(tracer1).must_equal(tracer2)
end

# Keyword calling conventions
it 'accepts all keyword arguments' do
tracer = tracer_provider.tracer(name: 'component', version: '1.0', attributes: { 'key' => 'value' })
_(tracer).must_be_instance_of(OpenTelemetry::Trace::Tracer)
end

it 'accepts name keyword only' do
tracer = tracer_provider.tracer(name: 'component')
_(tracer).must_be_instance_of(OpenTelemetry::Trace::Tracer)
end

# Mixed positional + keyword
it 'accepts positional arguments with attributes keyword' do
tracer = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
_(tracer).must_be_instance_of(OpenTelemetry::Trace::Tracer)
end

# Nil attributes equivalence
it 'returns the same tracer without attributes' do
tracer1 = tracer_provider.tracer('component', '1.0')
tracer2 = tracer_provider.tracer('component', '1.0', attributes: nil)
_(tracer1).must_equal(tracer2)
end
end
end
43 changes: 43 additions & 0 deletions api/test/opentelemetry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def tracer(name = nil, version = nil)
end
end

class AttributeAwareTracerProvider < OpenTelemetry::Trace::TracerProvider
attr_reader :last_name, :last_version, :last_attributes

def tracer(deprecated_name = nil, deprecated_version = nil, name: nil, version: nil, attributes: nil)
@last_name = name || deprecated_name
@last_version = version || deprecated_version
@last_attributes = attributes
CustomTracer.new
end
end

describe '.tracer_provider=' do
after do
# Ensure we don't leak custom tracer factories and tracers to other tests
Expand All @@ -63,6 +74,38 @@ def tracer(name = nil, version = nil)
OpenTelemetry.tracer_provider = CustomTracerProvider.new
_(default_tracer_provider.tracer).must_be_instance_of(CustomTracer)
end

it 'delegates to a provider that does not support attributes without error' do
OpenTelemetry.tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
OpenTelemetry.tracer_provider = CustomTracerProvider.new
_(OpenTelemetry.tracer_provider.tracer('component', '1.0')).must_be_instance_of(CustomTracer)
end

it 'delegates attributes to a provider that supports them' do
attrs = { 'key' => 'value' }
OpenTelemetry.tracer_provider.tracer('component', '1.0', attributes: attrs)
provider = AttributeAwareTracerProvider.new
OpenTelemetry.tracer_provider = provider
_(provider.last_attributes).must_equal(attrs)
end

it 'replays keyword-style tracers when delegate is set' do
OpenTelemetry.tracer_provider.tracer(name: 'component', version: '1.0', attributes: { 'k' => 'v' })
provider = AttributeAwareTracerProvider.new
OpenTelemetry.tracer_provider = provider
_(provider.last_name).must_equal('component')
_(provider.last_version).must_equal('1.0')
_(provider.last_attributes).must_equal('k' => 'v')
end

it 'delegates tracers obtained after delegate assignment with attributes' do
provider = AttributeAwareTracerProvider.new
OpenTelemetry.tracer_provider = provider
OpenTelemetry.tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
_(provider.last_name).must_equal('component')
_(provider.last_version).must_equal('1.0')
_(provider.last_attributes).must_equal('key' => 'value')
end
end

describe '.handle_error' do
Expand Down
3 changes: 2 additions & 1 deletion sdk/lib/opentelemetry/sdk/instrumentation_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module OpenTelemetry
module SDK
# InstrumentationScope is a struct containing scope information for export.
InstrumentationScope = Struct.new(:name,
:version)
:version,
:attributes)
end
end
10 changes: 6 additions & 4 deletions sdk/lib/opentelemetry/sdk/trace/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ class Tracer < OpenTelemetry::Trace::Tracer
#
# Returns a new {Tracer} instance.
#
# @param [String] name Instrumentation package name
# @param [String] version Instrumentation package version
# @param [String] name Instrumentation scope name
# @param [String] version Instrumentation scope version
# @param [TracerProvider] tracer_provider TracerProvider that initialized the tracer
# @param [Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}] attributes
# Instrumentation scope attributes
#
# @return [Tracer]
def initialize(name, version, tracer_provider)
@instrumentation_scope = InstrumentationScope.new(name, version)
def initialize(name, version, tracer_provider, attributes: nil)
@instrumentation_scope = InstrumentationScope.new(name, version, attributes || {}.freeze)
@tracer_provider = tracer_provider
end

Expand Down
31 changes: 23 additions & 8 deletions sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ module SDK
module Trace
# {TracerProvider} is the SDK implementation of {OpenTelemetry::Trace::TracerProvider}.
class TracerProvider < OpenTelemetry::Trace::TracerProvider # rubocop:disable Metrics/ClassLength
Key = Struct.new(:name, :version)
private_constant(:Key)
Key = Struct.new(:name, :version, :attributes)
EMPTY_ATTRIBUTES = {}.freeze

private_constant(:Key, :EMPTY_ATTRIBUTES)

attr_accessor :span_limits, :id_generator, :sampler
attr_reader :resource
Expand Down Expand Up @@ -44,15 +46,28 @@ def initialize(sampler: sampler_from_environment(Samplers.parent_based(root: Sam

# Returns a {Tracer} instance.
#
# @param [optional String] name Instrumentation package name
# @param [optional String] version Instrumentation package version
# Supports both positional arguments (legacy) and keyword arguments:
# tracer('name', '1.0') # legacy positional
# tracer(name: 'name', version: '1.0', attributes: {...}) # keyword
#
# When both positional and keyword arguments are provided for the same
# parameter, the keyword argument takes precedence.
#
# @param [String] name Instrumentation scope name
# @param [String] version Instrumentation scope version
# @param [Hash{String => String, Numeric, Boolean, Array<String, Numeric, Boolean>}] attributes
# Instrumentation scope attributes
#
# @return [Tracer]
def tracer(name = nil, version = nil)
name ||= ''
version ||= ''
def tracer(deprecated_name = nil, deprecated_version = nil, name: nil, version: nil, attributes: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
name ||= deprecated_name || ''
version ||= deprecated_version || ''
attributes = attributes&.dup&.freeze || EMPTY_ATTRIBUTES
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. We should probably do attribute validation here like we do on spans.

OpenTelemetry.logger.warn 'calling TracerProvider#tracer without providing a tracer name.' if name.empty?
@registry_mutex.synchronize { @registry[Key.new(name, version)] ||= Tracer.new(name, version, self) }
@registry_mutex.synchronize do
@registry[Key.new(name, version, attributes)] ||=
Tracer.new(name, version, self, attributes: attributes)
end
end

# Attempts to stop all the activity for this {TracerProvider}. Calls
Expand Down
65 changes: 64 additions & 1 deletion sdk/test/opentelemetry/sdk/trace/tracer_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
end

describe '#tracer' do
# Legacy positional calling conventions
it 'returns the same tracer for the same arguments' do
OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
tracer1 = tracer_provider.tracer('component', '1.0')
Expand All @@ -156,6 +157,11 @@
end
end

it 'defaults version to empty string when given positional name only' do
tracer = tracer_provider.tracer('component')
_(tracer).wont_be_nil
end

it 'returns different tracers for different names' do
tracer1 = tracer_provider.tracer('component1', '1.0')
tracer2 = tracer_provider.tracer('component2', '1.0')
Expand All @@ -168,11 +174,68 @@
_(tracer1).wont_equal(tracer2)
end

it 'warn when no name is passed for the tracer' do
it 'warns when no name is passed for the tracer' do
OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
tracer_provider.tracer
_(log_stream.string).must_match(/calling TracerProvider#tracer without providing a tracer name./)
end
end

# Keyword calling conventions
it 'accepts all keyword arguments' do
tracer = tracer_provider.tracer(name: 'component', version: '1.0', attributes: { 'key' => 'value' })
_(tracer).wont_be_nil
end

it 'returns the same tracer for equivalent positional and keyword arguments' do
tracer1 = tracer_provider.tracer('component', '1.0')
tracer2 = tracer_provider.tracer(name: 'component', version: '1.0')
_(tracer1).must_equal(tracer2)
end

# Mixed positional + keyword
it 'accepts positional name with keyword version' do
tracer = tracer_provider.tracer('component', version: '1.0')
_(tracer).wont_be_nil
end

it 'prefers keyword arguments over positional arguments' do
tracer1 = tracer_provider.tracer('positional', '1.0')
tracer2 = tracer_provider.tracer('keyword', '2.0', name: 'keyword', version: '2.0')
Copy link
Copy Markdown
Member

@robbkidd robbkidd Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

Suggested change
tracer2 = tracer_provider.tracer('keyword', '2.0', name: 'keyword', version: '2.0')
tracer2 = tracer_provider.tracer('positional', '1.0', name: 'keyword', version: '2.0')

I think these suggested tracer1 positional params given to tracer2 will properly exhibit the kw arg precedence. When all arguments are entirely different, tracer2 will always be different than tracer1.

_(tracer1).wont_equal(tracer2)
end

# Attributes
it 'returns the same tracer for the same name, version, and attributes' do
tracer1 = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
tracer2 = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
_(tracer1).must_equal(tracer2)
end

it 'returns different tracers for different attributes' do
tracer1 = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value1' })
tracer2 = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value2' })
_(tracer1).wont_equal(tracer2)
end

it 'returns different tracers for attributes vs no attributes' do
tracer1 = tracer_provider.tracer('component', '1.0')
tracer2 = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
_(tracer1).wont_equal(tracer2)
end

it 'treats nil attributes the same as no attributes' do
tracer1 = tracer_provider.tracer('component', '1.0')
tracer2 = tracer_provider.tracer('component', '1.0', attributes: nil)
_(tracer1).must_equal(tracer2)
end

it 'does not allow mutation of attributes after tracer creation' do
attrs = { 'key' => 'value' }
tracer_provider.tracer('component', '1.0', attributes: attrs)
attrs['key'] = 'mutated'
tracer = tracer_provider.tracer('component', '1.0', attributes: { 'key' => 'value' })
_(tracer).wont_be_nil
end
end
end
Loading
Loading