Skip to content

Commit 57ed60e

Browse files
committed
feat: Add basic support for schema_url
This adds basic schema URL support in the API + SDK, specifically in and around Resources, Tracers, Tracer Provider, and the OTLP exporter. Some notes, in no particular order: - I did not add schema URLs to any of our instrumentation, because that was a lot of work. - The _API_ ProxyTracer tests around "asking for a tracer multiple times returns the same thing" don't actually work, because we don't do anything with the name+version(+schema url, after this change) that you pass in. I don't think that matters, but I'm not really sure. - I didn't add the schema information to the jaeger/zipkin exporters, after surveying what Go and Python were doing. I don't see an easy place to add it in, either. 1. The behavior of merging two resources with different, non-empty schema URLs is implementation dependent. I chose to _drop_ the schema URL in that case and continue merging resources as before. *Is this what we want?* Other SDKs do things differently (Go returns an error, Python logs an error and returns the old resource, etc). 2. I did *not* add support to the Configurator for setting the schema_url on the default Resource that gets initialized. That seemed like the kind of thing folks would do incorrectly, but you can still do such a thing by creating and assigning an entire Resource if you wish. *Is this what we want?*
1 parent ee5ece7 commit 57ed60e

File tree

14 files changed

+156
-28
lines changed

14 files changed

+156
-28
lines changed

api/lib/opentelemetry/internal/proxy_tracer_provider.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ module Internal
1313
# It delegates to a "real" TracerProvider after the global tracer provider is registered.
1414
# It returns {ProxyTracer} instances until the delegate is installed.
1515
class ProxyTracerProvider < Trace::TracerProvider
16-
Key = Struct.new(:name, :version)
16+
Key = Struct.new(:name, :version, :schema_url)
1717
private_constant(:Key)
1818

1919
# Returns a new {ProxyTracerProvider} instance.
@@ -37,21 +37,22 @@ def delegate=(provider)
3737

3838
@mutex.synchronize do
3939
@delegate = provider
40-
@registry.each { |key, tracer| tracer.delegate = provider.tracer(key.name, key.version) }
40+
@registry.each { |key, tracer| tracer.delegate = provider.tracer(key.name, key.version, key.schema_url) }
4141
end
4242
end
4343

4444
# Returns a {Tracer} instance.
4545
#
4646
# @param [optional String] name Instrumentation package name
4747
# @param [optional String] version Instrumentation package version
48+
# @param [optional String] schema_url Schema URL to be recorded with traces
4849
#
4950
# @return [Tracer]
50-
def tracer(name = nil, version = nil)
51+
def tracer(name = nil, version = nil, schema_url = nil)
5152
@mutex.synchronize do
52-
return @delegate.tracer(name, version) unless @delegate.nil?
53+
return @delegate.tracer(name, version, schema_url) unless @delegate.nil?
5354

54-
@registry[Key.new(name, version)] ||= ProxyTracer.new
55+
@registry[Key.new(name, version, schema_url)] ||= ProxyTracer.new
5556
end
5657
end
5758
end

api/lib/opentelemetry/trace/tracer_provider.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ class TracerProvider
1212
#
1313
# @param [optional String] name Instrumentation package name
1414
# @param [optional String] version Instrumentation package version
15+
# @param [optional String] schema_url Schema URL to be recorded with traces
1516
#
1617
# @return [Tracer]
17-
def tracer(name = nil, version = nil)
18+
def tracer(name = nil, version = nil, schema_url = nil)
1819
@tracer ||= Tracer.new
1920
end
2021
end

api/test/opentelemetry/trace/tracer_provider_test.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
let(:tracer_provider) { OpenTelemetry::Trace::TracerProvider.new }
1111

1212
describe '.tracer' do
13+
# TODO: This test doesn't really do anything for the API tracer
1314
it 'returns the same tracer for the same arguments' do
14-
tracer1 = tracer_provider.tracer('component', '1.0')
15-
tracer2 = tracer_provider.tracer('component', '1.0')
15+
tracer1 = tracer_provider.tracer('component', '1.0', 'https://http.cat/404')
16+
tracer2 = tracer_provider.tracer('component', '1.0', 'https://http.cat/404')
1617
_(tracer1).must_equal(tracer2)
1718
end
1819
end

api/test/opentelemetry_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def start_root_span(*)
1818
end
1919

2020
class CustomTracerProvider < OpenTelemetry::Trace::TracerProvider
21-
def tracer(name = nil, version = nil)
21+
def tracer(name = nil, version = nil, schema_url = nil)
2222
CustomTracer.new
2323
end
2424
end

exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def encode(span_data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
292292
resource: Opentelemetry::Proto::Resource::V1::Resource.new(
293293
attributes: resource.attribute_enumerator.map { |key, value| as_otlp_key_value(key, value) }
294294
),
295+
schema_url: resource.schema_url,
295296
instrumentation_library_spans: span_datas
296297
.group_by(&:instrumentation_library)
297298
.map do |il, sds|
@@ -300,7 +301,8 @@ def encode(span_data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
300301
name: il.name,
301302
version: il.version
302303
),
303-
spans: sds.map { |sd| as_otlp_span(sd) }
304+
spans: sds.map { |sd| as_otlp_span(sd) },
305+
schema_url: il.schema_url
304306
)
305307
end
306308
)

exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,35 @@
467467
_(etsr.resource_spans.length).must_equal(2)
468468
end
469469

470+
it 'can encode resource schema_urls' do
471+
etsr = nil
472+
stub_post = stub_request(:post, 'http://localhost:4318/v1/traces').to_return do |request|
473+
proto = Zlib.gunzip(request.body)
474+
etsr = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(proto)
475+
{ status: 200 }
476+
end
477+
478+
span_data1 = create_span_data(
479+
resource: OpenTelemetry::SDK::Resources::Resource.create({'k1' => 'v1'}, 'https://http.cat/200'),
480+
instrumentation_library: OpenTelemetry::SDK::InstrumentationLibrary.new('', 'v0.0.1', 'https://http.cat/429')
481+
)
482+
span_data2 = create_span_data(
483+
resource: OpenTelemetry::SDK::Resources::Resource.create({'k2' => 'v2'}, 'https://http.cat/404'),
484+
instrumentation_library: OpenTelemetry::SDK::InstrumentationLibrary.new('', 'v0.0.1', 'https://http.cat/451')
485+
)
486+
result = exporter.export([span_data1, span_data2])
487+
488+
_(result).must_equal(SUCCESS)
489+
assert_requested(stub_post)
490+
491+
_(etsr.resource_spans.length).must_equal(2)
492+
_(etsr.resource_spans[0].schema_url).must_equal('https://http.cat/200')
493+
_(etsr.resource_spans[0].instrumentation_library_spans[0].schema_url).must_equal('https://http.cat/429')
494+
495+
_(etsr.resource_spans[1].schema_url).must_equal('https://http.cat/404')
496+
_(etsr.resource_spans[1].instrumentation_library_spans[0].schema_url).must_equal('https://http.cat/451')
497+
end
498+
470499
it 'translates all the things' do
471500
stub_request(:post, 'http://localhost:4318/v1/traces').to_return(status: 200)
472501
processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)

sdk/lib/opentelemetry/sdk/instrumentation_library.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module OpenTelemetry
88
module SDK
99
# InstrumentationLibrary is a struct containing library information for export.
1010
InstrumentationLibrary = Struct.new(:name,
11-
:version)
11+
:version,
12+
:schema_url)
1213
end
1314
end

sdk/lib/opentelemetry/sdk/resources/resource.rb

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,22 @@ class << self
1515

1616
# Returns a newly created {Resource} with the specified attributes
1717
#
18-
# @param [Hash{String => String, Numeric, Boolean} attributes Hash of key-value pairs to be used
18+
# @param [Hash{String => String, Numeric, Boolean}] attributes Hash of key-value pairs to be used
1919
# as attributes for this resource
20+
# @param [String] Specifies the Schema URL that should be recorded in the emitted resource.
2021
# @raise [ArgumentError] If attribute keys and values are not strings
2122
# @return [Resource]
22-
def create(attributes = {})
23+
def create(attributes = {}, schema_url = '')
24+
raise ArgumentError, 'Schema url must be a string' unless schema_url.is_a?(String)
25+
2326
frozen_attributes = attributes.each_with_object({}) do |(k, v), memo|
2427
raise ArgumentError, 'attribute keys must be strings' unless k.is_a?(String)
2528
raise ArgumentError, 'attribute values must be (array of) strings, integers, floats, or booleans' unless Internal.valid_value?(v)
2629

2730
memo[-k] = v.freeze
2831
end.freeze
2932

30-
new(frozen_attributes)
33+
new(frozen_attributes, schema_url)
3134
end
3235

3336
def default
@@ -81,8 +84,9 @@ def service_name_from_env
8184
# @param [Hash<String, String>] frozen_attributes Frozen-hash of frozen-string
8285
# key-value pairs to be used as attributes for this resource
8386
# @return [Resource]
84-
def initialize(frozen_attributes)
87+
def initialize(frozen_attributes, schema_url)
8588
@attributes = frozen_attributes
89+
@schema_url = schema_url
8690
end
8791

8892
# Returns an enumerator for attributes of this {Resource}
@@ -93,20 +97,34 @@ def attribute_enumerator
9397
end
9498

9599
# Returns a new, merged {Resource} by merging the current {Resource} with
96-
# the other {Resource}. In case of a collision, the current {Resource}
97-
# takes precedence
100+
# the other {Resource}. In case of an attributes collision, the other {Resource}
101+
# takes precedence. If two incompatible schema urls are given, then the schema
102+
# URL of the resulting resource will be unset.
98103
#
99104
# @param [Resource] other The other resource to merge
100105
# @return [Resource] A new resource formed by merging the current resource
101106
# with other
102107
def merge(other)
103108
return self unless other.is_a?(Resource)
104109

105-
self.class.send(:new, attributes.merge(other.attributes).freeze)
110+
# This is slightly verbose, but tries to follow the definition in the spec closely.
111+
new_schema_url = if schema_url == ''
112+
other.schema_url
113+
elsif other.schema_url == '' || schema_url == other.schema_url
114+
schema_url
115+
elsif schema_url != other.schema_url
116+
# According to the spec: The resulting resource is undefined, and its contents are implementation-specific.
117+
# We choose to simply un-set the resource URL and log a warning about it, and allow the attributes to merge.
118+
OpenTelemetry.logger.warn("Merging resources with schema version '#{schema_url}' and '#{other.schema_url}' is undefined.")
119+
''
120+
end
121+
122+
self.class.send(:new, attributes.merge(other.attributes).freeze, new_schema_url)
106123
end
107124

108-
protected
125+
attr_reader :schema_url
109126

127+
protected
110128
attr_reader :attributes
111129
end
112130
end

sdk/lib/opentelemetry/sdk/trace/tracer.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ class Tracer < OpenTelemetry::Trace::Tracer
1515
#
1616
# @param [String] name Instrumentation package name
1717
# @param [String] version Instrumentation package version
18+
# @param [String] schema_url Schema URL to be recorded with traces
1819
# @param [TracerProvider] tracer_provider TracerProvider that initialized the tracer
1920
#
2021
# @return [Tracer]
21-
def initialize(name, version, tracer_provider)
22-
@instrumentation_library = InstrumentationLibrary.new(name, version)
22+
def initialize(name, version, schema_url, tracer_provider)
23+
@instrumentation_library = InstrumentationLibrary.new(name, version, schema_url)
2324
@tracer_provider = tracer_provider
2425
end
2526

sdk/lib/opentelemetry/sdk/trace/tracer_provider.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module SDK
99
module Trace
1010
# {TracerProvider} is the SDK implementation of {OpenTelemetry::Trace::TracerProvider}.
1111
class TracerProvider < OpenTelemetry::Trace::TracerProvider # rubocop:disable Metrics/ClassLength
12-
Key = Struct.new(:name, :version)
12+
Key = Struct.new(:name, :version, :schema_url)
1313
private_constant(:Key)
1414

1515
attr_accessor :span_limits, :id_generator, :sampler
@@ -46,13 +46,15 @@ def initialize(sampler: sampler_from_environment(Samplers.parent_based(root: Sam
4646
#
4747
# @param [optional String] name Instrumentation package name
4848
# @param [optional String] version Instrumentation package version
49+
# @param [optional String] schema_url Schema URL to be recorded with traces
4950
#
5051
# @return [Tracer]
51-
def tracer(name = nil, version = nil)
52+
def tracer(name = nil, version = nil, schema_url = nil)
5253
name ||= ''
5354
version ||= ''
55+
schema_url ||= ''
5456
OpenTelemetry.logger.warn 'calling TracerProvider#tracer without providing a tracer name.' if name.empty?
55-
@registry_mutex.synchronize { @registry[Key.new(name, version)] ||= Tracer.new(name, version, self) }
57+
@registry_mutex.synchronize { @registry[Key.new(name, version, schema_url)] ||= Tracer.new(name, version, schema_url, self) }
5658
end
5759

5860
# Attempts to stop all the activity for this {TracerProvider}. Calls

0 commit comments

Comments
 (0)