Skip to content

Commit 5c64f66

Browse files
authored
Add datadog exporter (#216) (#222)
1 parent 33ee164 commit 5c64f66

File tree

17 files changed

+729
-19
lines changed

17 files changed

+729
-19
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ members = [
6161
"examples/aws-xray",
6262
"examples/basic",
6363
"examples/basic-otlp",
64+
"examples/datadog",
6465
"examples/grpc",
6566
"examples/http",
6667
"examples/zipkin",

benches/ddsketch.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
use criterion::{
2-
criterion_group, criterion_main, Criterion
3-
};
1+
use criterion::{criterion_group, criterion_main, Criterion};
42
use opentelemetry::api::metrics::{InstrumentKind, Number, NumberKind};
53
use opentelemetry::sdk::export::metrics::Aggregator;
64
use opentelemetry::{
7-
api::{
8-
metrics::Descriptor,
9-
},
5+
api::metrics::Descriptor,
106
sdk::{
117
export::metrics::Quantile,
128
metrics::aggregators::{ArrayAggregator, DDSKetchAggregator},
@@ -40,7 +36,9 @@ fn ddsketch(data: Vec<f64>) {
4036
}
4137
let new_aggregator: Arc<(dyn Aggregator + Send + Sync)> =
4238
Arc::new(DDSKetchAggregator::new(0.001, 2048, 1e-9, NumberKind::F64));
43-
aggregator.synchronized_move(&new_aggregator, &descriptor).unwrap();
39+
aggregator
40+
.synchronized_move(&new_aggregator, &descriptor)
41+
.unwrap();
4442
for quantile in get_test_quantile() {
4543
if let Some(new_aggregator) = new_aggregator.as_any().downcast_ref::<DDSKetchAggregator>() {
4644
let _ = new_aggregator.quantile(*quantile);
@@ -60,7 +58,9 @@ fn array(data: Vec<f64>) {
6058
aggregator.update(&Number::from(f), &descriptor).unwrap();
6159
}
6260
let new_aggregator: Arc<(dyn Aggregator + Send + Sync)> = Arc::new(ArrayAggregator::default());
63-
aggregator.synchronized_move(&new_aggregator, &descriptor).unwrap();
61+
aggregator
62+
.synchronized_move(&new_aggregator, &descriptor)
63+
.unwrap();
6464
for quantile in get_test_quantile() {
6565
if let Some(new_aggregator) = new_aggregator.as_any().downcast_ref::<ArrayAggregator>() {
6666
let _ = new_aggregator.quantile(*quantile);

examples/datadog/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "datadog"
3+
version = "0.1.0"
4+
edition = "2018"
5+
6+
[dependencies]
7+
opentelemetry = { path = "../../" }
8+
opentelemetry-contrib = { path = "../../opentelemetry-contrib", features = ["datadog"] }

examples/datadog/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Datadog Exporter Example
2+
3+
Sends spans to a datadog-agent collector.
4+
5+
## Usage
6+
7+
First run version 7.22.0 or above of the datadog-agent locally as described [here](https://docs.datadoghq.com/agent/)
8+
9+
Then run the example to report spans:
10+
11+
```shell
12+
$ cargo run
13+
```
14+
15+
Traces should appear in the datadog APM dashboard

examples/datadog/src/main.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use opentelemetry::api::{Key, Span, TraceContextExt, Tracer};
2+
use opentelemetry::global;
3+
use std::thread;
4+
use std::time::Duration;
5+
use opentelemetry_contrib::datadog::ApiVersion;
6+
7+
fn bar() {
8+
let tracer = global::tracer("component-bar");
9+
let span = tracer.start("bar");
10+
span.set_attribute(Key::new("span.type").string("sql"));
11+
span.set_attribute(Key::new("sql.query").string("SELECT * FROM table"));
12+
thread::sleep(Duration::from_millis(6));
13+
span.end()
14+
}
15+
16+
fn main() -> Result<(), Box<dyn std::error::Error>> {
17+
let tracer = opentelemetry_contrib::datadog::new_pipeline()
18+
.with_service_name("trace-demo")
19+
.with_version(ApiVersion::Version05)
20+
.install()?;
21+
22+
tracer.in_span("foo", |cx| {
23+
let span = cx.span();
24+
span.set_attribute(Key::new("span.type").string("web"));
25+
span.set_attribute(Key::new("http.url").string("http://localhost:8080/foo"));
26+
span.set_attribute(Key::new("http.method").string("GET"));
27+
span.set_attribute(Key::new("http.status_code").i64(200));
28+
29+
thread::sleep(Duration::from_millis(6));
30+
bar();
31+
thread::sleep(Duration::from_millis(6));
32+
});
33+
34+
Ok(())
35+
}

opentelemetry-contrib/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ edition = "2018"
1616

1717
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1818

19+
[features]
20+
default = []
21+
datadog = ["indexmap", "reqwest", "rmp"]
22+
1923
[dependencies]
24+
indexmap = { version = "1.6.0", optional = true }
2025
opentelemetry = { version = "0.8.0", path = ".." }
26+
reqwest = { version = "0.10", features = ["blocking"], optional = true }
27+
rmp = { version = "0.8", optional = true }
2128
lazy_static = "1.4"
29+
30+
[dev-dependencies]
31+
base64 = "0.12.3"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use indexmap::set::IndexSet;
2+
3+
pub(crate) struct StringInterner {
4+
data: IndexSet<String>,
5+
}
6+
7+
impl StringInterner {
8+
pub(crate) fn new() -> StringInterner {
9+
StringInterner {
10+
data: Default::default(),
11+
}
12+
}
13+
14+
pub(crate) fn intern(&mut self, data: &str) -> u32 {
15+
if let Some(idx) = self.data.get_index_of(data) {
16+
return idx as u32;
17+
}
18+
self.data.insert_full(data.to_string()).0 as u32
19+
}
20+
21+
pub(crate) fn iter(&self) -> impl Iterator<Item = &String> {
22+
self.data.iter()
23+
}
24+
25+
pub(crate) fn len(&self) -> u32 {
26+
self.data.len() as u32
27+
}
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
34+
#[test]
35+
fn test_intern() {
36+
let a = "a".to_string();
37+
let b = "b";
38+
let c = "c";
39+
40+
let mut intern = StringInterner::new();
41+
let a_idx = intern.intern(a.as_str());
42+
let b_idx = intern.intern(b);
43+
let c_idx = intern.intern(c);
44+
let d_idx = intern.intern(a.as_str());
45+
let e_idx = intern.intern(c);
46+
47+
assert_eq!(a_idx, 0);
48+
assert_eq!(b_idx, 1);
49+
assert_eq!(c_idx, 2);
50+
assert_eq!(d_idx, a_idx);
51+
assert_eq!(e_idx, c_idx);
52+
}
53+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//! # OpenTelemetry Datadog Exporter
2+
//!
3+
//! An OpenTelemetry exporter implementation
4+
//!
5+
//! See the [Datadog Docs](https://docs.datadoghq.com/agent/) for information on how to run the datadog-agent
6+
//!
7+
//! ## Quirks
8+
//!
9+
//! There are currently some incompatibilities between Datadog and OpenTelemetry, and this manifests
10+
//! as minor quirks to this exporter.
11+
//!
12+
//! Firstly Datadog uses operation_name to describe what OpenTracing would call a component.
13+
//! Or to put it another way, in OpenTracing the operation / span name's are relatively
14+
//! granular and might be used to identify a specific endpoint. In datadog, however, they
15+
//! are less granular - it is expected in Datadog that a service will have single
16+
//! primary span name that is the root of all traces within that service, with an additional piece of
17+
//! metadata called resource_name providing granularity - https://docs.datadoghq.com/tracing/guide/configuring-primary-operation/
18+
//!
19+
//! The Datadog Golang API takes the approach of using a `resource.name` OpenTelemetry attribute to set the
20+
//! resource_name - https://github.com/DataDog/dd-trace-go/blob/ecb0b805ef25b00888a2fb62d465a5aa95e7301e/ddtrace/opentracer/tracer.go#L10
21+
//!
22+
//! Unfortunately, this breaks compatibility with other OpenTelemetry exporters which expect
23+
//! a more granular operation name - as per the OpenTracing specification.
24+
//!
25+
//! This exporter therefore takes a different approach of naming the span with the name of the
26+
//! tracing provider, and using the span name to set the resource_name. This should in most cases
27+
//! lead to the behaviour that users expect.
28+
//!
29+
//! Datadog additionally has a span_type string that alters the rendering of the spans in the web UI.
30+
//! This can be set as the `span.type` OpenTelemetry span attribute.
31+
//!
32+
//! For standard values see here - https://github.com/DataDog/dd-trace-go/blob/ecb0b805ef25b00888a2fb62d465a5aa95e7301e/ddtrace/ext/app_types.go#L31
33+
//!
34+
//! ## Performance
35+
//!
36+
//! For optimal performance, a batch exporter is recommended as the simple
37+
//! exporter will export each span synchronously on drop. You can enable the
38+
//! [`tokio`] or [`async-std`] features to have a batch exporter configured for
39+
//! you automatically for either executor when you install the pipeline.
40+
//!
41+
//! ```toml
42+
//! [dependencies]
43+
//! opentelemetry = { version = "*", features = ["tokio"] }
44+
//! opentelemetry-datadog = "*"
45+
//! ```
46+
//!
47+
//! [`tokio`]: https://tokio.rs
48+
//! [`async-std`]: https://async.rs
49+
//!
50+
//! ## Kitchen Sink Full Configuration
51+
//!
52+
//! Example showing how to override all configuration options. See the
53+
//! [`DatadogPipelineBuilder`] docs for details of each option.
54+
//!
55+
//! [`DatadogPipelineBuilder`]: struct.DatadogPipelineBuilder.html
56+
//!
57+
//! ```no_run
58+
//! use opentelemetry::api::{KeyValue, Tracer};
59+
//! use opentelemetry::sdk::{trace, IdGenerator, Resource, Sampler};
60+
//!
61+
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
62+
//! let tracer = opentelemetry_contrib::datadog::new_pipeline()
63+
//! .with_service_name("my_app")
64+
//! .with_version(opentelemetry_contrib::datadog::ApiVersion::Version05)
65+
//! .with_agent_endpoint("http://localhost:8126")
66+
//! .with_trace_config(
67+
//! trace::config()
68+
//! .with_default_sampler(Sampler::AlwaysOn)
69+
//! .with_id_generator(IdGenerator::default())
70+
//! )
71+
//! .install()?;
72+
//!
73+
//! tracer.in_span("doing_work", |cx| {
74+
//! // Traced app logic here...
75+
//! });
76+
//!
77+
//! Ok(())
78+
//! }
79+
//! ```
80+
#![deny(missing_docs, unreachable_pub, missing_debug_implementations)]
81+
#![cfg_attr(test, deny(warnings))]
82+
83+
mod intern;
84+
mod model;
85+
86+
pub use model::ApiVersion;
87+
88+
use opentelemetry::{api::TracerProvider, exporter::trace, global, sdk};
89+
use reqwest::header::CONTENT_TYPE;
90+
use reqwest::Url;
91+
use std::error::Error;
92+
use std::sync::Arc;
93+
94+
/// Default Datadog collector endpoint
95+
const DEFAULT_AGENT_ENDPOINT: &str = "http://127.0.0.1:8126";
96+
97+
/// Default service name if no service is configured.
98+
const DEFAULT_SERVICE_NAME: &str = "OpenTelemetry";
99+
100+
/// Datadog span exporter
101+
#[derive(Debug)]
102+
pub struct DatadogExporter {
103+
client: reqwest::blocking::Client,
104+
request_url: Url,
105+
service_name: String,
106+
version: ApiVersion,
107+
}
108+
109+
impl DatadogExporter {
110+
fn new(service_name: String, agent_endpoint: Url, version: ApiVersion) -> Self {
111+
let mut request_url = agent_endpoint;
112+
request_url.set_path(version.path());
113+
114+
DatadogExporter {
115+
client: reqwest::blocking::Client::new(),
116+
request_url,
117+
service_name,
118+
version,
119+
}
120+
}
121+
}
122+
123+
/// Create a new Datadog exporter pipeline builder.
124+
pub fn new_pipeline() -> DatadogPipelineBuilder {
125+
DatadogPipelineBuilder::default()
126+
}
127+
128+
/// Builder for `ExporterConfig` struct.
129+
#[derive(Debug)]
130+
pub struct DatadogPipelineBuilder {
131+
service_name: String,
132+
agent_endpoint: String,
133+
trace_config: Option<sdk::Config>,
134+
version: ApiVersion,
135+
}
136+
137+
impl Default for DatadogPipelineBuilder {
138+
fn default() -> Self {
139+
DatadogPipelineBuilder {
140+
service_name: DEFAULT_SERVICE_NAME.to_string(),
141+
agent_endpoint: DEFAULT_AGENT_ENDPOINT.to_string(),
142+
trace_config: None,
143+
version: ApiVersion::Version05,
144+
}
145+
}
146+
}
147+
148+
impl DatadogPipelineBuilder {
149+
/// Create `ExporterConfig` struct from current `ExporterConfigBuilder`
150+
pub fn install(mut self) -> Result<sdk::Tracer, Box<dyn Error>> {
151+
let exporter = DatadogExporter::new(
152+
self.service_name.clone(),
153+
self.agent_endpoint.parse()?,
154+
self.version,
155+
);
156+
157+
let mut provider_builder = sdk::TracerProvider::builder().with_exporter(exporter);
158+
if let Some(config) = self.trace_config.take() {
159+
provider_builder = provider_builder.with_config(config);
160+
}
161+
let provider = provider_builder.build();
162+
let tracer = provider.get_tracer("opentelemetry-datadog", Some(env!("CARGO_PKG_VERSION")));
163+
global::set_provider(provider);
164+
165+
Ok(tracer)
166+
}
167+
168+
/// Assign the service name under which to group traces
169+
pub fn with_service_name<T: Into<String>>(mut self, name: T) -> Self {
170+
self.service_name = name.into();
171+
self
172+
}
173+
174+
/// Assign the Datadog collector endpoint
175+
pub fn with_agent_endpoint<T: Into<String>>(mut self, endpoint: T) -> Self {
176+
self.agent_endpoint = endpoint.into();
177+
self
178+
}
179+
180+
/// Assign the SDK trace configuration
181+
pub fn with_trace_config(mut self, config: sdk::Config) -> Self {
182+
self.trace_config = Some(config);
183+
self
184+
}
185+
186+
/// Set version of Datadog trace ingestion API
187+
pub fn with_version(mut self, version: ApiVersion) -> Self {
188+
self.version = version;
189+
self
190+
}
191+
}
192+
193+
impl trace::SpanExporter for DatadogExporter {
194+
/// Export spans to datadog-agent
195+
fn export(&self, batch: Vec<Arc<trace::SpanData>>) -> trace::ExportResult {
196+
let data = match self.version.encode(&self.service_name, batch) {
197+
Ok(data) => data,
198+
Err(_) => return trace::ExportResult::FailedNotRetryable,
199+
};
200+
201+
let resp = self
202+
.client
203+
.post(self.request_url.clone())
204+
.header(CONTENT_TYPE, self.version.content_type())
205+
.body(data)
206+
.send();
207+
208+
match resp {
209+
Ok(response) if response.status().is_success() => trace::ExportResult::Success,
210+
_ => trace::ExportResult::FailedRetryable,
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)