opentelemetry-exporters-datadog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +13 -0
- data/CONTRIBUTING.md +81 -0
- data/LICENSE +201 -0
- data/README.md +172 -0
- data/lib/opentelemetry-exporters-datadog.rb +8 -0
- data/lib/opentelemetry/exporters/datadog.rb +26 -0
- data/lib/opentelemetry/exporters/datadog/datadog_probability_sampler.rb +56 -0
- data/lib/opentelemetry/exporters/datadog/datadog_span_processor.rb +224 -0
- data/lib/opentelemetry/exporters/datadog/exporter.rb +105 -0
- data/lib/opentelemetry/exporters/datadog/exporter/span_encoder.rb +252 -0
- data/lib/opentelemetry/exporters/datadog/propagator.rb +138 -0
- data/lib/opentelemetry/exporters/datadog/version.rb +15 -0
- metadata +214 -0
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unless explicitly stated otherwise all files in this repository are licensed
|
4
|
+
# under the Apache 2.0 license (see LICENSE).
|
5
|
+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
6
|
+
# Copyright 2020 Datadog, Inc.
|
7
|
+
|
8
|
+
require 'opentelemetry/exporters/datadog'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unless explicitly stated otherwise all files in this repository are licensed
|
4
|
+
# under the Apache 2.0 license (see LICENSE).
|
5
|
+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
6
|
+
# Copyright 2020 Datadog, Inc.
|
7
|
+
|
8
|
+
# require_relative './datadog/exporter.rb'
|
9
|
+
# require_relative './datadog/version.rb'
|
10
|
+
# require_relative './datadog/datadog_span_processor.rb'
|
11
|
+
# require_relative './datadog/propagator.rb'
|
12
|
+
# require_relative './datadog_probability_sampler'
|
13
|
+
require 'opentelemetry/exporters/datadog/exporter'
|
14
|
+
require 'opentelemetry/exporters/datadog/version'
|
15
|
+
require 'opentelemetry/exporters/datadog/datadog_span_processor'
|
16
|
+
require 'opentelemetry/exporters/datadog/propagator'
|
17
|
+
require 'opentelemetry/exporters/datadog/datadog_probability_sampler'
|
18
|
+
|
19
|
+
# OpenTelemetry is an open source observability framework, providing a
|
20
|
+
# general-purpose API, SDK, and related tools required for the instrumentation
|
21
|
+
# of cloud-native software, frameworks, and libraries.
|
22
|
+
#
|
23
|
+
# The OpenTelemetry module provides global accessors for telemetry objects.
|
24
|
+
# See the documentation for the `opentelemetry-api` gem for details.
|
25
|
+
module OpenTelemetry
|
26
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unless explicitly stated otherwise all files in this repository are licensed
|
4
|
+
# under the Apache 2.0 license (see LICENSE).
|
5
|
+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
6
|
+
# Copyright 2020 Datadog, Inc.
|
7
|
+
|
8
|
+
require 'opentelemetry/sdk/trace/samplers/probability_sampler'
|
9
|
+
require 'opentelemetry/sdk/trace/samplers/decision'
|
10
|
+
require 'opentelemetry/sdk/trace/samplers/result'
|
11
|
+
|
12
|
+
module OpenTelemetry
|
13
|
+
module Exporters
|
14
|
+
module Datadog
|
15
|
+
# Implements sampling based on a probability but records all spans regardless.
|
16
|
+
class DatadogProbabilitySampler < OpenTelemetry::SDK::Trace::Samplers::ProbabilitySampler
|
17
|
+
RECORD_AND_SAMPLED = OpenTelemetry::SDK::Trace::Samplers::Result.new(decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLED)
|
18
|
+
RECORD = OpenTelemetry::SDK::Trace::Samplers::Result.new(decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD)
|
19
|
+
|
20
|
+
private_constant(:RECORD_AND_SAMPLED, :RECORD)
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
#
|
24
|
+
# See {Samplers}.
|
25
|
+
def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
|
26
|
+
# Ignored for sampling decision: links, name, kind, attributes.
|
27
|
+
|
28
|
+
if sample?(trace_id, parent_context)
|
29
|
+
RECORD_AND_SAMPLED
|
30
|
+
else
|
31
|
+
RECORD
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a new sampler. The probability of sampling a trace is equal
|
36
|
+
# to that of the specified probability.
|
37
|
+
#
|
38
|
+
# @param [Numeric] probability The desired probability of sampling.
|
39
|
+
# Must be within [0.0, 1.0].
|
40
|
+
def self.default_with_probability(probability = 1.0)
|
41
|
+
raise ArgumentError, 'probability must be in range [0.0, 1.0]' unless (0.0..1.0).include?(probability)
|
42
|
+
|
43
|
+
new(probability,
|
44
|
+
ignore_parent: false,
|
45
|
+
apply_to_remote_parent: :root_spans_and_remote_parent,
|
46
|
+
apply_to_all_spans: :root_spans_and_remote_parent)
|
47
|
+
end
|
48
|
+
|
49
|
+
DEFAULT = new(1.0,
|
50
|
+
ignore_parent: false,
|
51
|
+
apply_to_remote_parent: :root_spans_and_remote_parent,
|
52
|
+
apply_to_all_spans: :root_spans_and_remote_parent)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unless explicitly stated otherwise all files in this repository are licensed
|
4
|
+
# under the Apache 2.0 license (see LICENSE).
|
5
|
+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
6
|
+
# Copyright 2020 Datadog, Inc.
|
7
|
+
|
8
|
+
require 'timeout'
|
9
|
+
|
10
|
+
module OpenTelemetry
|
11
|
+
module Exporters
|
12
|
+
module Datadog
|
13
|
+
# Implementation of the duck type SpanProcessor that batches spans
|
14
|
+
# exported by the SDK into complete traces then pushes them
|
15
|
+
# to the exporter pipeline.
|
16
|
+
#
|
17
|
+
# All spans reported by the SDK implementation are first added to a
|
18
|
+
# synchronized in memory trace storage (with a {max_queue_size}
|
19
|
+
# maximum size, of trace size {max_trace_size} after the size of
|
20
|
+
# either is reached spans are dropped). When traces are designated
|
21
|
+
# as "complete" they're added to a queue that is exported every
|
22
|
+
# schedule_delay_millis to the exporter pipeline in batches of
|
23
|
+
# completed traces. The datadog writer and transport supplied
|
24
|
+
# to the exporter handle the bulk of the timeout and retry logic.
|
25
|
+
class DatadogSpanProcessor
|
26
|
+
SCHEDULE_DELAY_MILLIS = 3_000
|
27
|
+
MAX_QUEUE_SIZE = 2048
|
28
|
+
MAX_TRACE_SIZE = 1024
|
29
|
+
private_constant(:SCHEDULE_DELAY_MILLIS, :MAX_QUEUE_SIZE, :MAX_TRACE_SIZE)
|
30
|
+
|
31
|
+
def initialize(exporter:,
|
32
|
+
schedule_delay_millis: SCHEDULE_DELAY_MILLIS,
|
33
|
+
max_queue_size: MAX_QUEUE_SIZE,
|
34
|
+
max_trace_size: MAX_TRACE_SIZE)
|
35
|
+
raise ArgumentError if max_trace_size > max_queue_size
|
36
|
+
|
37
|
+
@exporter = exporter
|
38
|
+
@mutex = Mutex.new
|
39
|
+
@condition = ConditionVariable.new
|
40
|
+
@keep_running = true
|
41
|
+
@delay_seconds = schedule_delay_millis / 1000.0
|
42
|
+
@max_queue_size = max_queue_size
|
43
|
+
@max_trace_size = max_trace_size
|
44
|
+
@spans = []
|
45
|
+
@thread = Thread.new { work }
|
46
|
+
|
47
|
+
@traces = {}
|
48
|
+
@traces_spans_count = {}
|
49
|
+
@traces_spans_ended_count = {}
|
50
|
+
@check_traces_queue = []
|
51
|
+
@_spans_dropped = false
|
52
|
+
end
|
53
|
+
|
54
|
+
# datadog trace-agent endpoint requires a complete trace to be sent
|
55
|
+
# threadsafe may block on lock
|
56
|
+
def on_start(span)
|
57
|
+
context = span.context
|
58
|
+
trace_id = context.trace_id
|
59
|
+
|
60
|
+
lock do
|
61
|
+
if all_spans_count(traces_spans_count) >= max_queue_size
|
62
|
+
# instead of just dropping all new spans, dd-trace-rb drops a random trace
|
63
|
+
# https://github.com/DataDog/dd-trace-rb/blob/c6fbf2410a60495f1b2d8912bf7ea7dc63422141/lib/ddtrace/buffer.rb#L34-L36
|
64
|
+
# It allows for a more fair usage of the queue when under stress load,
|
65
|
+
# and will create proportional representation of code paths being instrumented at stress time.
|
66
|
+
unfinished_trace_id = fetch_unfinished_trace_id
|
67
|
+
|
68
|
+
# if there are no unfinished traces able to be dropped, don't add more spans, and return early
|
69
|
+
if unfinished_trace_id.nil?
|
70
|
+
OpenTelemetry.logger.warn('Max spans for all traces, spans will be dropped')
|
71
|
+
@_spans_dropped = true
|
72
|
+
return
|
73
|
+
end
|
74
|
+
|
75
|
+
drop_unfinished_trace(unfinished_trace_id)
|
76
|
+
OpenTelemetry.logger.warn('Max spans for all traces, traces will be dropped')
|
77
|
+
end
|
78
|
+
|
79
|
+
if traces[trace_id].nil?
|
80
|
+
traces[trace_id] = [span]
|
81
|
+
traces_spans_count[trace_id] = 1
|
82
|
+
else
|
83
|
+
if traces[trace_id].size >= max_trace_size
|
84
|
+
OpenTelemetry.logger.warn('Max spans for trace, spans will be dropped')
|
85
|
+
@_spans_dropped = true
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
traces[trace_id] << span
|
90
|
+
traces_spans_count[trace_id] += 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# adds a span to the batcher, threadsafe may block on lock
|
96
|
+
def on_finish(span)
|
97
|
+
if @keep_running == false
|
98
|
+
OpenTelemetry.logger.warn('Already shutdown, dropping span')
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
102
|
+
# TODO: determine if all "not-sampled" spans still get passed to on_finish?
|
103
|
+
# If so then we don't need to account for Probability Sampling
|
104
|
+
# and can likely incorporate Priority Sampling from DD
|
105
|
+
# If not, then we need to ensure the rate from OpenTelemetry.tracer_provider.active_trace_config.sampler
|
106
|
+
# can be expoed to the span or attached to spanData in some way
|
107
|
+
# return unless span.context.trace_flags.sampled?
|
108
|
+
|
109
|
+
context = span.context
|
110
|
+
trace_id = context.trace_id
|
111
|
+
|
112
|
+
lock do
|
113
|
+
if traces_spans_ended_count[trace_id].nil?
|
114
|
+
traces_spans_ended_count[trace_id] = 1
|
115
|
+
else
|
116
|
+
traces_spans_ended_count[trace_id] += 1
|
117
|
+
end
|
118
|
+
|
119
|
+
check_traces_queue.unshift(trace_id) if trace_exportable?(trace_id)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# TODO: test this explicitly.
|
124
|
+
# Export all ended traces to the configured `Exporter` that have not yet
|
125
|
+
# been exported.
|
126
|
+
#
|
127
|
+
# This method should only be called in cases where it is absolutely
|
128
|
+
# necessary, such as when using some FaaS providers that may suspend
|
129
|
+
# the process after an invocation, but before the `Processor` exports
|
130
|
+
# the completed spans.
|
131
|
+
def force_flush
|
132
|
+
snapshot = lock { fetch_batch }
|
133
|
+
export_batch(snapshot)
|
134
|
+
end
|
135
|
+
|
136
|
+
# shuts the consumer thread down and flushes the current accumulated buffer
|
137
|
+
# will block until the thread is finished
|
138
|
+
def shutdown
|
139
|
+
lock do
|
140
|
+
@keep_running = false
|
141
|
+
@condition.signal
|
142
|
+
end
|
143
|
+
|
144
|
+
@thread.join
|
145
|
+
force_flush
|
146
|
+
@exporter.shutdown
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
attr_reader :check_traces_queue, :max_queue_size, :max_trace_size, :traces, :traces_spans_count, :traces_spans_ended_count
|
152
|
+
|
153
|
+
def work
|
154
|
+
while @keep_running
|
155
|
+
trace_spans = lock do
|
156
|
+
@condition.wait(@mutex, @delay_seconds) if @keep_running
|
157
|
+
@condition.wait(@mutex, @delay_seconds) while check_traces_queue.empty? && @keep_running
|
158
|
+
return unless @keep_running
|
159
|
+
|
160
|
+
fetch_batch
|
161
|
+
end
|
162
|
+
|
163
|
+
export_batch(trace_spans)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def export_batch(trace_spans)
|
168
|
+
return if trace_spans.empty?
|
169
|
+
|
170
|
+
trace_spans.each do |spans|
|
171
|
+
@exporter.export(spans)
|
172
|
+
rescue StandardError => e
|
173
|
+
OpenTelemetry.logger.warn("Exception while exporting Span batch. #{e.message} , #{e.backtrace}")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def trace_exportable?(trace_id)
|
178
|
+
traces_spans_count[trace_id] - traces_spans_ended_count[trace_id] <= 0 if traces_spans_count.key?(trace_id) && traces_spans_ended_count.key?(trace_id)
|
179
|
+
end
|
180
|
+
|
181
|
+
def all_spans_count(traces_spans_count)
|
182
|
+
traces_spans_count.values.sum
|
183
|
+
end
|
184
|
+
|
185
|
+
def fetch_batch
|
186
|
+
export_traces = []
|
187
|
+
|
188
|
+
check_traces_queue.reverse_each do |trace_id|
|
189
|
+
next unless trace_exportable?(trace_id)
|
190
|
+
|
191
|
+
export_traces << fetch_spans(traces.delete(trace_id))
|
192
|
+
check_traces_queue.delete(trace_id)
|
193
|
+
traces_spans_count.delete(trace_id)
|
194
|
+
traces_spans_ended_count.delete(trace_id)
|
195
|
+
end
|
196
|
+
|
197
|
+
export_traces
|
198
|
+
end
|
199
|
+
|
200
|
+
def fetch_spans(spans)
|
201
|
+
spans.map!(&:to_span_data)
|
202
|
+
end
|
203
|
+
|
204
|
+
def fetch_unfinished_trace_id
|
205
|
+
# don't delete potentially finished trace awaiting export
|
206
|
+
unfinished_traces = traces.keys - check_traces_queue
|
207
|
+
unfinished_traces[rand(unfinished_traces.length)]
|
208
|
+
end
|
209
|
+
|
210
|
+
def drop_unfinished_trace(trace_id)
|
211
|
+
traces.delete(trace_id)
|
212
|
+
traces_spans_count.delete(trace_id)
|
213
|
+
traces_spans_ended_count.delete(trace_id)
|
214
|
+
end
|
215
|
+
|
216
|
+
def lock
|
217
|
+
@mutex.synchronize do
|
218
|
+
yield
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unless explicitly stated otherwise all files in this repository are licensed
|
4
|
+
# under the Apache 2.0 license (see LICENSE).
|
5
|
+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
6
|
+
# Copyright 2020 Datadog, Inc.
|
7
|
+
|
8
|
+
require 'uri'
|
9
|
+
require 'ddtrace'
|
10
|
+
require 'opentelemetry/sdk'
|
11
|
+
require 'opentelemetry/exporters/datadog/exporter/span_encoder'
|
12
|
+
# require_relative './exporter/span_encoder.rb'
|
13
|
+
|
14
|
+
module OpenTelemetry
|
15
|
+
module Exporters
|
16
|
+
module Datadog
|
17
|
+
# SpanExporter allows different tracing services to export
|
18
|
+
# recorded data for sampled spans in their own format.
|
19
|
+
#
|
20
|
+
# To export data an exporter MUST be registered to the {TracerProvider} using
|
21
|
+
# a {DatadogSpanProcessorr}.
|
22
|
+
class Exporter
|
23
|
+
DEFAULT_AGENT_URL = 'http://localhost:8126'
|
24
|
+
DEFAULT_SERVICE_NAME = 'my_service'
|
25
|
+
SUCCESS = begin
|
26
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
27
|
+
rescue NameError
|
28
|
+
0
|
29
|
+
end
|
30
|
+
FAILURE = begin
|
31
|
+
OpenTelemetry::SDK::Trace::Export::FAILURE
|
32
|
+
rescue NameError
|
33
|
+
1
|
34
|
+
end
|
35
|
+
private_constant(:SUCCESS, :FAILURE)
|
36
|
+
|
37
|
+
def initialize(service_name: nil, agent_url: nil, env: nil, version: nil, tags: nil)
|
38
|
+
@shutdown = false
|
39
|
+
@agent_url = agent_url || ENV.fetch('DD_TRACE_AGENT_URL', DEFAULT_AGENT_URL)
|
40
|
+
@service = service_name || ENV.fetch('DD_SERVICE', DEFAULT_SERVICE_NAME)
|
41
|
+
|
42
|
+
@env = env || ENV.fetch('DD_ENV', nil)
|
43
|
+
@version = version || ENV.fetch('DD_VERSION', nil)
|
44
|
+
@tags = tags || ENV.fetch('DD_TAGS', nil)
|
45
|
+
|
46
|
+
@agent_writer = get_writer(@agent_url)
|
47
|
+
|
48
|
+
@span_encoder = SpanEncoder.new
|
49
|
+
end
|
50
|
+
|
51
|
+
# Called to export sampled {Span}s.
|
52
|
+
#
|
53
|
+
# @param [Enumerable<Span>] spans the list of sampled {Span}s to be
|
54
|
+
# exported.
|
55
|
+
# @return [Integer] the result of the export.
|
56
|
+
def export(spans)
|
57
|
+
return FAILURE if @shutdown
|
58
|
+
|
59
|
+
if @agent_writer
|
60
|
+
datadog_spans = @span_encoder.translate_to_datadog(spans, @service, @env, @version, @tags)
|
61
|
+
@agent_writer.write(datadog_spans)
|
62
|
+
SUCCESS
|
63
|
+
else
|
64
|
+
OpenTelemetry.logger.debug('Agent writer not set')
|
65
|
+
FAILURE
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Called when {TracerProvider#shutdown} is called, if this exporter is
|
70
|
+
# registered to a {TracerProvider} object.
|
71
|
+
def shutdown
|
72
|
+
@shutdown = true
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def get_writer(uri)
|
78
|
+
uri_parsed = URI.parse(uri)
|
79
|
+
|
80
|
+
if %w[http https].include?(uri_parsed.scheme)
|
81
|
+
hostname = uri_parsed.hostname
|
82
|
+
port = uri_parsed.port
|
83
|
+
|
84
|
+
adapter = ::Datadog::Transport::HTTP::Adapters::Net.new(hostname, port)
|
85
|
+
|
86
|
+
transport = ::Datadog::Transport::HTTP.default do |t|
|
87
|
+
t.adapter adapter
|
88
|
+
end
|
89
|
+
|
90
|
+
::Datadog::Writer.new(transport: transport)
|
91
|
+
elsif uri_parsed.to_s.index('/sock')
|
92
|
+
# handle uds path
|
93
|
+
transport = ::Datadog::Transport::HTTP.default do |t|
|
94
|
+
t.adapter :unix, uri_parsed.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
::Datadog::Writer.new(transport: transport)
|
98
|
+
else
|
99
|
+
OpenTelemetry.logger.warn('only http/https and uds is supported at this time')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Unless explicitly stated otherwise all files in this repository are licensed
|
4
|
+
# under the Apache 2.0 license (see LICENSE).
|
5
|
+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
6
|
+
# Copyright 2020 Datadog, Inc.
|
7
|
+
|
8
|
+
require 'ddtrace/span'
|
9
|
+
require 'ddtrace/ext/http'
|
10
|
+
require 'ddtrace/ext/sql'
|
11
|
+
require 'ddtrace/ext/app_types'
|
12
|
+
require 'ddtrace/contrib/redis/ext'
|
13
|
+
require 'opentelemetry/trace/status'
|
14
|
+
require 'ddtrace/distributed_tracing/headers/headers'
|
15
|
+
|
16
|
+
module OpenTelemetry
|
17
|
+
module Exporters
|
18
|
+
module Datadog
|
19
|
+
class Exporter
|
20
|
+
# @api private
|
21
|
+
class SpanEncoder
|
22
|
+
ENV_KEY = 'env'
|
23
|
+
VERSION_KEY = 'version'
|
24
|
+
DD_ORIGIN = '_dd_origin'
|
25
|
+
USER_REJECT = -1
|
26
|
+
AUTO_REJECT = 0
|
27
|
+
AUTO_KEEP = 1
|
28
|
+
USER_KEEP = 2
|
29
|
+
INTERNAL_TRACE_REGEX = %r{/v\d\.\d/traces}.freeze
|
30
|
+
SAMPLE_RATE_METRIC_KEY = '_sample_rate'
|
31
|
+
SAMPLING_PRIORITY_KEY = '_sampling_priority_v1'
|
32
|
+
ORIGIN_REGEX = /#{DD_ORIGIN}\=(.*?)($|,)/.freeze
|
33
|
+
PROBABILITY_REGEX = /\d[.]\d{1,6}/.freeze
|
34
|
+
TRUNCATION_HELPER = ::Datadog::DistributedTracing::Headers::Headers.new({})
|
35
|
+
|
36
|
+
INSTRUMENTATION_SPAN_TYPES = {
|
37
|
+
'OpenTelemetry::Instrumentation::Ethon' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND,
|
38
|
+
'OpenTelemetry::Instrumentation::Excon' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND,
|
39
|
+
'OpenTelemetry::Instrumentation::Faraday' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND,
|
40
|
+
'OpenTelemetry::Instrumentation::Mysql2' => ::Datadog::Ext::SQL::TYPE,
|
41
|
+
'OpenTelemetry::Instrumentation::Net::HTTP' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND,
|
42
|
+
'OpenTelemetry::Instrumentation::Rack' => ::Datadog::Ext::HTTP::TYPE_INBOUND,
|
43
|
+
'OpenTelemetry::Instrumentation::Redis' => ::Datadog::Contrib::Redis::Ext::TYPE,
|
44
|
+
'OpenTelemetry::Instrumentation::RestClient' => ::Datadog::Ext::HTTP::TYPE_OUTBOUND,
|
45
|
+
'OpenTelemetry::Instrumentation::Sidekiq' => ::Datadog::Ext::AppTypes::WORKER,
|
46
|
+
'OpenTelemetry::Instrumentation::Sinatra' => ::Datadog::Ext::HTTP::TYPE_INBOUND
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
def translate_to_datadog(otel_spans, service, env = nil, version = nil, tags = nil) # rubocop:disable Metrics/AbcSize
|
50
|
+
datadog_spans = []
|
51
|
+
|
52
|
+
default_tags = get_default_tags(tags) || {}
|
53
|
+
|
54
|
+
otel_spans.each do |span|
|
55
|
+
trace_id, span_id, parent_id = get_trace_ids(span)
|
56
|
+
span_type = get_span_type(span)
|
57
|
+
span_name = get_span_name(span)
|
58
|
+
|
59
|
+
datadog_span = ::Datadog::Span.new(nil, span_name,
|
60
|
+
service: service,
|
61
|
+
trace_id: trace_id,
|
62
|
+
parent_id: parent_id,
|
63
|
+
resource: get_resource(span),
|
64
|
+
span_type: span_type)
|
65
|
+
|
66
|
+
# span_id is autogenerated so have to override
|
67
|
+
datadog_span.span_id = span_id
|
68
|
+
datadog_span.start_time = span.start_timestamp
|
69
|
+
datadog_span.end_time = span.end_timestamp
|
70
|
+
|
71
|
+
# set span.error, span tag error.msg/error.type
|
72
|
+
if span.status && span.status.canonical_code != OpenTelemetry::Trace::Status::OK
|
73
|
+
datadog_span.status = 1
|
74
|
+
|
75
|
+
exception_type, exception_msg, exception_stack = get_exception_info(span)
|
76
|
+
|
77
|
+
if exception_type && exception_msg && exception_stack
|
78
|
+
datadog_span.set_tag('error.type', exception_type)
|
79
|
+
datadog_span.set_tag('error.msg', exception_msg)
|
80
|
+
datadog_span.set_tag('error.stack', exception_stack)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# set default tags
|
85
|
+
default_tags&.keys&.each do |attribute|
|
86
|
+
datadog_span.set_tag(attribute, span.attributes[attribute])
|
87
|
+
end
|
88
|
+
|
89
|
+
origin = get_origin_string(span)
|
90
|
+
datadog_span.set_tag(DD_ORIGIN, origin) if origin && parent_id.zero?
|
91
|
+
datadog_span.set_tag(VERSION_KEY, version) if version && parent_id.zero?
|
92
|
+
datadog_span.set_tag(ENV_KEY, env) if env
|
93
|
+
|
94
|
+
# set tags - takes precedence over env vars
|
95
|
+
span.attributes&.keys&.each do |attribute|
|
96
|
+
datadog_span.set_tag(attribute, span.attributes[attribute])
|
97
|
+
end
|
98
|
+
|
99
|
+
sampling_rate = get_sampling_rate(span)
|
100
|
+
|
101
|
+
if filter_internal_request?(span)
|
102
|
+
datadog_span.set_metric(SAMPLE_RATE_METRIC_KEY, USER_REJECT)
|
103
|
+
elsif sampling_rate
|
104
|
+
datadog_span.set_metric(SAMPLE_RATE_METRIC_KEY, sampling_rate)
|
105
|
+
end
|
106
|
+
|
107
|
+
datadog_spans << datadog_span
|
108
|
+
end
|
109
|
+
|
110
|
+
datadog_spans
|
111
|
+
end
|
112
|
+
|
113
|
+
def int64(hex_string, base)
|
114
|
+
TRUNCATION_HELPER.value_to_id(hex_string, base)
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def get_trace_ids(span)
|
120
|
+
trace_id = int64(span.trace_id.unpack1('H*'), 16)
|
121
|
+
span_id = int64(span.span_id.unpack1('H*'), 16)
|
122
|
+
parent_id = span.parent_span_id ? int64(span.parent_span_id.unpack1('H*'), 16) || 0 : 0
|
123
|
+
|
124
|
+
[trace_id, span_id, parent_id]
|
125
|
+
rescue StandardError => e
|
126
|
+
OpenTelemetry.logger.debug("error encoding trace_ids #{e.message}")
|
127
|
+
[0, 0, 0]
|
128
|
+
end
|
129
|
+
|
130
|
+
def get_span_type(span)
|
131
|
+
# Get Datadog span type
|
132
|
+
return unless span.instrumentation_library
|
133
|
+
|
134
|
+
instrumentation_name = span.instrumentation_library.name
|
135
|
+
INSTRUMENTATION_SPAN_TYPES[instrumentation_name]
|
136
|
+
rescue NoMethodError
|
137
|
+
span.name
|
138
|
+
end
|
139
|
+
|
140
|
+
def get_exception_info(span)
|
141
|
+
# Parse span exception type, msg, and stack from span events
|
142
|
+
error_event = span&.events&.find { |ev| ev.name == 'error' }
|
143
|
+
|
144
|
+
return unless error_event
|
145
|
+
|
146
|
+
err_type = error_event.attributes['error.type']
|
147
|
+
err_msg = error_event.attributes['error.msg']
|
148
|
+
err_stack = error_event.attributes['error.stack']
|
149
|
+
|
150
|
+
[err_type, err_msg, err_stack]
|
151
|
+
rescue StandardError => e
|
152
|
+
OpenTelemetry.logger.debug("error on exception info from span events: #{span.events} , #{e.message}")
|
153
|
+
end
|
154
|
+
|
155
|
+
def get_resource(span)
|
156
|
+
# Get resource name for http related spans
|
157
|
+
# TODO: how to handle resource naming for broader span types, ie db/cache/queue etc
|
158
|
+
|
159
|
+
if span.attributes&.key?('http.method')
|
160
|
+
route = span.attributes['http.route'] || span.attributes['http.target']
|
161
|
+
|
162
|
+
return span.attributes['http.method'] + ' ' + route if route
|
163
|
+
|
164
|
+
return span.attributes['http.method']
|
165
|
+
end
|
166
|
+
|
167
|
+
span.name
|
168
|
+
rescue StandardError => e
|
169
|
+
OpenTelemetry.logger.debug("error encoding trace_ids #{e.message}")
|
170
|
+
span.name
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_sampling_rate(span)
|
174
|
+
get_rate_from_description(span) || 1 if span.trace_flags&.sampled?
|
175
|
+
end
|
176
|
+
|
177
|
+
def get_span_name(span)
|
178
|
+
# Get span name by using instrumentation and kind while backing off to
|
179
|
+
instrumentation_name = span.instrumentation_library&.name
|
180
|
+
kind = span.kind
|
181
|
+
instrumentation_name && kind ? "#{instrumentation_name.to_s.gsub(':', '_')}.#{kind}" : span.name
|
182
|
+
rescue NoMethodError
|
183
|
+
span.name
|
184
|
+
end
|
185
|
+
|
186
|
+
def get_origin_string(span)
|
187
|
+
tracestate = begin
|
188
|
+
span.tracestate
|
189
|
+
rescue NoMethodError
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
|
193
|
+
return if tracestate.nil? || tracestate.index(DD_ORIGIN).nil?
|
194
|
+
|
195
|
+
# Depending on the edge cases in tracestate values this might be
|
196
|
+
# less efficient than mapping string => array => hash.
|
197
|
+
origin_value = tracestate.match(ORIGIN_REGEX)
|
198
|
+
return if origin_value.nil?
|
199
|
+
|
200
|
+
origin_value[1]
|
201
|
+
rescue StandardError => e
|
202
|
+
OpenTelemetry.logger.debug("error getting origin from trace state, #{e.message}")
|
203
|
+
end
|
204
|
+
|
205
|
+
def get_default_tags(tags)
|
206
|
+
# Parse a string of tags typically provided via environment variables.
|
207
|
+
# The expected string is of the form: "key1:value1,key2:value2"
|
208
|
+
|
209
|
+
return {} if tags.nil?
|
210
|
+
|
211
|
+
tag_map = tags.split(',').map { |kv| kv.split(':') }.to_h
|
212
|
+
|
213
|
+
if tag_map.keys&.index('') || tag_map.values&.index('') || tag_map.values&.any? { |v| v.ends_with?(':') }
|
214
|
+
OpenTelemetry.logger.debug("malformed tag in default tags: #{tags}")
|
215
|
+
{}
|
216
|
+
else
|
217
|
+
tag_map
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def get_rate_from_description(span)
|
222
|
+
# format to parse of sampler description is
|
223
|
+
# "ProbabilitySampler{1.000000}" or
|
224
|
+
# "AlwaysOnSampler" / "AlwaysOffSampler"
|
225
|
+
# TODO: remove begin/rescue block if PR #282 is accepted+released
|
226
|
+
sampler = begin
|
227
|
+
span.sampler
|
228
|
+
rescue NoMethodError
|
229
|
+
nil
|
230
|
+
end
|
231
|
+
|
232
|
+
return nil unless sampler&.is_a?(ProbabilitySampler)
|
233
|
+
|
234
|
+
rate = sampler.description&.match(PROBABILITY_REGEX)
|
235
|
+
|
236
|
+
return nil unless rate
|
237
|
+
|
238
|
+
rate[0].to_f(4)
|
239
|
+
rescue StandardError => e
|
240
|
+
# rescue just in case the format changes dramatically in the future
|
241
|
+
OpenTelemetry.logger.warn("error while extracting sampling rate #{e.message} , #{e.backtrace}")
|
242
|
+
nil
|
243
|
+
end
|
244
|
+
|
245
|
+
def filter_internal_request?(span)
|
246
|
+
span.attributes['http.route'].match(INTERNAL_TRACE_REGEX) if span.attributes&.key?('http.route')
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|