class Datadog::Context
Context is used to keep track of a hierarchy of spans for the current execution flow. During each logical execution, the same Context is used to represent a single logical trace, even if the trace is built asynchronously.
A single code execution may use multiple Context if part of the execution must not be related to the current tracing. As example, a delayed job may compose a standalone trace instead of being related to the same trace that generates the job itself. On the other hand, if it's part of the same Context, it will be related to the original trace.
This data structure is thread-safe. rubocop:disable Metrics/ClassLength
Constants
- DEFAULT_MAX_LENGTH
100k spans is about a 100Mb footprint
Attributes
Public Class Methods
Initialize a new thread-safe Context.
# File lib/ddtrace/context.rb, line 31 def initialize(options = {}) @mutex = Mutex.new # max_length is the amount of spans above which, for a given trace, # the context will simply drop and ignore spans, avoiding high memory usage. @max_length = options.fetch(:max_length, DEFAULT_MAX_LENGTH) reset(options) end
Public Instance Methods
Add a span to the context trace list, keeping it as the last active span.
# File lib/ddtrace/context.rb, line 92 def add_span(span) @mutex.synchronize do # If hitting the hard limit, just drop spans. This is really a rare case # as it means despite the soft limit, the hard limit is reached, so the trace # by default has 10000 spans, all of which belong to unfinished parts of a # larger trace. This is a catch-all to reduce global memory usage. if @max_length > 0 && @trace.length >= @max_length # Detach the span from any context, it's being dropped and ignored. span.context = nil Datadog.logger.debug("context full, ignoring span #{span.name}") # If overflow has already occurred, don't send this metric. # Prevents metrics spam if buffer repeatedly overflows for the same trace. unless @overflow Datadog.health_metrics.error_context_overflow(1, tags: ["max_length:#{@max_length}"]) @overflow = true end return end set_current_span(span) @current_root_span = span if @trace.empty? @trace << span span.context = self end end
Set tags to root span required for flush
# File lib/ddtrace/context.rb, line 229 def annotate_for_flush!(span) attach_sampling_priority(span) if @sampled && @sampling_priority attach_origin(span) if @origin end
# File lib/ddtrace/context.rb, line 241 def attach_origin(span) span.set_tag( Ext::DistributedTracing::ORIGIN_KEY, @origin ) end
# File lib/ddtrace/context.rb, line 234 def attach_sampling_priority(span) span.set_metric( Ext::DistributedTracing::SAMPLING_PRIORITY_KEY, @sampling_priority ) end
Mark a span as a finished, increasing the internal counter to prevent cycles inside _trace list.
# File lib/ddtrace/context.rb, line 121 def close_span(span) @mutex.synchronize do @finished_spans += 1 # Current span is only meaningful for linear tree-like traces, # in other cases, this is just broken and one should rely # on per-instrumentation code to retrieve handle parent/child relations. set_current_span(span.parent) return if span.tracer.nil? if span.parent.nil? && !all_spans_finished? if Datadog.configuration.diagnostics.debug opened_spans = @trace.length - @finished_spans Datadog.logger.debug("root span #{span.name} closed but has #{opened_spans} unfinished spans:") end @trace.reject(&:finished?).group_by(&:name).each do |unfinished_span_name, unfinished_spans| Datadog.logger.debug("unfinished span: #{unfinished_spans.first}") if Datadog.configuration.diagnostics.debug Datadog.health_metrics.error_unfinished_spans( unfinished_spans.length, tags: ["name:#{unfinished_span_name}"] ) end end end end
# File lib/ddtrace/context.rb, line 85 def current_root_span @mutex.synchronize do return @current_root_span end end
Return the last active span that corresponds to the last inserted item in the trace list. This cannot be considered as the current active span in asynchronous environments, because some spans can be closed earlier while child spans still need to finish their traced execution.
# File lib/ddtrace/context.rb, line 79 def current_span @mutex.synchronize do return @current_span end end
Delete any span matching the condition. This is thread safe.
@return [Array<Span>] deleted spans
# File lib/ddtrace/context.rb, line 204 def delete_span_if @mutex.synchronize do [].tap do |deleted_spans| @trace.delete_if do |span| finished = span.finished? next unless yield span deleted_spans << span # We need to detach the span from the context, else, some code # finishing it afterwards would mess up with the number of # finished_spans and possibly cause other side effects. span.context = nil # Acknowledge there's one span less to finish, if needed. # It's very important to keep this balanced. @finished_spans -= 1 if finished true end end end end
Returns if the trace for the current Context
is finished or not. A Context is considered finished if all spans in this context are finished.
# File lib/ddtrace/context.rb, line 149 def finished? @mutex.synchronize do return all_spans_finished? end end
@@return [Numeric] numbers of finished spans
# File lib/ddtrace/context.rb, line 156 def finished_span_count @mutex.synchronize do @finished_spans end end
Generates equivalent context for forked processes.
When Context
from parent process is forked, child process should have a Context
belonging to the same trace but not have the parent process spans.
# File lib/ddtrace/context.rb, line 261 def fork_clone self.class.new( trace_id: trace_id, span_id: span_id, sampled: sampled?, sampling_priority: sampling_priority, origin: origin ) end
Returns both the trace list generated in the current context and if the context is sampled or not.
It returns +[nil,@sampled]+ if the Context is not finished.
If a trace is returned, the Context will be reset so that it can be re-used immediately.
This operation is thread-safe.
@return [Array<Array<Span>, Boolean>] finished trace and sampled flag
# File lib/ddtrace/context.rb, line 182 def get @mutex.synchronize do trace = @trace sampled = @sampled # still return sampled attribute, even if context is not finished return nil, sampled unless all_spans_finished? # Root span is finished at this point, we can configure it annotate_for_flush!(@current_root_span) # Allow caller to modify trace before context is reset yield(trace) if block_given? reset [trace, sampled] end end
# File lib/ddtrace/context.rb, line 63 def origin @mutex.synchronize do @origin end end
# File lib/ddtrace/context.rb, line 69 def origin=(origin) @mutex.synchronize do @origin = origin end end
Returns true if the context is sampled, that is, if it should be kept and sent to the trace agent.
# File lib/ddtrace/context.rb, line 164 def sampled? @mutex.synchronize do return @sampled end end
# File lib/ddtrace/context.rb, line 51 def sampling_priority @mutex.synchronize do @sampling_priority end end
# File lib/ddtrace/context.rb, line 57 def sampling_priority=(priority) @mutex.synchronize do @sampling_priority = priority end end
# File lib/ddtrace/context.rb, line 45 def span_id @mutex.synchronize do @parent_span_id end end
Return a string representation of the context.
# File lib/ddtrace/context.rb, line 249 def to_s @mutex.synchronize do # rubocop:disable Layout/LineLength "Context(trace.length:#{@trace.length},sampled:#{@sampled},finished_spans:#{@finished_spans},current_span:#{@current_span})" end end
# File lib/ddtrace/context.rb, line 39 def trace_id @mutex.synchronize do @parent_trace_id end end
Private Instance Methods
Returns if the trace for the current Context
is finished or not. Low-level internal function, not thread-safe.
# File lib/ddtrace/context.rb, line 299 def all_spans_finished? @finished_spans > 0 && @trace.length == @finished_spans end
Iterate on each span within the trace. This is thread safe.
# File lib/ddtrace/context.rb, line 320 def each_span(&block) @mutex.synchronize do @trace.each(&block) end end
Return the length of the current trace held by this context.
# File lib/ddtrace/context.rb, line 313 def length @mutex.synchronize do @trace.length end end
# File lib/ddtrace/context.rb, line 273 def reset(options = {}) @trace = [] @parent_trace_id = options.fetch(:trace_id, nil) @parent_span_id = options.fetch(:span_id, nil) @sampled = options.fetch(:sampled, false) @sampling_priority = options.fetch(:sampling_priority, nil) @origin = options.fetch(:origin, nil) @finished_spans = 0 @current_span = nil @current_root_span = nil @overflow = false end
# File lib/ddtrace/context.rb, line 286 def set_current_span(span) @current_span = span if span @parent_trace_id = span.trace_id @parent_span_id = span.span_id @sampled = span.sampled else @parent_span_id = nil end end
Return the start time of the root span, or nil if there are no spans or this is undefined.
# File lib/ddtrace/context.rb, line 304 def start_time @mutex.synchronize do return nil if @trace.empty? @trace[0].start_time end end