# frozen_string_literal: true
require_relative '../core/utils/safe_dup'
require_relative 'utils'
require_relative 'metadata/ext'
require_relative 'metadata'
module Datadog
module Tracing
# Represents a logical unit of work in the system. Each trace consists of one or more spans.
# Each span consists of a start time and a duration. For example, a span can describe the time
# spent on a distributed call on a separate machine, or the time spent in a small component
# within a larger operation. Spans can be nested within each other, and in those instances
# will have a parent-child relationship.
# @public_api
class Span
include Metadata
attr_accessor \
:end_time,
:id,
:meta,
:metrics,
:name,
:parent_id,
:resource,
:service,
:links,
:events,
:type,
:start_time,
:status,
:trace_id
attr_writer \
:duration
# Create a new span manually. Call the start() method to start the time
# measurement and then stop() once the timing operation is over.
#
# * +service+: the service name for this span
# * +resource+: the resource this span refers, or +name+ if it's missing.
# +nil+ can be used as a placeholder, when the resource value is not yet known at +#initialize+ time.
# * +type+: the type of the span (such as +http+, +db+ and so on)
# * +parent_id+: the identifier of the parent span
# * +trace_id+: the identifier of the root span for this trace
# * +service_entry+: whether it is a service entry span.
# * +events+: the list of events that occurred while a span was active.
def initialize(
name,
duration: nil,
end_time: nil,
id: nil,
meta: nil,
metrics: nil,
parent_id: 0,
resource: name,
service: nil,
start_time: nil,
status: 0,
type: nil,
trace_id: nil,
service_entry: nil,
links: nil,
events: nil
)
@name = Core::Utils::SafeDup.frozen_or_dup(name)
@service = Core::Utils::SafeDup.frozen_or_dup(service)
@resource = Core::Utils::SafeDup.frozen_or_dup(resource)
@type = Core::Utils::SafeDup.frozen_or_dup(type)
@id = id || Tracing::Utils.next_id
@parent_id = parent_id || 0
@trace_id = trace_id || Tracing::Utils.next_id
@meta = meta || {}
@metrics = metrics || {}
@status = status || 0
# start_time and end_time track wall clock. In Ruby, wall clock
# has less accuracy than monotonic clock, so if possible we look to only use wall clock
# to measure duration when a time is supplied by the user, or if monotonic clock
# is unsupported.
@start_time = start_time
@end_time = end_time
# duration_start and duration_end track monotonic clock, and may remain nil in cases where it
# is known that we have to use wall clock to measure duration.
@duration = duration
@service_entry = service_entry
@links = links || []
@events = events || []
# Mark with the service entry span metric, if applicable
set_metric(Metadata::Ext::TAG_TOP_LEVEL, 1.0) if service_entry
end
# Return whether the duration is started or not
def started?
!@start_time.nil?
end
# Return whether the duration is stopped or not.
def stopped?
!@end_time.nil?
end
alias :finished? :stopped?
def duration
return @duration if @duration
return @end_time - @start_time if @start_time && @end_time
end
def set_error(e)
@status = Metadata::Ext::Errors::STATUS
set_error_tags(e)
end
# Spans with the same ID are considered the same span
def ==(other)
other.instance_of?(Span) &&
@id == other.id
end
# Return a string representation of the span.
def to_s
"Span(name:#{@name},sid:#{@id},tid:#{@trace_id},pid:#{@parent_id})"
end
# Return the hash representation of the current span.
# TODO: Change this to reflect attributes when serialization
# isn't handled by this method.
def to_hash
h = {
error: @status,
meta: @meta,
metrics: @metrics,
name: @name,
parent_id: @parent_id,
resource: @resource,
service: @service,
span_id: @id,
trace_id: @trace_id,
type: @type,
span_links: @links.map(&:to_hash)
}
if stopped?
h[:start] = start_time_nano
h[:duration] = duration_nano
end
h[:meta]['events'] = @events.map(&:to_hash).to_json unless @events.empty?
h
end
# Return a human readable version of the span
def pretty_print(q)
start_time = (self.start_time.to_f * 1e9).to_i
end_time = (self.end_time.to_f * 1e9).to_i
q.group 0 do
q.breakable
q.text "Name: #{@name}\n"
q.text "Span ID: #{@id}\n"
q.text "Parent ID: #{@parent_id}\n"
q.text "Trace ID: #{@trace_id}\n"
q.text "Type: #{@type}\n"
q.text "Service: #{@service}\n"
q.text "Resource: #{@resource}\n"
q.text "Error: #{@status}\n"
q.text "Start: #{start_time}\n"
q.text "End: #{end_time}\n"
q.text "Duration: #{duration.to_f}\n"
q.group(2, 'Tags: [', "]\n") do
q.breakable
q.seplist @meta.each do |key, value|
q.text "#{key} => #{value}"
end
end
q.group(2, 'Metrics: [', ']') do
q.breakable
q.seplist @metrics.each do |key, value|
q.text "#{key} => #{value}"
end
end
end
end
private
# Used for serialization
# @return [Integer] in nanoseconds since Epoch
def start_time_nano
@start_time.to_i * 1000000000 + @start_time.nsec
end
# Used for serialization
# @return [Integer] in nanoseconds since Epoch
def duration_nano
(duration * 1e9).to_i
end
# https://docs.datadoghq.com/tracing/visualization/#service-entry-span
# A span is a service entry span when it is the entrypoint method for a request to a service.
# You can visualize this within Datadog APM when the color of the immediate parent on a flame graph is a different
# color. Services are also listed on the right when viewing a flame graph.
#
# @return [Boolean] `true` if the span is a serivce entry span
def service_entry?
@service_entry == true
end
end
end
end