# typed: true # frozen_string_literal: true require 'time' require 'ddtrace/utils' require 'ddtrace/ext/distributed' require 'ddtrace/ext/environment' require 'ddtrace/ext/errors' require 'ddtrace/ext/http' require 'ddtrace/ext/net' require 'ddtrace/ext/priority' require 'ddtrace/analytics' require 'ddtrace/forced_tracing' require 'ddtrace/diagnostics/health' require 'ddtrace/utils/time' module Datadog # 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. # # rubocop:disable Metrics/ClassLength class Span prepend Analytics::Span prepend ForcedTracing::Span # The max value for a \Span identifier. # Span and trace identifiers should be strictly positive and strictly inferior to this limit. # # Limited to +2<<62-1+ positive integers, as Ruby is able to represent such numbers "inline", # inside a +VALUE+ scalar, thus not requiring memory allocation. # # The range of IDs also has to consider portability across different languages and platforms. RUBY_MAX_ID = (1 << 62) - 1 # While we only generate 63-bit integers due to limitations in other languages, we support # parsing 64-bit integers for distributed tracing since an upstream system may generate one EXTERNAL_MAX_ID = 1 << 64 # This limit is for numeric tags because uint64 could end up rounded. NUMERIC_TAG_SIZE_RANGE = (-1 << 53..1 << 53).freeze # Some associated values should always be sent as Tags, never as Metrics, regardless # if their value is numeric or not. # The Datadog agent will look for these values only as Tags, not Metrics. # @see https://github.com/DataDog/datadog-agent/blob/2ae2cdd315bcda53166dd8fa0dedcfc448087b9d/pkg/trace/stats/aggregation.go#L13-L17 ENSURE_AGENT_TAGS = { Ext::DistributedTracing::ORIGIN_KEY => true, Ext::Environment::TAG_VERSION => true, Ext::HTTP::STATUS_CODE => true, Ext::NET::TAG_HOSTNAME => true }.freeze attr_accessor :name, :service, :resource, :span_type, :span_id, :trace_id, :parent_id, :status, :sampled, :tracer, :context attr_reader :parent, :start_time, :end_time, :resource_container attr_writer :duration # Create a new span linked to the given tracer. Call the \Tracer method start_span() # and then finish() once the tracer 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. # * +span_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 # * +context+: the context of the span def initialize(tracer, name, options = {}) @tracer = tracer @name = name @service = options.fetch(:service, nil) @resource = options.fetch(:resource, name) @span_type = options.fetch(:span_type, nil) @span_id = Datadog::Utils.next_id @parent_id = options.fetch(:parent_id, 0) @trace_id = options.fetch(:trace_id, Datadog::Utils.next_id) @context = options.fetch(:context, nil) @meta = {} @metrics = {} @status = 0 @parent = nil @sampled = true @allocation_count_start = now_allocations @allocation_count_finish = @allocation_count_start # 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 = nil @end_time = nil # 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_start = nil @duration_end = nil end # Set the given key / value tag pair on the span. Keys and values # must be strings. A valid example is: # # span.set_tag('http.method', request.method) def set_tag(key, value = nil) # Keys must be unique between tags and metrics @metrics.delete(key) # DEV: This is necessary because the agent looks at `meta[key]`, not `metrics[key]`. value = value.to_s if ENSURE_AGENT_TAGS[key] # NOTE: Adding numeric tags as metrics is stop-gap support # for numeric typed tags. Eventually they will become # tags again. # Any numeric that is not an integer greater than max size is logged as a metric. # Everything else gets logged as a tag. if value.is_a?(Numeric) && !(value.is_a?(Integer) && !NUMERIC_TAG_SIZE_RANGE.cover?(value)) set_metric(key, value) else @meta[key] = value.to_s end rescue StandardError => e Datadog.logger.debug("Unable to set the tag #{key}, ignoring it. Caused by: #{e}") end # Sets tags from given hash, for each key in hash it sets the tag with that key # and associated value from the hash. It is shortcut for `set_tag`. Keys and values # of the hash must be strings. Note that nested hashes are not supported. # A valid example is: # # span.set_tags({ "http.method" => "GET", "user.id" => "234" }) def set_tags(tags) tags.each { |k, v| set_tag(k, v) } end # This method removes a tag for the given key. def clear_tag(key) @meta.delete(key) end # Return the tag with the given key, nil if it doesn't exist. def get_tag(key) @meta[key] || @metrics[key] end # This method sets a tag with a floating point value for the given key. It acts # like `set_tag()` and it simply add a tag without further processing. def set_metric(key, value) # Keys must be unique between tags and metrics @meta.delete(key) # enforce that the value is a floating point number value = Float(value) @metrics[key] = value rescue StandardError => e Datadog.logger.debug("Unable to set the metric #{key}, ignoring it. Caused by: #{e}") end # This method removes a metric for the given key. It acts like {#remove_tag}. def clear_metric(key) @metrics.delete(key) end # Return the metric with the given key, nil if it doesn't exist. def get_metric(key) @metrics[key] || @meta[key] end # Mark the span with the given error. def set_error(e) e = Error.build_from(e) @status = Ext::Errors::STATUS set_tag(Ext::Errors::TYPE, e.type) unless e.type.empty? set_tag(Ext::Errors::MSG, e.message) unless e.message.empty? set_tag(Ext::Errors::STACK, e.backtrace) unless e.backtrace.empty? end # Mark the span started at the current time. def start(start_time = nil) # A span should not be started twice. However, this is existing # behavior and so we maintain it for backward compatibility for those # who are using async manual instrumentation that may rely on this @start_time = start_time || Utils::Time.now.utc @duration_start = start_time.nil? ? duration_marker : nil self end # for backwards compatibility def start_time=(time) time.tap { start(time) } end # for backwards compatibility def end_time=(time) time.tap { finish(time) } end # Mark the span finished at the current time and submit it. def finish(finish_time = nil) # A span should not be finished twice. Note that this is not thread-safe, # finish is called from multiple threads, a given span might be finished # several times. Again, one should not do this, so this test is more a # fallback to avoid very bad things and protect you in most common cases. return if finished? @allocation_count_finish = now_allocations now = Utils::Time.now.utc # Provide a default start_time if unset. # Using `now` here causes duration to be 0; this is expected # behavior when start_time is unknown. start(finish_time || now) unless started? @end_time = finish_time || now @duration_end = finish_time.nil? ? duration_marker : nil # Finish does not really do anything if the span is not bound to a tracer and a context. return self if @tracer.nil? || @context.nil? # spans without a service would be dropped, so here we provide a default. # This should really never happen with integrations in contrib, as a default # service is always set. It's only for custom instrumentation. @service ||= (@tracer && @tracer.default_service) begin @context.close_span(self) @tracer.record(self) rescue StandardError => e Datadog.logger.debug("error recording finished trace: #{e}") Datadog.health_metrics.error_span_finish(1, tags: ["error:#{e.class.name}"]) end self end # Return a string representation of the span. def to_s "Span(name:#{@name},sid:#{@span_id},tid:#{@trace_id},pid:#{@parent_id})" end # DEPRECATED: remove this function in the next release, replaced by ``parent=`` def set_parent(parent) self.parent = parent end # Set this span's parent, inheriting any properties not explicitly set. # If the parent is nil, set the span zero values. def parent=(parent) @parent = parent if parent.nil? @trace_id = @span_id @parent_id = 0 else @trace_id = parent.trace_id @parent_id = parent.span_id @service ||= parent.service @sampled = parent.sampled end end def allocations @allocation_count_finish - @allocation_count_start end # Return the hash representation of the current span. def to_hash h = { span_id: @span_id, parent_id: @parent_id, trace_id: @trace_id, name: @name, service: @service, resource: @resource, type: @span_type, meta: @meta, metrics: @metrics, allocations: allocations, error: @status } if finished? h[:start] = start_time_nano h[:duration] = duration_nano end h end # MessagePack serializer interface. Making this object # respond to `#to_msgpack` allows it to be automatically # serialized by MessagePack. # # This is more efficient than doing +MessagePack.pack(span.to_hash)+ # as we don't have to create an intermediate Hash. # # @param packer [MessagePack::Packer] serialization buffer, can be +nil+ with JRuby def to_msgpack(packer = nil) # As of 1.3.3, JRuby implementation doesn't pass an existing packer packer ||= MessagePack::Packer.new if finished? packer.write_map_header(13) # Set header with how many elements in the map packer.write('start') packer.write(start_time_nano) packer.write('duration') packer.write(duration_nano) else packer.write_map_header(11) # Set header with how many elements in the map end # DEV: We use strings as keys here, instead of symbols, as # DEV: MessagePack will ultimately convert them to strings. # DEV: By providing strings directly, we skip this indirection operation. packer.write('span_id') packer.write(@span_id) packer.write('parent_id') packer.write(@parent_id) packer.write('trace_id') packer.write(@trace_id) packer.write('name') packer.write(@name) packer.write('service') packer.write(@service) packer.write('resource') packer.write(@resource) packer.write('type') packer.write(@span_type) packer.write('meta') packer.write(@meta) packer.write('metrics') packer.write(@metrics) packer.write('allocations') packer.write(allocations) packer.write('error') packer.write(@status) packer end # JSON serializer interface. # Used by older version of the transport. def to_json(*args) to_hash.to_json(*args) 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: #{@span_id}\n" q.text "Parent ID: #{@parent_id}\n" q.text "Trace ID: #{@trace_id}\n" q.text "Type: #{@span_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 if finished?}\n" q.text "Allocations: #{allocations}\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 # Return whether the duration is started or not def started? !@start_time.nil? end # Return whether the duration is finished or not. def finished? !@end_time.nil? end def duration if @duration_end.nil? || @duration_start.nil? @end_time - @start_time else @duration_end - @duration_start end end private def duration_marker Utils::Time.get_time end if defined?(JRUBY_VERSION) || Gem::Version.new(RUBY_VERSION) < Gem::Version.new(VERSION::MINIMUM_RUBY_VERSION) def now_allocations 0 end elsif Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') def now_allocations GC.stat.fetch(:total_allocated_object) end else def now_allocations GC.stat(:total_allocated_objects) end end # 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 end end