objectstore_server/
observability.rs

1//! Initialization of error reporting and distributed tracing.
2//!
3//! Call [`init_sentry`] and [`init_tracing`] during server startup.
4//! Sentry must be initialized before the Tokio runtime is created so it can
5//! instrument async tasks from the start.
6
7use secrecy::ExposeSecret;
8use sentry::integrations::tracing as sentry_tracing;
9use tracing::Level;
10use tracing::level_filters::LevelFilter;
11use tracing_subscriber::{EnvFilter, prelude::*};
12
13use crate::config::{Config, LogFormat};
14
15/// The full release name including the objectstore version and SHA.
16const RELEASE: &str = std::env!("OBJECTSTORE_RELEASE");
17
18/// Initializes the Sentry error-reporting client, if a DSN is configured.
19///
20/// Returns `None` when `config.sentry.dsn` is not set. The returned
21/// [`sentry::ClientInitGuard`] must be kept alive for the duration of the process;
22/// dropping it flushes the event queue and shuts down the Sentry client.
23pub fn init_sentry(config: &Config) -> Option<sentry::ClientInitGuard> {
24    let config = &config.sentry;
25    let dsn = config.dsn.as_ref()?;
26
27    let guard = sentry::init(sentry::ClientOptions {
28        dsn: dsn.expose_secret().parse().ok(),
29        release: Some(RELEASE.into()),
30        environment: config.environment.clone(),
31        server_name: config.server_name.clone(),
32        sample_rate: config.sample_rate,
33        traces_sampler: {
34            let traces_sample_rate = config.traces_sample_rate;
35            let inherit_sampling_decision = config.inherit_sampling_decision;
36            Some(std::sync::Arc::new(move |ctx| {
37                if let Some(sampled) = ctx.sampled()
38                    && inherit_sampling_decision
39                {
40                    f32::from(sampled)
41                } else {
42                    traces_sample_rate
43                }
44            }))
45        },
46        enable_logs: true,
47        debug: config.debug,
48        ..Default::default()
49    });
50
51    sentry::configure_scope(|scope| {
52        for (k, v) in &config.tags {
53            scope.set_tag(k, v);
54        }
55    });
56
57    Some(guard)
58}
59
60/// Initializes the global tracing subscriber with structured logging and optional Sentry integration.
61///
62/// Reads `RUST_LOG` for filter directives; falls back to `INFO`-level logging with `TRACE`-level
63/// for internal objectstore crates. Log format (`pretty`, `simplified`, or `json`) is determined
64/// by `config.logging.format`, defaulting to pretty when a terminal is attached.
65pub fn init_tracing(config: &Config) {
66    // Same as the default filter, except it converts warnings into events
67    // and also sends everything at or above INFO as logs instead of breadcrumbs.
68    let sentry_layer = config.sentry.is_enabled().then(|| {
69        sentry_tracing::layer().event_filter(|metadata| match *metadata.level() {
70            Level::ERROR | Level::WARN => {
71                sentry_tracing::EventFilter::Event | sentry_tracing::EventFilter::Log
72            }
73            Level::INFO | Level::DEBUG => sentry_tracing::EventFilter::Log,
74            Level::TRACE => sentry_tracing::EventFilter::Ignore,
75        })
76    });
77
78    let format = tracing_subscriber::fmt::layer()
79        .with_writer(std::io::stderr)
80        .with_target(true);
81
82    let format = match (config.logging.format, console::user_attended()) {
83        (LogFormat::Auto, true) | (LogFormat::Pretty, _) => format.compact().without_time().boxed(),
84        (LogFormat::Auto, false) | (LogFormat::Simplified, _) => format.with_ansi(false).boxed(),
85        (LogFormat::Json, _) => format
86            .json()
87            .flatten_event(true)
88            .with_current_span(true)
89            .with_span_list(true)
90            .with_file(true)
91            .with_line_number(true)
92            .boxed(),
93    };
94
95    let env_filter = match EnvFilter::try_from_default_env() {
96        Ok(env_filter) => env_filter,
97        // INFO by default. Use stricter levels for noisy crates. Use looser levels
98        // for internal crates and essential dependencies.
99        Err(_) => EnvFilter::new(
100            "INFO,\
101            tower_http=DEBUG,\
102            objectstore=TRACE,\
103            objectstore_service=TRACE,\
104            objectstore_types=TRACE,\
105            ",
106        ),
107    };
108
109    tracing_subscriber::registry()
110        .with(format.with_filter(config.logging.level))
111        .with(sentry_layer)
112        .with(env_filter)
113        .init();
114}
115
116/// Logs an error to the configured logger or `stderr` if not yet configured.
117pub fn ensure_log_error(error: &anyhow::Error) {
118    if tracing::Level::ERROR <= tracing::level_filters::STATIC_MAX_LEVEL
119        && tracing::Level::ERROR <= LevelFilter::current()
120    {
121        tracing::error!(error = error.as_ref() as &dyn std::error::Error);
122    } else {
123        eprintln!("{error:?}");
124    }
125}