From c95f7cd450e9aaaf55a489182574cab4135c22e2 Mon Sep 17 00:00:00 2001 From: Matthew Wear Date: Thu, 6 Feb 2020 13:32:51 -0800 Subject: [PATCH] Context prop combined (#147) * Add Context.with_current to handle context nesting * Update active span tracking to use Context * Rename Context#update -> Context.set * Add Context#clear method * Introduce baggage context * Introduce basic Context object * Implement Context#attach and Context#detach * Rename set->update; freeze entries * Handle detach when parent is nil * Context propagation prototype * Comment out failing tests after rebase * Appease the cop with todos for missing docs * Move baggage implementation to SDK * Use method names from API keep implicit (block) form, for now * Update baggage to use explicit context * Use context keys for baggage and current span * Align Context methods more closely with Java prototype * Add baggage_manager accessor to OpenTelemetry Module This seems like the right thing to do so that different implementations can be specified. We might also want to consider making Baggage::Manager a class rather than a module. * Convert tests for distributed_context_manager to correlation_context_manager * Add minimal CorrelationContextManager implementation * Add constructor and test for Label * Cleanup context in an after block rather than before * Initial implementation of SDK correlation context * Simplify label The spec specifies that there should be Label::Key and Label::Value classes. Each wraps a string, which is a little heavyweight and cumbersome to use. This commit changes Key and Value to simple strings instead. * Initial implementation of SDK::CorrelationContextManager * Add initial Propagation API * Move SpanContext related propagation to Trace namespace * Add HttpTraceContextInjector/Extractor For simplicity, these are based off of the TextFormat class. Ultimately, we'll probably end up with HttpInjector/Extractor classes extracted from the TextFormat class. * Add global injectors and extractors * Make Baggage::Manager a class instead of a module * Add Tracer#current_span_context * Add Context.with_values * Use Context.with_values in Tracer#with_span * Implement HttpTraceContextExtractor / Injector directly * Remove TextFormat and references * Remove reference to binary format in TracerFactory * Remove references to CorrelationContext (temporarily) CorrelationContext has been left out of the current OTEP. It's likely to be back and we have an initial implementation. This commit removes references from the API and SDK, but leaves the files intact until we know what should be done with them in the long term. * Add tests for context on multiple threads * Use alternative context implementation We should still do some benchmarking, but I think this is going to be a better implementation, so I'm preemptively using it for the purpose of the initial PR. * Provide default key names for HttpTraceContext / Injector extractor * Standardize naming for Baggage and DistributedContext injector / extractors * Add readers for injectors / extractors to TracerFactory * Remove distributed context * Move Baggage -> CorrelationContext * Prefer current span over extracted span context as implicit parent * Propagate remote span context if there is not a current span * Rename remote_span_context_key -> extracted_span_context_key * Move propagator references from TracerFactory to global Propagation * Happy path correlation context extractor * Ignore correlation context properties OTel may use properties in the future for metadata. For now, we'll ignore them when extracting. * Initial HttpCorrelationContextInjector implmentation * Manage CorrelationContext values as Strings * Accept explicit context object in Tracer#start_span * Use Correlation-Context as the default key This is subject to change, but we'll use the key as it's spec'd today. * Add HttpCorrelationContextExtractor/Injectors to Propagation * Extract default getter / setters into modules * Log warning for non matching Context attach / detach * Fix @todo docs * Remove explicit block parameters in Context * Provide defaults for Propagation#inject and Propagation#extract This commit improves the ergonomics of Propagation#inject and Propagation#extract by providing defaults arguments where possible. For both methods, carrier is the only required argument. Context defaults to Context.current and the http_injectors / extractors default to those registered globally. * Add implicit context to CorrelationContext::Manager methods * Introduce Builder to facilitate multiple modifications to CorrelationContext This commit adds a builder to faciliate making multiple modifications to CorrelationContext without creating multiple, intermediary contexts. Manager#build_context should be used when making multiple modifications to the correlation context. When making a single modification, all other methods should be used. Some may object to the builder and might recommend a single method with kwargs, but I think this makes for a more fluent API. * Reorganize accessors for injector / extractors * Rename HttpCorrelationContextInjector/Extractor -> HttpInjector/Extractor * Cleanup ContextKeys * Remove Context#attach and Context#detach * Cleanup correlations * Remove with_context from Tracer#start_span, redefine with_parent_context This commit replaces with_context with with_parent_context and removes with_context. * Return original context if parsing TraceContext fails * Revert "Remove Context#attach and Context#detach" This reverts commit 88138ea66627f7d3153c4d668ed228a481f24e05. * Document Context#attach and Context#detach to be private * Introduce Context::Key for indexing and retrieving values from a Context * Return to Hash based context There are perf concerns about the linked list approach. * Don't test implementation details * Update docs * Fix unnecessary cop after rebase * Return original context on failed Correlation Context extraction * Update Faraday and Sinatra adapters to use OpenTelemetry.propagation * Rename OpenTelemetry.correlation_context_manager -> OpenTelemetry.correlations Co-authored-by: Francis Bogsanyi --- .../faraday/middlewares/tracer_middleware.rb | 7 +- .../sinatra/middlewares/tracer_middleware.rb | 2 +- api/lib/opentelemetry.rb | 22 ++- api/lib/opentelemetry/context.rb | 148 +++++++++++++-- api/lib/opentelemetry/context/key.rb | 29 +++ api/lib/opentelemetry/context/propagation.rb | 18 ++ .../context/propagation/default_getter.rb | 26 +++ .../context/propagation/default_setter.rb | 26 +++ .../context/propagation/propagation.rb | 76 ++++++++ api/lib/opentelemetry/correlation_context.rb | 16 ++ .../correlation_context/builder.rb | 18 ++ .../correlation_context/manager.rb | 36 ++++ .../correlation_context/propagation.rb | 57 ++++++ .../propagation/context_keys.rb | 27 +++ .../propagation/http_extractor.rb | 60 ++++++ .../propagation/http_injector.rb | 55 ++++++ api/lib/opentelemetry/distributed_context.rb | 19 -- .../distributed_context.rb | 24 --- .../distributed_context/entry.rb | 66 ------- .../distributed_context/manager.rb | 12 -- .../distributed_context/propagation.rb | 19 -- .../propagation/text_format.rb | 76 -------- api/lib/opentelemetry/trace.rb | 1 + api/lib/opentelemetry/trace/propagation.rb | 68 +++++++ .../propagation/binary_format.rb | 2 +- .../trace/propagation/context_keys.rb | 35 ++++ .../http_trace_context_extractor.rb | 56 ++++++ .../http_trace_context_injector.rb | 53 ++++++ .../propagation/trace_parent.rb | 2 +- api/lib/opentelemetry/trace/tracer.rb | 30 ++- api/lib/opentelemetry/trace/tracer_factory.rb | 23 --- api/test/opentelemetry/context/key_test.rb | 38 ++++ .../context/propagation/propagation_test.rb | 142 +++++++++++++++ api/test/opentelemetry/context_test.rb | 168 +++++++++++++++++ .../http_extractor_test.rb | 62 +++++++ .../correlation_context/http_injector_test.rb | 61 +++++++ .../correlation_context/propagation_test.rb | 31 ++++ .../propagation/text_format_test.rb | 116 ------------ .../http_trace_context_extractor_test.rb | 74 ++++++++ .../http_trace_context_injector_test.rb | 86 +++++++++ .../propagation/trace_parent_test.rb | 4 +- .../opentelemetry/trace/propagation_test.rb | 39 ++++ .../trace/tracer_factory_test.rb | 28 --- api/test/opentelemetry/trace/tracer_test.rb | 75 +++++++- api/test/opentelemetry_test.rb | 38 ++-- sdk/lib/opentelemetry/sdk.rb | 1 + .../opentelemetry/sdk/correlation_context.rb | 16 ++ .../sdk/correlation_context/builder.rb | 40 ++++ .../sdk/correlation_context/manager.rb | 87 +++++++++ sdk/lib/opentelemetry/sdk/trace/tracer.rb | 7 +- sdk/test/integration/api_trace_test.rb | 11 +- .../sdk/correlation_context/manager_test.rb | 172 ++++++++++++++++++ .../opentelemetry/sdk/trace/tracer_test.rb | 24 ++- 53 files changed, 1972 insertions(+), 457 deletions(-) create mode 100644 api/lib/opentelemetry/context/key.rb create mode 100644 api/lib/opentelemetry/context/propagation.rb create mode 100644 api/lib/opentelemetry/context/propagation/default_getter.rb create mode 100644 api/lib/opentelemetry/context/propagation/default_setter.rb create mode 100644 api/lib/opentelemetry/context/propagation/propagation.rb create mode 100644 api/lib/opentelemetry/correlation_context.rb create mode 100644 api/lib/opentelemetry/correlation_context/builder.rb create mode 100644 api/lib/opentelemetry/correlation_context/manager.rb create mode 100644 api/lib/opentelemetry/correlation_context/propagation.rb create mode 100644 api/lib/opentelemetry/correlation_context/propagation/context_keys.rb create mode 100644 api/lib/opentelemetry/correlation_context/propagation/http_extractor.rb create mode 100644 api/lib/opentelemetry/correlation_context/propagation/http_injector.rb delete mode 100644 api/lib/opentelemetry/distributed_context.rb delete mode 100644 api/lib/opentelemetry/distributed_context/distributed_context.rb delete mode 100644 api/lib/opentelemetry/distributed_context/entry.rb delete mode 100644 api/lib/opentelemetry/distributed_context/manager.rb delete mode 100644 api/lib/opentelemetry/distributed_context/propagation.rb delete mode 100644 api/lib/opentelemetry/distributed_context/propagation/text_format.rb create mode 100644 api/lib/opentelemetry/trace/propagation.rb rename api/lib/opentelemetry/{distributed_context => trace}/propagation/binary_format.rb (95%) create mode 100644 api/lib/opentelemetry/trace/propagation/context_keys.rb create mode 100644 api/lib/opentelemetry/trace/propagation/http_trace_context_extractor.rb create mode 100644 api/lib/opentelemetry/trace/propagation/http_trace_context_injector.rb rename api/lib/opentelemetry/{distributed_context => trace}/propagation/trace_parent.rb (99%) create mode 100644 api/test/opentelemetry/context/key_test.rb create mode 100644 api/test/opentelemetry/context/propagation/propagation_test.rb create mode 100644 api/test/opentelemetry/context_test.rb create mode 100644 api/test/opentelemetry/correlation_context/http_extractor_test.rb create mode 100644 api/test/opentelemetry/correlation_context/http_injector_test.rb create mode 100644 api/test/opentelemetry/correlation_context/propagation_test.rb delete mode 100644 api/test/opentelemetry/distributed_context/propagation/text_format_test.rb create mode 100644 api/test/opentelemetry/trace/propagation/http_trace_context_extractor_test.rb create mode 100644 api/test/opentelemetry/trace/propagation/http_trace_context_injector_test.rb rename api/test/opentelemetry/{distributed_context => trace}/propagation/trace_parent_test.rb (96%) create mode 100644 api/test/opentelemetry/trace/propagation_test.rb create mode 100644 sdk/lib/opentelemetry/sdk/correlation_context.rb create mode 100644 sdk/lib/opentelemetry/sdk/correlation_context/builder.rb create mode 100644 sdk/lib/opentelemetry/sdk/correlation_context/manager.rb create mode 100644 sdk/test/opentelemetry/sdk/correlation_context/manager_test.rb 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 2ca04157b3..ad3f7f6343 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 4fea4b23a3..ae47fbe061 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 d6c3f30005..a9b6b2931b 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 f23093f06d..6e4eb55652 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 0000000000..b71874d3ff --- /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 0000000000..9875020cef --- /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 0000000000..4824882fdb --- /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 0000000000..1742c9338b --- /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 0000000000..5d6ca7e904 --- /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 0000000000..26fefab5ea --- /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 0000000000..42a85495c3 --- /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 0000000000..dfb6dde2fb --- /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 0000000000..120d96d1a0 --- /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 0000000000..9ef99855bb --- /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 0000000000..5c64f39815 --- /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 0000000000..5303f70364 --- /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 b8c3eaa60d..0000000000 --- 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 1cb80111d8..0000000000 --- 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 9aeffbb225..0000000000 --- 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 406d6cfc58..0000000000 --- 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 453ec48231..0000000000 --- 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 632814d301..0000000000 --- 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 4bc1c9e92a..460db734bc 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 0000000000..bdc4ae15a1 --- /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 3cf07d98be..3b6f4df71d 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 0000000000..43e63e38e7 --- /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 0000000000..ddb259f05b --- /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 0000000000..54d7c0cb4d --- /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 6a2354f736..1f08f0a2b8 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 8b5c52753b..ebdd055464 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 ac74c4eec1..732a9e754d 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 0000000000..cf5a1003c5 --- /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 0000000000..4bdb812e1a --- /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 0000000000..bfb71311c3 --- /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 0000000000..c2e03dc39a --- /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 0000000000..6d8cef888e --- /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 0000000000..10ac8d190a --- /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 25609cf0a0..0000000000 --- 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 0000000000..b8aeffbcf3 --- /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 0000000000..8cf8bf28bd --- /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 cf1c52570e..4fcee7f5c6 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 0000000000..632f1d87d1 --- /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 6fd9585018..121d25d731 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 7fc1f70004..33daf76d5f 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 26a8061e73..c70faa0ab6 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 438845c66c..3f6757377a 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 0000000000..2b0976846e --- /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 0000000000..857ca71bdb --- /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 0000000000..f55568506b --- /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 c50f477bfa..0d8cd8ebd4 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 e667389053..ec9d6e122e 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 0000000000..8966d7d8ce --- /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 63f3837fe9..fed511c5e4 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