Skip to main content

osproxy_server/
log.rs

1//! Structured per-request logging.
2//!
3//! Each handled request can emit one **structured JSON log line**, the same
4//! shape-only `/debug/explain` document, which already carries the request's
5//! `trace_id` (`docs/05`). So logs correlate with the distributed trace and the
6//! OTLP spans by `trace_id`, and an aggregator can join them. The document is
7//! shape-only by construction, so the log line can never carry a tenant value.
8//!
9//! Logging is **opt-in**: the default [`NoLog`] reports [`RequestLog::enabled`]
10//! `false`, so the handler skips even fetching the document, "off" is near-zero
11//! cost.
12
13use osproxy_observe::DiagnosticSink;
14use serde_json::Value;
15
16/// Receives one structured record per handled request.
17///
18/// Implementations MUST NOT panic. `emit` is called inline after the response is
19/// produced, so it must be cheap (a line write); heavy delivery belongs behind a
20/// background sink.
21pub trait RequestLog: Send + Sync {
22    /// Whether this logger will emit. The handler checks this before assembling
23    /// the record, so a disabled logger costs only this call.
24    fn enabled(&self) -> bool {
25        true
26    }
27
28    /// Emits one request record (the shape-only explain document).
29    fn emit(&self, record: &Value);
30}
31
32/// The default logger: disabled, so no record is assembled or written.
33#[derive(Clone, Copy, Debug, Default)]
34pub struct NoLog;
35
36impl RequestLog for NoLog {
37    fn enabled(&self) -> bool {
38        false
39    }
40    fn emit(&self, _record: &Value) {}
41}
42
43/// Writes each record as one compact JSON line to stdout, the conventional
44/// structured-logging sink for a containerized service (the platform's log
45/// collector scrapes stdout).
46#[derive(Clone, Copy, Debug, Default)]
47pub struct StdoutJsonLog;
48
49impl RequestLog for StdoutJsonLog {
50    fn emit(&self, record: &Value) {
51        // `Value`'s Display is compact JSON: exactly one line per request.
52        println!("{record}");
53    }
54}
55
56/// A [`DiagnosticSink`] that writes each directive-selected capture as one tagged
57/// JSON line to stdout, the fleet-coherent counterpart of the local break-glass
58/// ring. The platform's log collector scrapes it, so an aggregator can serve the
59/// capture by the `trace_id` the explain doc carries, on any instance. Tagged
60/// `"kind":"diagnostic_capture"` so it is distinguishable from a request log line.
61#[derive(Clone, Copy, Debug, Default)]
62pub struct StdoutDiagnosticSink;
63
64impl DiagnosticSink for StdoutDiagnosticSink {
65    fn emit(&self, doc: Value) {
66        println!(
67            "{}",
68            serde_json::json!({ "kind": "diagnostic_capture", "capture": doc })
69        );
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn the_default_logger_is_disabled() {
79        assert!(!NoLog.enabled());
80        NoLog.emit(&serde_json::json!({})); // no panic, no output
81    }
82
83    #[test]
84    fn the_stdout_diagnostic_sink_is_enabled_and_does_not_panic() {
85        assert!(StdoutDiagnosticSink.enabled());
86        StdoutDiagnosticSink.emit(serde_json::json!({"trace_id": "abc"}));
87    }
88}