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}