Skip to content

Commit 89c147e

Browse files
authored
feat(datadog): allow custom mapping in datadog. (open-telemetry#770)
* feat(datadog): allow users to override the model. * feat(datadog): allow users to override the model. * feat(datadog): allow users to override the model mapping. * feat(datadog): update submodule * feat(datadog): update submodule * doc(datadog): clean up docs
1 parent d555741 commit 89c147e

File tree

6 files changed

+304
-37
lines changed

6 files changed

+304
-37
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "opentelemetry-proto/src/proto/opentelemetry-proto"]
22
path = opentelemetry-proto/src/proto/opentelemetry-proto
33
url = https://github.com/open-telemetry/opentelemetry-proto
4-
branch = tags/v0.9.0
4+
branch = tags/v0.14.0

opentelemetry-datadog/src/exporter/mod.rs

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ mod model;
33

44
pub use model::ApiVersion;
55
pub use model::Error;
6+
pub use model::FieldMappingFn;
67

8+
use std::fmt::{Debug, Formatter};
9+
10+
use crate::exporter::model::FieldMapping;
711
use async_trait::async_trait;
812
use http::{Method, Request, Uri};
913
use itertools::Itertools;
@@ -27,43 +31,72 @@ const DEFAULT_AGENT_ENDPOINT: &str = "http://127.0.0.1:8126";
2731
const DATADOG_TRACE_COUNT_HEADER: &str = "X-Datadog-Trace-Count";
2832

2933
/// Datadog span exporter
30-
#[derive(Debug)]
3134
pub struct DatadogExporter {
3235
client: Box<dyn HttpClient>,
3336
request_url: Uri,
34-
service_name: String,
37+
model_config: ModelConfig,
3538
version: ApiVersion,
39+
40+
resource_mapping: Option<FieldMapping>,
41+
name_mapping: Option<FieldMapping>,
42+
service_name_mapping: Option<FieldMapping>,
3643
}
3744

3845
impl DatadogExporter {
3946
fn new(
40-
service_name: String,
47+
model_config: ModelConfig,
4148
request_url: Uri,
4249
version: ApiVersion,
4350
client: Box<dyn HttpClient>,
51+
resource_mapping: Option<FieldMapping>,
52+
name_mapping: Option<FieldMapping>,
53+
service_name_mapping: Option<FieldMapping>,
4454
) -> Self {
4555
DatadogExporter {
4656
client,
4757
request_url,
48-
service_name,
58+
model_config,
4959
version,
60+
resource_mapping,
61+
name_mapping,
62+
service_name_mapping,
5063
}
5164
}
5265
}
5366

67+
impl Debug for DatadogExporter {
68+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69+
f.debug_struct("DatadogExporter")
70+
.field("model_config", &self.model_config)
71+
.field("request_url", &self.request_url)
72+
.field("version", &self.version)
73+
.field("client", &self.client)
74+
.field("resource_mapping", &mapping_debug(&self.resource_mapping))
75+
.field("name_mapping", &mapping_debug(&self.name_mapping))
76+
.field(
77+
"service_name_mapping",
78+
&mapping_debug(&self.service_name_mapping),
79+
)
80+
.finish()
81+
}
82+
}
83+
5484
/// Create a new Datadog exporter pipeline builder.
5585
pub fn new_pipeline() -> DatadogPipelineBuilder {
5686
DatadogPipelineBuilder::default()
5787
}
5888

5989
/// Builder for `ExporterConfig` struct.
60-
#[derive(Debug)]
6190
pub struct DatadogPipelineBuilder {
6291
service_name: Option<String>,
6392
agent_endpoint: String,
6493
trace_config: Option<sdk::trace::Config>,
6594
version: ApiVersion,
6695
client: Option<Box<dyn HttpClient>>,
96+
97+
resource_mapping: Option<FieldMapping>,
98+
name_mapping: Option<FieldMapping>,
99+
service_name_mapping: Option<FieldMapping>,
67100
}
68101

69102
impl Default for DatadogPipelineBuilder {
@@ -72,6 +105,9 @@ impl Default for DatadogPipelineBuilder {
72105
service_name: None,
73106
agent_endpoint: DEFAULT_AGENT_ENDPOINT.to_string(),
74107
trace_config: None,
108+
resource_mapping: None,
109+
name_mapping: None,
110+
service_name_mapping: None,
75111
version: ApiVersion::Version05,
76112
#[cfg(all(
77113
not(feature = "reqwest-client"),
@@ -97,6 +133,24 @@ impl Default for DatadogPipelineBuilder {
97133
}
98134
}
99135

136+
impl Debug for DatadogPipelineBuilder {
137+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
138+
f.debug_struct("DatadogExporter")
139+
.field("service_name", &self.service_name)
140+
.field("agent_endpoint", &self.agent_endpoint)
141+
.field("version", &self.version)
142+
.field("trace_config", &self.trace_config)
143+
.field("client", &self.client)
144+
.field("resource_mapping", &mapping_debug(&self.resource_mapping))
145+
.field("name_mapping", &mapping_debug(&self.name_mapping))
146+
.field(
147+
"service_name_mapping",
148+
&mapping_debug(&self.service_name_mapping),
149+
)
150+
.finish()
151+
}
152+
}
153+
100154
impl DatadogPipelineBuilder {
101155
/// Building a new exporter.
102156
///
@@ -148,12 +202,19 @@ impl DatadogPipelineBuilder {
148202
service_name: String,
149203
) -> Result<DatadogExporter, TraceError> {
150204
if let Some(client) = self.client {
205+
let model_config = ModelConfig {
206+
service_name,
207+
..Default::default()
208+
};
151209
let endpoint = self.agent_endpoint + self.version.path();
152210
let exporter = DatadogExporter::new(
153-
service_name,
211+
model_config,
154212
endpoint.parse().map_err::<Error, _>(Into::into)?,
155213
self.version,
156214
client,
215+
self.resource_mapping,
216+
self.name_mapping,
217+
self.service_name_mapping,
157218
);
158219
Ok(exporter)
159220
} else {
@@ -231,6 +292,36 @@ impl DatadogPipelineBuilder {
231292
self.version = version;
232293
self
233294
}
295+
296+
/// Custom the value used for `resource` field in datadog spans.
297+
/// See [`FieldMappingFn`] for details.
298+
pub fn with_resource_mapping<F>(mut self, f: F) -> Self
299+
where
300+
F: for<'a> Fn(&'a SpanData, &'a ModelConfig) -> &'a str + Send + Sync + 'static,
301+
{
302+
self.resource_mapping = Some(Arc::new(f));
303+
self
304+
}
305+
306+
/// Custom the value used for `name` field in datadog spans.
307+
/// See [`FieldMappingFn`] for details.
308+
pub fn with_name_mapping<F>(mut self, f: F) -> Self
309+
where
310+
F: for<'a> Fn(&'a SpanData, &'a ModelConfig) -> &'a str + Send + Sync + 'static,
311+
{
312+
self.name_mapping = Some(Arc::new(f));
313+
self
314+
}
315+
316+
/// Custom the value used for `service_name` field in datadog spans.
317+
/// See [`FieldMappingFn`] for details.
318+
pub fn with_service_name_mapping<F>(mut self, f: F) -> Self
319+
where
320+
F: for<'a> Fn(&'a SpanData, &'a ModelConfig) -> &'a str + Send + Sync + 'static,
321+
{
322+
self.service_name_mapping = Some(Arc::new(f));
323+
self
324+
}
234325
}
235326

236327
fn group_into_traces(spans: Vec<SpanData>) -> Vec<Vec<SpanData>> {
@@ -248,7 +339,13 @@ impl trace::SpanExporter for DatadogExporter {
248339
async fn export(&mut self, batch: Vec<SpanData>) -> trace::ExportResult {
249340
let traces: Vec<Vec<SpanData>> = group_into_traces(batch);
250341
let trace_count = traces.len();
251-
let data = self.version.encode(&self.service_name, traces)?;
342+
let data = self.version.encode(
343+
&self.model_config,
344+
traces,
345+
self.service_name_mapping.clone(),
346+
self.name_mapping.clone(),
347+
self.resource_mapping.clone(),
348+
)?;
252349
let req = Request::builder()
253350
.method(Method::POST)
254351
.uri(self.request_url.clone())
@@ -261,6 +358,24 @@ impl trace::SpanExporter for DatadogExporter {
261358
}
262359
}
263360

361+
/// Helper struct to custom the mapping between Opentelemetry spans and datadog spans.
362+
///
363+
/// This struct will be passed to [`FieldMappingFn`]
364+
#[derive(Default, Debug)]
365+
#[non_exhaustive]
366+
pub struct ModelConfig {
367+
pub service_name: String,
368+
}
369+
370+
fn mapping_debug(f: &Option<FieldMapping>) -> String {
371+
if f.is_some() {
372+
"custom mapping"
373+
} else {
374+
"default mapping"
375+
}
376+
.to_string()
377+
}
378+
264379
#[cfg(test)]
265380
mod tests {
266381
use super::*;

opentelemetry-datadog/src/exporter/model/mod.rs

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,65 @@
1-
use opentelemetry::sdk::export::{trace, ExportError};
1+
use crate::exporter::ModelConfig;
2+
use opentelemetry::sdk::export::{
3+
trace::{self, SpanData},
4+
ExportError,
5+
};
6+
use std::fmt::Debug;
27

38
mod v03;
49
mod v05;
510

11+
/// Custom mapping between opentelemetry spans and datadog spans.
12+
///
13+
/// User can provide custom function to change the mapping. It currently supports customizing the following
14+
/// fields in Datadog span protocol.
15+
///
16+
/// |field name|default value|
17+
/// |---------------|-------------|
18+
/// |service name| service name configuration from [`ModelConfig`]|
19+
/// |name | opentelemetry instrumentation library name |
20+
/// |resource| opentelemetry name|
21+
///
22+
/// The function takes a reference to [`SpanData`]() and a reference to [`ModelConfig`]() as parameters.
23+
/// It should return a `&str` which will be used as the value for the field.
24+
///
25+
/// If no custom mapping is provided. Default mapping detailed above will be used.
26+
///
27+
/// For example,
28+
/// ```no_run
29+
/// use opentelemetry_datadog::{ApiVersion, new_pipeline};
30+
/// fn main() -> Result<(), opentelemetry::trace::TraceError> {
31+
/// let tracer = new_pipeline()
32+
/// .with_service_name("my_app")
33+
/// .with_version(ApiVersion::Version05)
34+
/// // the custom mapping below will change the all spans' name to datadog spans
35+
/// .with_name_mapping(|span, model_config|{
36+
/// "datadog spans"
37+
/// })
38+
/// .with_agent_endpoint("http://localhost:8126")
39+
/// .install_batch(opentelemetry::runtime::Tokio)?;
40+
///
41+
/// Ok(())
42+
/// }
43+
/// ```
44+
pub type FieldMappingFn = dyn for<'a> Fn(&'a SpanData, &'a ModelConfig) -> &'a str + Send + Sync;
45+
46+
pub(crate) type FieldMapping = std::sync::Arc<FieldMappingFn>;
47+
48+
// Datadog uses some magic tags in their models. There is no recommended mapping defined in
49+
// opentelemetry spec. Below is default mapping we gonna uses. Users can override it by providing
50+
// their own implementations
51+
fn default_service_name_mapping<'a>(_span: &'a SpanData, config: &'a ModelConfig) -> &'a str {
52+
config.service_name.as_str()
53+
}
54+
55+
fn default_name_mapping<'a>(span: &'a SpanData, _config: &'a ModelConfig) -> &'a str {
56+
span.instrumentation_lib.name.as_ref()
57+
}
58+
59+
fn default_resource_mapping<'a>(span: &'a SpanData, _config: &'a ModelConfig) -> &'a str {
60+
span.name.as_ref()
61+
}
62+
663
/// Wrap type for errors from opentelemetry datadog exporter
764
#[derive(Debug, thiserror::Error)]
865
pub enum Error {
@@ -37,6 +94,7 @@ impl From<rmp::encode::ValueWriteError> for Error {
3794

3895
/// Version of datadog trace ingestion API
3996
#[derive(Debug, Copy, Clone)]
97+
#[non_exhaustive]
4098
pub enum ApiVersion {
4199
/// Version 0.3
42100
Version03,
@@ -61,12 +119,45 @@ impl ApiVersion {
61119

62120
pub(crate) fn encode(
63121
self,
64-
service_name: &str,
122+
model_config: &ModelConfig,
65123
traces: Vec<Vec<trace::SpanData>>,
124+
get_service_name: Option<FieldMapping>,
125+
get_name: Option<FieldMapping>,
126+
get_resource: Option<FieldMapping>,
66127
) -> Result<Vec<u8>, Error> {
67128
match self {
68-
Self::Version03 => v03::encode(service_name, traces),
69-
Self::Version05 => v05::encode(service_name, traces),
129+
Self::Version03 => v03::encode(
130+
model_config,
131+
traces,
132+
|span, config| match &get_service_name {
133+
Some(f) => f(span, config),
134+
None => default_service_name_mapping(span, config),
135+
},
136+
|span, config| match &get_name {
137+
Some(f) => f(span, config),
138+
None => default_name_mapping(span, config),
139+
},
140+
|span, config| match &get_resource {
141+
Some(f) => f(span, config),
142+
None => default_resource_mapping(span, config),
143+
},
144+
),
145+
Self::Version05 => v05::encode(
146+
model_config,
147+
traces,
148+
|span, config| match &get_service_name {
149+
Some(f) => f(span, config),
150+
None => default_service_name_mapping(span, config),
151+
},
152+
|span, config| match &get_name {
153+
Some(f) => f(span, config),
154+
None => default_name_mapping(span, config),
155+
},
156+
|span, config| match &get_resource {
157+
Some(f) => f(span, config),
158+
None => default_resource_mapping(span, config),
159+
},
160+
),
70161
}
71162
}
72163
}
@@ -124,7 +215,17 @@ pub(crate) mod tests {
124215
#[test]
125216
fn test_encode_v03() -> Result<(), Box<dyn std::error::Error>> {
126217
let traces = get_traces();
127-
let encoded = base64::encode(ApiVersion::Version03.encode("service_name", traces)?);
218+
let model_config = ModelConfig {
219+
service_name: "service_name".to_string(),
220+
..Default::default()
221+
};
222+
let encoded = base64::encode(ApiVersion::Version03.encode(
223+
&model_config,
224+
traces,
225+
None,
226+
None,
227+
None,
228+
)?);
128229

129230
assert_eq!(encoded.as_str(), "kZGLpHR5cGWjd2Vip3NlcnZpY2Wsc2VydmljZV9uYW1lpG5hbWWpY29tcG9uZW50qHJlc291cmNlqHJlc291cmNlqHRyYWNlX2lkzwAAAAAAAAAHp3NwYW5faWTPAAAAAAAAAGOpcGFyZW50X2lkzwAAAAAAAAABpXN0YXJ00wAAAAAAAAAAqGR1cmF0aW9u0wAAAAA7msoApWVycm9y0gAAAACkbWV0YYGpc3Bhbi50eXBlo3dlYg==");
130231

@@ -134,9 +235,20 @@ pub(crate) mod tests {
134235
#[test]
135236
fn test_encode_v05() -> Result<(), Box<dyn std::error::Error>> {
136237
let traces = get_traces();
137-
let encoded = base64::encode(ApiVersion::Version05.encode("service_name", traces)?);
138-
139-
assert_eq!(encoded.as_str(), "kpWsc2VydmljZV9uYW1lo3dlYqljb21wb25lbnSocmVzb3VyY2Wpc3Bhbi50eXBlkZGczgAAAADOAAAAAs4AAAADzwAAAAAAAAAHzwAAAAAAAABjzwAAAAAAAAAB0wAAAAAAAAAA0wAAAAA7msoA0gAAAACBzgAAAATOAAAAAYDOAAAAAQ==");
238+
let model_config = ModelConfig {
239+
service_name: "service_name".to_string(),
240+
..Default::default()
241+
};
242+
let encoded = base64::encode(ApiVersion::Version05.encode(
243+
&model_config,
244+
traces,
245+
None,
246+
None,
247+
None,
248+
)?);
249+
250+
assert_eq!(encoded.as_str(),
251+
"kpWjd2VirHNlcnZpY2VfbmFtZaljb21wb25lbnSocmVzb3VyY2Wpc3Bhbi50eXBlkZGczgAAAAHOAAAAAs4AAAADzwAAAAAAAAAHzwAAAAAAAABjzwAAAAAAAAAB0wAAAAAAAAAA0wAAAAA7msoA0gAAAACBzgAAAATOAAAAAIDOAAAAAA==");
140252

141253
Ok(())
142254
}

0 commit comments

Comments
 (0)