use std::collections::BTreeMap;
use std::collections::HashSet;
use std::io::IsTerminal;
use std::time::Duration;
use schemars::JsonSchema;
use schemars::gen::SchemaGenerator;
use schemars::schema::InstanceType;
use schemars::schema::Metadata;
use schemars::schema::ObjectValidation;
use schemars::schema::Schema;
use schemars::schema::SchemaObject;
use schemars::schema::SingleOrVec;
use schemars::schema::SubschemaValidation;
use serde::Deserialize;
use serde::Deserializer;
use serde::de::MapAccess;
use serde::de::Visitor;
use crate::configuration::ConfigurationError;
use crate::plugins::telemetry::config::AttributeValue;
use crate::plugins::telemetry::config::TraceIdFormat;
use crate::plugins::telemetry::config_new::experimental_when_header::HeaderLoggingCondition;
use crate::plugins::telemetry::resource::ConfigResource;
use crate::services::SupergraphRequest;
#[derive(Deserialize, JsonSchema, Clone, Default, Debug)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct Logging {
pub(crate) common: LoggingCommon,
pub(crate) stdout: StdOut,
#[serde(skip)]
pub(crate) file: File,
#[serde(rename = "experimental_when_header")]
pub(crate) when_header: Vec<HeaderLoggingCondition>,
}
impl Logging {
pub(crate) fn validate(&self) -> Result<(), ConfigurationError> {
let misconfiguration = self.when_header.iter().any(|cfg| match cfg {
HeaderLoggingCondition::Matching { headers, body, .. }
| HeaderLoggingCondition::Value { headers, body, .. } => !body && !headers,
});
if misconfiguration {
Err(ConfigurationError::InvalidConfiguration {
message: "'experimental_when_header' configuration for logging is invalid",
error: String::from(
"body and headers must not be both false because it doesn't enable any logs",
),
})
} else {
Ok(())
}
}
pub(crate) fn should_log(&self, req: &SupergraphRequest) -> (bool, bool) {
self.when_header
.iter()
.fold((false, false), |(log_headers, log_body), current| {
let (current_log_headers, current_log_body) = current.should_log(req);
(
log_headers || current_log_headers,
log_body || current_log_body,
)
})
}
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Default)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct LoggingCommon {
pub(crate) service_name: Option<String>,
pub(crate) service_namespace: Option<String>,
pub(crate) resource: BTreeMap<String, AttributeValue>,
}
impl ConfigResource for LoggingCommon {
fn service_name(&self) -> &Option<String> {
&self.service_name
}
fn service_namespace(&self) -> &Option<String> {
&self.service_namespace
}
fn resource(&self) -> &BTreeMap<String, AttributeValue> {
&self.resource
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct StdOut {
pub(crate) enabled: bool,
pub(crate) format: Format,
pub(crate) tty_format: Option<Format>,
pub(crate) rate_limit: RateLimit,
}
impl Default for StdOut {
fn default() -> Self {
StdOut {
enabled: true,
format: Format::default(),
tty_format: None,
rate_limit: RateLimit::default(),
}
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct RateLimit {
pub(crate) enabled: bool,
pub(crate) capacity: u32,
#[serde(deserialize_with = "humantime_serde::deserialize")]
#[schemars(with = "String")]
pub(crate) interval: Duration,
}
impl Default for RateLimit {
fn default() -> Self {
RateLimit {
enabled: false,
capacity: 1,
interval: Duration::from_secs(1),
}
}
}
#[allow(dead_code)]
#[derive(Deserialize, JsonSchema, Clone, Default, Debug)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct File {
pub(crate) enabled: bool,
pub(crate) path: String,
pub(crate) format: Format,
pub(crate) rollover: Rollover,
pub(crate) rate_limit: Option<RateLimit>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum Format {
Json(JsonFormat),
Text(TextFormat),
}
impl JsonSchema for Format {
fn schema_name() -> String {
"logging_format".to_string()
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let types = vec![
(
"json",
JsonFormat::json_schema(gen),
"Tracing subscriber https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Json.html",
),
(
"text",
TextFormat::json_schema(gen),
"Tracing subscriber https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Full.html",
),
];
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(
types
.into_iter()
.map(|(name, schema, description)| {
(
name,
ObjectValidation {
required: [name.to_string()].into(),
properties: [(name.to_string(), schema)].into(),
additional_properties: Some(Box::new(Schema::Bool(false))),
..Default::default()
},
description,
)
})
.flat_map(|(name, o, dec)| {
vec![
SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(dec.to_string()),
..Default::default()
})),
instance_type: Some(SingleOrVec::Single(Box::new(
InstanceType::Object,
))),
object: Some(Box::new(o)),
..Default::default()
},
SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(dec.to_string()),
..Default::default()
})),
instance_type: Some(SingleOrVec::Single(Box::new(
InstanceType::String,
))),
enum_values: Some(vec![serde_json::Value::String(
name.to_string(),
)]),
..Default::default()
},
]
})
.map(Schema::Object)
.collect::<Vec<_>>(),
),
..Default::default()
})),
..Default::default()
})
}
}
impl<'de> Deserialize<'de> for Format {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrStruct;
impl<'de> Visitor<'de> for StringOrStruct {
type Value = Format;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or enum")
}
fn visit_str<E>(self, value: &str) -> Result<Format, E>
where
E: serde::de::Error,
{
match value {
"json" => Ok(Format::Json(JsonFormat::default())),
"text" => Ok(Format::Text(TextFormat::default())),
_ => Err(E::custom(format!("unknown log format: {}", value))),
}
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let key = map.next_key::<String>()?;
match key.as_deref() {
Some("json") => Ok(Format::Json(map.next_value::<JsonFormat>()?)),
Some("text") => Ok(Format::Text(map.next_value::<TextFormat>()?)),
Some(value) => Err(serde::de::Error::custom(format!(
"unknown log format: {}",
value
))),
_ => Err(serde::de::Error::custom("unknown log format")),
}
}
}
deserializer.deserialize_any(StringOrStruct)
}
}
impl Default for Format {
fn default() -> Self {
if std::io::stdout().is_terminal() {
Format::Text(TextFormat::default())
} else {
Format::Json(JsonFormat::default())
}
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "snake_case", default)]
pub(crate) struct JsonFormat {
pub(crate) display_timestamp: bool,
pub(crate) display_target: bool,
pub(crate) display_level: bool,
pub(crate) display_thread_id: bool,
pub(crate) display_thread_name: bool,
pub(crate) display_filename: bool,
pub(crate) display_line_number: bool,
pub(crate) display_current_span: bool,
pub(crate) display_span_list: bool,
pub(crate) display_resource: bool,
pub(crate) display_trace_id: DisplayTraceIdFormat,
pub(crate) display_span_id: bool,
pub(crate) span_attributes: HashSet<String>,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields, rename_all = "snake_case", untagged)]
pub(crate) enum DisplayTraceIdFormat {
TraceIdFormat(TraceIdFormat),
Bool(bool),
}
impl Default for DisplayTraceIdFormat {
fn default() -> Self {
Self::TraceIdFormat(TraceIdFormat::default())
}
}
impl Default for JsonFormat {
fn default() -> Self {
JsonFormat {
display_timestamp: true,
display_target: true,
display_level: true,
display_thread_id: false,
display_thread_name: false,
display_filename: false,
display_line_number: false,
display_current_span: false,
display_span_list: true,
display_resource: true,
display_trace_id: DisplayTraceIdFormat::Bool(true),
display_span_id: true,
span_attributes: HashSet::new(),
}
}
}
#[derive(Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "snake_case", default)]
pub(crate) struct TextFormat {
pub(crate) ansi_escape_codes: bool,
pub(crate) display_timestamp: bool,
pub(crate) display_target: bool,
pub(crate) display_level: bool,
pub(crate) display_thread_id: bool,
pub(crate) display_thread_name: bool,
pub(crate) display_filename: bool,
pub(crate) display_line_number: bool,
pub(crate) display_service_namespace: bool,
pub(crate) display_service_name: bool,
pub(crate) display_resource: bool,
pub(crate) display_current_span: bool,
pub(crate) display_span_list: bool,
pub(crate) display_trace_id: DisplayTraceIdFormat,
pub(crate) display_span_id: bool,
}
impl Default for TextFormat {
fn default() -> Self {
TextFormat {
ansi_escape_codes: true,
display_timestamp: true,
display_target: false,
display_level: true,
display_thread_id: false,
display_thread_name: false,
display_filename: false,
display_line_number: false,
display_service_namespace: false,
display_service_name: false,
display_resource: false,
display_current_span: true,
display_span_list: true,
display_trace_id: DisplayTraceIdFormat::Bool(false),
display_span_id: false,
}
}
}
#[allow(dead_code)]
#[derive(Deserialize, JsonSchema, Clone, Default, Debug)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub(crate) enum Rollover {
Hourly,
Daily,
#[default]
Never,
}
#[cfg(test)]
mod test {
use regex::Regex;
use serde_json::json;
use crate::plugins::telemetry::config_new::experimental_when_header::HeaderLoggingCondition;
use crate::plugins::telemetry::config_new::logging::Format;
use crate::plugins::telemetry::config_new::logging::Logging;
use crate::services::SupergraphRequest;
#[test]
fn format_de() {
let format = serde_json::from_value::<Format>(json!("text")).unwrap();
assert_eq!(format, Format::Text(Default::default()));
let format = serde_json::from_value::<Format>(json!("json")).unwrap();
assert_eq!(format, Format::Json(Default::default()));
let format = serde_json::from_value::<Format>(json!({"text":{}})).unwrap();
assert_eq!(format, Format::Text(Default::default()));
let format = serde_json::from_value::<Format>(json!({"json":{}})).unwrap();
assert_eq!(format, Format::Json(Default::default()));
}
#[test]
fn test_logging_conf_validation() {
let logging_conf = Logging {
when_header: vec![HeaderLoggingCondition::Value {
name: "test".to_string(),
value: String::new(),
headers: true,
body: false,
}],
..Default::default()
};
logging_conf.validate().unwrap();
let logging_conf = Logging {
when_header: vec![HeaderLoggingCondition::Value {
name: "test".to_string(),
value: String::new(),
headers: false,
body: false,
}],
..Default::default()
};
let validate_res = logging_conf.validate();
assert!(validate_res.is_err());
assert_eq!(
validate_res.unwrap_err().to_string(),
"'experimental_when_header' configuration for logging is invalid: body and headers must not be both false because it doesn't enable any logs"
);
}
#[test]
fn test_logging_conf_should_log() {
let logging_conf = Logging {
when_header: vec![HeaderLoggingCondition::Matching {
name: "test".to_string(),
matching: Regex::new("^foo*").unwrap(),
headers: true,
body: false,
}],
..Default::default()
};
let req = SupergraphRequest::fake_builder()
.header("test", "foobar")
.build()
.unwrap();
assert_eq!(logging_conf.should_log(&req), (true, false));
let logging_conf = Logging {
when_header: vec![HeaderLoggingCondition::Value {
name: "test".to_string(),
value: String::from("foobar"),
headers: true,
body: false,
}],
..Default::default()
};
assert_eq!(logging_conf.should_log(&req), (true, false));
let logging_conf = Logging {
when_header: vec![
HeaderLoggingCondition::Matching {
name: "test".to_string(),
matching: Regex::new("^foo*").unwrap(),
headers: true,
body: false,
},
HeaderLoggingCondition::Matching {
name: "test".to_string(),
matching: Regex::new("^*bar$").unwrap(),
headers: false,
body: true,
},
],
..Default::default()
};
assert_eq!(logging_conf.should_log(&req), (true, true));
let logging_conf = Logging {
when_header: vec![HeaderLoggingCondition::Matching {
name: "testtest".to_string(),
matching: Regex::new("^foo*").unwrap(),
headers: true,
body: false,
}],
..Default::default()
};
assert_eq!(logging_conf.should_log(&req), (false, false));
}
}