1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Global tracing subscriber initialisation helpers.
//!
//! Why: Every trusty-* binary wants the same verbosity ladder and the same
//! `RUST_LOG` override semantics, and every daemon needs the same log-buffer
//! + stderr composition. Defining this once removes the boilerplate.
use crate::log_buffer;
/// Initialise the global tracing subscriber.
///
/// Why: Every trusty-* binary wants the same verbosity ladder and the same
/// `RUST_LOG` override semantics. Defining it once removes the boilerplate
/// from every `main.rs`.
/// What: `verbose_count` maps `0 → warn`, `1 → info`, `2 → debug`, `3+ →
/// trace`. If `RUST_LOG` is set in the environment it wins. Logs go to
/// stderr so stdout stays clean for MCP JSON-RPC.
/// Also installs the shared panic hook (`crate::panic_hook`) so panic
/// payloads land in this subscriber rather than only on raw stderr (#4764).
/// Test: side-effecting (global subscriber) — covered by integration with
/// `cargo run -- -v status` in downstream crates.
pub fn init_tracing(verbose_count: u8) {
// #4764: route panic payloads through this subscriber. A macOS `.ips`
// crash report carries mangled symbols but not the panic message, so a
// daemon abort is otherwise undiagnosable in production. Installing here
// rather than in each `main` binds the hook to the moment logging becomes
// available, and covers every trusty-* binary uniformly.
crate::panic_hook::install_panic_logger();
let default_filter = match verbose_count {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.with_target(false)
.try_init();
}
/// Initialise the global tracing subscriber and capture events into a
/// [`log_buffer::LogBuffer`] so the daemon can serve recent logs over HTTP.
///
/// Why: daemons expose `GET /logs/tail`, which needs an in-memory ring of
/// recent log lines. Routing capture through the subscriber means every
/// existing `tracing::info!` / `warn!` call site is mirrored automatically —
/// no second logging API to keep in sync. The stderr `fmt` layer is retained
/// so operators still see live logs in the terminal / launchd log file.
/// What: builds a `tracing_subscriber::registry` with two layers — the
/// standard stderr `fmt` layer (same verbosity ladder + `RUST_LOG` override
/// as [`init_tracing`]) and a [`log_buffer::LogBufferLayer`] feeding the
/// returned [`log_buffer::LogBuffer`]. Uses `try_init`, so a process that has
/// already installed a subscriber keeps it; the returned buffer is still
/// valid (just empty) in that case.
/// Also installs the shared panic hook (`crate::panic_hook`) so panic
/// payloads land in this subscriber rather than only on raw stderr (#4764).
/// Test: `cargo test -p trusty-common --features unconditional-only log_buffer`
/// covers the layer; the daemon `/logs/tail` integration tests cover the wired
/// path end-to-end.
#[must_use]
pub fn init_tracing_with_buffer(verbose_count: u8, capacity: usize) -> log_buffer::LogBuffer {
use tracing_subscriber::Layer as _;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
// #4764: route panic payloads through this subscriber. A macOS `.ips`
// crash report carries mangled symbols but not the panic message, so a
// daemon abort is otherwise undiagnosable in production. Installing here
// rather than in each `main` binds the hook to the moment logging becomes
// available, and covers every trusty-* binary uniformly.
crate::panic_hook::install_panic_logger();
let default_filter = match verbose_count {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let stderr_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
// The log-buffer layer must capture activity even when the stderr filter
// is set to `warn` (the default for `trusty-search start` without `-v`).
// `RUST_LOG_BUFFER` lets ops widen or narrow the buffer independently of
// stderr; the default of `info` matches the activity feed's intent.
let buffer_filter = tracing_subscriber::EnvFilter::try_from_env("RUST_LOG_BUFFER")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let buffer = log_buffer::LogBuffer::new(capacity);
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.with_filter(stderr_filter);
let buf_layer = log_buffer::LogBufferLayer::new(buffer.clone()).with_filter(buffer_filter);
let _ = tracing_subscriber::registry()
.with(fmt_layer)
.with(buf_layer)
.try_init();
buffer
}
/// Initialise the global tracing subscriber with a [`log_buffer::LogBuffer`]
/// **and** a [`crate::error_capture::BugCaptureLayer`] composed in one `try_init` call.
///
/// Why: `tracing_subscriber::registry().try_init()` can only succeed once per
/// process. Callers that need both the HTTP log-tail buffer (issue #35)
/// and Phase 1 bug capture must compose all three layers in a single call;
/// two separate `try_init` calls would leave the second one silently ignored.
/// This helper is the canonical entry-point for daemon binaries that want
/// both features wired together at startup.
/// What: builds an `EnvFilter`-gated stderr `fmt` layer, an info-level
/// `LogBufferLayer`, and a `BugCaptureLayer` for `app_name`/`crate_version`;
/// installs them together via `try_init`. Returns `(LogBuffer, ErrorStore)`
/// so the caller can stash both handles in the daemon's `AppState`.
/// All capture is to a JSONL file under `<dirs::data_dir()>/<app_name>/`
/// and an in-memory ring — nothing is written to stdout, so this is
/// MCP-safe. Honours `TRUSTY_NO_BUG_CAPTURE` for opt-out.
/// Also installs the shared panic hook (`crate::panic_hook`) so panic
/// payloads land in this subscriber rather than only on raw stderr (#4764).
/// Test: `cargo test -p trusty-common --features bug-capture -- init_tracing_with_capture`.
#[cfg(feature = "bug-capture")]
#[must_use]
pub fn init_tracing_with_buffer_and_capture(
verbose_count: u8,
capacity: usize,
app_name: &str,
crate_version: impl Into<String>,
) -> (log_buffer::LogBuffer, crate::error_capture::ErrorStore) {
use tracing_subscriber::Layer as _;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
// #4764: route panic payloads through this subscriber. A macOS `.ips`
// crash report carries mangled symbols but not the panic message, so a
// daemon abort is otherwise undiagnosable in production. Installing here
// rather than in each `main` binds the hook to the moment logging becomes
// available, and covers every trusty-* binary uniformly.
crate::panic_hook::install_panic_logger();
let default_filter = match verbose_count {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let stderr_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
let buffer_filter = tracing_subscriber::EnvFilter::try_from_env("RUST_LOG_BUFFER")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let buffer = log_buffer::LogBuffer::new(capacity);
let (capture_layer, store) = crate::error_capture::bug_capture_layer(
app_name,
crate::error_capture::DEFAULT_CAPTURE_CAPACITY,
crate_version,
);
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.with_filter(stderr_filter);
let buf_layer = log_buffer::LogBufferLayer::new(buffer.clone()).with_filter(buffer_filter);
let _ = tracing_subscriber::registry()
.with(fmt_layer)
.with(buf_layer)
.with(capture_layer)
.try_init();
(buffer, store)
}
/// Disable coloured terminal output when requested or when stdout is not a TTY.
///
/// Why: Pipe-friendly output is mandatory for scripting (`trusty-search list
/// | jq …`). `NO_COLOR` / `TERM=dumb` are the canonical signals; passing
/// `--no-color` should override too.
/// What: calls `colored::control::set_override(false)` when the caller asks
/// for it or when the standard heuristics indicate no colour.
/// Test: side-effecting global; trivially covered by manual `NO_COLOR=1 cargo
/// run -- list`.
pub fn maybe_disable_color(no_color: bool) {
let env_says_no =
std::env::var("NO_COLOR").is_ok() || std::env::var("TERM").as_deref() == Ok("dumb");
if no_color || env_says_no {
colored::control::set_override(false);
}
}