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

max_length[R]

Public Class Methods

new(options = {}) click to toggle source

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_span(span) click to toggle source

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
annotate_for_flush!(span) click to toggle source

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
attach_origin(span) click to toggle source
# File lib/ddtrace/context.rb, line 241
def attach_origin(span)
  span.set_tag(
    Ext::DistributedTracing::ORIGIN_KEY,
    @origin
  )
end
attach_sampling_priority(span) click to toggle source
# File lib/ddtrace/context.rb, line 234
def attach_sampling_priority(span)
  span.set_metric(
    Ext::DistributedTracing::SAMPLING_PRIORITY_KEY,
    @sampling_priority
  )
end
close_span(span) click to toggle source

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
current_root_span() click to toggle source
# File lib/ddtrace/context.rb, line 85
def current_root_span
  @mutex.synchronize do
    return @current_root_span
  end
end
current_span() click to toggle source

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_span_if() { |span| ... } click to toggle source

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
finished?() click to toggle source

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
finished_span_count() click to toggle source

@@return [Numeric] numbers of finished spans

# File lib/ddtrace/context.rb, line 156
def finished_span_count
  @mutex.synchronize do
    @finished_spans
  end
end
fork_clone() click to toggle source

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
get() { |trace| ... } click to toggle source

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
origin() click to toggle source
# File lib/ddtrace/context.rb, line 63
def origin
  @mutex.synchronize do
    @origin
  end
end
origin=(origin) click to toggle source
# File lib/ddtrace/context.rb, line 69
def origin=(origin)
  @mutex.synchronize do
    @origin = origin
  end
end
sampled?() click to toggle source

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
sampling_priority() click to toggle source
# File lib/ddtrace/context.rb, line 51
def sampling_priority
  @mutex.synchronize do
    @sampling_priority
  end
end
sampling_priority=(priority) click to toggle source
# File lib/ddtrace/context.rb, line 57
def sampling_priority=(priority)
  @mutex.synchronize do
    @sampling_priority = priority
  end
end
span_id() click to toggle source
# File lib/ddtrace/context.rb, line 45
def span_id
  @mutex.synchronize do
    @parent_span_id
  end
end
to_s() click to toggle source

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
trace_id() click to toggle source
# File lib/ddtrace/context.rb, line 39
def trace_id
  @mutex.synchronize do
    @parent_trace_id
  end
end

Private Instance Methods

all_spans_finished?() click to toggle source

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
each_span(&block) click to toggle source

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
length() click to toggle source

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
reset(options = {}) click to toggle source
# 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
set_current_span(span) click to toggle source
# 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
start_time() click to toggle source

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