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}