use schemars::JsonSchema;
use serde::Deserialize;
use super::conditional::Conditional;
use crate::plugins::telemetry::config_new::DefaultForLevel;
use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel;
use crate::plugins::telemetry::config_new::attributes::RouterAttributes;
use crate::plugins::telemetry::config_new::attributes::SubgraphAttributes;
use crate::plugins::telemetry::config_new::attributes::SupergraphAttributes;
use crate::plugins::telemetry::config_new::extendable::Extendable;
use crate::plugins::telemetry::config_new::selectors::RouterSelector;
use crate::plugins::telemetry::config_new::selectors::SubgraphSelector;
use crate::plugins::telemetry::config_new::selectors::SupergraphSelector;
use crate::plugins::telemetry::otlp::TelemetryDataKind;
use crate::plugins::telemetry::span_factory::SpanMode;
#[derive(Deserialize, JsonSchema, Clone, Default, Debug)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct Spans {
pub(crate) mode: SpanMode,
pub(crate) default_attribute_requirement_level: DefaultAttributeRequirementLevel,
pub(crate) router: RouterSpans,
pub(crate) supergraph: SupergraphSpans,
pub(crate) subgraph: SubgraphSpans,
}
impl Spans {
pub(crate) fn update_defaults(&mut self) {
self.router.defaults_for_levels(
self.default_attribute_requirement_level,
TelemetryDataKind::Traces,
);
self.supergraph.defaults_for_levels(
self.default_attribute_requirement_level,
TelemetryDataKind::Traces,
);
self.subgraph.defaults_for_levels(
self.default_attribute_requirement_level,
TelemetryDataKind::Traces,
);
}
pub(crate) fn validate(&self) -> Result<(), String> {
for (name, custom) in &self.router.attributes.custom {
custom
.validate()
.map_err(|err| format!("error for router span attribute {name:?}: {err}"))?;
}
for (name, custom) in &self.supergraph.attributes.custom {
custom
.validate()
.map_err(|err| format!("error for supergraph span attribute {name:?}: {err}"))?;
}
for (name, custom) in &self.subgraph.attributes.custom {
custom
.validate()
.map_err(|err| format!("error for subgraph span attribute {name:?}: {err}"))?;
}
Ok(())
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug, Default)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct RouterSpans {
pub(crate) attributes: Extendable<RouterAttributes, Conditional<RouterSelector>>,
}
impl DefaultForLevel for RouterSpans {
fn defaults_for_level(
&mut self,
requirement_level: DefaultAttributeRequirementLevel,
kind: TelemetryDataKind,
) {
self.attributes.defaults_for_level(requirement_level, kind);
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug, Default)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct SupergraphSpans {
pub(crate) attributes: Extendable<SupergraphAttributes, Conditional<SupergraphSelector>>,
}
impl DefaultForLevel for SupergraphSpans {
fn defaults_for_level(
&mut self,
requirement_level: DefaultAttributeRequirementLevel,
kind: TelemetryDataKind,
) {
self.attributes.defaults_for_level(requirement_level, kind);
}
}
#[derive(Deserialize, JsonSchema, Clone, Default, Debug)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct SubgraphSpans {
pub(crate) attributes: Extendable<SubgraphAttributes, Conditional<SubgraphSelector>>,
}
impl DefaultForLevel for SubgraphSpans {
fn defaults_for_level(
&mut self,
requirement_level: DefaultAttributeRequirementLevel,
kind: TelemetryDataKind,
) {
self.attributes.defaults_for_level(requirement_level, kind);
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use std::sync::Arc;
use http::header::USER_AGENT;
use opentelemetry_semantic_conventions::trace::GRAPHQL_DOCUMENT;
use opentelemetry_semantic_conventions::trace::HTTP_REQUEST_METHOD;
use opentelemetry_semantic_conventions::trace::NETWORK_PROTOCOL_VERSION;
use opentelemetry_semantic_conventions::trace::URL_PATH;
use opentelemetry_semantic_conventions::trace::USER_AGENT_ORIGINAL;
use parking_lot::Mutex;
use serde_json_bytes::path::JsonPathInst;
use crate::Context;
use crate::context::CONTAINS_GRAPHQL_ERROR;
use crate::context::OPERATION_KIND;
use crate::graphql;
use crate::plugins::telemetry::OTEL_NAME;
use crate::plugins::telemetry::config::AttributeValue;
use crate::plugins::telemetry::config_new::DefaultForLevel;
use crate::plugins::telemetry::config_new::Selectors;
use crate::plugins::telemetry::config_new::attributes::DefaultAttributeRequirementLevel;
use crate::plugins::telemetry::config_new::attributes::SUBGRAPH_GRAPHQL_DOCUMENT;
use crate::plugins::telemetry::config_new::attributes::StandardAttribute;
use crate::plugins::telemetry::config_new::conditional::Conditional;
use crate::plugins::telemetry::config_new::conditions::Condition;
use crate::plugins::telemetry::config_new::conditions::SelectorOrValue;
use crate::plugins::telemetry::config_new::selectors::ResponseStatus;
use crate::plugins::telemetry::config_new::selectors::RouterSelector;
use crate::plugins::telemetry::config_new::selectors::SubgraphSelector;
use crate::plugins::telemetry::config_new::selectors::SupergraphSelector;
use crate::plugins::telemetry::config_new::spans::RouterSpans;
use crate::plugins::telemetry::config_new::spans::SubgraphSpans;
use crate::plugins::telemetry::config_new::spans::SupergraphSpans;
use crate::plugins::telemetry::otlp::TelemetryDataKind;
use crate::services::router;
use crate::services::subgraph;
use crate::services::supergraph;
#[test]
fn test_router_spans_level_none() {
let mut spans = RouterSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::None,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header(USER_AGENT, "test")
.build()
.unwrap(),
);
assert!(
!values
.iter()
.any(|key_val| key_val.key == HTTP_REQUEST_METHOD)
);
assert!(
!values
.iter()
.any(|key_val| key_val.key == NETWORK_PROTOCOL_VERSION)
);
assert!(!values.iter().any(|key_val| key_val.key == URL_PATH));
assert!(
!values
.iter()
.any(|key_val| key_val.key == USER_AGENT_ORIGINAL)
);
}
#[test]
fn test_router_spans_level_required() {
let mut spans = RouterSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::Required,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header(USER_AGENT, "test")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == HTTP_REQUEST_METHOD)
);
assert!(
!values
.iter()
.any(|key_val| key_val.key == NETWORK_PROTOCOL_VERSION)
);
assert!(values.iter().any(|key_val| key_val.key == URL_PATH));
assert!(
!values
.iter()
.any(|key_val| key_val.key == USER_AGENT_ORIGINAL)
);
}
#[test]
fn test_router_spans_level_recommended() {
let mut spans = RouterSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::Recommended,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header(USER_AGENT, "test")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == HTTP_REQUEST_METHOD)
);
assert!(
values
.iter()
.any(|key_val| key_val.key == NETWORK_PROTOCOL_VERSION)
);
assert!(values.iter().any(|key_val| key_val.key == URL_PATH));
assert!(
values
.iter()
.any(|key_val| key_val.key == USER_AGENT_ORIGINAL)
);
}
#[test]
fn test_supergraph_spans_level_none() {
let mut spans = SupergraphSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::None,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&supergraph::Request::fake_builder()
.query("query { __typename }")
.build()
.unwrap(),
);
assert!(!values.iter().any(|key_val| key_val.key == GRAPHQL_DOCUMENT));
}
#[test]
fn test_supergraph_spans_level_required() {
let mut spans = SupergraphSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::Required,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&supergraph::Request::fake_builder()
.query("query { __typename }")
.build()
.unwrap(),
);
assert!(!values.iter().any(|key_val| key_val.key == GRAPHQL_DOCUMENT));
}
#[test]
fn test_supergraph_spans_level_recommended() {
let mut spans = SupergraphSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::Recommended,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&supergraph::Request::fake_builder()
.query("query { __typename }")
.build()
.unwrap(),
);
assert!(values.iter().any(|key_val| key_val.key == GRAPHQL_DOCUMENT));
}
#[test]
fn test_subgraph_spans_level_none() {
let mut spans = SubgraphSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::None,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&subgraph::Request::fake_builder()
.subgraph_request(
::http::Request::builder()
.uri("http://localhost/graphql")
.body(
graphql::Request::fake_builder()
.query("query { __typename }")
.build(),
)
.unwrap(),
)
.build(),
);
assert!(!values.iter().any(|key_val| key_val.key == GRAPHQL_DOCUMENT));
}
#[test]
fn test_subgraph_spans_level_required() {
let mut spans = SubgraphSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::Required,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&subgraph::Request::fake_builder()
.subgraph_request(
::http::Request::builder()
.uri("http://localhost/graphql")
.body(
graphql::Request::fake_builder()
.query("query { __typename }")
.build(),
)
.unwrap(),
)
.build(),
);
assert!(!values.iter().any(|key_val| key_val.key == GRAPHQL_DOCUMENT));
}
#[test]
fn test_subgraph_spans_level_recommended() {
let mut spans = SubgraphSpans::default();
spans.defaults_for_levels(
DefaultAttributeRequirementLevel::Recommended,
TelemetryDataKind::Traces,
);
let values = spans.attributes.on_request(
&subgraph::Request::fake_builder()
.subgraph_request(
::http::Request::builder()
.uri("http://localhost/graphql")
.body(
graphql::Request::fake_builder()
.query("query { __typename }")
.build(),
)
.unwrap(),
)
.build(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == SUBGRAPH_GRAPHQL_DOCUMENT)
);
}
#[test]
fn test_router_request_static_custom_attribute_on_graphql_error() {
let mut spans = RouterSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: RouterSelector::StaticField {
r#static: "my-static-value".to_string().into(),
},
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::Bool(true)),
SelectorOrValue::Selector(RouterSelector::OnGraphQLError {
on_graphql_error: true,
}),
])))),
value: Arc::new(Default::default()),
},
);
let context = Context::new();
context.insert_json_value(CONTAINS_GRAPHQL_ERROR, serde_json_bytes::Value::Bool(true));
let values = spans.attributes.on_response(
&router::Response::fake_builder()
.header("my-header", "test_val")
.context(context)
.build()
.unwrap(),
);
assert!(values.iter().any(|key_val| key_val.key
== opentelemetry::Key::from_static_str("test")
&& key_val.value
== opentelemetry::Value::String("my-static-value".to_string().into())));
}
#[test]
fn test_router_request_custom_attribute_on_graphql_error() {
let mut spans = RouterSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: RouterSelector::ResponseHeader {
response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::Bool(true)),
SelectorOrValue::Selector(RouterSelector::OnGraphQLError {
on_graphql_error: true,
}),
])))),
value: Arc::new(Default::default()),
},
);
let context = Context::new();
context.insert_json_value(CONTAINS_GRAPHQL_ERROR, serde_json_bytes::Value::Bool(true));
let values = spans.attributes.on_response(
&router::Response::fake_builder()
.header("my-header", "test_val")
.context(context)
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_router_request_custom_attribute_not_on_graphql_error() {
let mut spans = RouterSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: RouterSelector::ResponseHeader {
response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::Bool(true)),
SelectorOrValue::Selector(RouterSelector::OnGraphQLError {
on_graphql_error: true,
}),
])))),
value: Arc::new(Default::default()),
},
);
let context = Context::new();
context.insert_json_value(CONTAINS_GRAPHQL_ERROR, serde_json_bytes::Value::Bool(false));
let values = spans.attributes.on_response(
&router::Response::fake_builder()
.header("my-header", "test_val")
.context(context)
.build()
.unwrap(),
);
assert!(
!values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_router_request_custom_attribute_condition_true() {
let mut spans = RouterSpans::default();
let selector = RouterSelector::RequestHeader {
request_header: "my-header".to_string(),
redact: None,
default: None,
};
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: selector.clone(),
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::String("test_val".to_string())),
SelectorOrValue::Selector(selector),
])))),
value: Default::default(),
},
);
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_router_request_custom_attribute_condition_false() {
let mut spans = RouterSpans::default();
let selector = RouterSelector::RequestHeader {
request_header: "my-header".to_string(),
redact: None,
default: None,
};
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: selector.clone(),
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::String("test_val".to_string())),
SelectorOrValue::Selector(selector),
])))),
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header("my-header", "bar")
.build()
.unwrap(),
);
assert!(
!values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_router_request_custom_attribute() {
let mut spans = RouterSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: RouterSelector::RequestHeader {
request_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: None,
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_router_request_standard_attribute_aliased() {
let mut spans = RouterSpans::default();
spans.attributes.attributes.common.http_request_method = Some(StandardAttribute::Aliased {
alias: String::from("my.method"),
});
let values = spans.attributes.on_request(
&router::Request::fake_builder()
.method(http::Method::POST)
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("my.method"))
);
}
#[test]
fn test_router_response_custom_attribute() {
let mut spans = RouterSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: RouterSelector::ResponseHeader {
response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: None,
value: Arc::new(Default::default()),
},
);
spans.attributes.custom.insert(
OTEL_NAME.to_string(),
Conditional {
selector: RouterSelector::StaticField {
r#static: String::from("new_name").into(),
},
condition: None,
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_response(
&router::Response::fake_builder()
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
assert!(values.iter().any(|key_val| key_val.key
== opentelemetry::Key::from_static_str(OTEL_NAME)
&& key_val.value == opentelemetry::Value::String(String::from("new_name").into())));
}
#[test]
fn test_supergraph_request_custom_attribute() {
let mut spans = SupergraphSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: SupergraphSelector::RequestHeader {
request_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: None,
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_request(
&supergraph::Request::fake_builder()
.method(http::Method::POST)
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_supergraph_standard_attribute_aliased() {
let mut spans = SupergraphSpans::default();
spans.attributes.attributes.graphql_operation_type = Some(StandardAttribute::Aliased {
alias: String::from("my_op"),
});
let context = Context::new();
context.insert(OPERATION_KIND, "Query".to_string()).unwrap();
let values = spans.attributes.on_request(
&supergraph::Request::fake_builder()
.method(http::Method::POST)
.header("my-header", "test_val")
.query("Query { me { id } }")
.context(context)
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("my_op"))
);
}
#[test]
fn test_supergraph_response_event_custom_attribute() {
let mut spans = SupergraphSpans::default();
spans.attributes.custom.insert(
"otel.status_code".to_string(),
Conditional {
selector: SupergraphSelector::StaticField {
r#static: String::from("error").into(),
},
condition: Some(Arc::new(Mutex::new(Condition::Exists(
SupergraphSelector::ResponseErrors {
response_errors: JsonPathInst::from_str("$[0].extensions.code").unwrap(),
redact: None,
default: None,
},
)))),
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_response_event(
&graphql::Response::builder()
.error(
graphql::Error::builder()
.message("foo")
.extension_code("MY_EXTENSION_CODE")
.build(),
)
.build(),
&Context::new(),
);
assert!(values.iter().any(|key_val| key_val.key
== opentelemetry::Key::from_static_str("otel.status_code")
&& key_val.value == opentelemetry::Value::String(String::from("error").into())));
}
#[test]
fn test_supergraph_response_custom_attribute() {
let mut spans = SupergraphSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: SupergraphSelector::ResponseHeader {
response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: None,
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_response(
&supergraph::Response::fake_builder()
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_subgraph_request_custom_attribute() {
let mut spans = SubgraphSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: SubgraphSelector::SubgraphRequestHeader {
subgraph_request_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: None,
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_request(
&subgraph::Request::fake_builder()
.subgraph_request(
::http::Request::builder()
.uri("http://localhost/graphql")
.header("my-header", "test_val")
.body(
graphql::Request::fake_builder()
.query("query { __typename }")
.build(),
)
.unwrap(),
)
.build(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_subgraph_response_custom_attribute() {
let mut spans = SubgraphSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: SubgraphSelector::SubgraphResponseHeader {
subgraph_response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: None,
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_response(
&subgraph::Response::fake2_builder()
.header("my-header", "test_val")
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_subgraph_response_custom_attribute_good_condition() {
let mut spans = SubgraphSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: SubgraphSelector::SubgraphResponseHeader {
subgraph_response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::I64(200)),
SelectorOrValue::Selector(SubgraphSelector::SubgraphResponseStatus {
subgraph_response_status: ResponseStatus::Code,
}),
])))),
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_response(
&subgraph::Response::fake2_builder()
.header("my-header", "test_val")
.status_code(http::StatusCode::OK)
.build()
.unwrap(),
);
assert!(
values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
#[test]
fn test_subgraph_response_custom_attribute_bad_condition() {
let mut spans = SubgraphSpans::default();
spans.attributes.custom.insert(
"test".to_string(),
Conditional {
selector: SubgraphSelector::SubgraphResponseHeader {
subgraph_response_header: "my-header".to_string(),
redact: None,
default: None,
},
condition: Some(Arc::new(Mutex::new(Condition::Eq([
SelectorOrValue::Value(AttributeValue::I64(400)),
SelectorOrValue::Selector(SubgraphSelector::SubgraphResponseStatus {
subgraph_response_status: ResponseStatus::Code,
}),
])))),
value: Arc::new(Default::default()),
},
);
let values = spans.attributes.on_response(
&subgraph::Response::fake2_builder()
.header("my-header", "test_val")
.status_code(http::StatusCode::OK)
.build()
.unwrap(),
);
assert!(
!values
.iter()
.any(|key_val| key_val.key == opentelemetry::Key::from_static_str("test"))
);
}
}