diff --git a/Cargo.toml b/Cargo.toml index 1f226c5..47a9ee8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ opentelemetry-zipkin = { version = "0.31", default-features = false } rstest = "0.26" tokio = { version = "1", default-features = false } tokio-stream = { version = "0.1", default-features = false } -tonic = { version = "0.14", default-features = false } # should be sync with opentelemetry-proto +tonic = { version = "0.14", default-features = false } # should be sync with opentelemetry-proto tower = { version = "0.5", default-features = false } tracing = "0.1" tracing-opentelemetry = "0.32" diff --git a/examples/logging/Cargo.toml b/examples/logging/Cargo.toml new file mode 100644 index 0000000..20d3ed0 --- /dev/null +++ b/examples/logging/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "examples-logging" +publish = false +edition.workspace = true +version.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [ + "otlp", + "tracing_subscriber_ext", + "logs" +] } +memory-stats = "1" +opentelemetry = { workspace = true } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +tracing = { workspace = true } +tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk" } diff --git a/examples/logging/src/main.rs b/examples/logging/src/main.rs new file mode 100644 index 0000000..b5f7d40 --- /dev/null +++ b/examples/logging/src/main.rs @@ -0,0 +1,25 @@ +#[tracing::instrument] +async fn log() { + tracing::error!("This is ground control to Major Tom"); + tracing::warn!("Houston, we have a problem"); + tracing::info!("We have contact"); + tracing::debug!("Roger, copy that"); + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; +} + +#[tracing::instrument] +async fn calc(a: i32, b: i32) { + let result = a + b; + tracing::info!(result, "calculated result"); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // setting up tracing + let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; + + log().await; + calc(1, 2).await; + + Ok(()) +} diff --git a/init-tracing-opentelemetry/Cargo.toml b/init-tracing-opentelemetry/Cargo.toml index ec42ebd..4362333 100644 --- a/init-tracing-opentelemetry/Cargo.toml +++ b/init-tracing-opentelemetry/Cargo.toml @@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docs_rs"] [dependencies] opentelemetry = { workspace = true } +opentelemetry-appender-tracing = { version = "0.31", default-features = false, optional = true } opentelemetry-aws = { workspace = true, optional = true, features = ["trace"] } opentelemetry-jaeger-propagator = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true, features = [ @@ -28,9 +29,7 @@ opentelemetry-stdout = { workspace = true, features = [ ], optional = true } opentelemetry-semantic-conventions = { workspace = true, optional = true } opentelemetry-zipkin = { workspace = true, features = [], optional = true } -opentelemetry_sdk = { workspace = true, features = [ - "trace", -] } +opentelemetry_sdk = { workspace = true, features = ["trace"] } thiserror = "2" tonic = { workspace = true, optional = true } tracing = { workspace = true } @@ -81,6 +80,12 @@ tls = ["opentelemetry-otlp/tls", "tonic"] tls-roots = ["opentelemetry-otlp/tls-roots"] tls-webpki-roots = ["opentelemetry-otlp/tls-webpki-roots"] logfmt = ["dep:tracing-logfmt"] +logs = [ + "dep:opentelemetry-appender-tracing", + "opentelemetry-otlp/logs", + "opentelemetry_sdk/logs", + "opentelemetry/logs", +] metrics = [ "opentelemetry-otlp/metrics", "tracing-opentelemetry/metrics", diff --git a/init-tracing-opentelemetry/README.md b/init-tracing-opentelemetry/README.md index 332e574..77542a3 100644 --- a/init-tracing-opentelemetry/README.md +++ b/init-tracing-opentelemetry/README.md @@ -253,6 +253,56 @@ Configure the following set of environment variables to configure the metrics ex - `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` to set the temporality preference for the exporter - `OTEL_METRIC_EXPORT_INTERVAL` to set frequence of metrics export in **_milliseconds_**, defaults to 60s +## Logs + +To configure OpenTelemetry log export, enable the `logs` feature. This initializes a `SdkLoggerProvider` and adds a log bridge layer so that `tracing` events are forwarded to the OpenTelemetry log pipeline and exported via OTLP. + +```toml +[dependencies] +init-tracing-opentelemetry = { version = "*", features = ["otlp", "logs"] } +``` + +Standard `tracing` macros emit logs that are automatically bridged: + +```rust +#[tokio::main] +async fn main() -> Result<(), Box> { + let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?; + + tracing::error!("This is ground control to Major Tom"); + tracing::warn!("Houston, we have a problem"); + tracing::info!("We have contact"); + tracing::debug!("Roger, copy that"); + + Ok(()) +} +``` + +Log export can be toggled at runtime via `.with_logs(bool)`: + +```rust,no_run +TracingConfig::default() + .with_logs(false) // disable log export (default: enabled when feature is active) + .init_subscriber()?; +``` + +Configure the following environment variables to control the logs exporter (in addition to the shared variables above): + +- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` overrides `OTEL_EXPORTER_OTLP_ENDPOINT` for the log pipeline; for HTTP the path `/v1/logs` is appended automatically +- `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` overrides `OTEL_EXPORTER_OTLP_PROTOCOL`, falls back to port-based auto-detection + +```sh +# For GRPC: +export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://localhost:4317" +export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="grpc" + +# For HTTP: +export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://127.0.0.1:4318/v1/logs" +export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="http/protobuf" +``` + +> **Note:** A protocol must be set (via env var or inferable from the endpoint port). If neither is found, no log exporter is created and a warning is emitted on target `otel::setup`. + ## Changelog - History [CHANGELOG.md](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/main/CHANGELOG.md) diff --git a/init-tracing-opentelemetry/src/config.rs b/init-tracing-opentelemetry/src/config.rs index f0e523c..c4089dc 100644 --- a/init-tracing-opentelemetry/src/config.rs +++ b/init-tracing-opentelemetry/src/config.rs @@ -54,6 +54,8 @@ use crate::formats::{ CompactLayerBuilder, FullLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder, }; +#[cfg(feature = "logs")] +use crate::tracing_subscriber_ext::build_logger_layer_with_resource; #[cfg(feature = "metrics")] use crate::tracing_subscriber_ext::build_metrics_layer_with_resource; use crate::tracing_subscriber_ext::build_tracer_layer_with_resource_and_name; @@ -244,6 +246,8 @@ pub struct OtelConfig { pub enabled: bool, /// Resource configuration for OTEL pub resource_config: Option, + /// Enable log export via OTLP + pub logs_enabled: bool, /// Enable metrics collection pub metrics_enabled: bool, } @@ -253,6 +257,7 @@ impl Default for OtelConfig { Self { enabled: true, resource_config: None, + logs_enabled: cfg!(feature = "logs"), metrics_enabled: cfg!(feature = "metrics"), } } @@ -477,6 +482,13 @@ impl TracingConfig { self } + /// Enable or disable log export via OTLP + #[must_use] + pub fn with_logs(mut self, enabled: bool) -> Self { + self.otel_config.logs_enabled = enabled; + self + } + /// Enable or disable metrics collection #[must_use] pub fn with_metrics(mut self, enabled: bool) -> Self { @@ -626,16 +638,22 @@ impl TracingConfig { .clone() .unwrap_or_default() .build(); + #[cfg(feature = "logs")] + let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?; #[cfg(feature = "metrics")] let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?; let (trace_layer, tracer_provider) = build_tracer_layer_with_resource_and_name(otel_rsrc, self.tracer_name.clone())?; let subscriber = subscriber.with(trace_layer); + #[cfg(feature = "logs")] + let subscriber = subscriber.with(self.otel_config.logs_enabled.then_some(logs_layer)); #[cfg(feature = "metrics")] let subscriber = subscriber.with(metrics_layer); Ok(( subscriber, OtelGuard { + #[cfg(feature = "logs")] + logger_provider, #[cfg(feature = "metrics")] meter_provider, tracer_provider, diff --git a/init-tracing-opentelemetry/src/otlp/logs.rs b/init-tracing-opentelemetry/src/otlp/logs.rs new file mode 100644 index 0000000..9c25a7b --- /dev/null +++ b/init-tracing-opentelemetry/src/otlp/logs.rs @@ -0,0 +1,69 @@ +use super::infer_protocol; +use opentelemetry_otlp::{ExporterBuildError, LogExporter}; +use opentelemetry_sdk::{Resource, logs::LoggerProviderBuilder, logs::SdkLoggerProvider}; +#[cfg(feature = "tls")] +use {opentelemetry_otlp::WithTonicConfig, tonic::transport::ClientTlsConfig}; + +#[must_use] +pub fn identity(v: LoggerProviderBuilder) -> LoggerProviderBuilder { + v +} + +pub fn init_loggerprovider( + resource: Resource, + transform: F, +) -> Result +where + F: FnOnce(LoggerProviderBuilder) -> LoggerProviderBuilder, +{ + let (maybe_protocol, maybe_endpoint) = read_protocol_and_endpoint_from_env(); + let protocol = infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref()); + + let exporter: Option = match protocol.as_deref() { + Some("http/protobuf") => Some(LogExporter::builder().with_http().build()?), + #[cfg(feature = "tls")] + Some("grpc/tls") => Some( + LogExporter::builder() + .with_tonic() + .with_tls_config(ClientTlsConfig::new().with_enabled_roots()) + .build()?, + ), + Some("grpc") => Some(LogExporter::builder().with_tonic().build()?), + Some(x) => { + tracing::warn!( + "unknown '{x}' env var set or infered for OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no log exporter will be created" + ); + None + } + None => { + tracing::warn!( + "no env var set or infered for OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no log exporter will be created" + ); + None + } + }; + let mut logger_provider = SdkLoggerProvider::builder().with_resource(resource); + if let Some(exporter) = exporter { + logger_provider = logger_provider.with_batch_exporter(exporter); + } + + logger_provider = transform(logger_provider); + Ok(logger_provider.build()) +} + +fn read_protocol_and_endpoint_from_env() -> (Option, Option) { + let maybe_protocol = std::env::var("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL") + .or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL")) + .ok(); + let maybe_endpoint = std::env::var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") + .or_else(|_| { + std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol { + Some(protocol) if protocol.contains("http") => { + format!("{endpoint}/v1/logs") + } + _ => endpoint, + }) + }) + .ok(); + (maybe_protocol, maybe_endpoint) +} diff --git a/init-tracing-opentelemetry/src/otlp/mod.rs b/init-tracing-opentelemetry/src/otlp/mod.rs index 7221b30..7b8ac7e 100644 --- a/init-tracing-opentelemetry/src/otlp/mod.rs +++ b/init-tracing-opentelemetry/src/otlp/mod.rs @@ -1,25 +1,40 @@ +#[cfg(feature = "logs")] +pub mod logs; #[cfg(feature = "metrics")] pub mod metrics; pub mod traces; +#[cfg(feature = "logs")] +use opentelemetry::logs::LoggerProvider; #[cfg(feature = "metrics")] use opentelemetry::metrics::MeterProvider; +#[cfg(feature = "logs")] +use opentelemetry_sdk::logs::SdkLoggerProvider; #[cfg(feature = "metrics")] use opentelemetry_sdk::metrics::SdkMeterProvider; use opentelemetry::trace::TracerProvider; use opentelemetry_sdk::trace::SdkTracerProvider; -#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/metrics are sent to the server"] +#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/logs/metrics are sent to the server"] /// On Drop of the `OtelGuard` instance, -/// the wrapped Tracer/Meter Provider is force to flush and to shutdown (ignoring error). +/// the wrapped Tracer/Logger/Meter Provider is force to flush and to shutdown (ignoring error). +#[allow(clippy::struct_field_names)] pub struct OtelGuard { + #[cfg(feature = "logs")] + pub(crate) logger_provider: SdkLoggerProvider, #[cfg(feature = "metrics")] pub(crate) meter_provider: SdkMeterProvider, pub(crate) tracer_provider: SdkTracerProvider, } impl OtelGuard { + #[cfg(feature = "logs")] + #[must_use] + pub fn logger_provider(&self) -> &impl LoggerProvider { + &self.logger_provider + } + #[must_use] pub fn tracer_provider(&self) -> &impl TracerProvider { &self.tracer_provider @@ -37,6 +52,11 @@ impl Drop for OtelGuard { fn drop(&mut self) { let _ = self.tracer_provider.force_flush(); let _ = self.tracer_provider.shutdown(); + #[cfg(feature = "logs")] + { + let _ = self.logger_provider.force_flush(); + let _ = self.logger_provider.shutdown(); + } #[cfg(feature = "metrics")] { let _ = self.meter_provider.force_flush(); diff --git a/init-tracing-opentelemetry/src/tracing_subscriber_ext.rs b/init-tracing-opentelemetry/src/tracing_subscriber_ext.rs index 6f8ffea..81f61e3 100644 --- a/init-tracing-opentelemetry/src/tracing_subscriber_ext.rs +++ b/init-tracing-opentelemetry/src/tracing_subscriber_ext.rs @@ -2,6 +2,10 @@ use std::borrow::Cow; use opentelemetry::trace::TracerProvider; +#[cfg(feature = "logs")] +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +#[cfg(feature = "logs")] +use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider}; #[cfg(feature = "metrics")] use opentelemetry_sdk::metrics::SdkMeterProvider; use opentelemetry_sdk::{ @@ -95,15 +99,21 @@ pub fn register_otel_layers_with_resource( where S: Subscriber + for<'a> LookupSpan<'a>, { + #[cfg(feature = "logs")] + let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?; #[cfg(feature = "metrics")] let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?; let (trace_layer, tracer_provider) = build_tracer_layer_with_resource(otel_rsrc)?; let subscriber = subscriber.with(trace_layer); + #[cfg(feature = "logs")] + let subscriber = subscriber.with(logs_layer); #[cfg(feature = "metrics")] let subscriber = subscriber.with(metrics_layer); Ok(( subscriber, OtelGuard { + #[cfg(feature = "logs")] + logger_provider, #[cfg(feature = "metrics")] meter_provider, tracer_provider, @@ -135,6 +145,21 @@ where build_tracer_layer_with_resource_and_name(otel_rsrc, "") } +#[cfg(feature = "logs")] +pub(crate) fn build_logger_layer_with_resource( + otel_rsrc: Resource, +) -> Result< + ( + OpenTelemetryTracingBridge, + SdkLoggerProvider, + ), + crate::Error, +> { + let logger_provider = otlp::logs::init_loggerprovider(otel_rsrc, otlp::logs::identity)?; + let layer = OpenTelemetryTracingBridge::new(&logger_provider); + Ok((layer, logger_provider)) +} + pub(crate) fn build_tracer_layer_with_resource_and_name( otel_rsrc: Resource, tracer_name: impl Into>,