use apollo_compiler::executable::Field;
use apollo_compiler::executable::NamedType;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json_bytes::Value;
use sha2::Digest;
use tower::BoxError;
use crate::Context;
use crate::context::OPERATION_NAME;
use crate::plugins::telemetry::config::AttributeValue;
use crate::plugins::telemetry::config_new::Selector;
use crate::plugins::telemetry::config_new::Stage;
use crate::plugins::telemetry::config_new::instruments;
use crate::plugins::telemetry::config_new::instruments::InstrumentValue;
use crate::plugins::telemetry::config_new::instruments::StandardUnit;
use crate::plugins::telemetry::config_new::selectors::OperationName;
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub(crate) enum ListLength {
Value,
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub(crate) enum FieldName {
String,
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub(crate) enum FieldType {
Name,
Type,
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub(crate) enum TypeName {
String,
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, rename_all = "snake_case", untagged)]
pub(crate) enum GraphQLValue {
Unit(StandardUnit),
Custom(GraphQLSelector),
}
impl From<&GraphQLValue> for InstrumentValue<GraphQLSelector> {
fn from(value: &GraphQLValue) -> Self {
match value {
GraphQLValue::Unit(_) => InstrumentValue::Field(instruments::Field::Unit),
GraphQLValue::Custom(selector) => {
InstrumentValue::Field(instruments::Field::Custom(selector.clone()))
}
}
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, untagged)]
pub(crate) enum GraphQLSelector {
ListLength {
#[allow(dead_code)]
list_length: ListLength,
},
FieldName {
#[allow(dead_code)]
field_name: FieldName,
},
FieldType {
#[allow(dead_code)]
field_type: FieldType,
},
TypeName {
#[allow(dead_code)]
type_name: TypeName,
},
OperationName {
operation_name: OperationName,
default: Option<String>,
},
StaticField {
r#static: AttributeValue,
},
}
impl Selector for GraphQLSelector {
type Request = crate::services::supergraph::Request;
type Response = crate::services::supergraph::Response;
type EventResponse = crate::graphql::Response;
fn on_request(&self, _request: &Self::Request) -> Option<opentelemetry::Value> {
None
}
fn on_response(&self, _response: &Self::Response) -> Option<opentelemetry::Value> {
None
}
fn on_error(&self, _error: &BoxError, _ctx: &Context) -> Option<opentelemetry::Value> {
None
}
fn on_response_field(
&self,
ty: &NamedType,
field: &Field,
value: &Value,
ctx: &Context,
) -> Option<opentelemetry::Value> {
match self {
GraphQLSelector::ListLength { .. } => match value {
Value::Array(array) => Some((array.len() as i64).into()),
_ => None,
},
GraphQLSelector::FieldName { .. } => match value {
Value::Null => None,
_ => Some(name_to_otel_string(&field.name).into()),
},
GraphQLSelector::FieldType {
field_type: FieldType::Name,
} => match value {
Value::Null => None,
_ => Some(name_to_otel_string(field.definition.ty.inner_named_type()).into()),
},
GraphQLSelector::FieldType {
field_type: FieldType::Type,
} => match value {
Value::Null => None,
Value::Bool(_) | Value::Number(_) | Value::String(_) => Some("scalar".into()),
Value::Object(_) => Some("object".into()),
Value::Array(_) => Some("list".into()),
},
GraphQLSelector::TypeName { .. } => match value {
Value::Null => None,
_ => Some(name_to_otel_string(ty).into()),
},
GraphQLSelector::StaticField { r#static } => Some(r#static.clone().into()),
GraphQLSelector::OperationName {
operation_name,
default,
} => {
let op_name = ctx.get(OPERATION_NAME).ok().flatten();
match operation_name {
OperationName::String => op_name.or_else(|| default.clone()),
OperationName::Hash => op_name.or_else(|| default.clone()).map(|op_name| {
let mut hasher = sha2::Sha256::new();
hasher.update(op_name.as_bytes());
let result = hasher.finalize();
hex::encode(result)
}),
}
.map(opentelemetry::Value::from)
}
}
}
fn is_active(&self, stage: Stage) -> bool {
matches!(stage, Stage::ResponseField)
}
}
fn name_to_otel_string(name: &apollo_compiler::Name) -> opentelemetry::StringValue {
if let Some(static_str) = name.as_static_str() {
static_str.into()
} else {
name.to_cloned_arc()
.expect(
"expected `apollo_compiler::Name` to always contain \
either `&'static str` or `Arc<str>` but both conversions failed",
)
.into()
}
}
#[cfg(test)]
mod tests {
use opentelemetry::Value;
use serde_json_bytes::json;
use super::*;
use crate::plugins::telemetry::config_new::test::field;
use crate::plugins::telemetry::config_new::test::ty;
#[test]
fn array_length() {
let selector = GraphQLSelector::ListLength {
list_length: ListLength::Value,
};
let result = selector.on_response_field(
&ty(),
field(),
&json!(vec![true, true, true]),
&Context::default(),
);
assert_eq!(result, Some(Value::I64(3)));
}
#[test]
fn field_name() {
let selector = GraphQLSelector::FieldName {
field_name: FieldName::String,
};
let result = selector.on_response_field(&ty(), field(), &json!(true), &Context::default());
assert_eq!(result, Some(Value::String("field_name".into())));
}
#[test]
fn field_type() {
let selector = GraphQLSelector::FieldType {
field_type: FieldType::Name,
};
let result = selector.on_response_field(&ty(), field(), &json!(true), &Context::default());
assert_eq!(result, Some(Value::String("field_type".into())));
}
#[test]
fn field_type_scalar_type() {
assert_scalar(&ty(), field(), &json!("value"));
assert_scalar(&ty(), field(), &json!(1));
}
fn assert_scalar(ty: &NamedType, field: &Field, value: &serde_json_bytes::Value) {
let result = GraphQLSelector::FieldType {
field_type: FieldType::Type,
}
.on_response_field(ty, field, value, &Context::default());
assert_eq!(result, Some(Value::String("scalar".into())));
}
#[test]
fn field_type_object_type() {
let selector = GraphQLSelector::FieldType {
field_type: FieldType::Type,
};
let result = selector.on_response_field(&ty(), field(), &json!({}), &Context::default());
assert_eq!(result, Some(Value::String("object".into())));
}
#[test]
fn field_type_list_type() {
let selector = GraphQLSelector::FieldType {
field_type: FieldType::Type,
};
let result =
selector.on_response_field(&ty(), field(), &json!(vec![true]), &Context::default());
assert_eq!(result, Some(Value::String("list".into())));
}
#[test]
fn type_name() {
let selector = GraphQLSelector::TypeName {
type_name: TypeName::String,
};
let result =
selector.on_response_field(&ty(), field(), &json!("true"), &Context::default());
assert_eq!(result, Some(Value::String("type_name".into())));
}
#[test]
fn static_field() {
let selector = GraphQLSelector::StaticField {
r#static: "static_value".into(),
};
let result = selector.on_response_field(&ty(), field(), &json!(true), &Context::default());
assert_eq!(result, Some(Value::String("static_value".into())));
}
#[test]
fn operation_name() {
let selector = GraphQLSelector::OperationName {
operation_name: OperationName::String,
default: None,
};
let ctx = Context::default();
let _ = ctx.insert(OPERATION_NAME, "some-operation".to_string());
let result = selector.on_response_field(&ty(), field(), &json!(true), &ctx);
assert_eq!(result, Some(Value::String("some-operation".into())));
}
#[test]
fn operation_name_hash() {
let selector = GraphQLSelector::OperationName {
operation_name: OperationName::Hash,
default: None,
};
let ctx = Context::default();
let _ = ctx.insert(OPERATION_NAME, "some-operation".to_string());
let result = selector.on_response_field(&ty(), field(), &json!(true), &ctx);
assert_eq!(
result,
Some(Value::String(
"1d507f770a74cffd6cb014b190ea31160d442ff41d9bde088b634847aeafaafd".into()
))
);
}
#[test]
fn operation_name_defaulted() {
let selector = GraphQLSelector::OperationName {
operation_name: OperationName::String,
default: Some("no-operation".to_string()),
};
let result = selector.on_response_field(&ty(), field(), &json!(true), &Context::default());
assert_eq!(result, Some(Value::String("no-operation".into())));
}
}