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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
use std::fmt::{self, Write};
use tracing::{Event, Subscriber};
use tracing_subscriber::{
fmt::{
format::{FmtSpan, FormatEvent},
FmtContext,
},
EnvFilter,
};
/// Supported log output formats used with `tracing_subscriber`.
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum LogFormat {
/// Human-readable, single-line logs (default)
#[default]
Full,
/// Compact variant optimized for short line lengths
Compact,
/// Multi-line logs optimized for human readability
Pretty,
/// Newline-delimited JSON logs
Json,
/// Minimal format - only the message
Minimal,
}
/// Logger configuration used to initialize the global subscriber.
#[derive(Debug, Clone)]
pub struct LoggerConfig {
/// Desired log output format.
pub format: LogFormat,
/// Env filter string (e.g. `info`, `debug,my_crate=trace`).
pub env_filter: String,
/// Whether to enable ANSI colors in log output.
pub ansi: bool,
/// Whether to log span events (entry/exit from #[instrument]).
/// Set to false to disable span logging for local development.
pub log_spans: bool,
/// Whether to show span context (the full span path) before log messages.
/// When false, only the log message is shown without the span hierarchy.
/// Can be controlled via SHOW_SPAN_CONTEXT environment variable (default: true).
pub show_span_context: bool,
}
impl Default for LoggerConfig {
fn default() -> Self {
// Default: disable span logging (can be enabled by setting ENABLE_SPAN_LOGS=true)
let log_spans = std::env::var("ENABLE_SPAN_LOGS")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
// Default: show span context (can be disabled by setting SHOW_SPAN_CONTEXT=false)
let show_span_context = std::env::var("SHOW_SPAN_CONTEXT").map(|v| v != "false").unwrap_or(true);
let ansi = std::env::var("CHAIN_ID").map(|v| v == "31337").unwrap_or(false);
Self {
format: LogFormat::Full,
env_filter: "info".to_string(),
ansi: false,
log_spans,
show_span_context,
}
}
}
impl LoggerConfig {
/// Create a new logger configuration with the given `format`.
pub fn new(format: LogFormat) -> Self {
Self {
format,
..Default::default()
}
}
/// Set the `env_filter` used to configure log verbosity.
pub fn with_env_filter(mut self, filter: String) -> Self {
self.env_filter = filter;
self
}
/// Enable or disable ANSI colors in log output.
pub fn with_ansi(mut self, ansi: bool) -> Self {
self.ansi = ansi;
self
}
/// Enable or disable span event logging (entry/exit from #[instrument]).
pub fn with_spans(mut self, log_spans: bool) -> Self {
self.log_spans = log_spans;
self
}
/// Enable or disable span context display (the span path before log messages).
pub fn with_span_context(mut self, show_span_context: bool) -> Self {
self.show_span_context = show_span_context;
self
}
}
impl std::fmt::Display for LogFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogFormat::Full => write!(f, "full"),
LogFormat::Compact => write!(f, "compact"),
LogFormat::Pretty => write!(f, "pretty"),
LogFormat::Json => write!(f, "json"),
LogFormat::Minimal => write!(f, "minimal"),
}
}
}
impl std::str::FromStr for LogFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"full" => Ok(LogFormat::Full),
"compact" => Ok(LogFormat::Compact),
"pretty" => Ok(LogFormat::Pretty),
"json" => Ok(LogFormat::Json),
_ => Err(format!(
"Invalid log format: '{}'. Valid options: full, compact, pretty, json",
s
)),
}
}
}
/// Check if we're running in a local development environment
fn is_local_environment() -> bool {
// Check if we're using local Anvil chain (chain_id 31337)
std::env::var("CHAIN_ID")
.unwrap_or_else(|_| "31337".to_string())
.parse::<u64>()
.unwrap_or(31337)
== 31337
}
/// Custom formatter that omits span context (the span path before messages)
struct NoSpanContextFormatter;
impl<S, N> FormatEvent<S, N> for NoSpanContextFormatter
where
S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
N: for<'writer> tracing_subscriber::fmt::FormatFields<'writer> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, S, N>,
mut writer: tracing_subscriber::fmt::format::Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
use tracing_subscriber::fmt::time::{FormatTime, SystemTime};
let metadata = event.metadata();
let time_format = SystemTime;
// Write timestamp
time_format.format_time(&mut writer)?;
write!(&mut writer, " ")?;
// Write level
write!(&mut writer, "{:5} ", metadata.level())?;
// Write target (module path) if available
if let Some(module_path) = metadata.module_path() {
write!(&mut writer, "{}: ", module_path)?;
}
// Format fields (the actual message and fields) - this is where the span context would normally appear
// By not including span context here, we hide the span path
// Note: format_fields consumes the writer, but we can write the newline by creating a new writer
// that wraps the same underlying buffer
let mut field_writer = writer.by_ref();
ctx.field_format().format_fields(field_writer, event)?;
// Add newline after the message
if is_local_environment() {
writeln!(&mut writer)?;
}
Ok(())
}
}
/// Initialize logger with LoggerConfig
pub fn init_logger(config: LoggerConfig) {
// try to get from env first, fallback to config default
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.env_filter));
// Configure span events based on log_spans setting
let span_events = if config.log_spans { FmtSpan::FULL } else { FmtSpan::NONE };
let _ = match config.format {
LogFormat::Full => {
let builder = tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_ansi(config.ansi)
.with_span_events(span_events);
// When span context is disabled, use custom formatter that omits span path
if !config.show_span_context {
builder.event_format(NoSpanContextFormatter).try_init()
} else {
builder.try_init()
}
}
LogFormat::Compact => {
let builder = tracing_subscriber::fmt()
.compact()
.with_env_filter(env_filter)
.with_ansi(config.ansi)
.with_span_events(span_events);
if !config.show_span_context {
builder
.event_format(
tracing_subscriber::fmt::format()
.with_target(true)
.with_level(true)
.compact(),
)
.try_init()
} else {
builder.try_init()
}
}
LogFormat::Pretty => {
let builder = tracing_subscriber::fmt()
.pretty()
.with_env_filter(env_filter)
.with_ansi(config.ansi)
.with_span_events(span_events);
if !config.show_span_context {
builder.event_format(NoSpanContextFormatter).try_init()
} else {
builder.try_init()
}
}
LogFormat::Json => tracing_subscriber::fmt()
.json()
.with_env_filter(env_filter)
.with_ansi(config.ansi)
.with_span_events(span_events)
.with_current_span(true) // Include current span name for Datadog log correlation
.with_span_list(true) // Include full span hierarchy for trace correlation
.flatten_event(true) // Flatten event fields for easier Datadog parsing
.try_init(),
LogFormat::Minimal => {
let format = tracing_subscriber::fmt::format()
.with_target(false)
.with_level(false)
.without_time()
.compact();
tracing_subscriber::fmt()
.with_target(false)
.with_level(false)
.without_time()
.with_file(false)
.with_line_number(false)
.event_format(format)
.with_env_filter(env_filter)
.with_ansi(config.ansi)
.with_span_events(span_events)
.try_init()
}
};
}