linera_service/tracing/
mod.rs1pub mod chrome;
8pub mod opentelemetry;
9
10use std::{
11 env,
12 fs::{File, OpenOptions},
13 path::Path,
14 sync::Arc,
15};
16
17use is_terminal::IsTerminal as _;
18use tracing::Subscriber;
19use tracing_subscriber::{
20 fmt::{
21 self,
22 format::{FmtSpan, Format, Full},
23 time::FormatTime,
24 FormatFields, MakeWriter,
25 },
26 layer::{Layer, SubscriberExt as _},
27 registry::LookupSpan,
28 util::SubscriberInitExt,
29 EnvFilter,
30};
31#[cfg(not(target_arch = "wasm32"))]
32use {
33 ::opentelemetry::trace::TraceContextExt as _, tracing_opentelemetry::OtelData,
34 tracing_subscriber::fmt::FormatEvent,
35};
36
37pub(crate) struct EnvConfig {
38 pub(crate) env_filter: EnvFilter,
39 span_events: FmtSpan,
40 format: Option<String>,
41 color_output: bool,
42 log_name: String,
43}
44
45impl EnvConfig {
46 pub(crate) fn stderr_layer<S>(&self) -> Box<dyn Layer<S> + Send + Sync>
47 where
48 S: Subscriber + for<'span> LookupSpan<'span>,
49 {
50 prepare_formatted_layer(
51 self.format.as_deref(),
52 fmt::layer()
53 .with_span_events(self.span_events.clone())
54 .with_writer(std::io::stderr)
55 .with_ansi(self.color_output),
56 )
57 }
58
59 pub(crate) fn maybe_log_file_layer<S>(&self) -> Option<Box<dyn Layer<S> + Send + Sync>>
60 where
61 S: Subscriber + for<'span> LookupSpan<'span>,
62 {
63 open_log_file(&self.log_name).map(|file_writer| {
64 prepare_formatted_layer(
65 self.format.as_deref(),
66 fmt::layer()
67 .with_span_events(self.span_events.clone())
68 .with_writer(Arc::new(file_writer))
69 .with_ansi(false),
70 )
71 })
72 }
73}
74
75pub fn init(log_name: &str) {
90 let config = get_env_config(log_name);
91 let maybe_log_file_layer = config.maybe_log_file_layer();
92 let stderr_layer = config.stderr_layer();
93
94 tracing_subscriber::registry()
95 .with(config.env_filter)
96 .with(maybe_log_file_layer)
97 .with(stderr_layer)
98 .init();
99
100 linera_base::panic_hook::init();
101}
102
103pub(crate) fn get_env_config(log_name: &str) -> EnvConfig {
104 let env_filter = EnvFilter::builder()
105 .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
106 .from_env_lossy();
107
108 let span_events = std::env::var("RUST_LOG_SPAN_EVENTS")
109 .ok()
110 .map_or(FmtSpan::NONE, |s| fmt_span_from_str(&s));
111
112 let format = std::env::var("RUST_LOG_FORMAT").ok();
113 let color_output =
114 !std::env::var("NO_COLOR").is_ok_and(|x| !x.is_empty()) && std::io::stderr().is_terminal();
115
116 EnvConfig {
117 env_filter,
118 span_events,
119 format,
120 color_output,
121 log_name: log_name.to_string(),
122 }
123}
124
125pub(crate) fn open_log_file(log_name: &str) -> Option<File> {
132 let log_directory = env::var_os("LINERA_LOG_DIR")?;
133 let mut log_file_path = Path::new(&log_directory).join(log_name);
134 log_file_path.set_extension("log");
135
136 Some(
137 OpenOptions::new()
138 .append(true)
139 .create(true)
140 .open(log_file_path)
141 .expect("Failed to open log file for writing"),
142 )
143}
144
145#[cfg(not(target_arch = "wasm32"))]
146struct WithTraceContext;
147
148#[cfg(not(target_arch = "wasm32"))]
149impl<S, N> FormatEvent<S, N> for WithTraceContext
150where
151 S: Subscriber + for<'span> LookupSpan<'span>,
152 N: for<'writer> FormatFields<'writer> + 'static,
153{
154 fn format_event(
155 &self,
156 ctx: &fmt::FmtContext<'_, S, N>,
157 mut writer: fmt::format::Writer<'_>,
158 event: &tracing::Event<'_>,
159 ) -> std::fmt::Result {
160 if let Some(scope) = ctx.event_scope() {
161 for span in scope {
162 let extensions = span.extensions();
163 if let Some(otel_data) = extensions.get::<OtelData>() {
164 let trace_id = otel_data
167 .builder
168 .trace_id
169 .unwrap_or_else(|| otel_data.parent_cx.span().span_context().trace_id());
170 if trace_id != ::opentelemetry::trace::TraceId::INVALID {
171 write!(writer, "traceID={trace_id} ")?;
172 }
173 if let Some(span_id) = otel_data.builder.span_id {
174 write!(writer, "spanID={span_id} ")?;
175 }
176 break;
177 }
178 }
179 }
180 Format::default().format_event(ctx, writer, event)
181 }
182}
183
184pub(crate) fn prepare_formatted_layer<S, N, W, T>(
188 formatting: Option<&str>,
189 layer: fmt::Layer<S, N, Format<Full, T>, W>,
190) -> Box<dyn Layer<S> + Send + Sync>
191where
192 S: Subscriber + for<'span> LookupSpan<'span>,
193 N: for<'writer> FormatFields<'writer> + Send + Sync + 'static,
194 W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
195 T: FormatTime + Send + Sync + 'static,
196{
197 match formatting.unwrap_or("plain") {
198 "json" => layer.json().boxed(),
199 "pretty" => layer.pretty().boxed(),
200 "plain" => {
201 #[cfg(not(target_arch = "wasm32"))]
202 {
203 layer.event_format(WithTraceContext).boxed()
204 }
205 #[cfg(target_arch = "wasm32")]
206 {
207 layer.boxed()
208 }
209 }
210 format => {
211 panic!("Invalid RUST_LOG_FORMAT: `{format}`. Valid values are `json` or `pretty`.")
212 }
213 }
214}
215
216pub(crate) fn fmt_span_from_str(events: &str) -> FmtSpan {
217 let mut fmt_span = FmtSpan::NONE;
218 for event in events.split(',') {
219 fmt_span |= match event {
220 "new" => FmtSpan::NEW,
221 "enter" => FmtSpan::ENTER,
222 "exit" => FmtSpan::EXIT,
223 "close" => FmtSpan::CLOSE,
224 "active" => FmtSpan::ACTIVE,
225 "full" => FmtSpan::FULL,
226 _ => FmtSpan::NONE,
227 };
228 }
229 fmt_span
230}