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//! You can use the variable `OTEL_RESOURCE_ATTRIBUTES` to set OpenTelemetry resource
102//! attributes. 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)]
149#[must_use]
150pub struct Initializer {
151 /// Application environment variable prefix.
152 env_var_prefix: String,
153 /// Default level used for logging to stderr.
154 stderr_default_level: LevelFilter,
155 /// Format used for logging to stderr.
156 stderr_logging_format: Option<StderrLogFormat>,
157}
158
159impl Initializer {
160 /// Create a new [`Initializer`] with the given environment variable prefix.
161 pub fn new(env_var_prefix: &str) -> Self {
162 Self {
163 env_var_prefix: env_var_prefix.to_owned(),
164 stderr_default_level: LevelFilter::INFO,
165 stderr_logging_format: None,
166 }
167 }
168
169 /// Apply a configuration to the [`Initializer`].
170 pub fn apply(mut self, configuration: impl Configuration) -> Self {
171 configuration.apply_to(&mut self);
172 self
173 }
174
175 /// Initialize observability functionality.
176 pub fn init(self) -> FinalizeGuard {
177 let stderr_filter = EnvFilter::builder()
178 .with_default_directive(self.stderr_default_level.into())
179 .with_env_var(format!("{}_LOG", &self.env_var_prefix))
180 .from_env_lossy();
181 let stderr_format = self.stderr_logging_format.clone().unwrap_or_else(|| {
182 let format_env_var = format!("{}_LOG_FORMAT", self.env_var_prefix);
183 match std::env::var(&format_env_var).as_deref() {
184 Ok("full") => StderrLogFormat::Full,
185 Ok("compact") => StderrLogFormat::Compact,
186 Ok(_) | Err(std::env::VarError::NotUnicode(_)) => {
187 eprintln!("WARNING: Unsupported log format in '{format_env_var}' environment variable.");
188 StderrLogFormat::Compact
189 }
190 _ => StderrLogFormat::Compact,
191 }
192 });
193 let stderr_formatter = match stderr_format {
194 StderrLogFormat::Compact => StderrLogFormatter::Compact(
195 tracing_subscriber::fmt::format()
196 .without_time()
197 .with_ansi(console::colors_enabled_stderr())
198 .with_target(false)
199 .compact(),
200 ),
201 StderrLogFormat::Full => StderrLogFormatter::Full(
202 tracing_subscriber::fmt::format().with_ansi(console::colors_enabled_stderr()),
203 ),
204 };
205 let stderr_layer = tracing_subscriber::fmt::layer()
206 .with_writer(std::io::stderr)
207 .event_format(stderr_formatter)
208 .with_filter(stderr_filter);
209
210 let registry = tracing_subscriber::registry().with(stderr_layer);
211
212 #[cfg(feature = "otlp")]
213 let (registry, otlp_guard) = {
214 let (otlp_layer, otlp_guard) = otlp::setup_otlp_layer(&self);
215 (registry.with(otlp_layer), otlp_guard)
216 };
217
218 registry.init();
219
220 FinalizeGuard {
221 #[cfg(feature = "otlp")]
222 _otlp_guard: otlp_guard,
223 }
224 }
225}
226
227/// Configuration that can be applied to an [`Initializer`].
228pub trait Configuration: sealed::ConfigurationSealed {
229 /// Apply the configuration to the given [`Initializer`].
230 fn apply_to(&self, initializer: &mut Initializer);
231}
232
233impl<C: Configuration> Configuration for &C {
234 fn apply_to(&self, initializer: &mut Initializer) {
235 (**self).apply_to(initializer);
236 }
237}
238
239/// Format for log messages written to stderr.
240#[derive(Debug, Clone)]
241enum StderrLogFormat {
242 /// Compact format.
243 Compact,
244 /// Full format.
245 Full,
246}
247
248/// Formatter for log messages written to stderr.
249enum StderrLogFormatter {
250 /// Compact format.
251 Compact(tracing_subscriber::fmt::format::Format<tracing_subscriber::fmt::format::Compact, ()>),
252 /// Full format.
253 Full(tracing_subscriber::fmt::format::Format),
254}
255
256impl<S, N> FormatEvent<S, N> for StderrLogFormatter
257where
258 S: Subscriber + for<'a> LookupSpan<'a>,
259 N: for<'a> FormatFields<'a> + 'static,
260{
261 fn format_event(
262 &self,
263 ctx: &FmtContext<'_, S, N>,
264 writer: format::Writer<'_>,
265 event: &Event<'_>,
266 ) -> fmt::Result {
267 // Simply delegate to the internal formatter provided by `tracing_subscriber`.
268 match self {
269 StderrLogFormatter::Compact(formatter) => formatter.format_event(ctx, writer, event),
270 StderrLogFormatter::Full(formatter) => formatter.format_event(ctx, writer, event),
271 }
272 }
273}
274
275/// Finalization guard.
276#[derive(Debug)]
277#[must_use]
278pub struct FinalizeGuard {
279 #[cfg(feature = "otlp")]
280 _otlp_guard: otlp::FinalizeGuard,
281}
282
283impl FinalizeGuard {
284 /// Finalize everything by dropping the guard.
285 pub fn finalize(self) {
286 drop(self);
287 }
288}