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/// Default gRPC keepalive ping interval: idle time before the server sends a PING to check the
287/// connection is still alive.
288pub const DEFAULT_GRPC_KEEPALIVE_TIME: Duration = Duration::from_secs(2 * 60 * 60);
289
290/// Default gRPC keepalive ping timeout: time to wait for a PONG before closing the connection.
291pub const DEFAULT_GRPC_KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(20);
292
293/// Server-side keepalive parameters for the OTLP gRPC receiver.
294///
295/// All fields are `Duration`. A zero duration is the sentinel for "unset": the translator
296/// resolves `time` and `timeout` to their effective defaults, and treats `max_connection_age` and
297/// `max_connection_age_grace` as "no limit."
298#[derive(Clone, Debug, Default, PartialEq, Serialize)]
299pub struct KeepaliveServerParameters {
300    /// Maximum time a connection may exist before the server sends GOAWAY. A zero duration means no
301    /// limit. Lower this to force periodic connection rotation in long-lived deployments.
302    pub max_connection_age: Duration,
303
304    /// Grace period after `max_connection_age` before the connection is forcibly closed. A zero
305    /// duration means no limit. Increase this to give in-flight RPCs more time to finish during
306    /// age-based shutdown.
307    pub max_connection_age_grace: Duration,
308
309    /// Idle time before the server sends a keepalive PING. A zero duration is resolved by the
310    /// translator to the default of 2 hours. Lower this to detect dead connections faster at the
311    /// cost of more frequent PING traffic.
312    pub time: Duration,
313
314    /// Time to wait for a PONG after a keepalive PING before closing the connection. A zero duration
315    /// is resolved by the translator to the default of 20 seconds. Increase this on networks with
316    /// high latency or intermittent delays.
317    pub timeout: Duration,
318}
319
320/// TLS settings for an OTLP receiver (gRPC or HTTP).
321///
322/// These configure server-side TLS for the receiver. When `cert_file` and `key_file` are both set, the receiver
323/// accepts encrypted connections. When `ca_file` is also set, the server requests client certificates and verifies
324/// them if presented, but does not require them (optional verification).
325#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
326pub struct Tls {
327    /// Path to the PEM-encoded certificate chain file.
328    ///
329    /// When set together with `key_file`, enables TLS on the receiver. Defaults to empty (TLS disabled).
330    pub cert_file: String,
331
332    /// Path to the PEM-encoded private key file.
333    ///
334    /// The private key must correspond to the leaf certificate in `cert_file`. Defaults to empty (TLS disabled).
335    pub key_file: String,
336
337    /// Path to the PEM-encoded CA certificate file for verifying client certificates.
338    ///
339    /// When set, the server requests client certificates and verifies them against the CA certificates in this file.
340    /// Clients that present a certificate must provide a valid one; clients that present no certificate are still
341    /// accepted. Defaults to empty (no client certificate verification).
342    pub ca_file: String,
343}
344
345/// OTLP gRPC receiver.
346#[derive(Clone, Debug, Default, PartialEq, Serialize)]
347pub struct GrpcReceiver {
348    /// Address the gRPC receiver listens on.
349    pub endpoint: String,
350
351    /// Maximum inbound message size, in MiB.
352    pub max_recv_msg_size_mib: u64,
353
354    /// Transport the gRPC receiver binds. Defaults to `tcp`.
355    pub transport: GrpcTransport,
356
357    /// HTTP/2 maximum concurrent streams per connection.
358    ///
359    /// Defaults to `0`, which means no limit (the server applies no cap). A positive value sets
360    /// the `SETTINGS_MAX_CONCURRENT_STREAMS` HTTP/2 setting.
361    pub max_concurrent_streams: u32,
362
363    /// Server-side keepalive parameters. Always present; zero durations are resolved to the
364    /// grpc-go defaults (2 h interval, 20 s timeout) by the translator.
365    pub keepalive: KeepaliveServerParameters,
366
367    /// TLS settings for the gRPC receiver.
368    pub tls: Tls,
369}
370
371/// OTLP HTTP receiver.
372#[derive(Clone, Debug, PartialEq, Serialize)]
373pub struct HttpReceiver {
374    /// Address the HTTP receiver listens on.
375    pub endpoint: String,
376
377    /// Transport the HTTP receiver binds (for example, `tcp` or `unix`). (not in Datadog Agent
378    /// config schema)
379    pub transport: String,
380
381    /// CORS configuration for the HTTP receiver.
382    pub cors: Cors,
383
384    /// TLS settings for the HTTP receiver.
385    pub tls: Tls,
386
387    /// Maximum HTTP request body size, in bytes.
388    ///
389    /// Defaults to `0`, which applies the 20 MiB limit used by the Datadog Agent. A positive value
390    /// sets the limit in bytes.
391    pub max_request_body_size: u64,
392}
393
394impl Default for HttpReceiver {
395    fn default() -> Self {
396        Self {
397            // Witnessed; overwritten during drive.
398            endpoint: String::new(),
399            transport: "tcp".to_string(),
400            cors: Cors::default(),
401            tls: Tls::default(),
402            max_request_body_size: 0,
403        }
404    }
405}
406
407/// CORS configuration for the OTLP HTTP receiver.
408#[derive(Clone, Debug, Default, PartialEq, Serialize)]
409pub struct Cors {
410    /// Allowed origins for cross-origin requests. A bare `*` allows every origin; a partial
411    /// wildcard like `http://*.example.com` matches that prefix and suffix. Empty disables CORS.
412    /// Defaults to empty; configure this for browser-based exporters.
413    pub allowed_origins: Vec<String>,
414
415    /// Request headers allowed in preflight, beyond the implicit `Accept`, `Accept-Language`,
416    /// `Content-Type`, and `Content-Language`. Use `*` to allow any header. Empty also implicitly
417    /// allows `X-Requested-With`. Defaults to empty; add headers for browser exporters that send them.
418    pub allowed_headers: Vec<String>,
419
420    /// Response headers exposed to the browser via `Access-Control-Expose-Headers`.
421    /// Defaults to empty; add headers browser clients need to read.
422    pub exposed_headers: Vec<String>,
423
424    /// Seconds browsers may cache a preflight response. Defaults to `0` (no caching); increase
425    /// to avoid repeated preflight round-trips for frequent browser requests.
426    pub max_age: u64,
427}
428
429/// OTLP trace ingestion settings.
430#[derive(Clone, Debug, PartialEq, Serialize)]
431pub struct Traces {
432    /// Whether OTLP trace ingestion is enabled.
433    pub enabled: bool,
434
435    /// Internal port the OTLP trace receiver forwards to.
436    pub internal_port: u16,
437
438    /// Percentage of OTLP traces the probabilistic sampler keeps.
439    pub probabilistic_sampler_sampling_percentage: f64,
440
441    /// Non-zero byte budget for the OTLP trace context interner. (not in Datadog Agent config schema)
442    ///
443    /// Defaults to 512 KiB and cannot exceed 1 GiB.
444    pub string_interner_size: NonZeroUsize,
445
446    /// Whether top-level spans are computed from span kind on OTLP traces. (not in Datadog Agent
447    /// config schema)
448    pub enable_compute_top_level_by_span_kind: bool,
449
450    /// Whether spans missing intake-required fields are ingested rather than rejected. (not in
451    /// Datadog Agent config schema)
452    pub ignore_missing_datadog_fields: bool,
453}
454
455impl Default for Traces {
456    fn default() -> Self {
457        Self {
458            enabled: false,
459            internal_port: 0,
460            probabilistic_sampler_sampling_percentage: 0.0,
461            string_interner_size: DEFAULT_STRING_INTERNER_SIZE_BYTES,
462            enable_compute_top_level_by_span_kind: true,
463            ignore_missing_datadog_fields: false,
464        }
465    }
466}
467
468/// OTLP proxy gating: which signals the proxy forwards, and the proxy receiver endpoint.
469#[derive(Clone, Debug, Default, PartialEq, Serialize)]
470pub struct Proxy {
471    /// Whether the OTLP proxy is enabled.
472    pub enabled: bool,
473
474    /// Whether the proxy forwards logs.
475    pub logs_enabled: bool,
476
477    /// Whether the proxy forwards metrics.
478    pub metrics_enabled: bool,
479
480    /// Whether the proxy forwards traces.
481    pub traces_enabled: bool,
482
483    /// Address the proxy's gRPC receiver listens on.
484    pub grpc_endpoint: String,
485}
486
487/// OTLP context cache sizing.
488#[derive(Clone, Debug, PartialEq, Serialize)]
489pub struct Contexts {
490    /// Whether contexts may be heap-allocated when the interner is full. (not in Datadog Agent
491    /// config schema)
492    pub allow_context_heap_allocs: bool,
493
494    /// Maximum number of metric contexts held in the cache. (not in Datadog Agent config schema)
495    pub cached_contexts_limit: usize,
496
497    /// Maximum number of tagsets held in the cache. (not in Datadog Agent config schema)
498    pub cached_tagsets_limit: usize,
499
500    /// Size, in bytes, of the context string interner. (not in Datadog Agent config schema)
501    pub string_interner_size: u64,
502}
503
504impl Default for Contexts {
505    fn default() -> Self {
506        Self {
507            allow_context_heap_allocs: true,
508            cached_contexts_limit: 500_000,
509            cached_tagsets_limit: 500_000,
510            string_interner_size: 2 * 1024 * 1024,
511        }
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::{CumulativeMonotonicMode, GrpcReceiver, GrpcTransport, HttpReceiver, InitialCumulativeMonotonicValue};
518
519    #[test]
520    fn grpc_transport_parses_known_values() {
521        assert_eq!("tcp".parse::<GrpcTransport>().unwrap(), GrpcTransport::Tcp);
522        assert_eq!("unix".parse::<GrpcTransport>().unwrap(), GrpcTransport::Unix);
523    }
524
525    #[test]
526    fn grpc_transport_rejects_unknown_values() {
527        assert!("tcp4".parse::<GrpcTransport>().is_err());
528        assert!("udp".parse::<GrpcTransport>().is_err());
529    }
530
531    #[test]
532    fn cumulative_monotonic_mode_parses_known_values() {
533        assert_eq!(
534            "to_delta"
535                .parse::<CumulativeMonotonicMode>()
536                .expect("to_delta should parse"),
537            CumulativeMonotonicMode::ToDelta
538        );
539        assert_eq!(
540            "raw_value"
541                .parse::<CumulativeMonotonicMode>()
542                .expect("raw_value should parse"),
543            CumulativeMonotonicMode::RawValue
544        );
545    }
546
547    #[test]
548    fn cumulative_monotonic_mode_rejects_unknown_values() {
549        let error = "unsupported"
550            .parse::<CumulativeMonotonicMode>()
551            .expect_err("unsupported mode should be rejected");
552
553        assert_eq!(
554            error.to_string(),
555            "unknown cumulative monotonic sum mode `unsupported`; expected `to_delta` or `raw_value`"
556        );
557    }
558
559    #[test]
560    fn initial_cumulative_monotonic_value_parses_known_values() {
561        for (value, expected) in [
562            ("auto", InitialCumulativeMonotonicValue::Auto),
563            ("drop", InitialCumulativeMonotonicValue::Drop),
564            ("keep", InitialCumulativeMonotonicValue::Keep),
565        ] {
566            assert_eq!(
567                value
568                    .parse::<InitialCumulativeMonotonicValue>()
569                    .expect("known value should parse"),
570                expected
571            );
572        }
573    }
574
575    #[test]
576    fn initial_cumulative_monotonic_value_rejects_unknown_values() {
577        let error = "unsupported"
578            .parse::<InitialCumulativeMonotonicValue>()
579            .expect_err("unsupported value should be rejected");
580
581        assert_eq!(
582            error.to_string(),
583            "unknown initial cumulative monotonic value `unsupported`; expected `auto`, `drop`, or `keep`"
584        );
585    }
586
587    #[test]
588    fn grpc_receiver_defaults_to_agent_compatible_values() {
589        let grpc = GrpcReceiver::default();
590        assert_eq!(grpc.max_concurrent_streams, 0, "0 means no limit (Agent default)");
591    }
592
593    #[test]
594    fn http_receiver_defaults_to_agent_compatible_values() {
595        let http = HttpReceiver::default();
596        assert_eq!(http.max_request_body_size, 0, "0 means 20 MiB default (Agent default)");
597    }
598}