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}