Skip to content

feat: add OTLP log exporter#333

Merged
davidB merged 5 commits into
davidB:mainfrom
cozyGalvinism:feature/implement-log-exporter
Apr 26, 2026
Merged

feat: add OTLP log exporter#333
davidB merged 5 commits into
davidB:mainfrom
cozyGalvinism:feature/implement-log-exporter

Conversation

@cozyGalvinism

Copy link
Copy Markdown
Contributor

I have added a functional log exporter interface that more or less mirrors the setup of the tracing setup. I ran all required mise tasks and tested it in a short code example.

image

Traces are automatically attached to logs as well, so if the logs are queried in Grafana (for example), the trace automatically links to the log line.

image

Minimal code setup I used to test (running an LGTM stack in a Docker Compose with OTLP ports exposed):

// Using tokio, dotenvy, init-tracing-opentelemetry and tracing with the changes

#[tracing::instrument]
async fn log_stuff() {
    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");
    tracing::trace!("All systems go");
    tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
}

#[tracing::instrument]
async fn calc() {
    let result = 2 + 2;
    tracing::info!(result, "Calculated result");
}

#[tokio::main]
async fn main() {
    dotenvy::dotenv().ok();
    let _guard = init_tracing_opentelemetry::TracingConfig::debug()
        .init_subscriber()
        .expect("Failed to set up subscriber");

    log_stuff().await;
    calc().await;
}

@davidB davidB left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you

Can you add, as an example, a test, or just documentation in the README of init-tracing-opentelemetry, or just the sample you included in the PR's description?

@davidB

davidB commented Apr 20, 2026

Copy link
Copy Markdown
Owner

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces OTLP log exporting capabilities by adding a logs feature, implementing log provider initialization, and integrating a logging layer into the tracing subscriber. Review feedback identifies that the maybe_endpoint environment variable is currently ignored during exporter setup and that the logs_enabled configuration flag is not checked before adding the logging layer to the subscriber.

Comment on lines +22 to +44
let exporter: Option<LogExporter> = 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
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The maybe_endpoint retrieved from environment variables is never used to configure the LogExporter. This means the exporter will always use default endpoints (e.g., http://localhost:4318/v1/logs for HTTP or http://localhost:4317 for gRPC), ignoring the OTEL_EXPORTER_OTLP_LOGS_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT settings.

    let exporter: Option<LogExporter> = match protocol.as_deref() {
        Some("http/protobuf") => {
            let mut builder = LogExporter::builder().with_http();
            if let Some(endpoint) = maybe_endpoint {
                builder = builder.with_endpoint(endpoint);
            }
            Some(builder.build()?)
        }
        #[cfg(feature = "tls")]
        Some("grpc/tls") => {
            let mut builder = LogExporter::builder()
                .with_tonic()
                .with_tls_config(ClientTlsConfig::new().with_enabled_roots());
            if let Some(endpoint) = maybe_endpoint {
                builder = builder.with_endpoint(endpoint);
            }
            Some(builder.build()?)
        }
        Some("grpc") => {
            let mut builder = LogExporter::builder().with_tonic();
            if let Some(endpoint) = maybe_endpoint {
                builder = builder.with_endpoint(endpoint);
            }
            Some(builder.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
        }
    };

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar logic is used for tracing code, I pretty much copied the implementation from traces. Can fix that though.

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(logs_layer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logs_enabled field in OtelConfig is currently ignored. The log layer is added to the subscriber whenever the logs feature is enabled, regardless of the runtime configuration. It should be conditionally added based on self.otel_config.logs_enabled.

Suggested change
let subscriber = subscriber.with(logs_layer);
let subscriber = subscriber.with(self.otel_config.logs_enabled.then_some(logs_layer));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same logic exists for metrics. Copied it from there. I can fix this one or both. Up to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@cozyGalvinism

Copy link
Copy Markdown
Contributor Author

Thank you

Can you add, as an example, a test, or just documentation in the README of init-tracing-opentelemetry, or just the sample you included in the PR's description?

Sure, will do when I'm back home later

@davidB

davidB commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Sorry for the delay, I was nearly offline last week.

Thank you for the correction. I'll take care of aligning the 3 implementations and the CI issue related to the dependency update.

@davidB davidB changed the title Implement OTLP log exporter feat: add OTLP log exporter Apr 26, 2026
@davidB davidB merged commit 7315f86 into davidB:main Apr 26, 2026
1 of 2 checks passed
This was referenced Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants