require 'time' require 'thread' require 'ddtrace/utils' require 'ddtrace/ext/errors' 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. class Span # The max value for a \Span identifier. # Span and trace identifiers should be strictly positive and strictly inferior to this limit. # # Limited to 63-bit positive integers, as some other languages might be limited to this, # and IDs need to be easy to port across various languages and platforms. MAX_ID = 2**63 attr_accessor :name, :service, :resource, :span_type, :start_time, :end_time, :span_id, :trace_id, :parent_id, :status, :sampled, :tracer, :context attr_reader :parent # 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 # * +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 @start_time = nil # set by Tracer.start_span @end_time = nil # set by Span.finish 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) @meta[key] = value.to_s rescue StandardError => e Datadog::Tracer.log.debug("Unable to set the tag #{key}, ignoring it. Caused by: #{e}") end # Return the tag with the given key, nil if it doesn't exist. def get_tag(key) @meta[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) # enforce that the value is a floating point number value = Float(value) @metrics[key] = value rescue StandardError => e Datadog::Tracer.log.debug("Unable to set the metric #{key}, ignoring it. Caused by: #{e}") end # Return the metric with the given key, nil if it doesn't exist. def get_metric(key) @metrics[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 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? # Provide a default start_time if unset, but this should have been set by start_span. # Using now here causes 0-duration spans, still, this is expected, as we never # explicitely say when it started. @start_time ||= Time.now.utc @end_time = finish_time.nil? ? Time.now.utc : finish_time # finish this # 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.default_service unless @tracer.nil? begin @context.close_span(self) @tracer.record(self) rescue StandardError => e Datadog::Tracer.log.debug("error recording finished trace: #{e}") end self end # Return whether the span is finished or not. def finished? !@end_time.nil? 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 # 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, error: @status } if !@start_time.nil? && !@end_time.nil? h[:start] = (@start_time.to_f * 1e9).to_i h[:duration] = ((@end_time - @start_time) * 1e9).to_i end h end # Return a human readable version of the span def pretty_print(q) start_time = (@start_time.to_f * 1e9).to_i rescue '-' end_time = (@end_time.to_f * 1e9).to_i rescue '-' duration = ((@end_time - @start_time) * 1e9).to_i rescue 0 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}\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 end end