apollo_router/plugins/telemetry/config_new/
logging.rs

1use std::collections::BTreeMap;
2use std::collections::HashSet;
3use std::io::IsTerminal;
4use std::time::Duration;
5
6use schemars::JsonSchema;
7use schemars::Schema;
8use schemars::SchemaGenerator;
9use serde::Deserialize;
10use serde::Deserializer;
11use serde::de::MapAccess;
12use serde::de::Visitor;
13
14use crate::plugins::telemetry::config::AttributeValue;
15use crate::plugins::telemetry::config::TraceIdFormat;
16use crate::plugins::telemetry::resource::ConfigResource;
17
18/// Logging configuration.
19#[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)]
20#[serde(deny_unknown_fields, default)]
21pub(crate) struct Logging {
22    /// Common configuration
23    pub(crate) common: LoggingCommon,
24    /// Settings for logging to stdout.
25    pub(crate) stdout: StdOut,
26    #[serde(skip)]
27    /// Settings for logging to a file.
28    pub(crate) file: File,
29}
30
31#[derive(Clone, Debug, Deserialize, JsonSchema, Default, PartialEq)]
32#[serde(deny_unknown_fields, default)]
33pub(crate) struct LoggingCommon {
34    /// Set a service.name resource in your metrics
35    pub(crate) service_name: Option<String>,
36    /// Set a service.namespace attribute in your metrics
37    pub(crate) service_namespace: Option<String>,
38    /// The Open Telemetry resource
39    pub(crate) resource: BTreeMap<String, AttributeValue>,
40}
41
42impl ConfigResource for LoggingCommon {
43    fn service_name(&self) -> &Option<String> {
44        &self.service_name
45    }
46
47    fn service_namespace(&self) -> &Option<String> {
48        &self.service_namespace
49    }
50
51    fn resource(&self) -> &BTreeMap<String, AttributeValue> {
52        &self.resource
53    }
54}
55
56#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)]
57#[serde(deny_unknown_fields, default)]
58pub(crate) struct StdOut {
59    /// Set to true to log to stdout.
60    pub(crate) enabled: bool,
61    /// The format to log to stdout.
62    pub(crate) format: Format,
63    /// The format to log to stdout when you're running on an interactive terminal. When configured it will automatically use this `tty_format`` instead of the original `format` when an interactive terminal is detected
64    pub(crate) tty_format: Option<Format>,
65    /// Log rate limiting. The limit is set per type of log message
66    pub(crate) rate_limit: RateLimit,
67}
68
69impl Default for StdOut {
70    fn default() -> Self {
71        StdOut {
72            enabled: true,
73            format: Format::default(),
74            tty_format: None,
75            rate_limit: RateLimit::default(),
76        }
77    }
78}
79
80#[derive(Deserialize, JsonSchema, Clone, Debug, PartialEq)]
81#[serde(deny_unknown_fields, default)]
82pub(crate) struct RateLimit {
83    /// Set to true to limit the rate of log messages
84    pub(crate) enabled: bool,
85    /// Number of log lines allowed in interval per message
86    pub(crate) capacity: u32,
87    /// Interval for rate limiting
88    #[serde(deserialize_with = "humantime_serde::deserialize")]
89    #[schemars(with = "String")]
90    pub(crate) interval: Duration,
91}
92
93impl Default for RateLimit {
94    fn default() -> Self {
95        RateLimit {
96            enabled: false,
97            capacity: 1,
98            interval: Duration::from_secs(1),
99        }
100    }
101}
102
103/// Log to a file
104#[allow(dead_code)]
105#[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)]
106#[serde(deny_unknown_fields, default)]
107pub(crate) struct File {
108    /// Set to true to log to a file.
109    pub(crate) enabled: bool,
110    /// The path pattern of the file to log to.
111    pub(crate) path: String,
112    /// The format of the log file.
113    pub(crate) format: Format,
114    /// The period to rollover the log file.
115    pub(crate) rollover: Rollover,
116    /// Log rate limiting. The limit is set per type of log message
117    pub(crate) rate_limit: Option<RateLimit>,
118}
119
120/// The format for logging.
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub(crate) enum Format {
123    // !!!!WARNING!!!!, if you change this enum then be sure to add the changes to the JsonSchema AND the custom deserializer.
124
125    // Want to see support for these formats? Please open an issue!
126    // /// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html
127    // Aws,
128    // /// https://github.com/trentm/node-bunyan
129    // Bunyan,
130    // /// https://go2docs.graylog.org/5-0/getting_in_log_data/ingest_gelf.html#:~:text=The%20Graylog%20Extended%20Log%20Format,UDP%2C%20TCP%2C%20or%20HTTP.
131    // Gelf,
132    //
133    // /// https://cloud.google.com/logging/docs/structured-logging
134    // Google,
135    // /// https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-appender-log
136    // OpenTelemetry,
137    /// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Json.html
138    Json(JsonFormat),
139
140    /// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Full.html
141    Text(TextFormat),
142}
143
144// This custom implementation JsonSchema allows the user to supply an enum or a struct in the same way that the custom deserializer does.
145impl JsonSchema for Format {
146    fn schema_name() -> std::borrow::Cow<'static, str> {
147        "logging_format".into()
148    }
149
150    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
151        let types = vec![
152            (
153                "json",
154                JsonFormat::json_schema(generator),
155                "Tracing subscriber https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Json.html",
156            ),
157            (
158                "text",
159                TextFormat::json_schema(generator),
160                "Tracing subscriber https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/format/struct.Full.html",
161            ),
162        ];
163
164        let schemas = types
165            .into_iter()
166            .flat_map(|(name, schema, description)| {
167                [
168                    schemars::json_schema!({
169                        "type": "object",
170                        "description": description,
171                        "properties": {
172                            name.to_string(): schema,
173                        },
174                        "required": [name],
175                        "additionalProperties": false,
176                    }),
177                    schemars::json_schema!({
178                        "type": "string",
179                        "description": description,
180                        "enum": [name], // TODO(@goto-bus-stop): why not "const"?
181                    }),
182                ]
183            })
184            .collect::<Vec<_>>();
185
186        schemars::json_schema!({
187            "oneOf": schemas,
188        })
189    }
190}
191
192impl<'de> Deserialize<'de> for Format {
193    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
194    where
195        D: Deserializer<'de>,
196    {
197        struct StringOrStruct;
198
199        impl<'de> Visitor<'de> for StringOrStruct {
200            type Value = Format;
201
202            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
203                formatter.write_str("string or enum")
204            }
205
206            fn visit_str<E>(self, value: &str) -> Result<Format, E>
207            where
208                E: serde::de::Error,
209            {
210                match value {
211                    "json" => Ok(Format::Json(JsonFormat::default())),
212                    "text" => Ok(Format::Text(TextFormat::default())),
213                    _ => Err(E::custom(format!("unknown log format: {value}"))),
214                }
215            }
216
217            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
218            where
219                A: MapAccess<'de>,
220            {
221                let key = map.next_key::<String>()?;
222
223                match key.as_deref() {
224                    Some("json") => Ok(Format::Json(map.next_value::<JsonFormat>()?)),
225                    Some("text") => Ok(Format::Text(map.next_value::<TextFormat>()?)),
226                    Some(value) => Err(serde::de::Error::custom(format!(
227                        "unknown log format: {value}"
228                    ))),
229                    _ => Err(serde::de::Error::custom("unknown log format")),
230                }
231            }
232        }
233
234        deserializer.deserialize_any(StringOrStruct)
235    }
236}
237
238impl Default for Format {
239    fn default() -> Self {
240        if std::io::stdout().is_terminal() {
241            Format::Text(TextFormat::default())
242        } else {
243            Format::Json(JsonFormat::default())
244        }
245    }
246}
247
248#[derive(Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
249#[serde(deny_unknown_fields, rename_all = "snake_case", default)]
250pub(crate) struct JsonFormat {
251    /// Include the timestamp with the log event. (default: true)
252    pub(crate) display_timestamp: bool,
253    /// Include the target with the log event. (default: true)
254    pub(crate) display_target: bool,
255    /// Include the level with the log event. (default: true)
256    pub(crate) display_level: bool,
257    /// Include the thread_id with the log event.
258    pub(crate) display_thread_id: bool,
259    /// Include the thread_name with the log event.
260    pub(crate) display_thread_name: bool,
261    /// Include the filename with the log event.
262    pub(crate) display_filename: bool,
263    /// Include the line number with the log event.
264    pub(crate) display_line_number: bool,
265    /// Include the current span in this log event.
266    pub(crate) display_current_span: bool,
267    /// Include all of the containing span information with the log event. (default: true)
268    pub(crate) display_span_list: bool,
269    /// Include the resource with the log event. (default: true)
270    pub(crate) display_resource: bool,
271    /// Include the trace id (if any) with the log event. (default: true)
272    pub(crate) display_trace_id: DisplayTraceIdFormat,
273    /// Include the span id (if any) with the log event. (default: true)
274    pub(crate) display_span_id: bool,
275    /// List of span attributes to attach to the json log object
276    pub(crate) span_attributes: HashSet<String>,
277}
278
279#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Eq)]
280#[serde(deny_unknown_fields, rename_all = "snake_case", untagged)]
281pub(crate) enum DisplayTraceIdFormat {
282    // /// Format the Trace ID as a hexadecimal number
283    // ///
284    // /// (e.g. Trace ID 16 -> 00000000000000000000000000000010)
285    // #[default]
286    // Hexadecimal,
287    // /// Format the Trace ID as a hexadecimal number
288    // ///
289    // /// (e.g. Trace ID 16 -> 00000000000000000000000000000010)
290    // OpenTelemetry,
291    // /// Format the Trace ID as a decimal number
292    // ///
293    // /// (e.g. Trace ID 16 -> 16)
294    // Decimal,
295
296    // /// Datadog
297    // Datadog,
298
299    // /// UUID format with dashes
300    // /// (eg. 67e55044-10b1-426f-9247-bb680e5fe0c8)
301    // Uuid,
302    TraceIdFormat(TraceIdFormat),
303    Bool(bool),
304}
305
306impl Default for DisplayTraceIdFormat {
307    fn default() -> Self {
308        Self::TraceIdFormat(TraceIdFormat::default())
309    }
310}
311
312impl Default for JsonFormat {
313    fn default() -> Self {
314        JsonFormat {
315            display_timestamp: true,
316            display_target: true,
317            display_level: true,
318            display_thread_id: false,
319            display_thread_name: false,
320            display_filename: false,
321            display_line_number: false,
322            display_current_span: false,
323            display_span_list: true,
324            display_resource: true,
325            display_trace_id: DisplayTraceIdFormat::Bool(true),
326            display_span_id: true,
327            span_attributes: HashSet::new(),
328        }
329    }
330}
331
332#[derive(Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
333#[serde(deny_unknown_fields, rename_all = "snake_case", default)]
334pub(crate) struct TextFormat {
335    /// Process ansi escapes (default: true)
336    pub(crate) ansi_escape_codes: bool,
337    /// Include the timestamp with the log event. (default: true)
338    pub(crate) display_timestamp: bool,
339    /// Include the target with the log event.
340    pub(crate) display_target: bool,
341    /// Include the level with the log event. (default: true)
342    pub(crate) display_level: bool,
343    /// Include the thread_id with the log event.
344    pub(crate) display_thread_id: bool,
345    /// Include the thread_name with the log event.
346    pub(crate) display_thread_name: bool,
347    /// Include the filename with the log event.
348    pub(crate) display_filename: bool,
349    /// Include the line number with the log event.
350    pub(crate) display_line_number: bool,
351    /// Include the service namespace with the log event.
352    pub(crate) display_service_namespace: bool,
353    /// Include the service name with the log event.
354    pub(crate) display_service_name: bool,
355    /// Include the resource with the log event.
356    pub(crate) display_resource: bool,
357    /// Include the current span in this log event. (default: true)
358    pub(crate) display_current_span: bool,
359    /// Include all of the containing span information with the log event. (default: true)
360    pub(crate) display_span_list: bool,
361    /// Include the trace id (if any) with the log event. (default: false)
362    pub(crate) display_trace_id: DisplayTraceIdFormat,
363    /// Include the span id (if any) with the log event. (default: false)
364    pub(crate) display_span_id: bool,
365}
366
367impl Default for TextFormat {
368    fn default() -> Self {
369        TextFormat {
370            ansi_escape_codes: true,
371            display_timestamp: true,
372            display_target: false,
373            display_level: true,
374            display_thread_id: false,
375            display_thread_name: false,
376            display_filename: false,
377            display_line_number: false,
378            display_service_namespace: false,
379            display_service_name: false,
380            display_resource: false,
381            display_current_span: true,
382            display_span_list: true,
383            display_trace_id: DisplayTraceIdFormat::Bool(false),
384            display_span_id: false,
385        }
386    }
387}
388
389/// The period to rollover the log file.
390#[allow(dead_code)]
391#[derive(Deserialize, JsonSchema, Clone, Default, Debug, PartialEq)]
392#[serde(deny_unknown_fields, rename_all = "snake_case")]
393pub(crate) enum Rollover {
394    /// Roll over every hour.
395    Hourly,
396    /// Roll over every day.
397    Daily,
398    #[default]
399    /// Never roll over.
400    Never,
401}
402
403#[cfg(test)]
404mod test {
405    use serde_json::json;
406
407    use crate::plugins::telemetry::config_new::logging::Format;
408
409    #[test]
410    fn format_de() {
411        let format = serde_json::from_value::<Format>(json!("text")).unwrap();
412        assert_eq!(format, Format::Text(Default::default()));
413        let format = serde_json::from_value::<Format>(json!("json")).unwrap();
414        assert_eq!(format, Format::Json(Default::default()));
415        let format = serde_json::from_value::<Format>(json!({"text":{}})).unwrap();
416        assert_eq!(format, Format::Text(Default::default()));
417        let format = serde_json::from_value::<Format>(json!({"json":{}})).unwrap();
418        assert_eq!(format, Format::Json(Default::default()));
419    }
420}