Skip to content

Commit 7315f86

Browse files
feat: add OTLP log exporter (#333)
1 parent 970dea4 commit 7315f86

9 files changed

Lines changed: 238 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ opentelemetry-zipkin = { version = "0.31", default-features = false }
4242
rstest = "0.26"
4343
tokio = { version = "1", default-features = false }
4444
tokio-stream = { version = "0.1", default-features = false }
45-
tonic = { version = "0.14", default-features = false } # should be sync with opentelemetry-proto
45+
tonic = { version = "0.14", default-features = false } # should be sync with opentelemetry-proto
4646
tower = { version = "0.5", default-features = false }
4747
tracing = "0.1"
4848
tracing-opentelemetry = "0.32"

examples/logging/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "examples-logging"
3+
publish = false
4+
edition.workspace = true
5+
version.workspace = true
6+
repository.workspace = true
7+
license.workspace = true
8+
9+
[dependencies]
10+
init-tracing-opentelemetry = { path = "../../init-tracing-opentelemetry", features = [
11+
"otlp",
12+
"tracing_subscriber_ext",
13+
"logs"
14+
] }
15+
memory-stats = "1"
16+
opentelemetry = { workspace = true }
17+
serde_json = "1"
18+
tokio = { version = "1", features = ["full"] }
19+
tracing = { workspace = true }
20+
tracing-opentelemetry-instrumentation-sdk = { path = "../../tracing-opentelemetry-instrumentation-sdk" }

examples/logging/src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#[tracing::instrument]
2+
async fn log() {
3+
tracing::error!("This is ground control to Major Tom");
4+
tracing::warn!("Houston, we have a problem");
5+
tracing::info!("We have contact");
6+
tracing::debug!("Roger, copy that");
7+
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
8+
}
9+
10+
#[tracing::instrument]
11+
async fn calc(a: i32, b: i32) {
12+
let result = a + b;
13+
tracing::info!(result, "calculated result");
14+
}
15+
16+
#[tokio::main]
17+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
18+
// setting up tracing
19+
let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?;
20+
21+
log().await;
22+
calc(1, 2).await;
23+
24+
Ok(())
25+
}

init-tracing-opentelemetry/Cargo.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ rustdoc-args = ["--cfg", "docs_rs"]
1616

1717
[dependencies]
1818
opentelemetry = { workspace = true }
19+
opentelemetry-appender-tracing = { version = "0.31", default-features = false, optional = true }
1920
opentelemetry-aws = { workspace = true, optional = true, features = ["trace"] }
2021
opentelemetry-jaeger-propagator = { workspace = true, optional = true }
2122
opentelemetry-otlp = { workspace = true, optional = true, features = [
@@ -28,9 +29,7 @@ opentelemetry-stdout = { workspace = true, features = [
2829
], optional = true }
2930
opentelemetry-semantic-conventions = { workspace = true, optional = true }
3031
opentelemetry-zipkin = { workspace = true, features = [], optional = true }
31-
opentelemetry_sdk = { workspace = true, features = [
32-
"trace",
33-
] }
32+
opentelemetry_sdk = { workspace = true, features = ["trace"] }
3433
thiserror = "2"
3534
tonic = { workspace = true, optional = true }
3635
tracing = { workspace = true }
@@ -81,6 +80,12 @@ tls = ["opentelemetry-otlp/tls", "tonic"]
8180
tls-roots = ["opentelemetry-otlp/tls-roots"]
8281
tls-webpki-roots = ["opentelemetry-otlp/tls-webpki-roots"]
8382
logfmt = ["dep:tracing-logfmt"]
83+
logs = [
84+
"dep:opentelemetry-appender-tracing",
85+
"opentelemetry-otlp/logs",
86+
"opentelemetry_sdk/logs",
87+
"opentelemetry/logs",
88+
]
8489
metrics = [
8590
"opentelemetry-otlp/metrics",
8691
"tracing-opentelemetry/metrics",

init-tracing-opentelemetry/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,56 @@ Configure the following set of environment variables to configure the metrics ex
253253
- `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` to set the temporality preference for the exporter
254254
- `OTEL_METRIC_EXPORT_INTERVAL` to set frequence of metrics export in **_milliseconds_**, defaults to 60s
255255

256+
## Logs
257+
258+
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.
259+
260+
```toml
261+
[dependencies]
262+
init-tracing-opentelemetry = { version = "*", features = ["otlp", "logs"] }
263+
```
264+
265+
Standard `tracing` macros emit logs that are automatically bridged:
266+
267+
```rust
268+
#[tokio::main]
269+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
270+
let _guard = init_tracing_opentelemetry::TracingConfig::production().init_subscriber()?;
271+
272+
tracing::error!("This is ground control to Major Tom");
273+
tracing::warn!("Houston, we have a problem");
274+
tracing::info!("We have contact");
275+
tracing::debug!("Roger, copy that");
276+
277+
Ok(())
278+
}
279+
```
280+
281+
Log export can be toggled at runtime via `.with_logs(bool)`:
282+
283+
```rust,no_run
284+
TracingConfig::default()
285+
.with_logs(false) // disable log export (default: enabled when feature is active)
286+
.init_subscriber()?;
287+
```
288+
289+
Configure the following environment variables to control the logs exporter (in addition to the shared variables above):
290+
291+
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` overrides `OTEL_EXPORTER_OTLP_ENDPOINT` for the log pipeline; for HTTP the path `/v1/logs` is appended automatically
292+
- `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` overrides `OTEL_EXPORTER_OTLP_PROTOCOL`, falls back to port-based auto-detection
293+
294+
```sh
295+
# For GRPC:
296+
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://localhost:4317"
297+
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="grpc"
298+
299+
# For HTTP:
300+
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="http://127.0.0.1:4318/v1/logs"
301+
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="http/protobuf"
302+
```
303+
304+
> **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`.
305+
256306
## Changelog - History
257307

258308
[CHANGELOG.md](https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk/blob/main/CHANGELOG.md)

init-tracing-opentelemetry/src/config.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ use crate::formats::{
5454
CompactLayerBuilder, FullLayerBuilder, JsonLayerBuilder, LayerBuilder, PrettyLayerBuilder,
5555
};
5656

57+
#[cfg(feature = "logs")]
58+
use crate::tracing_subscriber_ext::build_logger_layer_with_resource;
5759
#[cfg(feature = "metrics")]
5860
use crate::tracing_subscriber_ext::build_metrics_layer_with_resource;
5961
use crate::tracing_subscriber_ext::build_tracer_layer_with_resource_and_name;
@@ -244,6 +246,8 @@ pub struct OtelConfig {
244246
pub enabled: bool,
245247
/// Resource configuration for OTEL
246248
pub resource_config: Option<DetectResource>,
249+
/// Enable log export via OTLP
250+
pub logs_enabled: bool,
247251
/// Enable metrics collection
248252
pub metrics_enabled: bool,
249253
}
@@ -253,6 +257,7 @@ impl Default for OtelConfig {
253257
Self {
254258
enabled: true,
255259
resource_config: None,
260+
logs_enabled: cfg!(feature = "logs"),
256261
metrics_enabled: cfg!(feature = "metrics"),
257262
}
258263
}
@@ -477,6 +482,13 @@ impl TracingConfig {
477482
self
478483
}
479484

485+
/// Enable or disable log export via OTLP
486+
#[must_use]
487+
pub fn with_logs(mut self, enabled: bool) -> Self {
488+
self.otel_config.logs_enabled = enabled;
489+
self
490+
}
491+
480492
/// Enable or disable metrics collection
481493
#[must_use]
482494
pub fn with_metrics(mut self, enabled: bool) -> Self {
@@ -626,16 +638,22 @@ impl TracingConfig {
626638
.clone()
627639
.unwrap_or_default()
628640
.build();
641+
#[cfg(feature = "logs")]
642+
let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?;
629643
#[cfg(feature = "metrics")]
630644
let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?;
631645
let (trace_layer, tracer_provider) =
632646
build_tracer_layer_with_resource_and_name(otel_rsrc, self.tracer_name.clone())?;
633647
let subscriber = subscriber.with(trace_layer);
648+
#[cfg(feature = "logs")]
649+
let subscriber = subscriber.with(self.otel_config.logs_enabled.then_some(logs_layer));
634650
#[cfg(feature = "metrics")]
635651
let subscriber = subscriber.with(metrics_layer);
636652
Ok((
637653
subscriber,
638654
OtelGuard {
655+
#[cfg(feature = "logs")]
656+
logger_provider,
639657
#[cfg(feature = "metrics")]
640658
meter_provider,
641659
tracer_provider,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use super::infer_protocol;
2+
use opentelemetry_otlp::{ExporterBuildError, LogExporter};
3+
use opentelemetry_sdk::{Resource, logs::LoggerProviderBuilder, logs::SdkLoggerProvider};
4+
#[cfg(feature = "tls")]
5+
use {opentelemetry_otlp::WithTonicConfig, tonic::transport::ClientTlsConfig};
6+
7+
#[must_use]
8+
pub fn identity(v: LoggerProviderBuilder) -> LoggerProviderBuilder {
9+
v
10+
}
11+
12+
pub fn init_loggerprovider<F>(
13+
resource: Resource,
14+
transform: F,
15+
) -> Result<SdkLoggerProvider, ExporterBuildError>
16+
where
17+
F: FnOnce(LoggerProviderBuilder) -> LoggerProviderBuilder,
18+
{
19+
let (maybe_protocol, maybe_endpoint) = read_protocol_and_endpoint_from_env();
20+
let protocol = infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref());
21+
22+
let exporter: Option<LogExporter> = match protocol.as_deref() {
23+
Some("http/protobuf") => Some(LogExporter::builder().with_http().build()?),
24+
#[cfg(feature = "tls")]
25+
Some("grpc/tls") => Some(
26+
LogExporter::builder()
27+
.with_tonic()
28+
.with_tls_config(ClientTlsConfig::new().with_enabled_roots())
29+
.build()?,
30+
),
31+
Some("grpc") => Some(LogExporter::builder().with_tonic().build()?),
32+
Some(x) => {
33+
tracing::warn!(
34+
"unknown '{x}' env var set or infered for OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no log exporter will be created"
35+
);
36+
None
37+
}
38+
None => {
39+
tracing::warn!(
40+
"no env var set or infered for OTEL_EXPORTER_OTLP_LOGS_PROTOCOL or OTEL_EXPORTER_OTLP_PROTOCOL; no log exporter will be created"
41+
);
42+
None
43+
}
44+
};
45+
let mut logger_provider = SdkLoggerProvider::builder().with_resource(resource);
46+
if let Some(exporter) = exporter {
47+
logger_provider = logger_provider.with_batch_exporter(exporter);
48+
}
49+
50+
logger_provider = transform(logger_provider);
51+
Ok(logger_provider.build())
52+
}
53+
54+
fn read_protocol_and_endpoint_from_env() -> (Option<String>, Option<String>) {
55+
let maybe_protocol = std::env::var("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")
56+
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
57+
.ok();
58+
let maybe_endpoint = std::env::var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT")
59+
.or_else(|_| {
60+
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol {
61+
Some(protocol) if protocol.contains("http") => {
62+
format!("{endpoint}/v1/logs")
63+
}
64+
_ => endpoint,
65+
})
66+
})
67+
.ok();
68+
(maybe_protocol, maybe_endpoint)
69+
}

init-tracing-opentelemetry/src/otlp/mod.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
1+
#[cfg(feature = "logs")]
2+
pub mod logs;
13
#[cfg(feature = "metrics")]
24
pub mod metrics;
35
pub mod traces;
46

7+
#[cfg(feature = "logs")]
8+
use opentelemetry::logs::LoggerProvider;
59
#[cfg(feature = "metrics")]
610
use opentelemetry::metrics::MeterProvider;
11+
#[cfg(feature = "logs")]
12+
use opentelemetry_sdk::logs::SdkLoggerProvider;
713
#[cfg(feature = "metrics")]
814
use opentelemetry_sdk::metrics::SdkMeterProvider;
915

1016
use opentelemetry::trace::TracerProvider;
1117
use opentelemetry_sdk::trace::SdkTracerProvider;
1218

13-
#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/metrics are sent to the server"]
19+
#[must_use = "Recommend holding with 'let _guard = ' pattern to ensure final traces/logs/metrics are sent to the server"]
1420
/// On Drop of the `OtelGuard` instance,
15-
/// the wrapped Tracer/Meter Provider is force to flush and to shutdown (ignoring error).
21+
/// the wrapped Tracer/Logger/Meter Provider is force to flush and to shutdown (ignoring error).
22+
#[allow(clippy::struct_field_names)]
1623
pub struct OtelGuard {
24+
#[cfg(feature = "logs")]
25+
pub(crate) logger_provider: SdkLoggerProvider,
1726
#[cfg(feature = "metrics")]
1827
pub(crate) meter_provider: SdkMeterProvider,
1928
pub(crate) tracer_provider: SdkTracerProvider,
2029
}
2130

2231
impl OtelGuard {
32+
#[cfg(feature = "logs")]
33+
#[must_use]
34+
pub fn logger_provider(&self) -> &impl LoggerProvider {
35+
&self.logger_provider
36+
}
37+
2338
#[must_use]
2439
pub fn tracer_provider(&self) -> &impl TracerProvider {
2540
&self.tracer_provider
@@ -37,6 +52,11 @@ impl Drop for OtelGuard {
3752
fn drop(&mut self) {
3853
let _ = self.tracer_provider.force_flush();
3954
let _ = self.tracer_provider.shutdown();
55+
#[cfg(feature = "logs")]
56+
{
57+
let _ = self.logger_provider.force_flush();
58+
let _ = self.logger_provider.shutdown();
59+
}
4060
#[cfg(feature = "metrics")]
4161
{
4262
let _ = self.meter_provider.force_flush();

init-tracing-opentelemetry/src/tracing_subscriber_ext.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
use std::borrow::Cow;
33

44
use opentelemetry::trace::TracerProvider;
5+
#[cfg(feature = "logs")]
6+
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
7+
#[cfg(feature = "logs")]
8+
use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider};
59
#[cfg(feature = "metrics")]
610
use opentelemetry_sdk::metrics::SdkMeterProvider;
711
use opentelemetry_sdk::{
@@ -95,15 +99,21 @@ pub fn register_otel_layers_with_resource<S>(
9599
where
96100
S: Subscriber + for<'a> LookupSpan<'a>,
97101
{
102+
#[cfg(feature = "logs")]
103+
let (logs_layer, logger_provider) = build_logger_layer_with_resource(otel_rsrc.clone())?;
98104
#[cfg(feature = "metrics")]
99105
let (metrics_layer, meter_provider) = build_metrics_layer_with_resource(otel_rsrc.clone())?;
100106
let (trace_layer, tracer_provider) = build_tracer_layer_with_resource(otel_rsrc)?;
101107
let subscriber = subscriber.with(trace_layer);
108+
#[cfg(feature = "logs")]
109+
let subscriber = subscriber.with(logs_layer);
102110
#[cfg(feature = "metrics")]
103111
let subscriber = subscriber.with(metrics_layer);
104112
Ok((
105113
subscriber,
106114
OtelGuard {
115+
#[cfg(feature = "logs")]
116+
logger_provider,
107117
#[cfg(feature = "metrics")]
108118
meter_provider,
109119
tracer_provider,
@@ -135,6 +145,21 @@ where
135145
build_tracer_layer_with_resource_and_name(otel_rsrc, "")
136146
}
137147

148+
#[cfg(feature = "logs")]
149+
pub(crate) fn build_logger_layer_with_resource(
150+
otel_rsrc: Resource,
151+
) -> Result<
152+
(
153+
OpenTelemetryTracingBridge<SdkLoggerProvider, SdkLogger>,
154+
SdkLoggerProvider,
155+
),
156+
crate::Error,
157+
> {
158+
let logger_provider = otlp::logs::init_loggerprovider(otel_rsrc, otlp::logs::identity)?;
159+
let layer = OpenTelemetryTracingBridge::new(&logger_provider);
160+
Ok((layer, logger_provider))
161+
}
162+
138163
pub(crate) fn build_tracer_layer_with_resource_and_name<S>(
139164
otel_rsrc: Resource,
140165
tracer_name: impl Into<Cow<'static, str>>,

0 commit comments

Comments
 (0)