1#![cfg_attr(
38 all(feature = "metrics", feature = "logs", feature = "tracing"),
39 doc = r####"
40## Example
41
42```no_run
43use kerniq_observability::prelude::*;
44
45# async fn demo() -> Result<(), kerniq_observability::Error> {
46let observability = Observability::builder()
47 .service_name("payment-api")
48 .service_version("1.0.0")
49 .environment("production")
50 // With the `prometheus` feature: MetricsConfig::prometheus().endpoint("/metrics")
51 .metrics(MetricsConfig::new())
52 .logs(LogsConfig::json())
53 // With the `otlp` feature: TracesConfig::otlp().sampling_ratio(0.10)
54 .traces(TracesConfig::new().sampling_ratio(0.10))
55 .build()
56 .await?;
57
58// With kerniq-server (feature `observability`):
59// App::new().observability(observability.clone()).get(...).run(..).await
60
61// Application metrics use the OpenTelemetry API directly:
62let payments = observability
63 .meter("payment-service")
64 .u64_counter("payments.processed")
65 .build();
66payments.add(1, &[KeyValue::new("payment.type", "transfer")]);
67
68// Flush and stop exporters (Kerniq's `App` does this after draining).
69let report = observability.shutdown().await;
70# Ok(()) }
71```
72"####
73)]
74#![warn(missing_docs)]
75#![cfg_attr(docsrs, feature(doc_cfg))]
76
77mod config;
78#[cfg(feature = "logs")]
79pub mod logging;
80#[cfg(feature = "otlp")]
81mod otlp;
82#[cfg(feature = "runtime")]
83mod runtime;
84mod stats;
85
86use std::sync::Arc;
87use std::sync::atomic::{AtomicBool, Ordering};
88use std::time::{Duration, Instant};
89
90#[cfg(feature = "logs")]
91pub use config::LogsConfig;
92#[cfg(feature = "metrics")]
93pub use config::MetricsConfig;
94#[cfg(feature = "tracing")]
95pub use config::TracesConfig;
96pub use config::{BatchConfig, ConsoleFormat, ConsoleTarget, Sampling};
97#[cfg(feature = "otlp-tls")]
98pub use otlp::TlsConfig;
99#[cfg(feature = "otlp")]
100pub use otlp::{OtlpExporter, Protocol, Retry};
101pub use stats::{SignalStats, TelemetryStats};
102
103pub use kerniq_observability_axum::HttpInstrumentation;
104#[cfg(feature = "tracing")]
106pub use kerniq_observability_axum::propagation;
107#[cfg(feature = "prometheus")]
108pub use kerniq_observability_prometheus::{PrometheusConfig, PrometheusExporter};
109pub use kerniq_telemetry::{self as telemetry, BoundedLabel, ServiceInfo};
110
111pub use opentelemetry;
113pub use opentelemetry::KeyValue;
114#[cfg(any(feature = "metrics", feature = "tracing", feature = "logs"))]
116pub use opentelemetry_sdk;
117
118use stats::Counters;
119
120pub mod prelude {
122 #[cfg(feature = "logs")]
123 pub use crate::LogsConfig;
124 #[cfg(feature = "metrics")]
125 pub use crate::MetricsConfig;
126 #[cfg(feature = "prometheus")]
127 pub use crate::PrometheusConfig;
128 #[cfg(feature = "tracing")]
129 pub use crate::TracesConfig;
130 pub use crate::{
131 BatchConfig, BoundedLabel, ConsoleFormat, Observability, ObservabilityBuilder, Sampling,
132 };
133 #[cfg(feature = "otlp")]
134 pub use crate::{OtlpExporter, Protocol};
135 pub use opentelemetry::KeyValue;
136 #[cfg(feature = "metrics")]
137 pub use opentelemetry::metrics::{Counter, Gauge, Histogram, Meter, UpDownCounter};
138}
139
140#[derive(Debug, thiserror::Error)]
142#[non_exhaustive]
143pub enum Error {
144 #[error("invalid observability configuration: {0}")]
146 Config(String),
147 #[error("failed to build the {signal} exporter: {source}")]
149 Exporter {
150 signal: &'static str,
152 #[source]
154 source: Box<dyn std::error::Error + Send + Sync>,
155 },
156 #[error(
158 "a global tracing subscriber is already installed; call \
159 `.install_subscriber(false)` and add `observability.tracing_layer()` to your own subscriber"
160 )]
161 SubscriberAlreadySet,
162 #[cfg(feature = "prometheus")]
164 #[error(transparent)]
165 Prometheus(#[from] kerniq_observability_prometheus::Error),
166}
167
168impl From<Error> for kerniq_core::Error {
169 fn from(error: Error) -> Self {
170 kerniq_core::Error::internal(error)
171 }
172}
173
174#[cfg(any(feature = "tracing", feature = "logs"))]
178const BRIDGE_EXCLUSIONS: &str = "opentelemetry=off,opentelemetry_sdk=off,opentelemetry_otlp=off,\
179 opentelemetry_prometheus=off,tonic=off,h2=off,hyper=off,\
180 hyper_util=off,reqwest=off,tower=off,tower_http=off";
181
182#[must_use]
188pub struct ObservabilityBuilder {
189 service: ServiceInfo,
190 #[cfg(feature = "metrics")]
191 metrics: Option<MetricsConfig>,
192 #[cfg(feature = "tracing")]
193 traces: Option<TracesConfig>,
194 #[cfg(feature = "logs")]
195 logs: Option<LogsConfig>,
196 install_subscriber: bool,
197 shutdown_timeout: Duration,
198 http_exclude: Vec<String>,
199 http_scheme: &'static str,
200}
201
202impl std::fmt::Debug for ObservabilityBuilder {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 let mut d = f.debug_struct("ObservabilityBuilder");
205 d.field("service", &self.service);
206 #[cfg(feature = "metrics")]
207 d.field("metrics", &self.metrics);
208 #[cfg(feature = "tracing")]
209 d.field("traces", &self.traces);
210 #[cfg(feature = "logs")]
211 d.field("logs", &self.logs);
212 d.field("install_subscriber", &self.install_subscriber)
213 .field("shutdown_timeout", &self.shutdown_timeout)
214 .field("http_exclude", &self.http_exclude)
215 .finish()
216 }
217}
218
219impl Default for ObservabilityBuilder {
220 fn default() -> Self {
221 Self {
222 service: ServiceInfo::default(),
223 #[cfg(feature = "metrics")]
224 metrics: None,
225 #[cfg(feature = "tracing")]
226 traces: None,
227 #[cfg(feature = "logs")]
228 logs: None,
229 install_subscriber: true,
230 shutdown_timeout: Duration::from_secs(5),
231 http_exclude: Vec::new(),
232 http_scheme: "http",
233 }
234 }
235}
236
237impl ObservabilityBuilder {
238 pub fn service_name(mut self, name: impl Into<String>) -> Self {
241 self.service.name = Some(name.into());
242 self
243 }
244
245 pub fn service_version(mut self, version: impl Into<String>) -> Self {
247 self.service.version = Some(version.into());
248 self
249 }
250
251 pub fn service_namespace(mut self, namespace: impl Into<String>) -> Self {
253 self.service.namespace = Some(namespace.into());
254 self
255 }
256
257 pub fn environment(mut self, environment: impl Into<String>) -> Self {
259 self.service.environment = Some(environment.into());
260 self
261 }
262
263 pub fn instance_id(mut self, id: impl Into<String>) -> Self {
265 self.service.instance_id = Some(id.into());
266 self
267 }
268
269 pub fn resource_attribute(
272 mut self,
273 key: impl Into<opentelemetry::Key>,
274 value: impl Into<opentelemetry::Value>,
275 ) -> Self {
276 self.service.attributes.push(KeyValue::new(key, value));
277 self
278 }
279
280 #[cfg(feature = "metrics")]
282 pub fn metrics(mut self, config: MetricsConfig) -> Self {
283 self.metrics = Some(config);
284 self
285 }
286
287 #[cfg(feature = "tracing")]
289 pub fn traces(mut self, config: TracesConfig) -> Self {
290 self.traces = Some(config);
291 self
292 }
293
294 #[cfg(feature = "logs")]
296 pub fn logs(mut self, config: LogsConfig) -> Self {
297 self.logs = Some(config);
298 self
299 }
300
301 pub fn install_subscriber(mut self, install: bool) -> Self {
307 self.install_subscriber = install;
308 self
309 }
310
311 pub fn shutdown_timeout(mut self, timeout: Duration) -> Self {
313 self.shutdown_timeout = timeout;
314 self
315 }
316
317 pub fn exclude_http_route(mut self, route: impl Into<String>) -> Self {
320 self.http_exclude.push(route.into());
321 self
322 }
323
324 pub fn http_scheme(mut self, scheme: &'static str) -> Self {
327 self.http_scheme = scheme;
328 self
329 }
330
331 pub async fn build(self) -> Result<Observability, Error> {
339 self.build_sync()
340 }
341
342 fn build_sync(self) -> Result<Observability, Error> {
343 let ObservabilityBuilder {
344 mut service,
345 #[cfg(feature = "metrics")]
346 metrics,
347 #[cfg(feature = "tracing")]
348 traces,
349 #[cfg(feature = "logs")]
350 logs,
351 install_subscriber,
352 shutdown_timeout,
353 http_exclude,
354 http_scheme,
355 } = self;
356
357 if service.instance_id.is_none() {
358 service.instance_id = Some(kerniq_telemetry::resource::default_instance_id());
359 }
360 #[cfg(any(feature = "metrics", feature = "tracing", feature = "logs"))]
361 let resource = {
362 let mut builder = opentelemetry_sdk::Resource::builder();
363 if let Some(name) = &service.name {
364 builder = builder.with_service_name(name.clone());
365 }
366 builder.with_attributes(service.to_attributes()).build()
367 };
368
369 let counters = Arc::new(Counters::default());
370
371 #[cfg(feature = "metrics")]
373 let mut http_metrics = false;
374 #[cfg(feature = "prometheus")]
375 let mut prometheus_handle = None;
376 #[cfg(feature = "metrics")]
377 let meter_provider = match metrics {
378 None => None,
379 Some(config) => {
380 http_metrics = config.http;
381 let (provider, _prometheus) = build_metrics(config, &resource, &counters)?;
382 #[cfg(feature = "prometheus")]
383 {
384 prometheus_handle = _prometheus;
385 }
386 Some(provider)
387 }
388 };
389
390 #[cfg(feature = "tracing")]
392 let (tracer_provider, traces_filter, activate_context) = match traces {
393 None => (None, None, false),
394 Some(config) => {
395 let filter = config.filter.clone();
396 let activate = config.activate_context;
397 (
398 Some(build_traces(config, &resource, &counters)?),
399 filter,
400 activate,
401 )
402 }
403 };
404
405 #[cfg(feature = "logs")]
407 let mut console = None;
408 #[cfg(feature = "logs")]
409 let mut console_guard = None;
410 #[cfg(feature = "logs")]
411 let (logger_provider, logs_filter, bridge_filter) = match logs {
412 None => (None, None, None),
413 Some(config) => {
414 let filter = config.filter.clone();
415 #[cfg(feature = "otlp")]
416 let bridge_filter = config.otlp_filter.clone().or_else(|| filter.clone());
417 #[cfg(not(feature = "otlp"))]
418 let bridge_filter = filter.clone();
419 if let Some((format, target)) = config.console {
420 let (writer, guard) = console_writer(target, config.non_blocking, &counters);
421 console = Some(ConsoleSetup {
422 format,
423 writer,
424 target,
425 });
426 console_guard = guard;
427 }
428 let provider = if config.has_bridge() {
429 Some(build_logs(config, &resource, &counters)?)
430 } else {
431 None
432 };
433 (provider, filter, bridge_filter)
434 }
435 };
436
437 #[cfg(feature = "tracing")]
439 let propagator: Arc<
440 dyn opentelemetry::propagation::TextMapPropagator + Send + Sync,
441 > = Arc::new(opentelemetry::propagation::TextMapCompositePropagator::new(
442 vec![
443 Box::new(opentelemetry_sdk::propagation::TraceContextPropagator::new()),
444 Box::new(opentelemetry_sdk::propagation::BaggagePropagator::new()),
445 ],
446 ));
447
448 #[cfg_attr(not(feature = "prometheus"), allow(unused_mut))]
449 let mut exclude = http_exclude;
450 #[cfg(feature = "prometheus")]
451 if let Some(prom) = &prometheus_handle {
452 exclude.push(prom.config().scrape_path().to_owned());
453 }
454
455 let inner = Inner {
456 #[cfg(any(feature = "metrics", feature = "tracing", feature = "logs"))]
457 resource,
458 #[cfg(feature = "metrics")]
459 meter_provider,
460 #[cfg(feature = "metrics")]
461 http_metrics,
462 #[cfg(feature = "prometheus")]
463 prometheus: prometheus_handle,
464 #[cfg(feature = "tracing")]
465 tracer_provider,
466 #[cfg(feature = "tracing")]
467 traces_filter,
468 #[cfg(feature = "tracing")]
469 activate_context,
470 #[cfg(feature = "tracing")]
471 propagator,
472 #[cfg(feature = "logs")]
473 logger_provider,
474 #[cfg(feature = "logs")]
475 console,
476 #[cfg(feature = "logs")]
477 console_guard: std::sync::Mutex::new(console_guard),
478 #[cfg(feature = "logs")]
479 logs_filter,
480 #[cfg(feature = "logs")]
481 bridge_filter,
482 service,
483 counters,
484 http_exclude: exclude,
485 http_scheme,
486 shutdown_timeout,
487 shut_down: AtomicBool::new(false),
488 };
489 let observability = Observability {
490 inner: Arc::new(inner),
491 };
492
493 if install_subscriber && observability.has_subscriber_layers() {
494 observability.install_subscriber()?;
495 }
496 Ok(observability)
497 }
498}
499
500#[cfg(feature = "metrics")]
501fn build_metrics(
502 config: MetricsConfig,
503 resource: &opentelemetry_sdk::Resource,
504 counters: &Arc<Counters>,
505) -> Result<
506 (
507 opentelemetry_sdk::metrics::SdkMeterProvider,
508 Option<PrometheusHandle>,
509 ),
510 Error,
511> {
512 use opentelemetry::metrics::MeterProvider as _;
513 use opentelemetry_sdk::metrics::SdkMeterProvider;
514
515 let mut builder = SdkMeterProvider::builder().with_resource(resource.clone());
516 if let Some(limit) = config.cardinality_limit {
517 if limit == 0 {
518 return Err(Error::Config(
519 "cardinality_limit must be greater than zero".into(),
520 ));
521 }
522 builder = builder.with_view(move |_: &opentelemetry_sdk::metrics::Instrument| {
523 opentelemetry_sdk::metrics::Stream::builder()
524 .with_cardinality_limit(limit)
525 .build()
526 .ok()
527 });
528 }
529
530 #[cfg(feature = "otlp")]
531 for exporter in &config.otlp {
532 let exporter = stats::metrics::CountingExporter {
533 inner: otlp::metric_exporter(exporter, config.delta_temporality)?,
534 counters: counters.clone(),
535 };
536 let reader = opentelemetry_sdk::metrics::PeriodicReader::builder(exporter)
537 .with_interval(config.export_interval)
538 .build();
539 builder = builder.with_reader(reader);
540 }
541
542 #[cfg(feature = "prometheus")]
543 let prometheus = match &config.prometheus {
544 Some(prom) => {
545 let (reader, handle) = PrometheusExporter::build(prom.clone())?;
546 builder = builder.with_reader(reader);
547 Some(handle)
548 }
549 None => None,
550 };
551 #[cfg(not(feature = "prometheus"))]
552 let prometheus = None;
553
554 for hook in config.readers {
555 builder = hook(builder, config.export_interval);
556 }
557 let provider = builder.build();
558
559 let internal = provider.meter("kerniq.observability");
560 stats::metrics::register(&internal, counters);
561 #[cfg(feature = "runtime")]
562 if config.runtime {
563 runtime::register(&internal);
564 }
565 Ok((provider, prometheus))
566}
567
568#[cfg(feature = "tracing")]
569fn build_traces(
570 config: TracesConfig,
571 resource: &opentelemetry_sdk::Resource,
572 counters: &Arc<Counters>,
573) -> Result<opentelemetry_sdk::trace::SdkTracerProvider, Error> {
574 use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
575
576 config.batch.validate("traces").map_err(Error::Config)?;
577 let sampler = match config.sampling {
578 Sampling::AlwaysOn => Sampler::AlwaysOn,
579 Sampling::AlwaysOff => Sampler::AlwaysOff,
580 Sampling::Ratio(ratio) => {
581 if !(0.0..=1.0).contains(&ratio) {
582 return Err(Error::Config(format!(
583 "sampling ratio {ratio} is outside 0.0..=1.0"
584 )));
585 }
586 Sampler::TraceIdRatioBased(ratio)
587 }
588 };
589 let sampler = if config.parent_based {
590 Sampler::ParentBased(Box::new(sampler))
591 } else {
592 sampler
593 };
594 let mut builder = SdkTracerProvider::builder()
595 .with_resource(resource.clone())
596 .with_sampler(sampler);
597
598 #[cfg(feature = "otlp")]
599 for exporter in &config.otlp {
600 let batch = opentelemetry_sdk::trace::BatchConfigBuilder::default()
601 .with_max_queue_size(config.batch.max_queue_size)
602 .with_max_export_batch_size(config.batch.max_export_batch_size)
603 .with_scheduled_delay(config.batch.scheduled_delay)
604 .build();
605 let exporter = stats::traces::CountingExporter {
606 inner: otlp::span_exporter(
607 &exporter.with_default_timeout(config.batch.export_timeout),
608 )?,
609 counters: counters.clone(),
610 };
611 let processor = opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter)
612 .with_batch_config(batch)
613 .build();
614 builder = builder.with_span_processor(stats::traces::CountingProcessor {
615 inner: processor,
616 counters: counters.clone(),
617 });
618 }
619 #[cfg(not(feature = "otlp"))]
620 let _ = counters;
621
622 for hook in config.processors {
623 builder = hook(builder);
624 }
625 Ok(builder.build())
626}
627
628#[cfg(feature = "logs")]
629fn build_logs(
630 config: LogsConfig,
631 resource: &opentelemetry_sdk::Resource,
632 counters: &Arc<Counters>,
633) -> Result<opentelemetry_sdk::logs::SdkLoggerProvider, Error> {
634 use opentelemetry_sdk::logs::SdkLoggerProvider;
635
636 config.batch.validate("logs").map_err(Error::Config)?;
637 let mut builder = SdkLoggerProvider::builder().with_resource(resource.clone());
638
639 #[cfg(feature = "otlp")]
640 for exporter in &config.otlp {
641 let batch = opentelemetry_sdk::logs::BatchConfigBuilder::default()
642 .with_max_queue_size(config.batch.max_queue_size)
643 .with_max_export_batch_size(config.batch.max_export_batch_size)
644 .with_scheduled_delay(config.batch.scheduled_delay)
645 .build();
646 let exporter = stats::logs::CountingExporter {
647 inner: otlp::log_exporter(&exporter.with_default_timeout(config.batch.export_timeout))?,
648 counters: counters.clone(),
649 };
650 let processor = opentelemetry_sdk::logs::BatchLogProcessor::builder(exporter)
651 .with_batch_config(batch)
652 .build();
653 builder = builder.with_log_processor(stats::logs::CountingProcessor {
654 inner: processor,
655 counters: counters.clone(),
656 });
657 }
658 #[cfg(not(feature = "otlp"))]
659 let _ = counters;
660
661 for hook in config.processors {
662 builder = hook(builder);
663 }
664 Ok(builder.build())
665}
666
667#[cfg(feature = "logs")]
668#[derive(Clone)]
669enum ConsoleWriter {
670 NonBlocking(tracing_appender::non_blocking::NonBlocking),
671 Stdout,
672 Stderr,
673}
674
675#[cfg(feature = "logs")]
676struct ConsoleSetup {
677 format: ConsoleFormat,
678 writer: ConsoleWriter,
679 target: ConsoleTarget,
680}
681
682#[cfg(feature = "logs")]
683fn console_writer(
684 target: ConsoleTarget,
685 buffered_lines: Option<usize>,
686 counters: &Arc<Counters>,
687) -> (
688 ConsoleWriter,
689 Option<tracing_appender::non_blocking::WorkerGuard>,
690) {
691 let Some(lines) = buffered_lines else {
692 return (
693 match target {
694 ConsoleTarget::Stdout => ConsoleWriter::Stdout,
695 ConsoleTarget::Stderr => ConsoleWriter::Stderr,
696 },
697 None,
698 );
699 };
700 let builder = tracing_appender::non_blocking::NonBlockingBuilder::default()
701 .buffered_lines_limit(lines.max(1))
702 .lossy(true)
703 .thread_name("kerniq-log-writer");
704 let (writer, guard) = match target {
705 ConsoleTarget::Stdout => builder.finish(std::io::stdout()),
706 ConsoleTarget::Stderr => builder.finish(std::io::stderr()),
707 };
708 let _ = counters.console_dropped.set(writer.error_counter());
709 (ConsoleWriter::NonBlocking(writer), Some(guard))
710}
711
712#[cfg(feature = "prometheus")]
713type PrometheusHandle = PrometheusExporter;
714#[cfg(all(feature = "metrics", not(feature = "prometheus")))]
715type PrometheusHandle = ();
716
717struct Inner {
722 #[cfg(any(feature = "metrics", feature = "tracing", feature = "logs"))]
723 resource: opentelemetry_sdk::Resource,
724 #[cfg(feature = "metrics")]
725 meter_provider: Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
726 #[cfg(feature = "metrics")]
727 http_metrics: bool,
728 #[cfg(feature = "prometheus")]
729 prometheus: Option<PrometheusExporter>,
730 #[cfg(feature = "tracing")]
731 tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
732 #[cfg(feature = "tracing")]
733 traces_filter: Option<String>,
734 #[cfg(feature = "tracing")]
735 activate_context: bool,
736 #[cfg(feature = "tracing")]
737 propagator: Arc<dyn opentelemetry::propagation::TextMapPropagator + Send + Sync>,
738 #[cfg(feature = "logs")]
739 logger_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
740 #[cfg(feature = "logs")]
741 console: Option<ConsoleSetup>,
742 #[cfg(feature = "logs")]
743 console_guard: std::sync::Mutex<Option<tracing_appender::non_blocking::WorkerGuard>>,
744 #[cfg(feature = "logs")]
745 logs_filter: Option<String>,
746 #[cfg(feature = "logs")]
747 bridge_filter: Option<String>,
748 service: ServiceInfo,
749 counters: Arc<Counters>,
750 http_exclude: Vec<String>,
751 http_scheme: &'static str,
752 shutdown_timeout: Duration,
753 shut_down: AtomicBool,
754}
755
756#[derive(Clone)]
763pub struct Observability {
764 inner: Arc<Inner>,
765}
766
767impl std::fmt::Debug for Observability {
768 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
769 let i = &*self.inner;
770 let mut d = f.debug_struct("Observability");
771 d.field("service", &i.service.name);
772 #[cfg(feature = "metrics")]
773 d.field("metrics", &i.meter_provider.is_some());
774 #[cfg(feature = "prometheus")]
775 d.field(
776 "prometheus",
777 &i.prometheus
778 .as_ref()
779 .map(|p| p.config().scrape_path().to_owned()),
780 );
781 #[cfg(feature = "tracing")]
782 d.field("traces", &i.tracer_provider.is_some());
783 #[cfg(feature = "logs")]
784 d.field("console_logs", &i.console.as_ref().map(|c| c.format))
785 .field("log_export", &i.logger_provider.is_some());
786 d.finish_non_exhaustive()
787 }
788}
789
790#[derive(Debug, Clone)]
792pub struct ShutdownReport {
793 pub duration: Duration,
795 pub stats: TelemetryStats,
798 pub errors: Vec<String>,
801 pub timed_out: bool,
803}
804
805impl Observability {
806 pub fn builder() -> ObservabilityBuilder {
808 ObservabilityBuilder::default()
809 }
810
811 pub fn disabled() -> Self {
813 ObservabilityBuilder::default()
814 .install_subscriber(false)
815 .build_sync()
816 .expect("an empty configuration is always valid")
817 }
818
819 pub fn service(&self) -> &ServiceInfo {
821 &self.inner.service
822 }
823
824 #[cfg(any(feature = "metrics", feature = "tracing", feature = "logs"))]
826 pub fn resource(&self) -> &opentelemetry_sdk::Resource {
827 &self.inner.resource
828 }
829
830 #[cfg(feature = "metrics")]
835 pub fn meter(&self, name: &'static str) -> opentelemetry::metrics::Meter {
836 use opentelemetry::metrics::MeterProvider as _;
837 match &self.inner.meter_provider {
838 Some(provider) => provider.meter(name),
839 None => opentelemetry::metrics::NoopMeterProvider::new().meter(name),
840 }
841 }
842
843 #[cfg(feature = "metrics")]
845 pub fn meter_provider(&self) -> Option<&opentelemetry_sdk::metrics::SdkMeterProvider> {
846 self.inner.meter_provider.as_ref()
847 }
848
849 #[cfg(feature = "tracing")]
853 pub fn tracer(&self, name: &'static str) -> opentelemetry_sdk::trace::Tracer {
854 use opentelemetry::trace::TracerProvider as _;
855 match &self.inner.tracer_provider {
856 Some(provider) => provider.tracer(name),
857 None => opentelemetry_sdk::trace::SdkTracerProvider::builder()
858 .with_sampler(opentelemetry_sdk::trace::Sampler::AlwaysOff)
859 .build()
860 .tracer(name),
861 }
862 }
863
864 #[cfg(feature = "tracing")]
866 pub fn tracer_provider(&self) -> Option<&opentelemetry_sdk::trace::SdkTracerProvider> {
867 self.inner.tracer_provider.as_ref()
868 }
869
870 #[cfg(feature = "tracing")]
873 pub fn propagator(
874 &self,
875 ) -> Arc<dyn opentelemetry::propagation::TextMapPropagator + Send + Sync> {
876 self.inner.propagator.clone()
877 }
878
879 #[cfg(feature = "logs")]
881 pub fn logger_provider(&self) -> Option<&opentelemetry_sdk::logs::SdkLoggerProvider> {
882 self.inner.logger_provider.as_ref()
883 }
884
885 #[cfg(feature = "prometheus")]
887 pub fn prometheus(&self) -> Option<&PrometheusExporter> {
888 self.inner.prometheus.as_ref()
889 }
890
891 pub fn http_instrumentation(&self) -> HttpInstrumentation {
896 let i = &*self.inner;
897 let mut http = HttpInstrumentation::new().scheme(i.http_scheme);
898 #[cfg(feature = "metrics")]
899 if i.http_metrics
900 && let Some(provider) = &i.meter_provider
901 {
902 use opentelemetry::metrics::MeterProvider as _;
903 let meter = provider.meter_with_scope(
904 opentelemetry::InstrumentationScope::builder("kerniq.http")
905 .with_version(env!("CARGO_PKG_VERSION"))
906 .build(),
907 );
908 http = http.meter(&meter);
909 }
910 #[cfg(feature = "tracing")]
911 {
912 http = http.tracing(i.tracer_provider.is_some() || self.has_console_logs());
913 if i.tracer_provider.is_some() {
914 http = http.shared_propagator(i.propagator.clone());
915 }
916 }
917 for route in &i.http_exclude {
918 http = http.exclude(route.clone());
919 }
920 http
921 }
922
923 #[cfg(feature = "tracing")]
927 fn needs_context_activation(&self) -> bool {
928 #[cfg(feature = "logs")]
929 if self.inner.logger_provider.is_some() {
930 return true;
931 }
932 self.inner.activate_context
933 }
934
935 #[cfg(feature = "tracing")]
936 fn has_console_logs(&self) -> bool {
937 #[cfg(feature = "logs")]
938 return self.inner.console.is_some();
939 #[cfg(not(feature = "logs"))]
940 false
941 }
942
943 pub fn stats(&self) -> TelemetryStats {
946 self.inner.counters.snapshot()
947 }
948
949 pub fn force_flush(&self) -> Vec<String> {
952 #[allow(unused_mut)]
953 let mut errors = Vec::new();
954 #[cfg(feature = "tracing")]
955 if let Some(p) = &self.inner.tracer_provider
956 && let Err(e) = p.force_flush()
957 {
958 errors.push(format!("traces: {e}"));
959 }
960 #[cfg(feature = "logs")]
961 if let Some(p) = &self.inner.logger_provider
962 && let Err(e) = p.force_flush()
963 {
964 errors.push(format!("logs: {e}"));
965 }
966 #[cfg(feature = "metrics")]
967 if let Some(p) = &self.inner.meter_provider
968 && let Err(e) = p.force_flush()
969 {
970 errors.push(format!("metrics: {e}"));
971 }
972 errors
973 }
974
975 pub async fn shutdown(&self) -> ShutdownReport {
983 let start = Instant::now();
984 if self.inner.shut_down.swap(true, Ordering::AcqRel) {
985 return ShutdownReport {
986 duration: Duration::ZERO,
987 stats: self.stats(),
988 errors: Vec::new(),
989 timed_out: false,
990 };
991 }
992 let timeout = self.inner.shutdown_timeout;
993 let inner = self.inner.clone();
994 let task = tokio::task::spawn_blocking(move || shutdown_providers(&inner, timeout));
995 #[allow(unused_mut)]
996 let (mut errors, timed_out) = match tokio::time::timeout(timeout, task).await {
997 Ok(Ok(errors)) => (errors, false),
998 Ok(Err(join)) => (vec![format!("shutdown task failed: {join}")], false),
999 Err(_) => (
1000 vec![format!(
1001 "telemetry shutdown did not finish within {timeout:?}"
1002 )],
1003 true,
1004 ),
1005 };
1006 #[cfg(feature = "logs")]
1008 {
1009 let guard = self
1010 .inner
1011 .console_guard
1012 .lock()
1013 .unwrap_or_else(|p| p.into_inner())
1014 .take();
1015 if let Some(guard) = guard
1016 && tokio::task::spawn_blocking(move || drop(guard))
1017 .await
1018 .is_err()
1019 {
1020 errors.push("console log writer did not flush".to_owned());
1021 }
1022 }
1023 let report = ShutdownReport {
1024 duration: start.elapsed(),
1025 stats: self.stats(),
1026 errors,
1027 timed_out,
1028 };
1029 tracing::debug!(duration = ?report.duration, timed_out, "telemetry shut down");
1030 report
1031 }
1032
1033 pub fn install_global_providers(&self) {
1037 #[cfg(feature = "metrics")]
1038 if let Some(p) = &self.inner.meter_provider {
1039 opentelemetry::global::set_meter_provider(p.clone());
1040 }
1041 #[cfg(feature = "tracing")]
1042 {
1043 if let Some(p) = &self.inner.tracer_provider {
1044 opentelemetry::global::set_tracer_provider(p.clone());
1045 }
1046 opentelemetry::global::set_text_map_propagator(
1047 opentelemetry::propagation::TextMapCompositePropagator::new(vec![
1048 Box::new(opentelemetry_sdk::propagation::TraceContextPropagator::new()),
1049 Box::new(opentelemetry_sdk::propagation::BaggagePropagator::new()),
1050 ]),
1051 );
1052 }
1053 }
1054
1055 fn has_subscriber_layers(&self) -> bool {
1056 #[allow(unused_mut)]
1057 let mut any = false;
1058 #[cfg(feature = "tracing")]
1059 {
1060 any |= self.inner.tracer_provider.is_some();
1061 }
1062 #[cfg(feature = "logs")]
1063 {
1064 any |= self.inner.console.is_some() || self.inner.logger_provider.is_some();
1065 }
1066 any
1067 }
1068
1069 fn install_subscriber(&self) -> Result<(), Error> {
1070 #[cfg(any(feature = "tracing", feature = "logs"))]
1071 {
1072 use tracing_subscriber::layer::SubscriberExt as _;
1073 let subscriber = tracing_subscriber::registry().with(self.tracing_layer());
1074 tracing::subscriber::set_global_default(subscriber)
1075 .map_err(|_| Error::SubscriberAlreadySet)?;
1076 }
1077 Ok(())
1078 }
1079
1080 #[cfg(any(feature = "tracing", feature = "logs"))]
1103 pub fn tracing_layer<S>(&self) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync>
1104 where
1105 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a> + Send + Sync,
1106 {
1107 use tracing_subscriber::Layer as _;
1108 type Boxed<S> = Box<dyn tracing_subscriber::Layer<S> + Send + Sync>;
1109 #[allow(unused_mut)]
1113 let mut layers: Vec<Boxed<S>> = Vec::new();
1114
1115 #[cfg(feature = "logs")]
1116 if let Some(console) = &self.inner.console {
1117 let filter = env_filter(self.inner.logs_filter.as_deref(), None);
1118 layers.push(console_layer::<S>(console).with_filter(filter).boxed());
1119 }
1120
1121 #[cfg(feature = "tracing")]
1122 if let Some(provider) = &self.inner.tracer_provider {
1123 use opentelemetry::trace::TracerProvider as _;
1124 let tracer = provider.tracer("kerniq");
1125 let filter = env_filter(
1126 Some(self.inner.traces_filter.as_deref().unwrap_or("info")),
1127 Some(BRIDGE_EXCLUSIONS),
1128 );
1129 layers.push(
1130 tracing_opentelemetry::layer()
1131 .with_tracer(tracer)
1132 .with_error_records_to_exceptions(true)
1133 .with_tracked_inactivity(false)
1136 .with_context_activation(self.needs_context_activation())
1137 .with_filter(filter)
1138 .boxed(),
1139 );
1140 }
1141
1142 #[cfg(feature = "logs")]
1143 if let Some(provider) = &self.inner.logger_provider {
1144 let filter = env_filter(self.inner.bridge_filter.as_deref(), Some(BRIDGE_EXCLUSIONS));
1145 layers.push(
1146 opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(provider)
1147 .with_filter(filter)
1148 .boxed(),
1149 );
1150 }
1151
1152 layers
1153 .into_iter()
1154 .reduce(|chain, layer| chain.and_then(layer).boxed())
1155 .unwrap_or_else(|| tracing_subscriber::layer::Identity::new().boxed())
1156 }
1157}
1158
1159#[cfg(feature = "tracing")]
1162pub fn current_context() -> opentelemetry::Context {
1163 use tracing_opentelemetry::OpenTelemetrySpanExt as _;
1164 tracing::Span::current().context()
1165}
1166
1167#[cfg(feature = "tracing")]
1171pub fn current_trace_id() -> Option<String> {
1172 use opentelemetry::trace::TraceContextExt as _;
1173 let cx = current_context();
1174 let span = cx.span();
1175 let sc = span.span_context();
1176 sc.is_valid().then(|| sc.trace_id().to_string())
1177}
1178
1179#[cfg(any(feature = "tracing", feature = "logs"))]
1180fn env_filter(directives: Option<&str>, extra: Option<&str>) -> tracing_subscriber::EnvFilter {
1181 let base = directives
1182 .map(str::to_owned)
1183 .or_else(|| {
1184 std::env::var("RUST_LOG")
1185 .ok()
1186 .filter(|s| !s.trim().is_empty())
1187 })
1188 .unwrap_or_else(|| "info".to_owned());
1189 let full = match extra {
1190 Some(extra) => format!("{base},{extra}"),
1191 None => base,
1192 };
1193 tracing_subscriber::EnvFilter::builder()
1194 .parse(&full)
1195 .unwrap_or_else(|err| {
1196 eprintln!("kerniq-observability: invalid log filter `{full}` ({err}); using `info`");
1197 tracing_subscriber::EnvFilter::new("info")
1198 })
1199}
1200
1201#[cfg(feature = "logs")]
1202fn console_layer<S>(console: &ConsoleSetup) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync>
1203where
1204 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a> + Send + Sync,
1205{
1206 use std::io::IsTerminal as _;
1207 use tracing_subscriber::Layer as _;
1208 use tracing_subscriber::fmt::writer::BoxMakeWriter;
1209
1210 let writer = match console.writer.clone() {
1211 ConsoleWriter::NonBlocking(w) => BoxMakeWriter::new(w),
1212 ConsoleWriter::Stdout => BoxMakeWriter::new(std::io::stdout),
1213 ConsoleWriter::Stderr => BoxMakeWriter::new(std::io::stderr),
1214 };
1215 let ansi = match console.target {
1216 ConsoleTarget::Stdout => std::io::stdout().is_terminal(),
1217 ConsoleTarget::Stderr => std::io::stderr().is_terminal(),
1218 };
1219 match console.format {
1220 ConsoleFormat::Json => logging::json_layer(writer),
1221 ConsoleFormat::Pretty => tracing_subscriber::fmt::layer()
1222 .pretty()
1223 .with_ansi(ansi)
1224 .with_writer(writer)
1225 .boxed(),
1226 ConsoleFormat::Compact => tracing_subscriber::fmt::layer()
1227 .compact()
1228 .with_ansi(ansi)
1229 .with_writer(writer)
1230 .boxed(),
1231 }
1232}
1233
1234fn shutdown_providers(inner: &Inner, timeout: Duration) -> Vec<String> {
1235 let deadline = Instant::now() + timeout;
1236 let remaining = || deadline.saturating_duration_since(Instant::now());
1237 #[allow(unused_mut)]
1238 let mut errors = Vec::new();
1239 #[cfg(feature = "tracing")]
1242 if let Some(p) = &inner.tracer_provider
1243 && let Err(e) = p.shutdown_with_timeout(remaining())
1244 {
1245 errors.push(format!("traces: {e}"));
1246 }
1247 #[cfg(feature = "logs")]
1248 if let Some(p) = &inner.logger_provider
1249 && let Err(e) = p.shutdown_with_timeout(remaining())
1250 {
1251 errors.push(format!("logs: {e}"));
1252 }
1253 #[cfg(feature = "metrics")]
1254 if let Some(p) = &inner.meter_provider
1255 && let Err(e) = p.shutdown_with_timeout(remaining())
1256 {
1257 errors.push(format!("metrics: {e}"));
1258 }
1259 let _ = (remaining, inner);
1260 errors
1261}