77//! fields for human readability and machine parseability.
88
99use std:: env;
10- use std:: sync:: OnceLock ;
10+ use std:: sync:: { Once , OnceLock } ;
1111
1212use opentelemetry_sdk:: metrics:: SdkMeterProvider ;
1313use opentelemetry_sdk:: trace:: SdkTracerProvider ;
1414
1515static TRACER_PROVIDER : OnceLock < SdkTracerProvider > = OnceLock :: new ( ) ;
1616static METER_PROVIDER : OnceLock < SdkMeterProvider > = OnceLock :: new ( ) ;
17+ static TEST_LOGGING_INIT : Once = Once :: new ( ) ;
18+ const DEFAULT_ENV_FILTER_DIRECTIVES : & str = "sqlpage=info,actix_web=info,tracing_actix_web=info" ;
1719
1820/// Initializes logging / tracing. Returns `true` if `OTel` was activated.
1921#[ must_use]
2022pub fn init_telemetry ( ) -> bool {
23+ init_telemetry_with_log_layer ( logfmt:: LogfmtLayer :: new ( ) )
24+ }
25+
26+ fn init_telemetry_with_log_layer ( logfmt_layer : logfmt:: LogfmtLayer ) -> bool {
2127 let otel_endpoint = env:: var ( "OTEL_EXPORTER_OTLP_ENDPOINT" ) . ok ( ) ;
2228 let otel_active = otel_endpoint. as_deref ( ) . is_some_and ( |v| !v. is_empty ( ) ) ;
2329
2430 if otel_active {
25- init_otel_tracing ( ) ;
31+ init_otel_tracing ( logfmt_layer ) ;
2632 } else {
27- init_tracing ( ) ;
33+ init_tracing ( logfmt_layer ) ;
2834 }
2935
3036 otel_active
3137}
3238
39+ /// Initializes logging once for tests using the same formatter as production.
40+ ///
41+ /// Unlike `init_telemetry`, this does not initialize OTEL exporters and does
42+ /// not panic on invalid `LOG_LEVEL` / `RUST_LOG` values.
43+ pub fn init_test_logging ( ) {
44+ TEST_LOGGING_INIT . call_once ( || {
45+ init_test_tracing ( ) ;
46+ } ) ;
47+ }
48+
3349/// Shuts down the `OTel` tracer provider, flushing pending spans.
3450pub fn shutdown_telemetry ( ) {
3551 if let Some ( provider) = TRACER_PROVIDER . get ( ) {
@@ -45,17 +61,27 @@ pub fn shutdown_telemetry() {
4561}
4662
4763/// Tracing subscriber without `OTel` export — logfmt output only.
48- fn init_tracing ( ) {
64+ fn init_tracing ( logfmt_layer : logfmt :: LogfmtLayer ) {
4965 use tracing_subscriber:: layer:: SubscriberExt ;
5066
5167 let subscriber = tracing_subscriber:: registry ( )
5268 . with ( default_env_filter ( ) )
53- . with ( logfmt:: LogfmtLayer :: new ( ) ) ;
69+ . with ( logfmt_layer) ;
70+
71+ set_global_subscriber ( subscriber) ;
72+ }
73+
74+ fn init_test_tracing ( ) {
75+ use tracing_subscriber:: layer:: SubscriberExt ;
76+
77+ let subscriber = tracing_subscriber:: registry ( )
78+ . with ( test_env_filter ( ) )
79+ . with ( logfmt:: LogfmtLayer :: test_writer ( ) ) ;
5480
5581 set_global_subscriber ( subscriber) ;
5682}
5783
58- fn init_otel_tracing ( ) {
84+ fn init_otel_tracing ( logfmt_layer : logfmt :: LogfmtLayer ) {
5985 use opentelemetry:: global;
6086 use opentelemetry:: trace:: TracerProvider as _;
6187 use opentelemetry_sdk:: propagation:: TraceContextPropagator ;
@@ -117,7 +143,7 @@ fn init_otel_tracing() {
117143
118144 let subscriber = tracing_subscriber:: registry ( )
119145 . with ( default_env_filter ( ) )
120- . with ( logfmt :: LogfmtLayer :: new ( ) )
146+ . with ( logfmt_layer )
121147 . with ( otel_layer)
122148 . with ( tracing_opentelemetry:: MetricsLayer :: new ( meter_provider) ) ;
123149
@@ -133,13 +159,26 @@ fn default_env_filter() -> tracing_subscriber::EnvFilter {
133159 . expect ( "Invalid log filter value in LOG_LEVEL or RUST_LOG" )
134160}
135161
162+ fn test_env_filter ( ) -> tracing_subscriber:: EnvFilter {
163+ env_filter_directives (
164+ env:: var ( "LOG_LEVEL" ) . ok ( ) . as_deref ( ) ,
165+ env:: var ( "RUST_LOG" ) . ok ( ) . as_deref ( ) ,
166+ )
167+ . parse ( )
168+ . unwrap_or_else ( |_| {
169+ DEFAULT_ENV_FILTER_DIRECTIVES
170+ . parse ( )
171+ . expect ( "Default filter directives should always be valid" )
172+ } )
173+ }
174+
136175fn env_filter_directives ( log_level : Option < & str > , rust_log : Option < & str > ) -> String {
137176 match (
138177 log_level. filter ( |value| !value. is_empty ( ) ) ,
139178 rust_log. filter ( |value| !value. is_empty ( ) ) ,
140179 ) {
141180 ( Some ( value) , _) | ( None , Some ( value) ) => value. to_owned ( ) ,
142- ( None , None ) => "sqlpage=info,actix_web=info,tracing_actix_web=info" . to_owned ( ) ,
181+ ( None , None ) => DEFAULT_ENV_FILTER_DIRECTIVES . to_owned ( ) ,
143182 }
144183}
145184
@@ -217,14 +256,29 @@ mod logfmt {
217256 const BOLD : & str = "\x1b [1m" ;
218257 const RESET : & str = "\x1b [0m" ;
219258
259+ #[ derive( Copy , Clone ) ]
260+ enum OutputMode {
261+ Stderr ,
262+ TestWriter ,
263+ }
264+
220265 pub ( super ) struct LogfmtLayer {
221266 use_colors : bool ,
267+ output_mode : OutputMode ,
222268 }
223269
224270 impl LogfmtLayer {
225271 pub fn new ( ) -> Self {
226272 Self {
227273 use_colors : io:: stderr ( ) . is_terminal ( ) ,
274+ output_mode : OutputMode :: Stderr ,
275+ }
276+ }
277+
278+ pub fn test_writer ( ) -> Self {
279+ Self {
280+ use_colors : false ,
281+ output_mode : OutputMode :: TestWriter ,
228282 }
229283 }
230284 }
@@ -280,7 +334,14 @@ mod logfmt {
280334
281335 buf. push ( '\n' ) ;
282336 write_multiline_message ( & mut buf, msg, multiline_msg) ;
283- let _ = io:: Write :: write_all ( & mut io:: stderr ( ) . lock ( ) , buf. as_bytes ( ) ) ;
337+ match self . output_mode {
338+ OutputMode :: Stderr => {
339+ let _ = io:: Write :: write_all ( & mut io:: stderr ( ) . lock ( ) , buf. as_bytes ( ) ) ;
340+ }
341+ OutputMode :: TestWriter => {
342+ eprint ! ( "{buf}" ) ;
343+ }
344+ }
284345 }
285346 }
286347
0 commit comments