wick_logger/
logger.rs

1use opentelemetry::global;
2use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
3use tracing_subscriber::fmt::time::UtcTime;
4use tracing_subscriber::prelude::*;
5use tracing_subscriber::Layer;
6mod otel;
7
8use crate::error::LoggerError;
9use crate::LoggingOptions;
10
11#[derive(Debug, PartialEq, Clone, Copy)]
12enum Environment {
13  Prod,
14  Test,
15}
16
17/// Initialize a logger or panic on failure
18pub fn init(opts: &LoggingOptions) -> LoggingGuard {
19  #![allow(clippy::trivially_copy_pass_by_ref, clippy::needless_borrow)]
20  match try_init(&opts, Environment::Prod) {
21    Ok(guard) => guard,
22    Err(e) => panic!("Error initializing logger: {}", e),
23  }
24}
25
26/// Initialize a logger for tests
27#[must_use]
28pub fn init_test(opts: &LoggingOptions) -> Option<LoggingGuard> {
29  #![allow(clippy::trivially_copy_pass_by_ref, clippy::needless_borrow)]
30  try_init(&opts, Environment::Test).ok()
31}
32
33#[must_use]
34#[derive(Debug)]
35/// Guard that - when dropped - flushes all log messages and drop I/O handles.
36pub struct LoggingGuard {
37  #[allow(unused)]
38  env: Environment,
39  #[allow(unused)]
40  logfile: Option<WorkerGuard>,
41  #[allow(unused)]
42  console: WorkerGuard,
43  #[allow(unused)]
44  tracer_provider: Option<opentelemetry::sdk::trace::TracerProvider>,
45}
46
47impl LoggingGuard {
48  fn new(
49    env: Environment,
50    logfile: Option<WorkerGuard>,
51    console: WorkerGuard,
52    tracer_provider: Option<opentelemetry::sdk::trace::TracerProvider>,
53  ) -> Self {
54    Self {
55      env,
56      logfile,
57      console,
58      tracer_provider,
59    }
60  }
61  /// Call this function when you are done with the logger.
62  #[allow(clippy::missing_const_for_fn)]
63  pub fn teardown(&self) {
64    // noop right now
65  }
66
67  /// Flush any remaining logs.
68  pub fn flush(&mut self) {
69    let has_otel = self.tracer_provider.take().is_some();
70
71    if has_otel {
72      // Shut down the global tracer provider.
73      // This has to be done in a separate thread because it will deadlock
74      // if any of its requests have stalled.
75      // See: https://github.com/open-telemetry/opentelemetry-rust/issues/868
76      let (sender, receiver) = std::sync::mpsc::channel();
77      let handle = std::thread::spawn(move || {
78        opentelemetry::global::shutdown_tracer_provider();
79        let _ = sender.send(());
80      });
81
82      // Wait a bit to see if the shutdown completes gracefully.
83      let _ = receiver.recv_timeout(std::time::Duration::from_millis(200));
84
85      // Otherwise, issue a warning because opentelemetry will complain
86      // and we want to add context to the warning.
87      if !handle.is_finished() {
88        debug!("open telemetry tracer provider did not shut down in time, forcing shutdown");
89      }
90    }
91  }
92}
93
94impl Drop for LoggingGuard {
95  fn drop(&mut self) {
96    self.flush();
97  }
98}
99
100fn get_stderr_writer(_opts: &LoggingOptions) -> (NonBlocking, WorkerGuard) {
101  let (stderr_writer, console_guard) = tracing_appender::non_blocking(std::io::stderr());
102
103  (stderr_writer, console_guard)
104}
105
106#[allow(clippy::too_many_lines)]
107fn try_init(opts: &LoggingOptions, environment: Environment) -> Result<LoggingGuard, LoggerError> {
108  #[cfg(windows)]
109  let with_color = ansi_term::enable_ansi_support().is_ok();
110  #[cfg(not(windows))]
111  let with_color = true;
112
113  let timer = UtcTime::new(time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]").unwrap());
114  let (stderr_writer, console_guard) = get_stderr_writer(opts);
115
116  let needs_simple_tracer = tokio::runtime::Handle::try_current().is_err() || environment == Environment::Test;
117
118  // Configure a jaeger tracer if we have a configured endpoint.
119  let (otel_layer, tracer_provider) = opts.otlp_endpoint.as_ref().map_or_else(
120    || (None, None),
121    |otlp_endpoint| {
122      let (tracer, provider) = if needs_simple_tracer {
123        otel::build_simple(otlp_endpoint).unwrap()
124      } else {
125        otel::build_batch(otlp_endpoint).unwrap() // unwrap OK for now, this is infallible.
126      };
127
128      let _ = global::set_tracer_provider(provider.clone());
129
130      let layer = Some(
131        tracing_opentelemetry::layer()
132          .with_tracer(tracer)
133          .with_filter(opts.levels.telemetry.clone()),
134      );
135      (layer, Some(provider))
136    },
137  );
138
139  // This is ugly. If you can improve it, go for it, but
140  // start here to understand why it's laid out like this: https://github.com/tokio-rs/tracing/issues/575
141  let (verbose_layer, normal_layer, logfile_guard, test_layer) = match environment {
142    Environment::Prod => {
143      if opts.verbose {
144        (
145          Some(
146            tracing_subscriber::fmt::layer()
147              .with_writer(stderr_writer)
148              .with_ansi(with_color)
149              .with_timer(timer)
150              .with_thread_names(cfg!(debug_assertions))
151              .with_target(cfg!(debug_assertions))
152              .with_file(cfg!(debug_assertions))
153              .with_line_number(cfg!(debug_assertions))
154              .with_filter(opts.levels.stderr.clone()),
155          ),
156          None,
157          None,
158          None,
159        )
160      } else {
161        (
162          None,
163          Some(
164            tracing_subscriber::fmt::layer()
165              .with_writer(stderr_writer)
166              .with_thread_names(false)
167              .with_ansi(with_color)
168              .with_target(false)
169              .with_timer(timer)
170              .with_filter(opts.levels.stderr.clone()),
171          ),
172          None,
173          None,
174        )
175      }
176    }
177    Environment::Test => (
178      None,
179      None,
180      None,
181      Some(
182        tracing_subscriber::fmt::layer()
183          .with_writer(stderr_writer)
184          .with_ansi(with_color)
185          .without_time()
186          .with_target(true)
187          .with_test_writer()
188          .with_filter(opts.levels.stderr.clone()),
189      ),
190    ),
191  };
192
193  let subscriber = tracing_subscriber::registry()
194    .with(otel_layer)
195    .with(test_layer)
196    .with(verbose_layer)
197    .with(normal_layer);
198
199  #[cfg(feature = "console")]
200  let subscriber = subscriber.with(console_subscriber::spawn());
201
202  tracing::subscriber::set_global_default(subscriber)?;
203  let guards = Ok(LoggingGuard::new(
204    environment,
205    logfile_guard,
206    console_guard,
207    tracer_provider,
208  ));
209  trace!(options=?opts,"logger initialized");
210
211  guards
212}