hwhkit_observability/
lib.rs1#![warn(missing_docs)]
15
16use std::error::Error as StdError;
17
18use serde::{Deserialize, Serialize};
19use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
20
21#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29pub enum ObservabilityError {
30 #[error("invalid log filter `{filter}`")]
33 BadFilter {
34 filter: String,
36 #[source]
38 source: Box<dyn StdError + Send + Sync>,
39 },
40
41 #[error("OTel exporter init failed")]
46 OtelInit {
47 #[allow(dead_code)]
49 context: Option<String>,
50 #[source]
52 source: Box<dyn StdError + Send + Sync>,
53 },
54
55 #[error("hwhkit-observability built without `otel` feature")]
60 OtelDisabled,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct LoggingConfig {
75 pub level: String,
78 pub format: String,
80}
81
82impl Default for LoggingConfig {
83 fn default() -> Self {
84 Self {
85 level: "info".to_string(),
86 format: "auto".to_string(),
87 }
88 }
89}
90
91impl LoggingConfig {
92 pub fn pretty(level: impl Into<String>) -> Self {
94 Self {
95 level: level.into(),
96 format: "pretty".to_string(),
97 }
98 }
99 pub fn json(level: impl Into<String>) -> Self {
101 Self {
102 level: level.into(),
103 format: "json".to_string(),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
112#[non_exhaustive]
113pub struct OtelConfig {
114 pub enabled: bool,
117 pub endpoint: String,
119 pub service_name: String,
121 pub service_version: String,
123 pub environment: String,
125}
126
127impl Default for OtelConfig {
128 fn default() -> Self {
129 Self {
130 enabled: false,
131 endpoint: "http://localhost:4317".to_string(),
132 service_name: "hwhkit-service".to_string(),
133 service_version: env!("CARGO_PKG_VERSION").to_string(),
134 environment: "dev".to_string(),
135 }
136 }
137}
138
139fn detect_tty() -> bool {
140 #[cfg(unix)]
143 {
144 unsafe { libc_inline::isatty(1) != 0 }
146 }
147 #[cfg(not(unix))]
148 {
149 false
150 }
151}
152
153#[cfg(unix)]
154mod libc_inline {
155 extern "C" {
156 pub fn isatty(fd: i32) -> i32;
157 }
158}
159
160fn make_filter(level: &str) -> Result<EnvFilter, ObservabilityError> {
161 EnvFilter::try_from_default_env()
162 .or_else(|_| EnvFilter::try_new(level))
163 .map_err(|e| ObservabilityError::BadFilter {
164 filter: level.to_string(),
165 source: Box::new(e),
166 })
167}
168
169pub fn init_logging(config: &LoggingConfig) -> Result<(), ObservabilityError> {
171 let filter = make_filter(&config.level)?;
172 let format = resolve_format(&config.format);
173 let registry = Registry::default().with(filter);
174
175 let _ = match format {
176 ResolvedFormat::Json => registry.with(fmt::layer().json()).try_init(),
177 ResolvedFormat::Pretty => registry
178 .with(
179 fmt::layer()
180 .with_target(false)
181 .with_file(false)
182 .with_line_number(false)
183 .with_thread_ids(false),
184 )
185 .try_init(),
186 };
187
188 Ok(())
189}
190
191#[derive(Debug, Clone, Copy)]
192enum ResolvedFormat {
193 Json,
194 Pretty,
195}
196
197fn resolve_format(raw: &str) -> ResolvedFormat {
198 match raw {
199 "json" => ResolvedFormat::Json,
200 "pretty" => ResolvedFormat::Pretty,
201 _ => {
203 if detect_tty() {
204 ResolvedFormat::Pretty
205 } else {
206 ResolvedFormat::Json
207 }
208 }
209 }
210}
211
212#[cfg(feature = "otel")]
213pub mod otel_layer {
214 use super::*;
217 use opentelemetry::trace::TracerProvider as _;
218 use opentelemetry::KeyValue;
219 use opentelemetry_otlp::WithExportConfig;
220 use opentelemetry_sdk::{
221 propagation::TraceContextPropagator,
222 trace::{self as sdktrace, RandomIdGenerator, Sampler},
223 Resource,
224 };
225 use tracing_opentelemetry::OpenTelemetryLayer;
226
227 pub fn init_with_otel(
230 log_cfg: &LoggingConfig,
231 otel_cfg: &OtelConfig,
232 ) -> Result<OtelGuard, ObservabilityError> {
233 let filter = make_filter(&log_cfg.level)?;
234 let format = resolve_format(&log_cfg.format);
235
236 let resource = Resource::new(vec![
237 KeyValue::new("service.name", otel_cfg.service_name.clone()),
238 KeyValue::new("service.version", otel_cfg.service_version.clone()),
239 KeyValue::new("deployment.environment", otel_cfg.environment.clone()),
240 ]);
241
242 opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
243
244 let exporter = opentelemetry_otlp::new_exporter()
245 .tonic()
246 .with_endpoint(otel_cfg.endpoint.clone());
247
248 let provider = opentelemetry_otlp::new_pipeline()
249 .tracing()
250 .with_exporter(exporter)
251 .with_trace_config(
252 sdktrace::Config::default()
253 .with_sampler(Sampler::AlwaysOn)
254 .with_id_generator(RandomIdGenerator::default())
255 .with_resource(resource),
256 )
257 .install_batch(opentelemetry_sdk::runtime::Tokio)
258 .map_err(|e| ObservabilityError::OtelInit {
259 context: Some("install_batch failed".to_string()),
260 source: Box::new(e),
261 })?;
262
263 let tracer = provider.tracer("hwhkit");
264 let otel_layer = OpenTelemetryLayer::new(tracer);
265
266 let registry = Registry::default().with(filter).with(otel_layer);
267 let _ = match format {
268 ResolvedFormat::Json => registry.with(fmt::layer().json()).try_init(),
269 ResolvedFormat::Pretty => registry
270 .with(
271 fmt::layer()
272 .with_target(false)
273 .with_file(false)
274 .with_line_number(false),
275 )
276 .try_init(),
277 };
278
279 Ok(OtelGuard {
280 _provider: provider,
281 })
282 }
283
284 pub struct OtelGuard {
286 _provider: opentelemetry_sdk::trace::TracerProvider,
287 }
288
289 impl Drop for OtelGuard {
290 fn drop(&mut self) {
291 opentelemetry::global::shutdown_tracer_provider();
292 }
293 }
294}
295
296#[cfg(not(feature = "otel"))]
297pub mod otel_layer {
298 use super::*;
301 pub struct OtelGuard;
304 pub fn init_with_otel(
308 _: &LoggingConfig,
309 _: &OtelConfig,
310 ) -> Result<OtelGuard, ObservabilityError> {
311 Err(ObservabilityError::OtelDisabled)
312 }
313}
314
315#[cfg(feature = "otel-sqlx")]
316pub mod sqlx_instrument;
317
318#[cfg(feature = "otel-redis")]
319pub mod redis_instrument;
320
321#[cfg(feature = "otel-reqwest")]
322pub mod reqwest_instrument;