Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add span links to the Datadog API #3546

Merged
merged 7 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/datadog/opentelemetry/sdk/span_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def span_arguments(span, attributes)

# DEV: There's no `flat_map!`; we have to split it into two operations
attributes = attributes.map do |key, value|
Datadog::OpenTelemetry::Trace::Span.serialize_attribute(key, value)
Datadog::Tracing::Utils.serialize_attribute(key, value)
end
attributes.flatten!(1)

Expand Down
20 changes: 3 additions & 17 deletions lib/datadog/opentelemetry/sdk/trace/span.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'datadog/tracing/utils'

module Datadog
module OpenTelemetry
module Trace
Expand Down Expand Up @@ -36,22 +38,6 @@ def status=(s)
span.set_error(status.description) if status && status.code == ::OpenTelemetry::Trace::Status::ERROR
end

# Serialize values into Datadog span tags and metrics.
# Notably, arrays are exploded into many keys, each with
# a numeric suffix representing the array index, for example:
# `'foo' => ['a','b']` becomes `'foo.0' => 'a', 'foo.1' => 'b'`
def self.serialize_attribute(key, value)
if value.is_a?(Array)
value.flat_map.with_index do |v, idx|
serialize_attribute("#{key}.#{idx}", v)
end
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
[[key, value.to_s]]
else
[[key, value]]
end
end

# Create a meaningful Datadog operation name from the OpenTelemetry
# semantic convention for span kind and span attributes.
# @see https://opentelemetry.io/docs/specs/semconv/general/trace/
Expand Down Expand Up @@ -123,7 +109,7 @@ def datadog_set_attribute(key)
span.name = rich_name.downcase
end

Span.serialize_attribute(key, @attributes[key]).each do |new_key, value|
Tracing::Utils.serialize_attribute(key, @attributes[key]).each do |new_key, value|
override_datadog_values(span, new_key, value)

# When an attribute is used to override a Datadog Span property,
Expand Down
9 changes: 7 additions & 2 deletions lib/datadog/tracing/span.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Span
:parent_id,
:resource,
:service,
:links,
:type,
:start_time,
:status,
Expand Down Expand Up @@ -58,7 +59,8 @@ def initialize(
status: 0,
type: nil,
trace_id: nil,
service_entry: nil
service_entry: nil,
links: nil
)
@name = Core::Utils::SafeDup.frozen_or_dup(name)
@service = Core::Utils::SafeDup.frozen_or_dup(service)
Expand Down Expand Up @@ -86,6 +88,8 @@ def initialize(

@service_entry = service_entry

@links = links || []

# Mark with the service entry span metric, if applicable
set_metric(Metadata::Ext::TAG_TOP_LEVEL, 1.0) if service_entry
end
Expand Down Expand Up @@ -136,7 +140,8 @@ def to_hash
service: @service,
span_id: @id,
trace_id: @trace_id,
type: @type
type: @type,
span_links: @links.map(&:to_hash)
}

if stopped?
Expand Down
86 changes: 86 additions & 0 deletions lib/datadog/tracing/span_link.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

module Datadog
module Tracing
# SpanLink represents a causal link between two spans.
# @public_api
class SpanLink
# @!attribute [r] span_id
# Datadog id for the currently active span.
# @return [Integer]
attr_reader :span_id

# @!attribute [r] trace_id
# Datadog id for the currently active trace.
# @return [Integer]
attr_reader :trace_id

# @!attribute [r] attributes
# Datadog-specific tags that support richer distributed tracing association.
# @return [Hash<String,String>]
attr_reader :attributes

# @!attribute [r] trace_flags
# The W3C "trace-flags" extracted from a distributed context. This field is an 8-bit unsigned integer.
# @return [Integer]
# @see https://www.w3.org/TR/trace-context/#trace-flags
attr_reader :trace_flags

# @!attribute [r] trace_state
# The W3C "tracestate" extracted from a distributed context.
# This field is a string representing vendor-specific distribution data.
# The `dd=` entry is removed from `trace_state` as its value is dynamically calculated
# on every propagation injection.
# @return [String]
# @see https://www.w3.org/TR/trace-context/#tracestate-header
attr_reader :trace_state

# @!attribute [r] dropped_attributes
# The number of attributes that were discarded due to serialization limits.
# @return [Integer]
attr_reader :dropped_attributes

def initialize(
attributes: nil,
digest: nil
)
@span_id = digest&.span_id
@trace_id = digest&.trace_id
@trace_flags = digest&.trace_flags
@trace_state = digest&.trace_state && digest&.trace_state.dup
@dropped_attributes = 0
mabdinur marked this conversation as resolved.
Show resolved Hide resolved
@attributes = (attributes && attributes.dup) || {}
end

def to_hash
h = {
span_id: @span_id || 0,
trace_id: Tracing::Utils::TraceId.to_low_order(@trace_id) || 0,
}
# Optimization: Hash non empty attributes
if @trace_id.to_i > Tracing::Utils::EXTERNAL_MAX_ID
h[:trace_id_high] =
Tracing::Utils::TraceId.to_high_order(@trace_id)
end
unless @attributes&.empty?
h[:attributes] = {}
@attributes.each do |k1, v1|
Tracing::Utils.serialize_attribute(k1, v1).each do |new_k1, value|
h[:attributes][new_k1] = value.to_s
end
end
end
h[:dropped_attributes_count] = @dropped_attributes if @dropped_attributes > 0
h[:tracestate] = @trace_state if @trace_state
# If traceflags set, the high bit (bit 31) should be set to 1 (uint32).
# This helps us distinguish between when the sample decision is zero or not set
h[:flags] = if @trace_flags.nil?
0
else
@trace_flags | (1 << 31)
end
h
end
end
end
end
4 changes: 3 additions & 1 deletion lib/datadog/tracing/transport/serializable_trace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def initialize(span)
def to_msgpack(packer = nil)
packer ||= MessagePack::Packer.new

number_of_elements_to_write = 10
number_of_elements_to_write = 11

if span.stopped?
packer.write_map_header(number_of_elements_to_write + 2) # Set header with how many elements in the map
Expand Down Expand Up @@ -93,6 +93,8 @@ def to_msgpack(packer = nil)
packer.write(span.meta)
packer.write('metrics')
packer.write(span.metrics)
packer.write('span_links')
packer.write(span.links.map(&:to_hash))
packer.write('error')
packer.write(span.status)
packer
Expand Down
16 changes: 16 additions & 0 deletions lib/datadog/tracing/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ def self.reset!
@id_rng = Random.new
end

# Serialize values into Datadog span tags and metrics.
# Notably, arrays are exploded into many keys, each with
# a numeric suffix representing the array index, for example:
# `'foo' => ['a','b']` becomes `'foo.0' => 'a', 'foo.1' => 'b'`
def self.serialize_attribute(key, value)
if value.is_a?(Array)
value.flat_map.with_index do |v, idx|
serialize_attribute("#{key}.#{idx}", v)
end
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
[[key, value.to_s]]
else
[[key, value]]
end
end

private_class_method :id_rng, :reset!

# The module handles bitwise operation for trace id
Expand Down
58 changes: 58 additions & 0 deletions sig/datadog/tracing/span_link.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Datadog
module Tracing
# SpanLink represents a causal link between two spans.
# @public_api
class SpanLink
@span_id: untyped

@trace_id: untyped

@trace_flags: untyped

@trace_state: untyped

@dropped_attributes: untyped

@attributes: untyped

# @!attribute [r] span_id
# Datadog id for the currently active span.
# @return [Integer]
attr_reader span_id: untyped

# @!attribute [r] trace_id
# Datadog id for the currently active trace.
# @return [Integer]
attr_reader trace_id: untyped

# @!attribute [r] attributes
# Datadog-specific tags that support richer distributed tracing association.
# @return [Hash<String,String>]
attr_reader attributes: untyped

# @!attribute [r] trace_flags
# The W3C "trace-flags" extracted from a distributed context. This field is an 8-bit unsigned integer.
# @return [Integer]
# @see https://www.w3.org/TR/trace-context/#trace-flags
attr_reader trace_flags: untyped

# @!attribute [r] trace_state
# The W3C "tracestate" extracted from a distributed context.
# This field is a string representing vendor-specific distribution data.
# The `dd=` entry is removed from `trace_state` as its value is dynamically calculated
# on every propagation injection.
# @return [String]
# @see https://www.w3.org/TR/trace-context/#tracestate-header
attr_reader trace_state: untyped

# @!attribute [r] dropped_attributes
# The number of attributes that were discarded due to serialization limits.
# @return [Integer]
attr_reader dropped_attributes: untyped

def initialize: (?attributes: untyped?, ?digest: untyped?) -> void

def to_hash: () -> untyped
end
end
end
5 changes: 4 additions & 1 deletion sig/datadog/tracing/utils.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module Datadog
def self.id_rng: () -> untyped

def self.reset!: () -> untyped

def self.serialize_attribute: (untyped key, untyped value) -> (untyped | ::Array[::Array[untyped]])

module TraceId
MAX: untyped
def self?.next_id: () -> untyped
Expand All @@ -22,4 +25,4 @@ module Datadog
end
end
end
end
end
Loading
Loading