Skip to main content

origin_telemetry/
lib.rs

1//! Logging and observability.
2//!
3//! Logging is a platform concern, not something every product configures from scratch.
4//!
5//! # Rules
6//!
7//! - Never log a secret. `Secret` redacts itself in `Debug`; do not undo that by
8//!   logging `secret.expose()`.
9//! - Never log personal data unfiltered — email addresses, repository contents,
10//!   analytics dimensions that identify a person.
11//! - Attach correlation fields ([`spans`]) instead of writing ids into the message.
12
13pub mod spans;
14
15use tracing_subscriber::EnvFilter;
16use tracing_subscriber::fmt::format::FmtSpan;
17
18/// Log output format.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum Format {
21    /// Human-readable. The default for development.
22    #[default]
23    Pretty,
24    /// One JSON object per line, for shipped builds and log files.
25    Json,
26}
27
28#[derive(Debug, Clone)]
29pub struct TelemetryConfig {
30    /// Fallback filter when `RUST_LOG` is unset, e.g. `"info,origin_sync=debug"`.
31    pub default_filter: String,
32    pub format: Format,
33    /// Log span open/close, which makes sync and job timings visible.
34    pub log_span_events: bool,
35
36    /// Write to stderr instead of stdout.
37    ///
38    /// Mandatory when the process speaks a protocol on stdout — an MCP server over
39    /// stdio, for instance. A single log line on stdout corrupts the stream, and the
40    /// client reports a parse error rather than anything that points at logging.
41    pub to_stderr: bool,
42}
43
44impl Default for TelemetryConfig {
45    fn default() -> Self {
46        Self {
47            default_filter: "info".to_owned(),
48            format: Format::default(),
49            log_span_events: false,
50            to_stderr: false,
51        }
52    }
53}
54
55impl TelemetryConfig {
56    /// Configuration for a process that speaks a protocol on stdout.
57    pub fn for_stdout_protocol() -> Self {
58        Self {
59            to_stderr: true,
60            ..Self::default()
61        }
62    }
63}
64
65/// Install the global tracing subscriber.
66///
67/// Returns `false` if a subscriber was already installed — which happens in test
68/// binaries and is harmless, so this never panics.
69pub fn init(config: TelemetryConfig) -> bool {
70    let filter = EnvFilter::try_from_default_env()
71        .unwrap_or_else(|_| EnvFilter::new(&config.default_filter));
72
73    let span_events = if config.log_span_events {
74        FmtSpan::NEW | FmtSpan::CLOSE
75    } else {
76        FmtSpan::NONE
77    };
78
79    macro_rules! install {
80        ($builder:expr) => {
81            match config.format {
82                Format::Pretty => $builder.try_init().is_ok(),
83                Format::Json => $builder.json().try_init().is_ok(),
84            }
85        };
86    }
87
88    let builder = tracing_subscriber::fmt()
89        .with_env_filter(filter)
90        .with_span_events(span_events)
91        .with_target(true);
92
93    if config.to_stderr {
94        install!(builder.with_writer(std::io::stderr))
95    } else {
96        install!(builder)
97    }
98}