diff --git a/adapters/faraday/lib/opentelemetry/adapters/faraday/middlewares/tracer_middleware.rb b/adapters/faraday/lib/opentelemetry/adapters/faraday/middlewares/tracer_middleware.rb index 2ca04157b..ad3f7f634 100644 --- a/adapters/faraday/lib/opentelemetry/adapters/faraday/middlewares/tracer_middleware.rb +++ b/adapters/faraday/lib/opentelemetry/adapters/faraday/middlewares/tracer_middleware.rb @@ -35,13 +35,8 @@ def disable_span_reporting?(_env) attr_reader :app - # Outbound requests should only need to inject the current span. def propagate_context(span, env) - propagator.inject(span.context, env.request_headers) - end - - def propagator - OpenTelemetry.tracer_factory.http_text_format + OpenTelemetry.propagation.inject(env.request_headers) end def tracer diff --git a/adapters/sinatra/lib/opentelemetry/adapters/sinatra/middlewares/tracer_middleware.rb b/adapters/sinatra/lib/opentelemetry/adapters/sinatra/middlewares/tracer_middleware.rb index 4fea4b23a..ae47fbe06 100644 --- a/adapters/sinatra/lib/opentelemetry/adapters/sinatra/middlewares/tracer_middleware.rb +++ b/adapters/sinatra/lib/opentelemetry/adapters/sinatra/middlewares/tracer_middleware.rb @@ -32,7 +32,7 @@ def call(env) attr_reader :app def parent_context(env) - OpenTelemetry.tracer_factory.http_text_format.extract(env) + OpenTelemetry.propagation.extract(env) end def tracer diff --git a/api/lib/opentelemetry.rb b/api/lib/opentelemetry.rb index d6c3f3000..a9b6b2931 100644 --- a/api/lib/opentelemetry.rb +++ b/api/lib/opentelemetry.rb @@ -8,7 +8,7 @@ require 'opentelemetry/error' require 'opentelemetry/context' -require 'opentelemetry/distributed_context' +require 'opentelemetry/correlation_context' require 'opentelemetry/internal' require 'opentelemetry/instrumentation' require 'opentelemetry/metrics' @@ -23,7 +23,7 @@ module OpenTelemetry extend self - attr_writer :tracer_factory, :meter_factory, :distributed_context_manager + attr_writer :tracer_factory, :meter_factory, :correlations attr_accessor :logger @@ -39,17 +39,23 @@ def meter_factory @meter_factory ||= Metrics::MeterFactory.new end - # @return [Object, DistributedContext::Manager] registered distributed - # context manager or a default no-op implementation of the manager - def distributed_context_manager - @distributed_context_manager ||= DistributedContext::Manager.new - end - # @return [Instrumentation::Registry] registry containing all known # instrumentation def instrumentation_registry @instrumentation_registry ||= Instrumentation::Registry.new end + # @return [Object, CorrelationContext::Manager] registered + # correlation context manager or a default no-op implementation of the + # manager. + def correlations + @correlations ||= CorrelationContext::Manager.new + end + + # @return [Context::Propagation::Propagation] an instance of the propagation API + def propagation + @propagation ||= Context::Propagation::Propagation.new + end + self.logger = Logger.new(STDOUT) end diff --git a/api/lib/opentelemetry/context.rb b/api/lib/opentelemetry/context.rb index f23093f06..6e4eb5565 100644 --- a/api/lib/opentelemetry/context.rb +++ b/api/lib/opentelemetry/context.rb @@ -4,28 +4,146 @@ # # SPDX-License-Identifier: Apache-2.0 +require 'opentelemetry/context/key' +require 'opentelemetry/context/propagation' + module OpenTelemetry - # The Context module provides per-thread storage. - module Context - extend self + # Manages context on a per-fiber basis + class Context + KEY = :__opentelemetry_context__ + EMPTY_ENTRIES = {}.freeze + + class << self + # Returns a key used to index a value in a Context + # + # @param [String] name The key name + # @return [Context::Key] + def create_key(name) + Key.new(name) + end + + # Returns current context, which is never nil + # + # @return [Context] + def current + Thread.current[KEY] ||= ROOT + end + + # Sets the current context + # + # @param [Context] ctx The context to be made active + def current=(ctx) + Thread.current[KEY] = ctx + end + + # Executes a block with ctx as the current context. It restores + # the previous context upon exiting. + # + # @param [Context] ctx The context to be made active + def with_current(ctx) + prev = ctx.attach + yield + ensure + ctx.detach(prev) + end + + # Execute a block in a new context with key set to value. Restores the + # previous context after the block executes. + + # @param [String] key The lookup key + # @param [Object] value The object stored under key + # @param [Callable] Block to execute in a new context + def with_value(key, value) + ctx = current.set_value(key, value) + prev = ctx.attach + yield value + ensure + ctx.detach(prev) + end + + # Execute a block in a new context where its values are merged with the + # incoming values. Restores the previous context after the block executes. + + # @param [String] key The lookup key + # @param [Hash] values Will be merged with values of the current context + # and returned in a new context + # @param [Callable] Block to execute in a new context + def with_values(values) + ctx = current.set_values(values) + prev = ctx.attach + yield values + ensure + ctx.detach(prev) + end + + # Returns the value associated with key in the current context + # + # @param [String] key The lookup key + def value(key) + current.value(key) + end + + def clear + self.current = ROOT + end - def get(key) - storage[key] + def empty + new(nil, EMPTY_ENTRIES) + end end - def with(key, value) - store = storage - previous = store[key] - store[key] = value - yield value - ensure - store[key] = previous + def initialize(parent, entries) + @parent = parent + @entries = entries.freeze end - private + # Returns the corresponding value (or nil) for key + # + # @param [Key] key The lookup key + # @return [Object] + def value(key) + @entries[key] + end + + alias [] value + + # Returns a new Context where entries contains the newly added key and value + # + # @param [Key] key The key to store this value under + # @param [Object] value Object to be stored under key + # @return [Context] + def set_value(key, value) + new_entries = @entries.dup + new_entries[key] = value + Context.new(self, new_entries) + end + + # Returns a new Context with the current context's entries merged with the + # new entries + # + # @param [Hash] values The values to be merged with the current context's + # entries. + # @param [Object] value Object to be stored under key + # @return [Context] + def set_values(values) # rubocop:disable Naming/AccessorMethodName: + Context.new(self, @entries.merge(values)) + end - def storage - Thread.current[:__opentelemetry__] ||= {} + # @api private + def attach + prev = self.class.current + self.class.current = self + prev end + + # @api private + def detach(ctx_to_attach = nil) + OpenTelemetry.logger.warn 'Calls to detach should match corresponding calls to attach' if self.class.current != self + + ctx_to_attach ||= @parent || ROOT + ctx_to_attach.attach + end + + ROOT = empty.freeze end end diff --git a/api/lib/opentelemetry/context/key.rb b/api/lib/opentelemetry/context/key.rb new file mode 100644 index 000000000..b71874d3f --- /dev/null +++ b/api/lib/opentelemetry/context/key.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + class Context + # The Key class provides mechanisms to index and access values from a + # Context + class Key + attr_reader :name + + # @api private + # Use Context.create_key to obtain a Key instance. + def initialize(name) + @name = name + end + + # Returns the value indexed by this Key in the specified context + # + # @param [optional Context] context The Context to lookup the key from. + # Defaults to +Context.current+. + def get(context = Context.current) + context[self] + end + end + end +end diff --git a/api/lib/opentelemetry/context/propagation.rb b/api/lib/opentelemetry/context/propagation.rb new file mode 100644 index 000000000..9875020ce --- /dev/null +++ b/api/lib/opentelemetry/context/propagation.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/context/propagation/default_getter' +require 'opentelemetry/context/propagation/default_setter' +require 'opentelemetry/context/propagation/propagation' + +module OpenTelemetry + class Context + # The propagation module contains APIs and utilities to interact with context + # and propagate across process boundaries. + module Propagation + end + end +end diff --git a/api/lib/opentelemetry/context/propagation/default_getter.rb b/api/lib/opentelemetry/context/propagation/default_getter.rb new file mode 100644 index 000000000..4824882fd --- /dev/null +++ b/api/lib/opentelemetry/context/propagation/default_getter.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + class Context + module Propagation + # The default getter module provides a common method for reading + # a key from a carrier that implements +[]+ + module DefaultGetter + DEFAULT_GETTER = ->(carrier, key) { carrier[key] } + private_constant :DEFAULT_GETTER + + # Returns a callable that can read a key from a carrier that implements + # +[]+. Useful for extract operations. + # + # @return [Callable] + def default_getter + DEFAULT_GETTER + end + end + end + end +end diff --git a/api/lib/opentelemetry/context/propagation/default_setter.rb b/api/lib/opentelemetry/context/propagation/default_setter.rb new file mode 100644 index 000000000..1742c9338 --- /dev/null +++ b/api/lib/opentelemetry/context/propagation/default_setter.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + class Context + module Propagation + # The default setter module provides a common method for writing + # a key into a carrier that implements +[]=+ + module DefaultSetter + DEFAULT_SETTER = ->(carrier, key, value) { carrier[key] = value } + private_constant :DEFAULT_SETTER + + # Returns a callable that can write a key into a carrier that implements + # +[]=+. Useful for inject operations. + # + # @return [Callable] + def default_setter + DEFAULT_SETTER + end + end + end + end +end diff --git a/api/lib/opentelemetry/context/propagation/propagation.rb b/api/lib/opentelemetry/context/propagation/propagation.rb new file mode 100644 index 000000000..5d6ca7e90 --- /dev/null +++ b/api/lib/opentelemetry/context/propagation/propagation.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + class Context + module Propagation + # The Propagation class provides methods to inject and extract context + # to pass across process boundaries + class Propagation + EMPTY_ARRAY = [].freeze + + private_constant :EMPTY_ARRAY + + # Get or set global http_extractors + # + # @param [Array<#extract>] extractors When setting, provide an array + # of extractors + # + # @return Array<#extract> + attr_accessor :http_extractors + + # Get or set global http_injectors + # + # @param [Array<#inject>] injectors When setting, provide an array + # of injectors + # + # @return Array<#inject> + attr_accessor :http_injectors + + def initialize + @http_extractors = EMPTY_ARRAY + @http_injectors = EMPTY_ARRAY + end + + # Injects context into carrier to be propagated across process + # boundaries + # + # @param [Object] carrier A carrier of HTTP headers to inject + # context into + # @param [optional Context] context Context to be injected into carrier. Defaults + # to +Context.current+ + # @param [optional Array] http_injectors An array of HTTP injectors. Each + # injector will be invoked once with given context and carrier. Defaults to the + # globally registered +http_injectors+ + # + # @return [Object] carrier + def inject(carrier, context: Context.current, http_injectors: self.http_injectors) + http_injectors.inject(carrier) do |memo, injector| + injector.inject(context, memo) + end + end + + # Extracts context from a carrier + # + # @param [Object] carrier A carrier of HTTP headers to extract context + # from + # @param [optional Context] context Context to be updated with the state + # extracted from the carrier. Defaults to +Context.current+ + # @param [optional Array] http_extractors An array of HTTP extractors. + # Each extractor will be invoked once with given context and carrier. Defaults + # to the globally registered +http_extractors+ + # + # @return [Context] a new context updated with state extracted from the + # carrier + def extract(carrier, context: Context.current, http_extractors: self.http_extractors) + http_extractors.inject(context) do |ctx, extractor| + extractor.extract(ctx, carrier) + end + end + end + end + end +end diff --git a/api/lib/opentelemetry/correlation_context.rb b/api/lib/opentelemetry/correlation_context.rb new file mode 100644 index 000000000..26fefab5e --- /dev/null +++ b/api/lib/opentelemetry/correlation_context.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/correlation_context/builder' +require 'opentelemetry/correlation_context/manager' +require 'opentelemetry/correlation_context/propagation' + +module OpenTelemetry + # The CorrelationContext module provides functionality to record and propagate + # correlations in a distributed trace + module CorrelationContext + end +end diff --git a/api/lib/opentelemetry/correlation_context/builder.rb b/api/lib/opentelemetry/correlation_context/builder.rb new file mode 100644 index 000000000..42a85495c --- /dev/null +++ b/api/lib/opentelemetry/correlation_context/builder.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module CorrelationContext + # No op implementation of CorrelationContext::Builder + class Builder + def set_value(key, value); end + + def remove_value(key); end + + def clear; end + end + end +end diff --git a/api/lib/opentelemetry/correlation_context/manager.rb b/api/lib/opentelemetry/correlation_context/manager.rb new file mode 100644 index 000000000..dfb6dde2f --- /dev/null +++ b/api/lib/opentelemetry/correlation_context/manager.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module CorrelationContext + # No op implementation of CorrelationContext::Manager + class Manager + NOOP_BUILDER = Builder.new + private_constant :NOOP_BUILDER + + def build(context: Context.current) + yield NOOP_BUILDER + context + end + + def set_value(key, value, context: Context.current) + context + end + + def value(key, context: Context.current) + nil + end + + def remove_value(key, context: Context.current) + context + end + + def clear(context: Context.current) + context + end + end + end +end diff --git a/api/lib/opentelemetry/correlation_context/propagation.rb b/api/lib/opentelemetry/correlation_context/propagation.rb new file mode 100644 index 000000000..120d96d1a --- /dev/null +++ b/api/lib/opentelemetry/correlation_context/propagation.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/correlation_context/propagation/context_keys' +require 'opentelemetry/correlation_context/propagation/http_injector' +require 'opentelemetry/correlation_context/propagation/http_extractor' + +module OpenTelemetry + module CorrelationContext + # The Correlation::Propagation module contains injectors and + # extractors for sending and receiving correlation context over the wire + module Propagation + extend self + + HTTP_EXTRACTOR = HttpExtractor.new + HTTP_INJECTOR = HttpInjector.new + RACK_HTTP_EXTRACTOR = HttpExtractor.new( + correlation_context_key: 'HTTP_CORRELATION_CONTEXT' + ) + RACK_HTTP_INJECTOR = HttpInjector.new( + correlation_context_key: 'HTTP_CORRELATION_CONTEXT' + ) + + private_constant :HTTP_INJECTOR, :HTTP_EXTRACTOR, :RACK_HTTP_INJECTOR, + :RACK_HTTP_EXTRACTOR + + # Returns an extractor that extracts context using the W3C Correlation + # Context format for HTTP + def http_injector + HTTP_INJECTOR + end + + # Returns an injector that injects context using the W3C Correlation + # Context format for HTTP + def http_extractor + HTTP_EXTRACTOR + end + + # Returns an extractor that extracts context using the W3C Correlation + # Context format for HTTP with Rack normalized keys (upcased and + # prefixed with HTTP_) + def rack_http_injector + RACK_HTTP_INJECTOR + end + + # Returns an injector that injects context using the W3C Correlation + # Context format for HTTP with Rack normalized keys (upcased and + # prefixed with HTTP_) + def rack_http_extractor + RACK_HTTP_EXTRACTOR + end + end + end +end diff --git a/api/lib/opentelemetry/correlation_context/propagation/context_keys.rb b/api/lib/opentelemetry/correlation_context/propagation/context_keys.rb new file mode 100644 index 000000000..9ef99855b --- /dev/null +++ b/api/lib/opentelemetry/correlation_context/propagation/context_keys.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module CorrelationContext + module Propagation + # The ContextKeys module contains the keys used to index correlations + # in a {Context} instance + module ContextKeys + extend self + + CORRELATION_CONTEXT_KEY = Context.create_key('correlation-context') + private_constant :CORRELATION_CONTEXT_KEY + + # Returns the context key that correlations are indexed by + # + # @return [Context::Key] + def correlation_context_key + CORRELATION_CONTEXT_KEY + end + end + end + end +end diff --git a/api/lib/opentelemetry/correlation_context/propagation/http_extractor.rb b/api/lib/opentelemetry/correlation_context/propagation/http_extractor.rb new file mode 100644 index 000000000..5c64f3981 --- /dev/null +++ b/api/lib/opentelemetry/correlation_context/propagation/http_extractor.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'cgi' + +module OpenTelemetry + module CorrelationContext + module Propagation + # Extracts correlations from carriers in the W3C Correlation Context format + class HttpExtractor + include Context::Propagation::DefaultGetter + + # Returns a new HttpExtractor that extracts context using the specified + # header key + # + # @param [String] correlation_context_key The correlation context header + # key used in the carrier + # @return [HttpExtractor] + def initialize(correlation_context_key: 'Correlation-Context') + @correlation_context_key = correlation_context_key + end + + # Extract remote correlations from the supplied carrier. + # If extraction fails, the original context will be returned + # + # @param [Context] context The context to be updated with extracted correlations + # @param [Carrier] carrier The carrier to get the header from + # @param [optional Callable] getter An optional callable that takes a carrier and a key and + # returns the value associated with the key. If omitted the default getter will be used + # which expects the carrier to respond to [] and []=. + # @yield [Carrier, String] if an optional getter is provided, extract will yield the carrier + # and the header key to the getter. + # @return [Context] context updated with extracted correlations, or the original context + # if extraction fails + def extract(context, carrier, &getter) + getter ||= default_getter + header = getter.call(carrier, @correlation_context_key) + + entries = header.gsub(/\s/, '').split(',') + + correlations = entries.each_with_object({}) do |entry, memo| + # The ignored variable below holds properties as per the W3C spec. + # OTel is not using them currently, but they might be used for + # metadata in the future + kv, = entry.split(';', 2) + k, v = kv.split('=').map!(&CGI.method(:unescape)) + memo[k] = v + end + + context.set_value(ContextKeys.correlation_context_key, correlations) + rescue StandardError + context + end + end + end + end +end diff --git a/api/lib/opentelemetry/correlation_context/propagation/http_injector.rb b/api/lib/opentelemetry/correlation_context/propagation/http_injector.rb new file mode 100644 index 000000000..5303f7036 --- /dev/null +++ b/api/lib/opentelemetry/correlation_context/propagation/http_injector.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'cgi' + +module OpenTelemetry + module CorrelationContext + module Propagation + # Injects correlation context using the W3C Correlation Context format + class HttpInjector + include Context::Propagation::DefaultSetter + + # Returns a new HttpInjector that injects context using the specified + # header key + # + # @param [String] correlation_context_header_key The correlation context header + # key used in the carrier + # @return [HttpInjector] + def initialize(correlation_context_key: 'Correlation-Context') + @correlation_context_key = correlation_context_key + end + + # Inject in-process correlations into the supplied carrier. + # + # @param [Context] context The context to read correlations from + # @param [Carrier] carrier The carrier to inject correlations into + # @param [optional Callable] getter An optional callable that takes a carrier and a key and + # returns the value associated with the key. If omitted the default getter will be used + # which expects the carrier to respond to [] and []=. + # @yield [Carrier, String] if an optional getter is provided, inject will yield the carrier + # and the header key to the getter. + # @return [Object] carrier with injected correlations + def inject(context, carrier, &setter) + return carrier unless (correlations = context[ContextKeys.correlation_context_key]) && !correlations.empty? + + setter ||= default_setter + setter.call(carrier, @correlation_context_key, encode(correlations)) + + carrier + end + + private + + def encode(correlations) + correlations.inject(+'') do |memo, (k, v)| + memo << CGI.escape(k.to_s) << '=' << CGI.escape(v.to_s) << ',' + end.chop! + end + end + end + end +end diff --git a/api/lib/opentelemetry/distributed_context.rb b/api/lib/opentelemetry/distributed_context.rb deleted file mode 100644 index b8c3eaa60..000000000 --- a/api/lib/opentelemetry/distributed_context.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'opentelemetry/distributed_context/distributed_context' -require 'opentelemetry/distributed_context/entry' -require 'opentelemetry/distributed_context/manager' -require 'opentelemetry/distributed_context/propagation' - -module OpenTelemetry - # DistributedContext is an abstract data type that represents a collection of entries. Each key of a DistributedContext is - # associated with exactly one value. DistributedContext is serializable, to facilitate propagating it not only inside the - # process but also across process boundaries. DistributedContext is used to annotate telemetry with the name:value pair - # Entry. Those values can be used to add dimensions to the metric or additional context properties to logs and traces. - module DistributedContext - end -end diff --git a/api/lib/opentelemetry/distributed_context/distributed_context.rb b/api/lib/opentelemetry/distributed_context/distributed_context.rb deleted file mode 100644 index 1cb80111d..000000000 --- a/api/lib/opentelemetry/distributed_context/distributed_context.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module DistributedContext - # An immutable implementation of the DistributedContext that does not contain any entries. - class DistributedContext - EMPTY_ENTRIES = [].freeze - - private_constant(:EMPTY_ENTRIES) - - def entries - EMPTY_ENTRIES - end - - def [](_key) - nil - end - end - end -end diff --git a/api/lib/opentelemetry/distributed_context/entry.rb b/api/lib/opentelemetry/distributed_context/entry.rb deleted file mode 100644 index 9aeffbb22..000000000 --- a/api/lib/opentelemetry/distributed_context/entry.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module DistributedContext - # An Entry consists of Entry::Metadata, Entry::Key, and Entry::Value. - class Entry - attr_reader :metadata, :key, :value - - # Entry::Key is the name of the Entry. Entry::Key along with Entry::Value can be used to aggregate and group stats, - # annotate traces and logs, etc. - # - # Restrictions - # - Must contain only printable ASCII (codes between 32 and 126 inclusive) - # - Must have length greater than zero and less than 256. - # - Must not be empty. - class Key - attr_reader :name - - def initialize(name) - raise ArgumentError unless Internal.printable_ascii?(name) && (1..255).include?(name.length) - - @name = -name - end - end - - # Entry::Value wraps a string. It MUST contain only printable ASCII (codes between 32 and 126). - class Value - def initialize(value) - raise ArgumentError unless Internal.printable_ascii?(value) - - @value = -value - end - - def to_s - @value - end - end - - # Entry::Metadata contains properties associated with an Entry. For now only the property entry_ttl is defined. - # In future, additional properties may be added to address specific situations. - # - # The creator of entries determines metadata of an entry it creates. - class Metadata - attr_reader :entry_ttl - - # An @see Entry with NO_PROPAGATION is considered to have local scope and is used within the process - # where it is created. - NO_PROPAGATION = 0 - - # An @see Entry with UNLIMITED_PROPAGATION can propagate unlimited hops. However, it is still subject - # to outgoing and incoming (on remote side) filter criteria. - UNLIMITED_PROPAGATION = -1 - - def initialize(entry_ttl) - raise ArgumentError unless entry_ttl.is_a?(Integer) - - @entry_ttl = entry_ttl - end - end - end - end -end diff --git a/api/lib/opentelemetry/distributed_context/manager.rb b/api/lib/opentelemetry/distributed_context/manager.rb deleted file mode 100644 index 406d6cfc5..000000000 --- a/api/lib/opentelemetry/distributed_context/manager.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -module OpenTelemetry - module DistributedContext - class Manager - end - end -end diff --git a/api/lib/opentelemetry/distributed_context/propagation.rb b/api/lib/opentelemetry/distributed_context/propagation.rb deleted file mode 100644 index 453ec4823..000000000 --- a/api/lib/opentelemetry/distributed_context/propagation.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'opentelemetry/distributed_context/propagation/binary_format' -require 'opentelemetry/distributed_context/propagation/trace_parent' -require 'opentelemetry/distributed_context/propagation/text_format' - -module OpenTelemetry - module DistributedContext - # Propagation API consists of two main formats: - # - @see BinaryFormat is used to serialize and deserialize a value into a binary representation. - # - @see TextFormat is used to inject and extract a value as text into carriers that travel in-band across process boundaries. - module Propagation - end - end -end diff --git a/api/lib/opentelemetry/distributed_context/propagation/text_format.rb b/api/lib/opentelemetry/distributed_context/propagation/text_format.rb deleted file mode 100644 index 632814d30..000000000 --- a/api/lib/opentelemetry/distributed_context/propagation/text_format.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 -module OpenTelemetry - module DistributedContext - module Propagation - # TextFormat is a formatter that injects and extracts a value as text into carriers that travel in-band across - # process boundaries. - # Encoding is expected to conform to the HTTP Header Field semantics. Values are often encoded as RPC/HTTP request - # headers. - # - # The carrier of propagated data on both the client (injector) and server (extractor) side is usually an http request. - # Propagation is usually implemented via library-specific request interceptors, where the client-side injects values - # and the server-side extracts them. - class TextFormat - DEFAULT_GETTER = ->(carrier, key) { carrier[key] } - DEFAULT_SETTER = ->(carrier, key, value) { carrier[key] = value } - private_constant(:DEFAULT_GETTER, :DEFAULT_SETTER) - - # Returns an array with the trace context header keys used by this formatter - attr_reader :fields - - # Returns a new TextFormat that injects and extracts using the specified trace context - # header keys - # - # @param [String] traceparent_header_key The traceparent header key used in the carrier - # @param [String] tracestate_header_key The tracestate header key used in the carrier - # @return [TextFormatter] - def initialize(traceparent_header_key:, tracestate_header_key:) - @traceparent_header_key = traceparent_header_key - @tracestate_header_key = tracestate_header_key - @fields = [traceparent_header_key, tracestate_header_key].freeze - end - - # Return a remote {Trace::SpanContext} extracted from the supplied carrier. Expects the - # the supplied carrier to have keys in rack normalized format (HTTP_#{UPPERCASE_KEY}). - # Invalid headers will result in a new, valid, non-remote {Trace::SpanContext}. - # - # @param [Carrier] carrier The carrier to get the header from. - # @param [optional Callable] getter An optional callable that takes a carrier and a key and - # returns the value associated with the key. If omitted the default getter will be used - # which expects the carrier to respond to [] and []=. - # @yield [Carrier, String] if an optional getter is provided, extract will yield the carrier - # and the header key to the getter. - # @return [SpanContext] the span context from the header, or a new one if parsing fails. - def extract(carrier, &getter) - getter ||= DEFAULT_GETTER - header = getter.call(carrier, @traceparent_header_key) - tp = TraceParent.from_string(header) - - tracestate = getter.call(carrier, @tracestate_header_key) - - Trace::SpanContext.new(trace_id: tp.trace_id, span_id: tp.span_id, trace_flags: tp.flags, tracestate: tracestate, remote: true) - rescue OpenTelemetry::Error - Trace::SpanContext.new - end - - # Set the span context on the supplied carrier. - # - # @param [SpanContext] context The active {Trace::SpanContext}. - # @param [optional Callable] setter An optional callable that takes a carrier and a key and - # a value and assigns the key-value pair in the carrier. If omitted the default setter - # will be used which expects the carrier to respond to [] and []=. - # @yield [Carrier, String, String] if an optional setter is provided, inject will yield - # carrier, header key, header value to the setter. - def inject(context, carrier, &setter) - setter ||= DEFAULT_SETTER - setter.call(carrier, @traceparent_header_key, TraceParent.from_context(context).to_s) - setter.call(carrier, @tracestate_header_key, context.tracestate) unless context.tracestate.nil? - end - end - end - end -end diff --git a/api/lib/opentelemetry/trace.rb b/api/lib/opentelemetry/trace.rb index 4bc1c9e92..460db734b 100644 --- a/api/lib/opentelemetry/trace.rb +++ b/api/lib/opentelemetry/trace.rb @@ -43,6 +43,7 @@ def self.generate_span_id require 'opentelemetry/trace/event' require 'opentelemetry/trace/link' +require 'opentelemetry/trace/propagation' require 'opentelemetry/trace/trace_flags' require 'opentelemetry/trace/span_context' require 'opentelemetry/trace/span_kind' diff --git a/api/lib/opentelemetry/trace/propagation.rb b/api/lib/opentelemetry/trace/propagation.rb new file mode 100644 index 000000000..bdc4ae15a --- /dev/null +++ b/api/lib/opentelemetry/trace/propagation.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/trace/propagation/binary_format' +require 'opentelemetry/trace/propagation/trace_parent' +require 'opentelemetry/trace/propagation/context_keys' +require 'opentelemetry/trace/propagation/http_trace_context_extractor' +require 'opentelemetry/trace/propagation/http_trace_context_injector' + +module OpenTelemetry + module Trace + # The Trace::Propagation module contains injectors and extractors for + # sending and receiving span context over the wire + module Propagation + extend self + + HTTP_TRACE_CONTEXT_EXTRACTOR = HttpTraceContextExtractor.new + HTTP_TRACE_CONTEXT_INJECTOR = HttpTraceContextInjector.new + RACK_HTTP_TRACE_CONTEXT_EXTRACTOR = HttpTraceContextExtractor.new( + traceparent_header_key: 'HTTP_TRACEPARENT', + tracestate_header_key: 'HTTP_TRACESTATE' + ) + RACK_HTTP_TRACE_CONTEXT_INJECTOR = HttpTraceContextInjector.new( + traceparent_header_key: 'HTTP_TRACEPARENT', + tracestate_header_key: 'HTTP_TRACESTATE' + ) + BINARY_FORMAT = BinaryFormat.new + + private_constant :HTTP_TRACE_CONTEXT_INJECTOR, :HTTP_TRACE_CONTEXT_EXTRACTOR, + :RACK_HTTP_TRACE_CONTEXT_INJECTOR, :RACK_HTTP_TRACE_CONTEXT_EXTRACTOR, + :BINARY_FORMAT + + # Returns an extractor that extracts context using the W3C Trace Context + # format for HTTP + def http_trace_context_extractor + HTTP_TRACE_CONTEXT_EXTRACTOR + end + + # Returns an injector that injects context using the W3C Trace Context + # format for HTTP + def http_trace_context_injector + HTTP_TRACE_CONTEXT_INJECTOR + end + + # Returns an extractor that extracts context using the W3C Trace Context + # format for HTTP with Rack normalized keys (upcased and prefixed with + # HTTP_) + def rack_http_trace_context_extractor + RACK_HTTP_TRACE_CONTEXT_EXTRACTOR + end + + # Returns an injector that injects context using the W3C Trace Context + # format for HTTP with Rack normalized keys (upcased and prefixed with + # HTTP_) + def rack_http_trace_context_injector + RACK_HTTP_TRACE_CONTEXT_INJECTOR + end + + # Returns a propagator for the binary format + def binary_format + BINARY_FORMAT + end + end + end +end diff --git a/api/lib/opentelemetry/distributed_context/propagation/binary_format.rb b/api/lib/opentelemetry/trace/propagation/binary_format.rb similarity index 95% rename from api/lib/opentelemetry/distributed_context/propagation/binary_format.rb rename to api/lib/opentelemetry/trace/propagation/binary_format.rb index 3cf07d98b..3b6f4df71 100644 --- a/api/lib/opentelemetry/distributed_context/propagation/binary_format.rb +++ b/api/lib/opentelemetry/trace/propagation/binary_format.rb @@ -5,7 +5,7 @@ # SPDX-License-Identifier: Apache-2.0 module OpenTelemetry - module DistributedContext + module Trace module Propagation # Formatter for serializing and deserializing a SpanContext into a binary format. class BinaryFormat diff --git a/api/lib/opentelemetry/trace/propagation/context_keys.rb b/api/lib/opentelemetry/trace/propagation/context_keys.rb new file mode 100644 index 000000000..43e63e38e --- /dev/null +++ b/api/lib/opentelemetry/trace/propagation/context_keys.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Trace + module Propagation + # Contains the keys used to index the current span, or extracted span + # context in a {Context} instance + module ContextKeys + extend self + + EXTRACTED_SPAN_CONTEXT_KEY = Context.create_key('extracted-span-context') + CURRENT_SPAN_KEY = Context.create_key('current-span') + private_constant :EXTRACTED_SPAN_CONTEXT_KEY, :CURRENT_SPAN_KEY + + # Returns the context key that an extracted span context is indexed by + # + # @return [Context::Key] + def extracted_span_context_key + EXTRACTED_SPAN_CONTEXT_KEY + end + + # Returns the context key that the current span is indexed by + # + # @return [Context::Key] + def current_span_key + CURRENT_SPAN_KEY + end + end + end + end +end diff --git a/api/lib/opentelemetry/trace/propagation/http_trace_context_extractor.rb b/api/lib/opentelemetry/trace/propagation/http_trace_context_extractor.rb new file mode 100644 index 000000000..ddb259f05 --- /dev/null +++ b/api/lib/opentelemetry/trace/propagation/http_trace_context_extractor.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 +module OpenTelemetry + module Trace + module Propagation + # Extracts context from carriers in the W3C Trace Context format + class HttpTraceContextExtractor + include Context::Propagation::DefaultGetter + + # Returns a new HttpTraceContextExtractor that extracts context using the + # specified header keys + # + # @param [String] traceparent_header_key The traceparent header key used in the carrier + # @param [String] tracestate_header_key The tracestate header key used in the carrier + # @return [HttpTraceContextExtractor] + def initialize(traceparent_header_key: 'traceparent', + tracestate_header_key: 'tracestate') + @traceparent_header_key = traceparent_header_key + @tracestate_header_key = tracestate_header_key + end + + # Extract a remote {Trace::SpanContext} from the supplied carrier. + # Invalid headers will result in a new, valid, non-remote {Trace::SpanContext}. + # + # @param [Context] context The context to be updated with extracted context + # @param [Carrier] carrier The carrier to get the header from. + # @param [optional Callable] getter An optional callable that takes a carrier and a key and + # returns the value associated with the key. If omitted the default getter will be used + # which expects the carrier to respond to [] and []=. + # @yield [Carrier, String] if an optional getter is provided, extract will yield the carrier + # and the header key to the getter. + # @return [Context] Updated context with span context from the header, or the original + # context if parsing fails. + def extract(context, carrier, &getter) + getter ||= default_getter + header = getter.call(carrier, @traceparent_header_key) + tp = TraceParent.from_string(header) + + tracestate = getter.call(carrier, @tracestate_header_key) + + span_context = Trace::SpanContext.new(trace_id: tp.trace_id, + span_id: tp.span_id, + trace_flags: tp.flags, + tracestate: tracestate, + remote: true) + context.set_value(ContextKeys.extracted_span_context_key, span_context) + rescue OpenTelemetry::Error + context + end + end + end + end +end diff --git a/api/lib/opentelemetry/trace/propagation/http_trace_context_injector.rb b/api/lib/opentelemetry/trace/propagation/http_trace_context_injector.rb new file mode 100644 index 000000000..54d7c0cb4 --- /dev/null +++ b/api/lib/opentelemetry/trace/propagation/http_trace_context_injector.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 +module OpenTelemetry + module Trace + module Propagation + # Injects context into carriers using the W3C Trace Context format + class HttpTraceContextInjector + include Context::Propagation::DefaultSetter + + # Returns a new HttpTraceContextInjector that injects context using the + # specified header keys + # + # @param [String] traceparent_header_key The traceparent header key used in the carrier + # @param [String] tracestate_header_key The tracestate header key used in the carrier + # @return [HttpTraceContextInjector] + def initialize(traceparent_header_key: 'traceparent', + tracestate_header_key: 'tracestate') + @traceparent_header_key = traceparent_header_key + @tracestate_header_key = tracestate_header_key + end + + # Set the span context on the supplied carrier. + # + # @param [Context] context The active {Context}. + # @param [optional Callable] setter An optional callable that takes a carrier and a key and + # a value and assigns the key-value pair in the carrier. If omitted the default setter + # will be used which expects the carrier to respond to [] and []=. + # @yield [Carrier, String, String] if an optional setter is provided, inject will yield + # carrier, header key, header value to the setter. + # @return [Object] the carrier with context injected + def inject(context, carrier, &setter) + return carrier unless (span_context = span_context_from(context)) + + setter ||= DEFAULT_SETTER + setter.call(carrier, @traceparent_header_key, TraceParent.from_context(span_context).to_s) + setter.call(carrier, @tracestate_header_key, span_context.tracestate) unless span_context.tracestate.nil? + + carrier + end + + private + + def span_context_from(context) + context[ContextKeys.current_span_key]&.context || + context[ContextKeys.extracted_span_context_key] + end + end + end + end +end diff --git a/api/lib/opentelemetry/distributed_context/propagation/trace_parent.rb b/api/lib/opentelemetry/trace/propagation/trace_parent.rb similarity index 99% rename from api/lib/opentelemetry/distributed_context/propagation/trace_parent.rb rename to api/lib/opentelemetry/trace/propagation/trace_parent.rb index 6a2354f73..1f08f0a2b 100644 --- a/api/lib/opentelemetry/distributed_context/propagation/trace_parent.rb +++ b/api/lib/opentelemetry/trace/propagation/trace_parent.rb @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: Apache-2.0 module OpenTelemetry - module DistributedContext + module Trace module Propagation # A TraceParent is an implementation of the W3C trace context specification # https://www.w3.org/TR/trace-context/ diff --git a/api/lib/opentelemetry/trace/tracer.rb b/api/lib/opentelemetry/trace/tracer.rb index 8b5c52753..ebdd05546 100644 --- a/api/lib/opentelemetry/trace/tracer.rb +++ b/api/lib/opentelemetry/trace/tracer.rb @@ -8,11 +8,29 @@ module OpenTelemetry module Trace # No-op implementation of Tracer. class Tracer - CONTEXT_SPAN_KEY = :__span__ - private_constant(:CONTEXT_SPAN_KEY) + EXTRACTED_SPAN_CONTEXT_KEY = Propagation::ContextKeys.extracted_span_context_key + CURRENT_SPAN_KEY = Propagation::ContextKeys.current_span_key + + private_constant :EXTRACTED_SPAN_CONTEXT_KEY, :CURRENT_SPAN_KEY def current_span - Context.get(CONTEXT_SPAN_KEY) || Span::INVALID + Context.value(CURRENT_SPAN_KEY) || Span::INVALID + end + + # Returns the the active span context from the given {Context}, or current + # if one is not explicitly passed in. The active span context may refer to + # a {SpanContext} that has been extracted. If both a current {Span} and an + # extracted, {SpanContext} exist, the context of the current {Span} will be + # returned. + # + # @param [optional Context] context The context to lookup the active + # {SpanContext} from. + # + def active_span_context(context = nil) + context ||= Context.current + context.value(CURRENT_SPAN_KEY)&.context || + context.value(EXTRACTED_SPAN_CONTEXT_KEY) || + SpanContext::INVALID end # This is a helper for the default use-case of extending the current trace with a span. @@ -38,7 +56,7 @@ def in_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil, # # On exit, the Span that was active before calling this method will be reactivated. def with_span(span) - Context.with(CONTEXT_SPAN_KEY, span) { |s| yield s } + Context.with_value(CURRENT_SPAN_KEY, span) { |s| yield s } end def start_root_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil, sampling_hint: nil) @@ -52,12 +70,12 @@ def start_root_span(name, attributes: nil, links: nil, start_timestamp: nil, kin # # @param [optional Span] with_parent Explicitly managed parent Span, overrides # +with_parent_context+. - # @param [optional SpanContext] with_parent_context Explicitly managed. Overridden by + # @param [optional Context] with_parent_context Explicitly managed. Overridden by # +with_parent+. # # @return [Span] def start_span(name, with_parent: nil, with_parent_context: nil, attributes: nil, links: nil, start_timestamp: nil, kind: nil, sampling_hint: nil) - span_context = with_parent&.context || with_parent_context || current_span.context + span_context = with_parent&.context || active_span_context(with_parent_context) if span_context.valid? Span.new(span_context: span_context) else diff --git a/api/lib/opentelemetry/trace/tracer_factory.rb b/api/lib/opentelemetry/trace/tracer_factory.rb index ac74c4eec..732a9e754 100644 --- a/api/lib/opentelemetry/trace/tracer_factory.rb +++ b/api/lib/opentelemetry/trace/tracer_factory.rb @@ -8,17 +8,6 @@ module OpenTelemetry module Trace # No-op implementation of a tracer factory. class TracerFactory - HTTP_TEXT_FORMAT = DistributedContext::Propagation::TextFormat.new( - traceparent_header_key: 'traceparent', - tracestate_header_key: 'tracestate' - ) - RACK_HTTP_TEXT_FORMAT = DistributedContext::Propagation::TextFormat.new( - traceparent_header_key: 'HTTP_TRACEPARENT', - tracestate_header_key: 'HTTP_TRACESTATE' - ) - BINARY_FORMAT = DistributedContext::Propagation::BinaryFormat.new - private_constant(:HTTP_TEXT_FORMAT, :RACK_HTTP_TEXT_FORMAT, :BINARY_FORMAT) - # Returns a {Tracer} instance. # # @param [optional String] name Instrumentation package name @@ -28,18 +17,6 @@ class TracerFactory def tracer(name = nil, version = nil) @tracer ||= Tracer.new end - - def binary_format - BINARY_FORMAT - end - - def http_text_format - HTTP_TEXT_FORMAT - end - - def rack_http_text_format - RACK_HTTP_TEXT_FORMAT - end end end end diff --git a/api/test/opentelemetry/context/key_test.rb b/api/test/opentelemetry/context/key_test.rb new file mode 100644 index 000000000..cf5a1003c --- /dev/null +++ b/api/test/opentelemetry/context/key_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Context::Key do + Context = OpenTelemetry::Context + + after do + Context.clear + end + + it 'can be used for indexing' do + key = Context::Key.new('k') + ctx = Context.empty.set_value(key, 'v') + _(ctx.value(key)).must_equal('v') + end + + it 'indexes properly with duplicate name' do + k1 = Context::Key.new('k') + k2 = Context::Key.new('k') + ctx = Context.empty.set_value(k1, 'v1') + ctx = ctx.set_value(k2, 'v2') + _(ctx.value(k1)).must_equal('v1') + _(ctx.value(k2)).must_equal('v2') + end + + describe '.get' do + it 'retrieves associated entry from Context' do + key = Context::Key.new('k') + ctx = Context.empty.set_value(key, 'v') + _(key.get(ctx)).must_equal('v') + end + end +end diff --git a/api/test/opentelemetry/context/propagation/propagation_test.rb b/api/test/opentelemetry/context/propagation/propagation_test.rb new file mode 100644 index 000000000..4bdb812e1 --- /dev/null +++ b/api/test/opentelemetry/context/propagation/propagation_test.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Context::Propagation::Propagation do + class SimpleInjector + def initialize(key) + @key = key + end + + def inject(context, carrier) + carrier[@key] = context[@key] + carrier + end + end + + class SimpleExtractor + def initialize(key) + @key = key + end + + def extract(context, carrier) + context.set_value(@key, carrier[@key]) + end + end + + let(:propagation) { OpenTelemetry::Context::Propagation::Propagation.new } + let(:injectors) { %w[k1 k2 k3].map { |k| SimpleInjector.new(k) } } + let(:extractors) { %w[k1 k2 k3].map { |k| SimpleExtractor.new(k) } } + + after do + Context.clear + propagation.http_injectors = [] + propagation.http_extractors = [] + end + + describe '.http_injectors' do + it 'is settable' do + _(propagation.http_injectors).must_equal([]) + propagation.http_injectors = injectors + _(propagation.http_injectors).must_equal(injectors) + end + end + + describe '.http_extractors' do + it 'is settable' do + _(propagation.http_extractors).must_equal([]) + propagation.http_extractors = extractors + _(propagation.http_extractors).must_equal(extractors) + end + end + + describe '#inject' do + it 'returns carrier with empty injectors' do + Context.with_value('k1', 'v1') do + Context.with_value('k2', 'v2') do + Context.with_value('k3', 'v3') do + carrier_before = {} + carrier_after = propagation.inject(carrier_before) + _(carrier_before).must_equal(carrier_after) + end + end + end + end + + it 'injects values from current context into carrier' do + Context.with_value('k1', 'v1') do + Context.with_value('k2', 'v2') do + Context.with_value('k3', 'v3') do + carrier = propagation.inject({}, http_injectors: injectors) + _(carrier).must_equal('k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3') + end + end + end + end + + it 'uses global injectors' do + propagation.http_injectors = injectors + Context.with_value('k1', 'v1') do + Context.with_value('k2', 'v2') do + Context.with_value('k3', 'v3') do + carrier = propagation.inject({}) + _(carrier).must_equal('k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3') + end + end + end + end + + it 'accepts explicit context' do + propagation.http_injectors = injectors + Context.with_value('k1', 'v1') do + Context.with_value('k2', 'v2') do + ctx = Context.current.set_value('k3', 'v3') do + carrier = propagation.inject({}, context: ctx) + _(carrier).must_equal('k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3') + end + end + end + end + end + + describe '#extract' do + it 'returns original context with empty extractors' do + context_before = Context.current + carrier = { 'k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3' } + context_after = propagation.extract(carrier) + _(context_before).must_equal(context_after) + end + + it 'extracts values from carrier into context' do + carrier = { 'k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3' } + context = propagation.extract(carrier, http_extractors: extractors) + _(context['k1']).must_equal('v1') + _(context['k2']).must_equal('v2') + _(context['k3']).must_equal('v3') + end + + it 'uses global extractors' do + propagation.http_extractors = extractors + carrier = { 'k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3' } + context = propagation.extract(carrier) + _(context['k1']).must_equal('v1') + _(context['k2']).must_equal('v2') + _(context['k3']).must_equal('v3') + end + + it 'accepts explicit context' do + ctx = Context.empty.set_value('k0', 'v0') + propagation.http_extractors = extractors + carrier = { 'k1' => 'v1', 'k2' => 'v2', 'k3' => 'v3' } + context = propagation.extract(carrier, context: ctx) + _(context['k0']).must_equal('v0') + _(context['k1']).must_equal('v1') + _(context['k2']).must_equal('v2') + _(context['k3']).must_equal('v3') + end + end +end diff --git a/api/test/opentelemetry/context_test.rb b/api/test/opentelemetry/context_test.rb new file mode 100644 index 000000000..bfb71311c --- /dev/null +++ b/api/test/opentelemetry/context_test.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'logger' +require 'stringio' + +describe OpenTelemetry::Context do + Context = OpenTelemetry::Context + + after do + Context.clear + end + + let(:foo_key) { Context.create_key('foo') } + let(:bar_key) { Context.create_key('bar') } + let(:baz_key) { Context.create_key('baz') } + let(:new_context) { Context.empty.set_value(foo_key, 'bar') } + + describe '.current' do + it 'defaults to the root context' do + _(Context.current).must_equal(Context::ROOT) + end + end + + describe '.with_current' do + it 'handles nested contexts' do + c1 = new_context + Context.with_current(c1) do + _(Context.current).must_equal(c1) + c2 = Context.current.set_value(bar_key, 'baz') + Context.with_current(c2) do + _(Context.current).must_equal(c2) + end + _(Context.current).must_equal(c1) + end + end + + it 'resets context when an exception is raised' do + c1 = new_context + Context.current = c1 + + _(proc do + c2 = Context.current.set_value(bar_key, 'baz') + Context.with_current(c2) do + raise 'oops' + end + end).must_raise(StandardError) + + _(Context.current).must_equal(c1) + end + end + + describe '.with_value' do + it 'executes block within new context' do + orig_ctx = Context.current + + block_called = false + + Context.with_value(foo_key, 'bar') do |value| + _(Context.current.value(foo_key)).must_equal('bar') + _(value).must_equal('bar') + block_called = true + end + + _(Context.current).must_equal(orig_ctx) + _(block_called).must_equal(true) + end + end + + describe '#value' do + it 'returns corresponding value for key' do + ctx = new_context + _(ctx.value(foo_key)).must_equal('bar') + end + end + + describe '.with_values' do + it 'executes block within new context' do + orig_ctx = Context.current + + block_called = false + + Context.with_values(foo_key => 'bar', bar_key => 'baz') do |values| + _(Context.current.value(foo_key)).must_equal('bar') + _(Context.current.value(bar_key)).must_equal('baz') + _(values).must_equal(foo_key => 'bar', bar_key => 'baz') + block_called = true + end + + _(Context.current).must_equal(orig_ctx) + _(block_called).must_equal(true) + end + end + + describe '#set_values' do + it 'assigns multiple values' do + ctx = new_context + ctx2 = ctx.set_values(bar_key => 'baz', baz_key => 'quux') + _(ctx2.value(foo_key)).must_equal('bar') + _(ctx2.value(bar_key)).must_equal('baz') + _(ctx2.value(baz_key)).must_equal('quux') + end + + it 'merges new values' do + ctx = new_context + ctx2 = ctx.set_values(foo_key => 'foobar', bar_key => 'baz') + _(ctx2.value(foo_key)).must_equal('foobar') + _(ctx2.value(bar_key)).must_equal('baz') + end + end + + describe '#update' do + it 'returns new context with entry' do + c1 = Context.current + c2 = c1.set_value(foo_key, 'bar') + _(c1.value(foo_key)).must_be_nil + _(c2.value(foo_key)).must_equal('bar') + end + end + + describe 'threading' do + it 'unwinds the stack on each thread' do + ctx = new_context + t1_ctx_before = Context.current + Context.with_current(ctx) do + Thread.new do + t2_ctx_before = Context.current + Context.with_current(ctx) do + Context.with_value(bar_key, 'foobar') do + _(Context.current).wont_equal(t2_ctx_before) + end + end + _(Context.current).must_equal(t2_ctx_before) + end.join + Context.with_value(bar_key, 'baz') do + _(Context.current).wont_equal(t1_ctx_before) + end + end + _(Context.current).must_equal(t1_ctx_before) + end + + it 'scopes changes to the current thread' do + ctx = new_context + Context.with_current(ctx) do + Thread.new do + Context.with_current(ctx) do + Context.with_value(bar_key, 'foobar') do + Thread.pass + _(Context.current[foo_key]).must_equal('bar') + _(Context.current[bar_key]).must_equal('foobar') + end + _(Context.current[bar_key]).must_be_nil + end + end.join + Context.with_value(bar_key, 'baz') do + Thread.pass + _(Context.current[foo_key]).must_equal('bar') + _(Context.current[bar_key]).must_equal('baz') + end + _(Context.current[bar_key]).must_be_nil + end + end + end +end diff --git a/api/test/opentelemetry/correlation_context/http_extractor_test.rb b/api/test/opentelemetry/correlation_context/http_extractor_test.rb new file mode 100644 index 000000000..c2e03dc39 --- /dev/null +++ b/api/test/opentelemetry/correlation_context/http_extractor_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::CorrelationContext::Propagation::HttpExtractor do + let(:extractor) do + OpenTelemetry::CorrelationContext::Propagation::HttpExtractor.new + end + let(:header_key) do + 'Correlation-Context' + end + let(:context_key) do + OpenTelemetry::CorrelationContext::Propagation::ContextKeys.correlation_context_key + end + + describe '#extract' do + describe 'valid headers' do + it 'extracts key-value pairs' do + carrier = { header_key => 'key1=val1,key2=val2' } + context = extractor.extract(Context.empty, carrier) + correlations = context[context_key] + _(correlations['key1']).must_equal('val1') + _(correlations['key2']).must_equal('val2') + end + + it 'extracts entries with spaces' do + carrier = { header_key => ' key1 = val1, key2=val2 ' } + context = extractor.extract(Context.empty, carrier) + correlations = context[context_key] + _(correlations['key1']).must_equal('val1') + _(correlations['key2']).must_equal('val2') + end + + it 'ignores properties' do + carrier = { header_key => 'key1=val1,key2=val2;prop1=propval1;prop2=propval2' } + context = extractor.extract(Context.empty, carrier) + correlations = context[context_key] + _(correlations['key1']).must_equal('val1') + _(correlations['key2']).must_equal('val2') + end + + it 'extracts urlencoded entries' do + carrier = { header_key => 'key%3A1=val1%2C1,key%3A2=val2%2C2' } + context = extractor.extract(Context.empty, carrier) + correlations = context[context_key] + _(correlations['key:1']).must_equal('val1,1') + _(correlations['key:2']).must_equal('val2,2') + end + + it 'returns original context on failure' do + orig_context = Context.empty.set_value('k1', 'v1') + carrier = { header_key => 'key1=val1,key2=val2' } + context = extractor.extract(orig_context, carrier) { raise 'mwahaha' } + _(context).must_equal(orig_context) + end + end + end +end diff --git a/api/test/opentelemetry/correlation_context/http_injector_test.rb b/api/test/opentelemetry/correlation_context/http_injector_test.rb new file mode 100644 index 000000000..6d8cef888 --- /dev/null +++ b/api/test/opentelemetry/correlation_context/http_injector_test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::CorrelationContext::Propagation::HttpInjector do + let(:injector) do + OpenTelemetry::CorrelationContext::Propagation::HttpInjector.new + end + let(:header_key) do + 'Correlation-Context' + end + let(:context_key) do + OpenTelemetry::CorrelationContext::Propagation::ContextKeys.correlation_context_key + end + + describe '#inject' do + it 'injects correlations' do + context = Context.empty.set_value(context_key, 'key1' => 'val1', + 'key2' => 'val2') + + carrier = injector.inject(context, {}) + + _(carrier[header_key]).must_equal('key1=val1,key2=val2') + end + + it 'injects numeric correlations' do + context = Context.empty.set_value(context_key, 'key1' => 1, + 'key2' => 3.14) + + carrier = injector.inject(context, {}) + + _(carrier[header_key]).must_equal('key1=1,key2=3.14') + end + + it 'injects boolean correlations' do + context = Context.empty.set_value(context_key, 'key1' => true, + 'key2' => false) + + carrier = injector.inject(context, {}) + + _(carrier[header_key]).must_equal('key1=true,key2=false') + end + + it 'does not inject correlation key is not present' do + carrier = injector.inject(Context.empty, {}) + _(carrier).must_be(:empty?) + end + + it 'injects boolean correlations' do + context = Context.empty.set_value(context_key, {}) + + carrier = injector.inject(context, {}) + + _(carrier).must_be(:empty?) + end + end +end diff --git a/api/test/opentelemetry/correlation_context/propagation_test.rb b/api/test/opentelemetry/correlation_context/propagation_test.rb new file mode 100644 index 000000000..10ac8d190 --- /dev/null +++ b/api/test/opentelemetry/correlation_context/propagation_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::CorrelationContext::Propagation do + describe '#http_extractor, #rack_http_extractor' do + it 'returns an instance of HttpTraceContextExtractor' do + %i[http_extractor rack_http_extractor].each do |extractor_method| + extractor = OpenTelemetry::CorrelationContext::Propagation.send(extractor_method) + _(extractor).must_be_instance_of( + OpenTelemetry::CorrelationContext::Propagation::HttpExtractor + ) + end + end + end + + describe '#http_injector, #rack_http_injector' do + it 'returns an instance of HttpTraceContextInjector' do + %i[http_injector rack_http_injector].each do |injector_method| + injector = OpenTelemetry::CorrelationContext::Propagation.send(injector_method) + _(injector).must_be_instance_of( + OpenTelemetry::CorrelationContext::Propagation::HttpInjector + ) + end + end + end +end diff --git a/api/test/opentelemetry/distributed_context/propagation/text_format_test.rb b/api/test/opentelemetry/distributed_context/propagation/text_format_test.rb deleted file mode 100644 index 25609cf0a..000000000 --- a/api/test/opentelemetry/distributed_context/propagation/text_format_test.rb +++ /dev/null @@ -1,116 +0,0 @@ -# frozen_string_literal: true - -# Copyright 2019 OpenTelemetry Authors -# -# SPDX-License-Identifier: Apache-2.0 - -require 'test_helper' - -describe OpenTelemetry::DistributedContext::Propagation::TextFormat do - let(:traceparent_header_key) { 'traceparent' } - let(:tracestate_header_key) { 'tracestate' } - let(:formatter) do - OpenTelemetry::DistributedContext::Propagation::TextFormat.new( - traceparent_header_key: traceparent_header_key, - tracestate_header_key: tracestate_header_key - ) - end - let(:valid_traceparent_header) do - '00-000000000000000000000000000000AA-00000000000000ea-01' - end - let(:invalid_traceparent_header) do - 'FF-000000000000000000000000000000AA-00000000000000ea-01' - end - let(:tracestate_header) { 'vendorname=opaquevalue' } - - describe '#extract' do - let(:carrier) do - { - traceparent_header_key => valid_traceparent_header, - tracestate_header_key => tracestate_header - } - end - - it 'yields the carrier and the header key' do - yielded_keys = [] - formatter.extract(carrier) do |c, key| - _(c).must_equal(carrier) - yielded_keys << key - c[key] - end - _(yielded_keys.sort).must_equal([traceparent_header_key, tracestate_header_key]) - end - - it 'returns a remote SpanContext with fields from the traceparent and tracestate headers' do - context = formatter.extract(carrier) { |c, k| c[k] } - _(context).must_be :remote? - _(context.trace_id).must_equal('000000000000000000000000000000aa') - _(context.span_id).must_equal('00000000000000ea') - _(context.trace_flags).must_be :sampled? - _(context.tracestate).must_equal('vendorname=opaquevalue') - end - - it 'uses a default getter if one is not provided' do - context = formatter.extract(carrier) - _(context).must_be :remote? - _(context.trace_id).must_equal('000000000000000000000000000000aa') - _(context.span_id).must_equal('00000000000000ea') - _(context.trace_flags).must_be :sampled? - _(context.tracestate).must_equal('vendorname=opaquevalue') - end - - it 'returns a valid non-remote SpanContext on error' do - context = formatter.extract({}) { invalid_traceparent_header } - _(context).wont_be :remote? - _(context).must_be :valid? - end - end - - describe '#inject' do - let(:span_context) do - OpenTelemetry::Trace::SpanContext.new(trace_id: 'f' * 32, span_id: '1' * 16) - end - - let(:span_context_with_tracestate) do - OpenTelemetry::Trace::SpanContext.new(trace_id: 'f' * 32, span_id: '1' * 16, tracestate: tracestate_header) - end - - it 'yields the carrier, key, and traceparent value from the context' do - carrier = {} - yielded = false - formatter.inject(span_context, carrier) do |c, k, v| - _(c).must_equal(carrier) - _(k).must_equal(traceparent_header_key) - _(v).must_equal('00-ffffffffffffffffffffffffffffffff-1111111111111111-00') - yielded = true - c - end - _(yielded).must_equal(true) - end - - it 'does not yield the tracestate from the context, if nil' do - carrier = {} - formatter.inject(span_context, carrier) { |c, k, v| c[k] = v } - _(carrier).wont_include(tracestate_header_key) - end - - it 'yields the tracestate from the context, if provided' do - carrier = {} - formatter.inject(span_context_with_tracestate, carrier) { |c, k, v| c[k] = v } - _(carrier).must_include(tracestate_header_key) - end - - it 'uses the default setter if one is not provided' do - carrier = {} - formatter.inject(span_context_with_tracestate, carrier) - _(carrier[traceparent_header_key]).must_equal('00-ffffffffffffffffffffffffffffffff-1111111111111111-00') - _(carrier[tracestate_header_key]).must_equal(tracestate_header) - end - end - - describe '#fields' do - it 'returns an array with the W3C traceparent header' do - _(formatter.fields.sort).must_equal([traceparent_header_key, tracestate_header_key]) - end - end -end diff --git a/api/test/opentelemetry/trace/propagation/http_trace_context_extractor_test.rb b/api/test/opentelemetry/trace/propagation/http_trace_context_extractor_test.rb new file mode 100644 index 000000000..b8aeffbcf --- /dev/null +++ b/api/test/opentelemetry/trace/propagation/http_trace_context_extractor_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Trace::Propagation::HttpTraceContextExtractor do + let(:span_context_key) do + OpenTelemetry::Trace::Propagation::ContextKeys.extracted_span_context_key + end + let(:traceparent_header_key) { 'traceparent' } + let(:tracestate_header_key) { 'tracestate' } + let(:extractor) do + OpenTelemetry::Trace::Propagation::HttpTraceContextExtractor.new( + traceparent_header_key: traceparent_header_key, + tracestate_header_key: tracestate_header_key + ) + end + let(:valid_traceparent_header) do + '00-000000000000000000000000000000AA-00000000000000ea-01' + end + let(:invalid_traceparent_header) do + 'FF-000000000000000000000000000000AA-00000000000000ea-01' + end + let(:tracestate_header) { 'vendorname=opaquevalue' } + let(:carrier) do + { + traceparent_header_key => valid_traceparent_header, + tracestate_header_key => tracestate_header + } + end + let(:context) { Context.empty } + + describe '#extract' do + it 'yields the carrier and the header key' do + yielded_keys = [] + extractor.extract(context, carrier) do |c, key| + _(c).must_equal(carrier) + yielded_keys << key + c[key] + end + _(yielded_keys.sort).must_equal([traceparent_header_key, tracestate_header_key]) + end + + it 'returns a remote SpanContext with fields from the traceparent and tracestate headers' do + ctx = extractor.extract(context, carrier) { |c, k| c[k] } + span_context = ctx[span_context_key] + _(span_context).must_be :remote? + _(span_context.trace_id).must_equal('000000000000000000000000000000aa') + _(span_context.span_id).must_equal('00000000000000ea') + _(span_context.trace_flags).must_be :sampled? + _(span_context.tracestate).must_equal('vendorname=opaquevalue') + end + + it 'uses a default getter if one is not provided' do + ctx = extractor.extract(context, carrier) + span_context = ctx[span_context_key] + _(span_context).must_be :remote? + _(span_context.trace_id).must_equal('000000000000000000000000000000aa') + _(span_context.span_id).must_equal('00000000000000ea') + _(span_context.trace_flags).must_be :sampled? + _(span_context.tracestate).must_equal('vendorname=opaquevalue') + end + + it 'returns original context on error' do + ctx = extractor.extract(context, {}) { invalid_traceparent_header } + _(ctx).must_equal(context) + span_context = ctx[span_context_key] + _(span_context).must_be_nil + end + end +end diff --git a/api/test/opentelemetry/trace/propagation/http_trace_context_injector_test.rb b/api/test/opentelemetry/trace/propagation/http_trace_context_injector_test.rb new file mode 100644 index 000000000..8cf8bf28b --- /dev/null +++ b/api/test/opentelemetry/trace/propagation/http_trace_context_injector_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Trace::Propagation::HttpTraceContextInjector do + Span = OpenTelemetry::Trace::Span + SpanContext = OpenTelemetry::Trace::SpanContext + + let(:current_span_key) do + OpenTelemetry::Trace::Propagation::ContextKeys.current_span_key + end + let(:extracted_span_context_key) do + OpenTelemetry::Trace::Propagation::ContextKeys.extracted_span_context_key + end + let(:traceparent_header_key) { 'traceparent' } + let(:tracestate_header_key) { 'tracestate' } + let(:injector) do + OpenTelemetry::Trace::Propagation::HttpTraceContextInjector.new( + traceparent_header_key: traceparent_header_key, + tracestate_header_key: tracestate_header_key + ) + end + let(:valid_traceparent_header) do + '00-000000000000000000000000000000AA-00000000000000ea-01' + end + let(:invalid_traceparent_header) do + 'FF-000000000000000000000000000000AA-00000000000000ea-01' + end + let(:tracestate_header) { 'vendorname=opaquevalue' } + let(:context) do + span_context = SpanContext.new(trace_id: 'f' * 32, span_id: '1' * 16) + span = Span.new(span_context: span_context) + Context.empty.set_value(current_span_key, span) + end + let(:context_with_tracestate) do + span_context = SpanContext.new(trace_id: 'f' * 32, span_id: '1' * 16, + tracestate: tracestate_header) + span = Span.new(span_context: span_context) + Context.empty.set_value(current_span_key, span) + end + let(:context_without_current_span) do + span_context = SpanContext.new(trace_id: 'f' * 32, span_id: '1' * 16, + tracestate: tracestate_header) + Context.empty.set_value(extracted_span_context_key, span_context) + end + + describe '#inject' do + it 'yields the carrier, key, and traceparent value from the context' do + yielded = false + injector.inject(context, {}) do |c, k, v| + _(c).must_equal({}) + _(k).must_equal(traceparent_header_key) + _(v).must_equal('00-ffffffffffffffffffffffffffffffff-1111111111111111-00') + yielded = true + c + end + _(yielded).must_equal(true) + end + + it 'does not yield the tracestate from the context, if nil' do + carrier = injector.inject(context, {}) { |c, k, v| c[k] = v } + _(carrier).wont_include(tracestate_header_key) + end + + it 'yields the tracestate from the context, if provided' do + carrier = injector.inject(context_with_tracestate, {}) { |c, k, v| c[k] = v } + _(carrier).must_include(tracestate_header_key) + end + + it 'uses the default setter if one is not provided' do + carrier = injector.inject(context_with_tracestate, {}) + _(carrier[traceparent_header_key]).must_equal('00-ffffffffffffffffffffffffffffffff-1111111111111111-00') + _(carrier[tracestate_header_key]).must_equal(tracestate_header) + end + + it 'propagates remote context without current span' do + carrier = injector.inject(context_with_tracestate, {}) + _(carrier[traceparent_header_key]).must_equal('00-ffffffffffffffffffffffffffffffff-1111111111111111-00') + _(carrier[tracestate_header_key]).must_equal(tracestate_header) + end + end +end diff --git a/api/test/opentelemetry/distributed_context/propagation/trace_parent_test.rb b/api/test/opentelemetry/trace/propagation/trace_parent_test.rb similarity index 96% rename from api/test/opentelemetry/distributed_context/propagation/trace_parent_test.rb rename to api/test/opentelemetry/trace/propagation/trace_parent_test.rb index cf1c52570..4fcee7f5c 100644 --- a/api/test/opentelemetry/distributed_context/propagation/trace_parent_test.rb +++ b/api/test/opentelemetry/trace/propagation/trace_parent_test.rb @@ -5,8 +5,8 @@ # SPDX-License-Identifier: Apache-2.0 require 'test_helper' -describe OpenTelemetry::DistributedContext::Propagation::TraceParent do - TraceParent = OpenTelemetry::DistributedContext::Propagation::TraceParent +describe OpenTelemetry::Trace::Propagation::TraceParent do + TraceParent = OpenTelemetry::Trace::Propagation::TraceParent Trace = OpenTelemetry::Trace let(:good) do flags = Trace::TraceFlags.from_byte(1) diff --git a/api/test/opentelemetry/trace/propagation_test.rb b/api/test/opentelemetry/trace/propagation_test.rb new file mode 100644 index 000000000..632f1d87d --- /dev/null +++ b/api/test/opentelemetry/trace/propagation_test.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Trace::Propagation do + describe '#http_trace_context_extractor, #rack_http_trace_context_extractor' do + it 'returns an instance of HttpTraceContextExtractor' do + %i[http_trace_context_extractor rack_http_trace_context_extractor].each do |extractor_method| + extractor = OpenTelemetry::Trace::Propagation.send(extractor_method) + _(extractor).must_be_instance_of( + OpenTelemetry::Trace::Propagation::HttpTraceContextExtractor + ) + end + end + end + + describe '#http_trace_context_injector, #rack_http_trace_context_injector' do + it 'returns an instance of HttpTraceContextInjector' do + %i[http_trace_context_injector rack_http_trace_context_injector].each do |injector_method| + injector = OpenTelemetry::Trace::Propagation.send(injector_method) + _(injector).must_be_instance_of( + OpenTelemetry::Trace::Propagation::HttpTraceContextInjector + ) + end + end + end + + describe '#binary_format' do + it 'returns an instance of BinaryFormat' do + _(OpenTelemetry::Trace::Propagation.binary_format).must_be_instance_of( + OpenTelemetry::Trace::Propagation::BinaryFormat + ) + end + end +end diff --git a/api/test/opentelemetry/trace/tracer_factory_test.rb b/api/test/opentelemetry/trace/tracer_factory_test.rb index 6fd958501..121d25d73 100644 --- a/api/test/opentelemetry/trace/tracer_factory_test.rb +++ b/api/test/opentelemetry/trace/tracer_factory_test.rb @@ -16,32 +16,4 @@ _(tracer1).must_equal(tracer2) end end - - describe '#binary_format' do - it 'returns an instance of BinaryFormat' do - _(tracer_factory.binary_format).must_be_instance_of( - Propagation::BinaryFormat - ) - end - end - - describe '#http_text_format' do - it 'returns a formatter for lowercase trace context keys' do - formatter = tracer_factory.http_text_format - _(formatter).must_be_instance_of( - Propagation::TextFormat - ) - _(formatter.fields).must_equal(%w[traceparent tracestate]) - end - end - - describe '#rack_http_text_format' do - it 'returns a formatter for Rack normalized trace context keys' do - formatter = tracer_factory.rack_http_text_format - _(formatter).must_be_instance_of( - Propagation::TextFormat - ) - _(formatter.fields).must_equal(%w[HTTP_TRACEPARENT HTTP_TRACESTATE]) - end - end end diff --git a/api/test/opentelemetry/trace/tracer_test.rb b/api/test/opentelemetry/trace/tracer_test.rb index 7fc1f7000..33daf76d5 100644 --- a/api/test/opentelemetry/trace/tracer_test.rb +++ b/api/test/opentelemetry/trace/tracer_test.rb @@ -7,7 +7,7 @@ require 'test_helper' describe OpenTelemetry::Trace::Tracer do - Propagation = OpenTelemetry::DistributedContext::Propagation + Propagation = OpenTelemetry::Trace::Propagation Tracer = OpenTelemetry::Trace::Tracer # Tracer to verify expectation that `Span#finish` is called @@ -19,7 +19,22 @@ def start_span(*) end let(:invalid_span) { OpenTelemetry::Trace::Span::INVALID } + let(:invalid_span_context) { OpenTelemetry::Trace::SpanContext::INVALID } + let(:invalid_parent_context) do + OpenTelemetry::Context.empty.set_value( + OpenTelemetry::Trace::Propagation::ContextKeys.extracted_span_context_key, + invalid_span_context + ) + end let(:tracer) { Tracer.new } + let(:context_key) + let(:parent_span_context) { OpenTelemetry::Trace::SpanContext.new } + let(:parent_context) do + OpenTelemetry::Context.empty.set_value( + OpenTelemetry::Trace::Propagation::ContextKeys.extracted_span_context_key, + parent_span_context + ) + end describe '#current_span' do let(:current_span) { tracer.start_span('current') } @@ -37,9 +52,40 @@ def start_span(*) end end + describe '#active_span_context' do + let(:current_span) { tracer.start_span('current') } + + it 'returns an invalid span context by default' do + _(tracer.active_span_context).must_equal(invalid_span_context) + end + + it 'returns the span context from the current context by default' do + wrapper_span = tracer.start_span('wrapper') + + tracer.with_span(wrapper_span) do + _(tracer.active_span_context).must_equal(wrapper_span.context) + end + end + + it 'returns span context from implicit and explicit contexts' do + wrapper_span = tracer.start_span('wrapper') + wrapper_ctx = nil + + tracer.with_span(wrapper_span) do + wrapper_ctx = Context.current + end + + inner_span = tracer.start_span('inner') + + tracer.with_span(inner_span) do + _(tracer.active_span_context).must_equal(inner_span.context) + _(tracer.active_span_context(wrapper_ctx)).must_equal(wrapper_span.context) + end + end + end + describe '#in_span' do let(:parent) { tracer.start_span('parent') } - let(:parent_context) { OpenTelemetry::Trace::SpanContext.new } it 'yields the new span' do tracer.in_span('wrapper') do |span| @@ -70,7 +116,7 @@ def start_span(*) it 'yields a span with the parent context' do tracer.in_span('op', with_parent_context: parent_context) do |span| _(span.context).must_be :valid? - _(span.context).must_equal(parent_context) + _(span.context).must_equal(parent_span_context) end end end @@ -98,6 +144,21 @@ def start_span(*) _(tracer.current_span).must_equal(outer) end end + + it 'should reactivate the span context after the block' do + outer = tracer.start_span('outer') + inner = tracer.start_span('inner') + + tracer.with_span(outer) do + _(tracer.active_span_context).must_equal(outer.context) + + tracer.with_span(inner) do + _(tracer.active_span_context).must_equal(inner.context) + end + + _(tracer.active_span_context).must_equal(outer.context) + end + end end describe '#start_root_span' do @@ -108,14 +169,12 @@ def start_span(*) end describe '#start_span' do - let(:invalid_context) { OpenTelemetry::Trace::SpanContext::INVALID } let(:parent) { tracer.start_span('parent') } - let(:parent_context) { OpenTelemetry::Trace::SpanContext.new } it 'returns a valid span with the parent context' do span = tracer.start_span('op', with_parent_context: parent_context) _(span.context).must_be :valid? - _(span.context).must_equal(parent_context) + _(span.context).must_equal(parent_span_context) end it 'returns a span with a new context by default' do @@ -131,9 +190,9 @@ def start_span(*) end it 'returns a span with a new context when passed an invalid context' do - span = tracer.start_span('op', with_parent_context: invalid_context) + span = tracer.start_span('op', with_parent_context: invalid_parent_context) _(span.context).must_be :valid? - _(span.context).wont_equal(invalid_context) + _(span.context).wont_equal(invalid_span_context) end end end diff --git a/api/test/opentelemetry_test.rb b/api/test/opentelemetry_test.rb index 26a8061e7..c70faa0ab 100644 --- a/api/test/opentelemetry_test.rb +++ b/api/test/opentelemetry_test.rb @@ -66,27 +66,27 @@ end end - describe '.distributed_context_manager' do + describe '.correlations' do after do - # Ensure we don't leak custom distributed_context_manager to other tests - OpenTelemetry.distributed_context_manager = nil + # Ensure we don't leak custom correlations to other tests + OpenTelemetry.correlations = nil end - it 'returns instance of DistributedContext::Manager by default' do - manager = OpenTelemetry.distributed_context_manager - _(manager).must_be_instance_of(OpenTelemetry::DistributedContext::Manager) + it 'returns CorrelationContext::Manager by default' do + manager = OpenTelemetry.correlations + _(manager).must_be_instance_of(OpenTelemetry::CorrelationContext::Manager) end it 'returns the same instance when accessed multiple times' do - _(OpenTelemetry.distributed_context_manager).must_equal( - OpenTelemetry.distributed_context_manager + _(OpenTelemetry.correlations).must_equal( + OpenTelemetry.correlations ) end - it 'returns user specified distributed_context_manager' do - custom_manager = 'a custom distributed_context_manager' - OpenTelemetry.distributed_context_manager = custom_manager - _(OpenTelemetry.distributed_context_manager).must_equal(custom_manager) + it 'returns user specified correlations' do + custom_manager = 'a custom correlations' + OpenTelemetry.correlations = custom_manager + _(OpenTelemetry.correlations).must_equal(custom_manager) end end @@ -97,4 +97,18 @@ ) end end + + describe '.propagation' do + it 'returns instance of Context::Propagation::Propagation by default' do + _(OpenTelemetry.propagation).must_be_instance_of( + OpenTelemetry::Context::Propagation::Propagation + ) + end + + it 'returns the same instance when accessed multiple times' do + _(OpenTelemetry.propagation).must_equal( + OpenTelemetry.propagation + ) + end + end end diff --git a/sdk/lib/opentelemetry/sdk.rb b/sdk/lib/opentelemetry/sdk.rb index 438845c66..3f6757377 100644 --- a/sdk/lib/opentelemetry/sdk.rb +++ b/sdk/lib/opentelemetry/sdk.rb @@ -61,6 +61,7 @@ def configure end require 'opentelemetry/sdk/configurator' +require 'opentelemetry/sdk/correlation_context' require 'opentelemetry/sdk/internal' require 'opentelemetry/sdk/resources' require 'opentelemetry/sdk/trace' diff --git a/sdk/lib/opentelemetry/sdk/correlation_context.rb b/sdk/lib/opentelemetry/sdk/correlation_context.rb new file mode 100644 index 000000000..2b0976846 --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/correlation_context.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk/correlation_context/builder' +require 'opentelemetry/sdk/correlation_context/manager' + +module OpenTelemetry + module SDK + # Contains operational implementataions of the CorrelationContext::Manager + module CorrelationContext + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/correlation_context/builder.rb b/sdk/lib/opentelemetry/sdk/correlation_context/builder.rb new file mode 100644 index 000000000..857ca71bd --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/correlation_context/builder.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module CorrelationContext + # SDK implementation of CorrelationContext::Builder + class Builder + attr_reader :entries + + def initialize(entries) + @entries = entries + end + + # Set key-value in the to-be-created correlation context + # + # @param [String] key The key to store this value under + # @param [String] value String value to be stored under key + def set_value(key, value) + @entries[key] = value.to_s + end + + # Removes key from the to-be-created correlation context + # + # @param [String] key The key to remove + def remove_value(key) + @entries.delete(key) + end + + # Clears all correlations from the to-be-created correlation context + def clear + @entries.clear + end + end + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/correlation_context/manager.rb b/sdk/lib/opentelemetry/sdk/correlation_context/manager.rb new file mode 100644 index 000000000..f55568506 --- /dev/null +++ b/sdk/lib/opentelemetry/sdk/correlation_context/manager.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module CorrelationContext + # Manages correlation context + class Manager + CORRELATION_CONTEXT_KEY = OpenTelemetry::CorrelationContext::Propagation::ContextKeys.correlation_context_key + EMPTY_CORRELATION_CONTEXT = {}.freeze + private_constant(:CORRELATION_CONTEXT_KEY, :EMPTY_CORRELATION_CONTEXT) + + # Used to chain modifications to correlation context. The result is a + # context with an updated correlation context. If only a single + # modification is being made to correlation context, use the other + # methods on +Manager+, if multiple modifications are being made, use + # this one. + # + # @param [optional Context] context The context to update with with new + # modified correlation context. Defaults to +Context.current+ + # @return [Context] + def build_context(context: Context.current) + builder = Builder.new(correlations_for(context).dup) + yield builder + context.set_value(CORRELATION_CONTEXT_KEY, builder.entries) + end + + # Returns a new context with empty correlations + # + # @param [optional Context] context Context to clear correlations from. Defaults + # to +Context.current+ + # @return [Context] + def clear(context: Context.current) + context.set_value(CORRELATION_CONTEXT_KEY, EMPTY_CORRELATION_CONTEXT) + end + + # Returns the corresponding correlation value (or nil) for key + # + # @param [String] key The lookup key + # @param [optional Context] context The context from which to retrieve + # the key. + # Defaults to +Context.current+ + # @return [String] + def value(key, context: Context.current) + correlations_for(context)[key] + end + + # Returns a new context with new key-value pair + # + # @param [String] key The key to store this value under + # @param [String] value String value to be stored under key + # @param [optional Context] context The context to update with new + # value. Defaults to +Context.current+ + # @return [Context] + def set_value(key, value, context: Context.current) + new_correlations = correlations_for(context).dup + new_correlations[key] = value + context.set_value(CORRELATION_CONTEXT_KEY, new_correlations) + end + + # Returns a new context with value at key removed + # + # @param [String] key The key to remove + # @param [optional Context] context The context to remove correlation + # from. Defaults to +Context.current+ + # @return [Context] + def remove_value(key, context: Context.current) + correlations = correlations_for(context) + return context unless correlations.key?(key) + + new_correlations = correlations.dup + new_correlations.delete(key) + context.set_value(CORRELATION_CONTEXT_KEY, new_correlations) + end + + private + + def correlations_for(context) + context.value(CORRELATION_CONTEXT_KEY) || EMPTY_CORRELATION_CONTEXT + end + end + end + end +end diff --git a/sdk/lib/opentelemetry/sdk/trace/tracer.rb b/sdk/lib/opentelemetry/sdk/trace/tracer.rb index c50f477bf..0d8cd8ebd 100644 --- a/sdk/lib/opentelemetry/sdk/trace/tracer.rb +++ b/sdk/lib/opentelemetry/sdk/trace/tracer.rb @@ -27,14 +27,13 @@ def initialize(name, version) end def start_root_span(name, attributes: nil, links: nil, start_timestamp: nil, kind: nil, sampling_hint: nil) - parent_span_context = OpenTelemetry::Trace::SpanContext::INVALID - start_span(name, with_parent_context: parent_span_context, attributes: attributes, links: links, start_timestamp: start_timestamp, kind: kind, sampling_hint: sampling_hint) + start_span(name, with_parent_context: Context.empty, attributes: attributes, links: links, start_timestamp: start_timestamp, kind: kind, sampling_hint: sampling_hint) end - def start_span(name, with_parent: nil, with_parent_context: nil, attributes: nil, links: nil, start_timestamp: nil, kind: nil, sampling_hint: nil) # rubocop:disable Metrics/AbcSize + def start_span(name, with_parent: nil, with_parent_context: nil, attributes: nil, links: nil, start_timestamp: nil, kind: nil, sampling_hint: nil) name ||= 'empty' - parent_span_context = with_parent&.context || with_parent_context || current_span.context + parent_span_context = with_parent&.context || active_span_context(with_parent_context) parent_span_context = nil unless parent_span_context.valid? parent_span_id = parent_span_context&.span_id tracestate = parent_span_context&.tracestate diff --git a/sdk/test/integration/api_trace_test.rb b/sdk/test/integration/api_trace_test.rb index e66738905..ec9d6e122 100644 --- a/sdk/test/integration/api_trace_test.rb +++ b/sdk/test/integration/api_trace_test.rb @@ -49,9 +49,16 @@ end describe 'tracing child-of-remote spans' do + let(:context_with_remote_parent) do + OpenTelemetry::Context.empty.set_value( + OpenTelemetry::Trace::Propagation::ContextKeys.extracted_span_context_key, + remote_span_context + ) + end + before do - @remote_span = tracer.start_span('remote', with_parent_context: remote_span_context) - @child_of_remote = tracer.start_span('child1', with_parent_context: @remote_span.context) + @remote_span = tracer.start_span('remote', with_parent_context: context_with_remote_parent) + @child_of_remote = tracer.start_span('child1', with_parent: @remote_span) end it 'has a child' do diff --git a/sdk/test/opentelemetry/sdk/correlation_context/manager_test.rb b/sdk/test/opentelemetry/sdk/correlation_context/manager_test.rb new file mode 100644 index 000000000..8966d7d8c --- /dev/null +++ b/sdk/test/opentelemetry/sdk/correlation_context/manager_test.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::CorrelationContext::Manager do + Context = OpenTelemetry::Context + let(:manager) { OpenTelemetry::SDK::CorrelationContext::Manager.new } + + after do + Context.clear + end + + describe '.set_value' do + describe 'explicit context' do + it 'sets key/value in context' do + ctx = Context.empty + _(manager.value('foo', context: ctx)).must_be_nil + + ctx2 = manager.set_value('foo', 'bar', context: ctx) + _(manager.value('foo', context: ctx2)).must_equal('bar') + + _(manager.value('foo', context: ctx)).must_be_nil + end + end + + describe 'implicit context' do + it 'sets key/value in implicit context' do + _(manager.value('foo')).must_be_nil + + Context.with_current(manager.set_value('foo', 'bar')) do + _(manager.value('foo')).must_equal('bar') + end + + _(manager.value('foo')).must_be_nil + end + end + end + + describe '.clear' do + describe 'explicit context' do + it 'returns context with empty correlation context' do + ctx = manager.set_value('foo', 'bar', context: Context.empty) + _(manager.value('foo', context: ctx)).must_equal('bar') + + ctx2 = manager.clear(context: ctx) + _(manager.value('foo', context: ctx2)).must_be_nil + end + end + + describe 'implicit context' do + it 'returns context with empty correlation context' do + ctx = manager.set_value('foo', 'bar') + _(manager.value('foo', context: ctx)).must_equal('bar') + + ctx2 = manager.clear + _(manager.value('foo', context: ctx2)).must_be_nil + end + end + end + + describe '.remove_value' do + describe 'explicit context' do + it 'returns context with key removed from correlation context' do + ctx = manager.set_value('foo', 'bar', context: Context.empty) + _(manager.value('foo', context: ctx)).must_equal('bar') + + ctx2 = manager.remove_value('foo', context: ctx) + _(manager.value('foo', context: ctx2)).must_be_nil + end + end + + describe 'implicit context' do + it 'returns context with key removed from correlation context' do + Context.with_current(manager.set_value('foo', 'bar')) do + _(manager.value('foo')).must_equal('bar') + + ctx = manager.remove_value('foo') + _(manager.value('foo', context: ctx)).must_be_nil + end + end + end + end + + describe '.build_context' do + let(:initial_context) { manager.set_value('k1', 'v1') } + + describe 'explicit context' do + it 'sets entries' do + ctx = initial_context + ctx = manager.build_context(context: ctx) do |correlations| + correlations.set_value('k2', 'v2') + correlations.set_value('k3', 'v3') + end + _(manager.value('k1', context: ctx)).must_equal('v1') + _(manager.value('k2', context: ctx)).must_equal('v2') + _(manager.value('k3', context: ctx)).must_equal('v3') + end + + it 'removes entries' do + ctx = initial_context + ctx = manager.build_context(context: ctx) do |correlations| + correlations.remove_value('k1') + correlations.set_value('k2', 'v2') + end + _(manager.value('k1', context: ctx)).must_be_nil + _(manager.value('k2', context: ctx)).must_equal('v2') + end + + it 'clears entries' do + ctx = initial_context + ctx = manager.build_context(context: ctx) do |correlations| + correlations.clear + correlations.set_value('k2', 'v2') + end + _(manager.value('k1', context: ctx)).must_be_nil + _(manager.value('k2', context: ctx)).must_equal('v2') + end + end + + describe 'implicit context' do + it 'sets entries' do + Context.with_current(initial_context) do + ctx = manager.build_context do |correlations| + correlations.set_value('k2', 'v2') + correlations.set_value('k3', 'v3') + end + Context.with_current(ctx) do + _(manager.value('k1')).must_equal('v1') + _(manager.value('k2')).must_equal('v2') + _(manager.value('k3')).must_equal('v3') + end + end + end + + it 'removes entries' do + Context.with_current(initial_context) do + _(manager.value('k1')).must_equal('v1') + + ctx = manager.build_context do |correlations| + correlations.remove_value('k1') + correlations.set_value('k2', 'v2') + end + + Context.with_current(ctx) do + _(manager.value('k1')).must_be_nil + _(manager.value('k2')).must_equal('v2') + end + end + end + + it 'clears entries' do + Context.with_current(initial_context) do + _(manager.value('k1')).must_equal('v1') + + ctx = manager.build_context do |correlations| + correlations.clear + correlations.set_value('k2', 'v2') + end + + Context.with_current(ctx) do + _(manager.value('k1')).must_be_nil + _(manager.value('k2')).must_equal('v2') + end + end + end + end + end +end diff --git a/sdk/test/opentelemetry/sdk/trace/tracer_test.rb b/sdk/test/opentelemetry/sdk/trace/tracer_test.rb index 63f3837fe..fed511c5e 100644 --- a/sdk/test/opentelemetry/sdk/trace/tracer_test.rb +++ b/sdk/test/opentelemetry/sdk/trace/tracer_test.rb @@ -142,7 +142,15 @@ end describe '#start_span' do - let(:context) { OpenTelemetry::Trace::SpanContext.new(tracestate: 'vendorname=opaquevalue') } + let(:span_context) do + OpenTelemetry::Trace::SpanContext.new(tracestate: 'vendorname=opaquevalue') + end + let(:context) do + OpenTelemetry::Context.empty.set_value( + OpenTelemetry::Trace::Propagation::ContextKeys.extracted_span_context_key, + span_context + ) + end it 'provides a default name' do _(tracer.start_span(nil, with_parent_context: context).name).wont_be_nil @@ -155,17 +163,17 @@ it 'returns a span with the same trace ID as the parent context' do span = tracer.start_span('op', with_parent_context: context) - _(span.context.trace_id).must_equal(context.trace_id) + _(span.context.trace_id).must_equal(span_context.trace_id) end it 'returns a span with the parent context span ID' do span = tracer.start_span('op', with_parent_context: context) - _(span.parent_span_id).must_equal(context.span_id) + _(span.parent_span_id).must_equal(span_context.span_id) end it 'returns a span with the parent context tracestate' do span = tracer.start_span('op', with_parent_context: context) - _(span.context.tracestate).must_equal(context.tracestate) + _(span.context.tracestate).must_equal(span_context.tracestate) end it 'returns a no-op span if sampler says do not record events' do @@ -198,7 +206,7 @@ attributes = Minitest::Mock.new result = Result.new(decision: Decision::NOT_RECORD) mock_sampler = Minitest::Mock.new - mock_sampler.expect(:call, result, [{ trace_id: context.trace_id, span_id: span_id, parent_context: context, hint: hint, links: links, name: name, kind: kind, attributes: attributes }]) + mock_sampler.expect(:call, result, [{ trace_id: span_context.trace_id, span_id: span_id, parent_context: span_context, hint: hint, links: links, name: name, kind: kind, attributes: attributes }]) activate_trace_config TraceConfig.new(sampler: mock_sampler) OpenTelemetry::Trace.stub :generate_span_id, span_id do tracer.start_span(name, with_parent_context: context, attributes: attributes, links: links, kind: kind, sampling_hint: hint) @@ -211,7 +219,7 @@ span = tracer.start_span('op', with_parent_context: context) _(span.context.trace_flags).wont_be :sampled? _(span).wont_be :recording? - _(span.context.trace_id).must_equal(context.trace_id) + _(span.context.trace_id).must_equal(span_context.trace_id) end it 'creates a span with all supplied parameters' do @@ -225,8 +233,8 @@ _(span.name).must_equal(name) _(span.kind).must_equal(kind) _(span.attributes).must_equal(attributes) - _(span.parent_span_id).must_equal(context.span_id) - _(span.context.trace_id).must_equal(context.trace_id) + _(span.parent_span_id).must_equal(span_context.span_id) + _(span.context.trace_id).must_equal(span_context.trace_id) _(span.start_timestamp).must_equal(start_timestamp) end