agent_data_plane_config/domains/
otlp.rs

1//! OTLP domain: the OTLP receiver (gRPC/HTTP transports, logs/metrics activation), the OTLP proxy
2//! gating, and OTLP context sizing. OTLP trace handling lives in the `traces` domain.
3
4use std::time::Duration;
5use std::{num::NonZeroUsize, str::FromStr};
6
7use serde::{Deserialize, Serialize};
8
9use crate::defaults::DEFAULT_STRING_INTERNER_SIZE_BYTES;
10use crate::{domains::dogstatsd::OriginTagCardinality, Error};
11
12/// Resolved OTLP configuration.
13#[derive(Clone, Debug, Default, PartialEq, Serialize)]
14pub struct Domain {
15    /// OTLP receiver transports and per-signal activation.
16    pub receiver: Receiver,
17
18    /// OTLP metrics translation settings.
19    pub metrics: Metrics,
20
21    /// OTLP trace ingestion settings.
22    pub traces: Traces,
23
24    /// OTLP proxy gating and endpoint.
25    pub proxy: Proxy,
26
27    /// OTLP context cache sizing.
28    pub contexts: Contexts,
29}
30
31/// Default TTL for cached prior points used when converting cumulative sums to deltas.
32pub const DEFAULT_DELTA_TTL: Duration = Duration::from_secs(3600);
33
34/// OTLP metrics translation settings.
35#[derive(Clone, Debug, PartialEq, Serialize)]
36pub struct Metrics {
37    /// Tag cardinality applied to entity and global tags enriched onto OTLP metrics.
38    ///
39    /// Defaults to `low`. Set this to `orchestrator` or `high` when the additional series cardinality is acceptable.
40    /// `none` disables entity and global tag enrichment.
41    pub tag_cardinality: OriginTagCardinality,
42
43    /// How explicit histogram buckets are reported.
44    pub histogram_mode: HistogramMode,
45
46    /// Whether histogram count, sum, minimum, and maximum metrics are emitted when available.
47    ///
48    /// The `nobuckets` mode requires this setting. Defaults to `false`.
49    pub send_histogram_aggregations: bool,
50
51    /// Whether every resource attribute is added as a raw metric tag, in addition to the
52    /// semantic-convention mappings that are always applied.
53    pub resource_attributes_as_tags: bool,
54
55    /// Whether instrumentation scope name, version, and attributes are added as metric tags.
56    ///
57    /// Defaults to `true`. When `false`, no scope tags are emitted (no `n/a` placeholders).
58    /// Disable this in high-cardinality scope environments where per-scope tag overhead outweighs queryability.
59    pub instrumentation_scope_metadata_as_tags: bool,
60
61    /// OTLP sum translation settings.
62    pub sums: Sums,
63
64    /// Comma-separated list of tags to add to every emitted metric.
65    ///
66    /// Defaults to empty. When the static-tag resolver produces no tags, this value is preserved.
67    /// When it produces one or more tags, those tags replace this value; the two sets are not merged.
68    pub tags: String,
69
70    /// OTLP summary translation settings.
71    pub summaries: Summaries,
72
73    /// Time-to-live for cached prior data points used when converting cumulative monotonic sums
74    /// to deltas. Defaults to `3600` seconds. Must be greater than zero.
75    pub delta_ttl: Duration,
76}
77
78impl Default for Metrics {
79    fn default() -> Self {
80        Self {
81            tag_cardinality: OriginTagCardinality::default(),
82            histogram_mode: HistogramMode::default(),
83            send_histogram_aggregations: false,
84            resource_attributes_as_tags: false,
85            instrumentation_scope_metadata_as_tags: true,
86            sums: Sums::default(),
87            tags: String::new(),
88            summaries: Summaries::default(),
89            delta_ttl: DEFAULT_DELTA_TTL,
90        }
91    }
92}
93
94/// How explicit OTLP histogram buckets are reported.
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
96pub enum HistogramMode {
97    /// Omit bucket metrics.
98    NoBuckets,
99
100    /// Report each bucket as a counter.
101    Counters,
102
103    /// Report buckets as distributions.
104    #[default]
105    Distributions,
106}
107
108impl FromStr for HistogramMode {
109    type Err = Error;
110
111    fn from_str(value: &str) -> Result<Self, Self::Err> {
112        match value {
113            "nobuckets" => Ok(Self::NoBuckets),
114            "counters" => Ok(Self::Counters),
115            "distributions" => Ok(Self::Distributions),
116            other => Err(Error::new_without_source(format!(
117                "unknown histogram mode `{other}`; expected `nobuckets`, `counters`, or `distributions`"
118            ))),
119        }
120    }
121}
122
123/// How cumulative monotonic sums are reported.
124#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
125pub enum CumulativeMonotonicMode {
126    /// Converts cumulative values to deltas and reports them as counts.
127    #[default]
128    ToDelta,
129
130    /// Reports cumulative values as gauges without converting them to deltas.
131    RawValue,
132}
133
134impl FromStr for CumulativeMonotonicMode {
135    type Err = Error;
136
137    fn from_str(value: &str) -> Result<Self, Self::Err> {
138        match value {
139            "to_delta" => Ok(Self::ToDelta),
140            "raw_value" => Ok(Self::RawValue),
141            other => Err(Error::new_without_source(format!(
142                "unknown cumulative monotonic sum mode `{other}`; expected `to_delta` or `raw_value`"
143            ))),
144        }
145    }
146}
147
148/// Controls how the first value of a cumulative monotonic sum is reported.
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
150pub enum InitialCumulativeMonotonicValue {
151    /// Reports the first value when its series started after the translator process.
152    #[default]
153    Auto,
154
155    /// Always drops the first value.
156    Drop,
157
158    /// Always reports the first value.
159    Keep,
160}
161
162impl FromStr for InitialCumulativeMonotonicValue {
163    type Err = Error;
164
165    fn from_str(value: &str) -> Result<Self, Self::Err> {
166        match value {
167            "auto" => Ok(Self::Auto),
168            "drop" => Ok(Self::Drop),
169            "keep" => Ok(Self::Keep),
170            other => Err(Error::new_without_source(format!(
171                "unknown initial cumulative monotonic value `{other}`; expected `auto`, `drop`, or `keep`"
172            ))),
173        }
174    }
175}
176
177/// OTLP sum translation settings.
178#[derive(Clone, Debug, Default, PartialEq, Serialize)]
179pub struct Sums {
180    /// Cumulative monotonic sum reporting mode.
181    ///
182    /// Defaults to `to_delta`, which converts cumulative values to delta counts. Set to `raw_value` to emit
183    /// cumulative values as gauges.
184    pub cumulative_monotonic_mode: CumulativeMonotonicMode,
185
186    /// Initial cumulative monotonic sum reporting behavior.
187    ///
188    /// Defaults to `auto`, which reports the value only when its series started after the translator process.
189    /// Set this to `drop` to always discard the first value or `keep` to always report it.
190    pub initial_cumulative_monotonic_value: InitialCumulativeMonotonicValue,
191}
192
193/// OTLP summary translation settings.
194#[derive(Clone, Debug, Default, PartialEq, Serialize)]
195pub struct Summaries {
196    /// How summary quantiles are reported.
197    ///
198    /// Defaults to `gauges`, which emits one gauge metric per quantile. Set to `noquantiles` to omit quantile
199    /// metrics.
200    pub mode: SummaryMode,
201}
202
203/// How OTLP summary quantiles are reported.
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
205pub enum SummaryMode {
206    /// Report one gauge metric per quantile.
207    #[default]
208    Gauges,
209
210    /// Omit quantile metrics.
211    NoQuantiles,
212}
213
214impl FromStr for SummaryMode {
215    type Err = Error;
216
217    fn from_str(value: &str) -> Result<Self, Self::Err> {
218        match value {
219            "gauges" => Ok(Self::Gauges),
220            "noquantiles" => Ok(Self::NoQuantiles),
221            other => Err(Error::new_without_source(format!(
222                "unknown summary mode `{other}`; expected `gauges` or `noquantiles`"
223            ))),
224        }
225    }
226}
227
228/// Transport accepted by the OTLP gRPC receiver.
229#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
230#[serde(rename_all = "lowercase")]
231pub enum GrpcTransport {
232    /// TCP transport.
233    #[default]
234    Tcp,
235    /// Unix stream socket transport.
236    Unix,
237}
238
239impl GrpcTransport {
240    /// Returns the configuration spelling of this transport.
241    pub const fn as_str(self) -> &'static str {
242        match self {
243            Self::Tcp => "tcp",
244            Self::Unix => "unix",
245        }
246    }
247}
248
249impl FromStr for GrpcTransport {
250    type Err = Error;
251
252    fn from_str(value: &str) -> Result<Self, Self::Err> {
253        match value {
254            "tcp" => Ok(Self::Tcp),
255            "unix" => Ok(Self::Unix),
256            other => Err(Error::new_without_source(format!(
257                "unknown gRPC transport `{other}`; expected `tcp` or `unix`"
258            ))),
259        }
260    }
261}
262
263/// OTLP receiver transports and per-signal activation.
264#[derive(Clone, Debug, Default, PartialEq, Serialize)]
265pub struct Receiver {
266    /// Whether the receiver accepts OTLP logs.
267    pub logs_enabled: bool,
268
269    /// Whether the receiver accepts OTLP metrics.
270    pub metrics_enabled: bool,
271
272    /// gRPC receiver settings.
273    pub grpc: GrpcReceiver,
274
275    /// HTTP receiver settings.
276    pub http: HttpReceiver,
277}
278
279/// Default gRPC maximum inbound message size, in MiB.
280///
281/// The Datadog schema default for `max_recv_msg_size_mib` is `0`, which grpc-go treats as "apply the
282/// built-in 4 MiB limit". Translation substitutes this constant for a configured `0` so the model
283/// always carries an effective limit.
284pub const DEFAULT_GRPC_MAX_RECV_MSG_SIZE_MIB: u64 = 4;
285
286/// OTLP gRPC receiver.
287#[derive(Clone, Debug, Default, PartialEq, Serialize)]
288pub struct GrpcReceiver {
289    /// Address the gRPC receiver listens on.
290    pub endpoint: String,
291
292    /// Maximum inbound message size, in MiB.
293    pub max_recv_msg_size_mib: u64,
294
295    /// Transport the gRPC receiver binds. Defaults to `tcp`.
296    pub transport: GrpcTransport,
297}
298
299/// OTLP HTTP receiver.
300#[derive(Clone, Debug, PartialEq, Serialize)]
301pub struct HttpReceiver {
302    /// Address the HTTP receiver listens on.
303    pub endpoint: String,
304
305    /// Transport the HTTP receiver binds (for example, `tcp` or `unix`). (not in Datadog Agent
306    /// config schema)
307    pub transport: String,
308
309    /// CORS configuration for the HTTP receiver.
310    pub cors: Cors,
311}
312
313impl Default for HttpReceiver {
314    fn default() -> Self {
315        Self {
316            // Witnessed; overwritten during drive.
317            endpoint: String::new(),
318            transport: "tcp".to_string(),
319            cors: Cors::default(),
320        }
321    }
322}
323
324/// CORS configuration for the OTLP HTTP receiver.
325#[derive(Clone, Debug, Default, PartialEq, Serialize)]
326pub struct Cors {
327    /// Allowed origins for cross-origin requests. A bare `*` allows every origin; a partial
328    /// wildcard like `http://*.example.com` matches that prefix and suffix. Empty disables CORS.
329    /// Defaults to empty; configure this for browser-based exporters.
330    pub allowed_origins: Vec<String>,
331
332    /// Request headers allowed in preflight, beyond the implicit `Accept`, `Accept-Language`,
333    /// `Content-Type`, and `Content-Language`. Use `*` to allow any header. Empty also implicitly
334    /// allows `X-Requested-With`. Defaults to empty; add headers for browser exporters that send them.
335    pub allowed_headers: Vec<String>,
336
337    /// Response headers exposed to the browser via `Access-Control-Expose-Headers`.
338    /// Defaults to empty; add headers browser clients need to read.
339    pub exposed_headers: Vec<String>,
340
341    /// Seconds browsers may cache a preflight response. Defaults to `0` (no caching); increase
342    /// to avoid repeated preflight round-trips for frequent browser requests.
343    pub max_age: u64,
344}
345
346/// OTLP trace ingestion settings.
347#[derive(Clone, Debug, PartialEq, Serialize)]
348pub struct Traces {
349    /// Whether OTLP trace ingestion is enabled.
350    pub enabled: bool,
351
352    /// Internal port the OTLP trace receiver forwards to.
353    pub internal_port: u16,
354
355    /// Percentage of OTLP traces the probabilistic sampler keeps.
356    pub probabilistic_sampler_sampling_percentage: f64,
357
358    /// Non-zero byte budget for the OTLP trace context interner. (not in Datadog Agent config schema)
359    ///
360    /// Defaults to 512 KiB and cannot exceed 1 GiB.
361    pub string_interner_size: NonZeroUsize,
362
363    /// Whether top-level spans are computed from span kind on OTLP traces. (not in Datadog Agent
364    /// config schema)
365    pub enable_compute_top_level_by_span_kind: bool,
366
367    /// Whether spans missing intake-required fields are ingested rather than rejected. (not in
368    /// Datadog Agent config schema)
369    pub ignore_missing_datadog_fields: bool,
370}
371
372impl Default for Traces {
373    fn default() -> Self {
374        Self {
375            enabled: false,
376            internal_port: 0,
377            probabilistic_sampler_sampling_percentage: 0.0,
378            string_interner_size: DEFAULT_STRING_INTERNER_SIZE_BYTES,
379            enable_compute_top_level_by_span_kind: true,
380            ignore_missing_datadog_fields: false,
381        }
382    }
383}
384
385/// OTLP proxy gating: which signals the proxy forwards, and the proxy receiver endpoint.
386#[derive(Clone, Debug, Default, PartialEq, Serialize)]
387pub struct Proxy {
388    /// Whether the OTLP proxy is enabled.
389    pub enabled: bool,
390
391    /// Whether the proxy forwards logs.
392    pub logs_enabled: bool,
393
394    /// Whether the proxy forwards metrics.
395    pub metrics_enabled: bool,
396
397    /// Whether the proxy forwards traces.
398    pub traces_enabled: bool,
399
400    /// Address the proxy's gRPC receiver listens on.
401    pub grpc_endpoint: String,
402}
403
404/// OTLP context cache sizing.
405#[derive(Clone, Debug, PartialEq, Serialize)]
406pub struct Contexts {
407    /// Whether contexts may be heap-allocated when the interner is full. (not in Datadog Agent
408    /// config schema)
409    pub allow_context_heap_allocs: bool,
410
411    /// Maximum number of metric contexts held in the cache. (not in Datadog Agent config schema)
412    pub cached_contexts_limit: usize,
413
414    /// Maximum number of tagsets held in the cache. (not in Datadog Agent config schema)
415    pub cached_tagsets_limit: usize,
416
417    /// Size, in bytes, of the context string interner. (not in Datadog Agent config schema)
418    pub string_interner_size: u64,
419}
420
421impl Default for Contexts {
422    fn default() -> Self {
423        Self {
424            allow_context_heap_allocs: true,
425            cached_contexts_limit: 500_000,
426            cached_tagsets_limit: 500_000,
427            string_interner_size: 2 * 1024 * 1024,
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::{CumulativeMonotonicMode, GrpcTransport, InitialCumulativeMonotonicValue};
435
436    #[test]
437    fn grpc_transport_parses_known_values() {
438        assert_eq!("tcp".parse::<GrpcTransport>().unwrap(), GrpcTransport::Tcp);
439        assert_eq!("unix".parse::<GrpcTransport>().unwrap(), GrpcTransport::Unix);
440    }
441
442    #[test]
443    fn grpc_transport_rejects_unknown_values() {
444        assert!("tcp4".parse::<GrpcTransport>().is_err());
445        assert!("udp".parse::<GrpcTransport>().is_err());
446    }
447
448    #[test]
449    fn cumulative_monotonic_mode_parses_known_values() {
450        assert_eq!(
451            "to_delta"
452                .parse::<CumulativeMonotonicMode>()
453                .expect("to_delta should parse"),
454            CumulativeMonotonicMode::ToDelta
455        );
456        assert_eq!(
457            "raw_value"
458                .parse::<CumulativeMonotonicMode>()
459                .expect("raw_value should parse"),
460            CumulativeMonotonicMode::RawValue
461        );
462    }
463
464    #[test]
465    fn cumulative_monotonic_mode_rejects_unknown_values() {
466        let error = "unsupported"
467            .parse::<CumulativeMonotonicMode>()
468            .expect_err("unsupported mode should be rejected");
469
470        assert_eq!(
471            error.to_string(),
472            "unknown cumulative monotonic sum mode `unsupported`; expected `to_delta` or `raw_value`"
473        );
474    }
475
476    #[test]
477    fn initial_cumulative_monotonic_value_parses_known_values() {
478        for (value, expected) in [
479            ("auto", InitialCumulativeMonotonicValue::Auto),
480            ("drop", InitialCumulativeMonotonicValue::Drop),
481            ("keep", InitialCumulativeMonotonicValue::Keep),
482        ] {
483            assert_eq!(
484                value
485                    .parse::<InitialCumulativeMonotonicValue>()
486                    .expect("known value should parse"),
487                expected
488            );
489        }
490    }
491
492    #[test]
493    fn initial_cumulative_monotonic_value_rejects_unknown_values() {
494        let error = "unsupported"
495            .parse::<InitialCumulativeMonotonicValue>()
496            .expect_err("unsupported value should be rejected");
497
498        assert_eq!(
499            error.to_string(),
500            "unknown initial cumulative monotonic value `unsupported`; expected `auto`, `drop`, or `keep`"
501        );
502    }
503}