Skip to content

Commit bc6fdcd

Browse files
committed
feat(otlp): added CORS support
1 parent 0f47357 commit bc6fdcd

15 files changed

Lines changed: 431 additions & 18 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/agent-data-plane/configuration/configuration.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,6 @@ ways that are not yet fully characterized.
464464
| `otlp_config.receiver.protocols.grpc.tls.tpm.path` | gRPC TLS TPM path | |
465465
| `otlp_config.receiver.protocols.grpc.write_buffer_size` | gRPC write buffer size | |
466466
| `otlp_config.receiver.protocols.http.compression_algorithms` | HTTP compression algorithms | |
467-
| `otlp_config.receiver.protocols.http.cors.allowed_headers` | HTTP CORS allowed headers | |
468-
| `otlp_config.receiver.protocols.http.cors.allowed_origins` | HTTP CORS allowed origins | |
469-
| `otlp_config.receiver.protocols.http.cors.exposed_headers` | HTTP CORS exposed headers | |
470-
| `otlp_config.receiver.protocols.http.cors.max_age` | HTTP CORS max age | |
471467
| `otlp_config.receiver.protocols.http.idle_timeout` | HTTP idle timeout | |
472468
| `otlp_config.receiver.protocols.http.include_metadata` | HTTP include metadata in context | |
473469
| `otlp_config.receiver.protocols.http.keep_alives_enabled` | HTTP keep-alives enabled | |
@@ -835,6 +831,10 @@ Both commands scrub recognized secret values before writing JSON to standard out
835831
| `otlp_config.receiver.protocols.grpc.endpoint` | otlp_config.receiver.protocols.grpc.endpoint |
836832
| `otlp_config.receiver.protocols.grpc.max_recv_msg_size_mib` | Max OTLP inbound gRPC message size (MiB) |
837833
| `otlp_config.receiver.protocols.grpc.transport` | otlp_config.receiver.protocols.grpc.transport |
834+
| `otlp_config.receiver.protocols.http.cors.allowed_headers` | HTTP CORS allowed headers |
835+
| `otlp_config.receiver.protocols.http.cors.allowed_origins` | HTTP CORS allowed origins |
836+
| `otlp_config.receiver.protocols.http.cors.exposed_headers` | HTTP CORS exposed headers |
837+
| `otlp_config.receiver.protocols.http.cors.max_age` | HTTP CORS max age |
838838
| `otlp_config.receiver.protocols.http.endpoint` | otlp_config.receiver.protocols.http.endpoint |
839839
| `otlp_config.traces.enabled` | otlp_config.traces.enabled |
840840
| `otlp_config.traces.internal_port` | otlp_config.traces.internal_port |

lib/agent-data-plane-config-system/src/translators/datadog_translator.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,30 @@ impl DatadogConfigWitness for DatadogTranslator<'_> {
10051005
self.config.domains.otlp.receiver.http.endpoint = value;
10061006
}
10071007

1008+
fn consume_otlp_config_receiver_protocols_http_cors_allowed_headers(&mut self, value: Vec<String>) {
1009+
self.config.domains.otlp.receiver.http.cors.allowed_headers = value;
1010+
}
1011+
1012+
fn consume_otlp_config_receiver_protocols_http_cors_allowed_origins(&mut self, value: Vec<String>) {
1013+
self.config.domains.otlp.receiver.http.cors.allowed_origins = value;
1014+
}
1015+
1016+
fn consume_otlp_config_receiver_protocols_http_cors_exposed_headers(&mut self, value: Vec<String>) {
1017+
self.config.domains.otlp.receiver.http.cors.exposed_headers = value;
1018+
}
1019+
1020+
fn consume_otlp_config_receiver_protocols_http_cors_max_age(&mut self, value: Option<i64>) {
1021+
if let Some(v) = value {
1022+
match u64::try_from(v) {
1023+
Ok(max_age) => self.config.domains.otlp.receiver.http.cors.max_age = max_age,
1024+
Err(error) => self.record_error(TranslateError::new(
1025+
"otlp_config.receiver.protocols.http.cors.max_age",
1026+
error,
1027+
)),
1028+
}
1029+
}
1030+
}
1031+
10081032
fn consume_otlp_config_traces_enabled(&mut self, value: bool) {
10091033
self.config.domains.otlp.traces.enabled = value;
10101034
}

lib/agent-data-plane-config/src/domains/otlp.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ pub struct HttpReceiver {
240240
/// Transport the HTTP receiver binds (for example, `tcp` or `unix`). (not in Datadog Agent
241241
/// config schema)
242242
pub transport: String,
243+
244+
/// CORS configuration for the HTTP receiver.
245+
pub cors: Cors,
243246
}
244247

245248
impl Default for HttpReceiver {
@@ -248,10 +251,38 @@ impl Default for HttpReceiver {
248251
// Witnessed; overwritten during drive.
249252
endpoint: String::new(),
250253
transport: "tcp".to_string(),
254+
cors: Cors::default(),
251255
}
252256
}
253257
}
254258

259+
/// CORS configuration for the OTLP HTTP receiver.
260+
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
261+
pub struct Cors {
262+
/// Origins allowed to make cross-origin requests. Allows for wildcard character when describing
263+
/// domains (for example: "http://*.domains.com")
264+
///
265+
/// Defaults to an empty list (disabling CORS).
266+
pub allowed_origins: Vec<String>,
267+
268+
/// Headers allowed in CORS requests, in addition to the implicitly allowed `Accept`,
269+
/// `Accept-Language`, `Content-Type`, and `Content-Language` headers.
270+
///
271+
/// When empty, `X-Requested-With` is also implicitly allowed. Include `*` to allow any
272+
/// request header. Defaults to an empty list.
273+
pub allowed_headers: Vec<String>,
274+
275+
/// Headers safe to expose to the API of a CORS response.
276+
///
277+
/// Sets the `Access-Control-Expose-Headers` response header. Defaults to an empty list.
278+
pub exposed_headers: Vec<String>,
279+
280+
/// Number of seconds browsers should cache a CORS preflight response.
281+
///
282+
/// Defaults to `0` (which prevents caching)
283+
pub max_age: u64,
284+
}
285+
255286
/// OTLP trace ingestion settings.
256287
#[derive(Clone, Debug, PartialEq, Serialize)]
257288
pub struct Traces {

lib/datadog-agent/config-testing/src/config_registry/otlp.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,50 @@ crate::declare_annotations! {
355355
test_json: None,
356356
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::Otlp]),
357357
};
358+
/// `otlp_config.receiver.protocols.http.cors.allowed_headers`-HTTP CORS allowed headers
359+
OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_ALLOWED_HEADERS = SalukiAnnotation {
360+
schema: &schema::OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_ALLOWED_HEADERS,
361+
support_level: SupportLevel::Full,
362+
additional_yaml_paths: &[],
363+
env_var_override: None,
364+
used_by: &[structs::TYPED_CONFIG_SYSTEM],
365+
value_type_override: None,
366+
test_json: None,
367+
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::Otlp]),
368+
};
369+
/// `otlp_config.receiver.protocols.http.cors.allowed_origins`-HTTP CORS allowed origins
370+
OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_ALLOWED_ORIGINS = SalukiAnnotation {
371+
schema: &schema::OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_ALLOWED_ORIGINS,
372+
support_level: SupportLevel::Full,
373+
additional_yaml_paths: &[],
374+
env_var_override: None,
375+
used_by: &[structs::TYPED_CONFIG_SYSTEM],
376+
value_type_override: None,
377+
test_json: None,
378+
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::Otlp]),
379+
};
380+
/// `otlp_config.receiver.protocols.http.cors.exposed_headers`-HTTP CORS exposed headers
381+
OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_EXPOSED_HEADERS = SalukiAnnotation {
382+
schema: &schema::OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_EXPOSED_HEADERS,
383+
support_level: SupportLevel::Full,
384+
additional_yaml_paths: &[],
385+
env_var_override: None,
386+
used_by: &[structs::TYPED_CONFIG_SYSTEM],
387+
value_type_override: None,
388+
test_json: None,
389+
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::Otlp]),
390+
};
391+
/// `otlp_config.receiver.protocols.http.cors.max_age`-HTTP CORS max age
392+
OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_MAX_AGE = SalukiAnnotation {
393+
schema: &schema::OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_MAX_AGE,
394+
support_level: SupportLevel::Full,
395+
additional_yaml_paths: &[],
396+
env_var_override: None,
397+
used_by: &[structs::TYPED_CONFIG_SYSTEM],
398+
value_type_override: None,
399+
test_json: None,
400+
pipeline_affinity: PipelineAffinity::Pipelines(&[Pipeline::Otlp]),
401+
};
358402
/// `tags`-Global tags for EKS Fargate OTLP metrics
359403
TAGS = SalukiAnnotation {
360404
schema: &schema::TAGS,

lib/datadog-agent/config/build/datadog_config_gen.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ fn permissivize(file: &mut syn::File) {
583583
LeafKind::Number => "crate::cast_de::deserialize_f64",
584584
LeafKind::Text => "crate::cast_de::deserialize_string",
585585
LeafKind::OptionalText => "crate::cast_de::deserialize_optional_string",
586+
LeafKind::OptionalInteger => "crate::cast_de::deserialize_optional_i64",
586587
LeafKind::Exempt => continue,
587588
LeafKind::Unknown => panic!(
588589
"field `{}.{name}` has no declared coercion; classify its type in `leaf_kind` and \
@@ -604,6 +605,7 @@ enum LeafKind {
604605
Number,
605606
Text,
606607
OptionalText,
608+
OptionalInteger,
607609
/// A nested section, or a leaf whose shape another pass or its own consumer handles.
608610
Exempt,
609611
Unknown,
@@ -620,6 +622,14 @@ fn leaf_kind(ty: &syn::Type, struct_names: &HashSet<String>) -> LeafKind {
620622
if option_inner(ty).is_some_and(is_plain_string) {
621623
return LeafKind::OptionalText;
622624
}
625+
// An optional `integer` leaf uses the same permissive coercion as a plain `i64`, while an
626+
// absent or null value stays `None`.
627+
if option_inner(ty)
628+
.and_then(plain_ident)
629+
.is_some_and(|ident| ident == "i64")
630+
{
631+
return LeafKind::OptionalInteger;
632+
}
623633
if is_vec_string(ty) || is_string_map_vec_string(ty) || is_json_container(ty) || is_duration(ty) {
624634
return LeafKind::Exempt;
625635
}

lib/datadog-agent/config/schema/schema_overlay.yaml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,20 +2344,40 @@ inventory:
23442344
description: "HTTP compression algorithms"
23452345

23462346
otlp_config.receiver.protocols.http.cors.allowed_headers:
2347-
support: unknown
2347+
support: full
2348+
pipelines: [otlp]
23482349
description: "HTTP CORS allowed headers"
2350+
test_support:
2351+
used_by: [TypedConfigSystem]
2352+
additional_attributes:
2353+
config_registry_filename: otlp.rs
23492354

23502355
otlp_config.receiver.protocols.http.cors.allowed_origins:
2351-
support: unknown
2356+
support: full
2357+
pipelines: [otlp]
23522358
description: "HTTP CORS allowed origins"
2359+
test_support:
2360+
used_by: [TypedConfigSystem]
2361+
additional_attributes:
2362+
config_registry_filename: otlp.rs
23532363

23542364
otlp_config.receiver.protocols.http.cors.exposed_headers:
2355-
support: unknown
2365+
support: full
2366+
pipelines: [otlp]
23562367
description: "HTTP CORS exposed headers"
2368+
test_support:
2369+
used_by: [TypedConfigSystem]
2370+
additional_attributes:
2371+
config_registry_filename: otlp.rs
23572372

23582373
otlp_config.receiver.protocols.http.cors.max_age:
2359-
support: unknown
2374+
support: full
2375+
pipelines: [otlp]
23602376
description: "HTTP CORS max age"
2377+
test_support:
2378+
used_by: [TypedConfigSystem]
2379+
additional_attributes:
2380+
config_registry_filename: otlp.rs
23612381

23622382
otlp_config.receiver.protocols.http.endpoint:
23632383
support: full

lib/datadog-agent/config/src/cast_de.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ where
144144
deserializer.deserialize_option(OptionalStringVisitor)
145145
}
146146

147+
/// Deserializes an optional `integer` leaf, where an absent or null value stays `None`.
148+
///
149+
/// # Errors
150+
///
151+
/// Same as [`deserialize_i64`] for a present value.
152+
pub(crate) fn deserialize_optional_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
153+
where
154+
D: Deserializer<'de>,
155+
{
156+
deserializer.deserialize_option(OptionalI64Visitor)
157+
}
158+
147159
struct BoolVisitor;
148160

149161
impl Visitor<'_> for BoolVisitor {
@@ -314,6 +326,28 @@ impl<'de> Visitor<'de> for OptionalStringVisitor {
314326
}
315327
}
316328

329+
struct OptionalI64Visitor;
330+
331+
impl<'de> Visitor<'de> for OptionalI64Visitor {
332+
type Value = Option<i64>;
333+
334+
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335+
f.write_str("an integer, a numeric string, a boolean, or null")
336+
}
337+
338+
fn visit_none<E: de::Error>(self) -> Result<Option<i64>, E> {
339+
Ok(None)
340+
}
341+
342+
fn visit_unit<E: de::Error>(self) -> Result<Option<i64>, E> {
343+
Ok(None)
344+
}
345+
346+
fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Option<i64>, D::Error> {
347+
deserializer.deserialize_any(I64Visitor).map(Some)
348+
}
349+
}
350+
317351
#[cfg(test)]
318352
mod tests {
319353
use serde::Deserialize;

lib/datadog-agent/config/src/generated/datadog_configuration.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,9 @@ impl Default for OtlpConfigReceiverProtocolsGrpc {
16291629

16301630
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
16311631
pub struct OtlpConfigReceiverProtocolsHttp {
1632+
#[serde(default)]
1633+
pub cors: OtlpConfigReceiverProtocolsHttpCors,
1634+
16321635
#[serde(
16331636
default = "defaults::datadog_configuration_otlp_config_receiver_protocols_http_endpoint"
16341637
)]
@@ -1639,11 +1642,42 @@ pub struct OtlpConfigReceiverProtocolsHttp {
16391642
impl Default for OtlpConfigReceiverProtocolsHttp {
16401643
fn default() -> Self {
16411644
Self {
1645+
cors: Default::default(),
16421646
endpoint: defaults::datadog_configuration_otlp_config_receiver_protocols_http_endpoint(),
16431647
}
16441648
}
16451649
}
16461650

1651+
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
1652+
pub struct OtlpConfigReceiverProtocolsHttpCors {
1653+
#[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
1654+
#[serde(deserialize_with = "crate::list_de::deserialize_space_separated_or_seq")]
1655+
pub allowed_headers: Vec<String>,
1656+
1657+
#[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
1658+
#[serde(deserialize_with = "crate::list_de::deserialize_space_separated_or_seq")]
1659+
pub allowed_origins: Vec<String>,
1660+
1661+
#[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
1662+
#[serde(deserialize_with = "crate::list_de::deserialize_space_separated_or_seq")]
1663+
pub exposed_headers: Vec<String>,
1664+
1665+
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
1666+
#[serde(deserialize_with = "crate::cast_de::deserialize_optional_i64")]
1667+
pub max_age: Option<i64>,
1668+
}
1669+
1670+
impl Default for OtlpConfigReceiverProtocolsHttpCors {
1671+
fn default() -> Self {
1672+
Self {
1673+
allowed_headers: Default::default(),
1674+
allowed_origins: Default::default(),
1675+
exposed_headers: Default::default(),
1676+
max_age: Default::default(),
1677+
}
1678+
}
1679+
}
1680+
16471681
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
16481682
pub struct OtlpConfigTraces {
16491683
#[serde(default = "defaults::default_bool::<true>")]

lib/datadog-agent/config/src/generated/env_keys.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,47 @@ pub static DATADOG_ENV_KEYS: &[EnvKey] = &[
835835
path: &["otlp_config", "receiver", "protocols", "grpc", "transport"],
836836
decode: EnvDecode::RawString,
837837
},
838+
EnvKey {
839+
env_vars: &["DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_ALLOWED_HEADERS"],
840+
path: &[
841+
"otlp_config",
842+
"receiver",
843+
"protocols",
844+
"http",
845+
"cors",
846+
"allowed_headers",
847+
],
848+
decode: EnvDecode::StringList,
849+
},
850+
EnvKey {
851+
env_vars: &["DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_ALLOWED_ORIGINS"],
852+
path: &[
853+
"otlp_config",
854+
"receiver",
855+
"protocols",
856+
"http",
857+
"cors",
858+
"allowed_origins",
859+
],
860+
decode: EnvDecode::StringList,
861+
},
862+
EnvKey {
863+
env_vars: &["DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_EXPOSED_HEADERS"],
864+
path: &[
865+
"otlp_config",
866+
"receiver",
867+
"protocols",
868+
"http",
869+
"cors",
870+
"exposed_headers",
871+
],
872+
decode: EnvDecode::StringList,
873+
},
874+
EnvKey {
875+
env_vars: &["DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_CORS_MAX_AGE"],
876+
path: &["otlp_config", "receiver", "protocols", "http", "cors", "max_age"],
877+
decode: EnvDecode::Integer,
878+
},
838879
EnvKey {
839880
env_vars: &["DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT"],
840881
path: &["otlp_config", "receiver", "protocols", "http", "endpoint"],

0 commit comments

Comments
 (0)