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 28
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 89
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 222
def annotate_for_flush!(span)
  attach_sampling_priority(span) if @sampled && @sampling_priority
  attach_origin(span) if @origin
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 118
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 82
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 76
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 197
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 145
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 152
def finished_span_count
  @mutex.synchronize do
    @finished_spans
  end
end
get() 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 178
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)

    reset
    [trace, sampled]
  end
end
origin() click to toggle source
# File lib/ddtrace/context.rb, line 60
def origin
  @mutex.synchronize do
    @origin
  end
end
origin=(origin) click to toggle source
# File lib/ddtrace/context.rb, line 66
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 160
def sampled?
  @mutex.synchronize do
    return @sampled
  end
end
sampling_priority() click to toggle source
# File lib/ddtrace/context.rb, line 48
def sampling_priority
  @mutex.synchronize do
    @sampling_priority
  end
end
sampling_priority=(priority) click to toggle source
# File lib/ddtrace/context.rb, line 54
def sampling_priority=(priority)
  @mutex.synchronize do
    @sampling_priority = priority
  end
end
span_id() click to toggle source
# File lib/ddtrace/context.rb, line 42
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 228
def to_s
  @mutex.synchronize do
    # rubocop:disable Metrics/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 36
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 263
def all_spans_finished?
  @finished_spans > 0 && @trace.length == @finished_spans
end
attach_origin(span) click to toggle source
# File lib/ddtrace/context.rb, line 274
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 267
def attach_sampling_priority(span)
  span.set_metric(
    Ext::DistributedTracing::SAMPLING_PRIORITY_KEY,
    @sampling_priority
  )
end
each_span() { |span| ... } click to toggle source

Iterate on each span within the trace. This is thread safe.

# File lib/ddtrace/context.rb, line 297
def each_span
  @mutex.synchronize do
    @trace.each do |span|
      yield span
    end
  end
end
length() click to toggle source

Return the length of the current trace held by this context.

# File lib/ddtrace/context.rb, line 290
def length
  @mutex.synchronize do
    @trace.length
  end
end
reset(options = {}) click to toggle source
# File lib/ddtrace/context.rb, line 237
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 250
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 282
def start_time
  @mutex.synchronize do
    return nil if @trace.empty?
    @trace[0].start_time
  end
end