si_observability/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2//! This crate provides a reusable basis for developing applications with strong, built-in
3//! observability.
4//!
5//! **Note: This crate is intended for internal use within applications developed by
6//! [Silitics]. It is open-source and you are free to use it for whatever purpose you see
7//! fit, however, we will not accept any contributions other than bug fixes.**
8//!
9//! [Silitics]: https://silitics.com
10//!
11//! At Silitics, we consider observability a fundamental aspect of building reliable and
12//! maintainable systems. This crate builds upon the well-established [`tracing`]
13//! ecosystem, which we rely on for instrumentation and structured logging. On top of
14//! [`tracing`], this crate provides a reusable basis for configuration and logging
15//! initialization—reducing boilerplate, promoting best practices, and supporting useful
16//! log output for users at the console and the integration with observability platforms.
17//! Most functionality provided by this crate is gated by feature flags and can be
18//! configured via environment variables at runtime.
19//!
20//! The [`Initializer`] is the primary interface for setting everything up. Here is a
21//! minimal example:
22//!
23//! ```
24//! si_observability::Initializer::new("APP").init();
25//! ```
26//!
27//! In this example, the string `APP` is an application-defined prefix for configuration
28//! environment variables. In the following, we will use `APP` as a placeholder for the
29//! application-defined prefix defined via the [`Initializer`].
30//!
31//! Additional configurations can be [applied][Initializer::apply] to the [`Initializer`]
32//! via the sealed [`Configuration`] trait.
33//!
34//! Upon initialization, the [`Initializer`] returns a [`FinalizeGuard`] which must be
35//! kept around for the lifetime of the application. When dropped, this guard will cleanup
36//! resources and flush internal buffers, e.g., containing logs.
37//!
38//!
39//! ## Logging to Stderr
40//!
41//! Logging to stderr is enabled by default for informational events. Applications should
42//! use informational events to communicate status information to the user. Applications
43//! should **not** use [`println!`] or [`eprintln!`] for that purpose.
44//!
45//! Logging to stderr can be configured via the `APP_LOG` environment variable using
46//! [`EnvFilter`] directives.
47//!
48//! Logging to stderr produces colorful output using ANSI codes in accordance with the
49//! [`clicolors` specification][clicolors].
50//!
51//! [clicolors]: https://bixense.com/clicolors/
52//!
53//! The environment variable `APP_LOG_FORMAT` can be set to one of the following log
54//! formats:
55//!
56//! - `compact`: Compact format for everyday use (the default).
57//! - `full`: Verbose format with additional information like timestamps and span
58//!   attributes.
59//!
60//! In addition, an application may make logging to stderr configurable via standardized
61//! command line arguments. Command line arguments have the advantage that they are
62//! discoverable by users by calling the application with `--help`. To standardize the
63//! respective arguments, this crate provides pre-made integrations with [`clap`][clap].
64//! Here is an example:
65//!
66//! ```rust
67//! # use clap4 as clap;
68//! # use clap::Parser;
69//! use si_observability::clap4::LoggingArgs;
70//!
71//! #[derive(Debug, Parser)]
72//! pub struct AppArgs {
73//!     #[clap(flatten)]
74//!     logging: LoggingArgs,
75//! }
76//!
77//! let args = AppArgs::parse();
78//!
79//! si_observability::Initializer::new("APP").apply(&args.logging).init();
80//! ```
81//!
82//! [clap]: https://crates.io/crates/clap
83//!
84//! Note that we consider adding arguments with the prefix `--log-` a **non-breaking**
85//! change.
86//!
87//!
88//! ## OpenTelemetry
89//!
90//! When the `otlp` feature is enabled, this crate supports exporting traces via [OTLP] to
91//! monitoring and observability tools. While primarily intended for monitoring cloud
92//! applications, this can also be useful for local debugging.
93//!
94//! [OTLP]: https://opentelemetry.io/docs/specs/otel/protocol/
95//!
96//! At runtime, OTLP export is enabled and configured via the `APP_LOG_OTLP` environment
97//! variable using [`EnvFilter`] directives. Additional environment variables, e.g., for
98//! the configuration of OTLP endpoints and headers, follow the [OpenTelemetry standard].
99//! At the moment, trace export is limited to the `http/protobuf` protocol.
100//!
101//! Use the variable `OTEL_RESOURCE_ATTRIBUTES` to set OpenTelemetry resource attributes.
102//! For instance:
103//!
104//! ```plain
105//! OTEL_RESOURCE_ATTRIBUTES="service.name=my-app,service.instance.id=my-app-instance-1"
106//! ```
107//!
108//! [OpenTelemetry standard]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
109//!
110//! For local development and debugging, you can run a [Jaeger] instance as follows:
111//!
112//! ```sh
113//! docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:latest
114//! ```
115//!
116//! It then suffices to set `APP_LOG_OTLP=info` to send traces to Jaeger. To view the
117//! traces, go to <http://localhost:16686>.
118//!
119//! [Jaeger]: https://www.jaegertracing.io/
120//!
121//!
122//! ## Feature Flags
123//!
124//! This crate has the following feature flags:
125//!
126//! - `clap4`: Support for [`clap`][clap4](version 4) CLI arguments.
127//! - `otlp`: Support for exporting traces via [OTLP].
128
129use core::fmt;
130
131use tracing::level_filters::LevelFilter;
132use tracing::{Event, Subscriber};
133use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, format};
134use tracing_subscriber::layer::SubscriberExt;
135use tracing_subscriber::registry::LookupSpan;
136use tracing_subscriber::util::SubscriberInitExt;
137use tracing_subscriber::{EnvFilter, Layer};
138
139mod sealed;
140
141#[cfg(feature = "clap4")]
142pub mod clap4;
143
144#[cfg(feature = "otlp")]
145mod otlp;
146
147/// Observability initializer.
148#[derive(Debug, Clone)]
149pub struct Initializer {
150    /// Application environment variable prefix.
151    env_var_prefix: String,
152    /// Default level used for logging to stderr.
153    stderr_default_level: LevelFilter,
154    /// Format used for logging to stderr.
155    stderr_logging_format: Option<StderrLogFormat>,
156}
157
158impl Initializer {
159    /// Create a new [`Initializer`] with the given environment variable prefix.
160    pub fn new(env_var_prefix: &str) -> Self {
161        Self {
162            env_var_prefix: env_var_prefix.to_owned(),
163            stderr_default_level: LevelFilter::INFO,
164            stderr_logging_format: None,
165        }
166    }
167
168    /// Apply a configuration to the [`Initializer`].
169    pub fn apply(mut self, configuration: impl Configuration) -> Self {
170        configuration.apply_to(&mut self);
171        self
172    }
173
174    /// Initialize observability functionality.
175    pub fn init(self) -> FinalizeGuard {
176        let stderr_filter = EnvFilter::builder()
177            .with_default_directive(self.stderr_default_level.into())
178            .with_env_var(format!("{}_LOG", &self.env_var_prefix))
179            .from_env_lossy();
180        let stderr_format = self.stderr_logging_format.clone().unwrap_or_else(|| {
181            let format_env_var = format!("{}_LOG_FORMAT", self.env_var_prefix);
182            match std::env::var(&format_env_var).as_deref() {
183                Ok("full") => StderrLogFormat::Full,
184                Ok("compact") => StderrLogFormat::Compact,
185                Ok(_) | Err(std::env::VarError::NotUnicode(_)) => {
186                    eprintln!("WARNING: Unsupported log format in '{format_env_var}' environment variable.");
187                    StderrLogFormat::Compact
188                }
189                _ => StderrLogFormat::Compact,
190            }
191        });
192        let stderr_formatter = match stderr_format {
193            StderrLogFormat::Compact => StderrLogFormatter::Compact(
194                tracing_subscriber::fmt::format()
195                    .without_time()
196                    .with_ansi(console::colors_enabled_stderr())
197                    .with_target(false)
198                    .compact(),
199            ),
200            StderrLogFormat::Full => StderrLogFormatter::Full(
201                tracing_subscriber::fmt::format().with_ansi(console::colors_enabled_stderr()),
202            ),
203        };
204        let stderr_layer = tracing_subscriber::fmt::layer()
205            .with_writer(std::io::stderr)
206            .event_format(stderr_formatter)
207            .with_filter(stderr_filter);
208
209        let registry = tracing_subscriber::registry().with(stderr_layer);
210
211        #[cfg(feature = "otlp")]
212        let (registry, otlp_guard) = {
213            let (otlp_layer, otlp_guard) = otlp::setup_otlp_layer(&self);
214            (registry.with(otlp_layer), otlp_guard)
215        };
216
217        registry.init();
218
219        FinalizeGuard {
220            #[cfg(feature = "otlp")]
221            _otlp_guard: otlp_guard,
222        }
223    }
224}
225
226/// Configuration that can be applied to an [`Initializer`].
227pub trait Configuration: sealed::ConfigurationSealed {
228    /// Apply the configuration to the given [`Initializer`].
229    fn apply_to(&self, initializer: &mut Initializer);
230}
231
232impl<C: Configuration> Configuration for &C {
233    fn apply_to(&self, initializer: &mut Initializer) {
234        (**self).apply_to(initializer);
235    }
236}
237
238/// Format for log messages written to stderr.
239#[derive(Debug, Clone)]
240enum StderrLogFormat {
241    /// Compact format.
242    Compact,
243    /// Full format.
244    Full,
245}
246
247/// Formatter for log messages written to stderr.
248enum StderrLogFormatter {
249    /// Compact format.
250    Compact(tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::Compact, ()>),
251    /// Full format.
252    Full(tracing_subscriber::fmt::format::Format),
253}
254
255impl<S, N> FormatEvent<S, N> for StderrLogFormatter
256where
257    S: Subscriber + for<'a> LookupSpan<'a>,
258    N: for<'a> FormatFields<'a> + 'static,
259{
260    fn format_event(
261        &self,
262        ctx: &FmtContext<'_, S, N>,
263        writer: format::Writer<'_>,
264        event: &Event<'_>,
265    ) -> fmt::Result {
266        // Simply delegate to the internal formatter provided by `tracing_subscriber`.
267        match self {
268            StderrLogFormatter::Compact(formatter) => formatter.format_event(ctx, writer, event),
269            StderrLogFormatter::Full(formatter) => formatter.format_event(ctx, writer, event),
270        }
271    }
272}
273
274/// Finalization guard.
275#[derive(Debug)]
276#[must_use]
277pub struct FinalizeGuard {
278    #[cfg(feature = "otlp")]
279    _otlp_guard: otlp::FinalizeGuard,
280}
281
282impl FinalizeGuard {
283    /// Finalize everything by dropping the guard.
284    pub fn finalize(self) {
285        drop(self)
286    }
287}