Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ skip = [
"r-efi",
# "socket2",
# "wasi",
"wit-bindgen",
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
Expand Down
Binary file added examples/logging/screenshot_grafana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion init-tracing-opentelemetry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Log export can be toggled at runtime via `.with_logs(bool)`:

```rust,no_run
use init_tracing_opentelemetry::TracingConfig;
//...
TracingConfig::default()
.with_logs(false) // disable log export (default: enabled when feature is active)
.init_subscriber()?;
.init_subscriber()
.expect("valid tracing configuration");
Comment on lines +288 to +289

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 Guard returned by init_subscriber() must be assigned to a variable (e.g., let _guard = ...) to ensure it remains in scope for the duration of the application. If the guard is dropped immediately (as in this example), the tracing subscriber and OpenTelemetry exporters will be shut down, and pending traces or logs may not be flushed. Note that the Guard struct is annotated with #[must_use] to help prevent this mistake.

```
> 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.

![screenshot of grafana logs](https://raw.githubusercontent.com/davidB/tracing-opentelemetry-instrumentation-sdk/refs/heads/main/examples/logging/screenshot_grafana.png)

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

Consider using a relative path for the image link. This ensures the image displays correctly when viewing the README on different branches or forks of the repository, and is generally more robust than an absolute URL to the main branch.

Suggested change
![screenshot of grafana logs](https://raw.githubusercontent.com/davidB/tracing-opentelemetry-instrumentation-sdk/refs/heads/main/examples/logging/screenshot_grafana.png)
![screenshot of grafana logs](../examples/logging/screenshot_grafana.png)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

except when the README is imported into a documentation system (crates.io, docs.rs) that only retreive the project repository, not the full repo


Configure the following environment variables to control the logs exporter (in addition to the shared variables above):

Expand Down
2 changes: 1 addition & 1 deletion init-tracing-opentelemetry/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ impl TracingConfig {
#[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);
let subscriber = subscriber.with(self.otel_config.metrics_enabled.then_some(metrics_layer));
Ok((
subscriber,
OtelGuard {
Expand Down
20 changes: 9 additions & 11 deletions init-tracing-opentelemetry/src/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,15 @@ impl LayerBuilder for LogfmtLayerBuilder {
// Note: tracing_logfmt doesn't support the same configuration options
// as the standard fmt layer, so we have limited configuration ability

match &config.writer {
WriterConfig::Stderr => {
// For stderr, we need to use the builder pattern since layer() doesn't support with_writer
// However, the current tracing_logfmt version may not support this
// For now, we'll fall back to the basic layer
Ok(Box::new(tracing_logfmt::layer()))
}
_ => {
// Default behavior uses stdout
Ok(Box::new(tracing_logfmt::layer()))
}
if let WriterConfig::Stdout = &config.writer {
// Default behavior uses stdout
Ok(Box::new(tracing_logfmt::layer()))
} else {
// For stderr, we need to use the builder pattern since layer() doesn't support with_writer
// However, the current tracing_logfmt version may not support this
// For now, we'll fall back to the basic layer
tracing::warn!("logfmt only support stdout");
Ok(Box::new(tracing_logfmt::layer()))
}
}
}
27 changes: 7 additions & 20 deletions init-tracing-opentelemetry/src/otlp/logs.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::infer_protocol;
use super::infer_protocol_from_env;
use opentelemetry_otlp::{ExporterBuildError, LogExporter};
use opentelemetry_sdk::{Resource, logs::LoggerProviderBuilder, logs::SdkLoggerProvider};
#[cfg(feature = "tls")]
Expand All @@ -16,9 +16,13 @@ pub fn init_loggerprovider<F>(
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 protocol = infer_protocol_from_env(
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"v1/logs",
);

// builders used the environment variables to determine the endpoint (but not to setup the protocol)
let exporter: Option<LogExporter> = match protocol.as_deref() {
Some("http/protobuf") => Some(LogExporter::builder().with_http().build()?),
#[cfg(feature = "tls")]
Expand Down Expand Up @@ -50,20 +54,3 @@ where
logger_provider = transform(logger_provider);
Ok(logger_provider.build())
}

fn read_protocol_and_endpoint_from_env() -> (Option<String>, Option<String>) {
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)
}
27 changes: 7 additions & 20 deletions init-tracing-opentelemetry/src/otlp/metrics.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::infer_protocol;
use super::infer_protocol_from_env;
use opentelemetry_otlp::{ExporterBuildError, MetricExporter, WithExportConfig};
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::metrics::{
Expand All @@ -21,8 +21,11 @@ pub fn init_meterprovider<F>(
where
F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder,
{
let (maybe_protocol, maybe_endpoint) = read_protocol_and_endpoint_from_env();
let protocol = infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref());
let protocol = infer_protocol_from_env(
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"v1/metrics",
);
let timeout = env::var("OTEL_EXPORTER_OTLP_METRICS_TIMEOUT")
.ok()
.and_then(|var| var.parse::<u64>().ok())
Expand All @@ -43,6 +46,7 @@ where
.and_then(|var| var.parse::<u64>().ok())
.map_or(Duration::from_secs(60), Duration::from_millis);

// builders used the environment variables to determine the endpoint (but not to setup the protocol)
let exporter = match protocol.as_deref() {
Some("http/protobuf") => Some(
MetricExporter::builder()
Expand Down Expand Up @@ -90,20 +94,3 @@ where
meter_provider = transform(meter_provider);
Ok(meter_provider.build())
}

fn read_protocol_and_endpoint_from_env() -> (Option<String>, Option<String>) {
let maybe_protocol = std::env::var("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL")
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
.ok();
let maybe_endpoint = std::env::var("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT")
.or_else(|_| {
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol {
Some(protocol) if protocol.contains("http") => {
format!("{endpoint}/v1/metrics")
}
_ => endpoint,
})
})
.ok();
(maybe_protocol, maybe_endpoint)
}
52 changes: 49 additions & 3 deletions init-tracing-opentelemetry/src/otlp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,18 @@ impl Drop for OtelGuard {
}

#[allow(unused_mut)]
pub(crate) fn infer_protocol(
maybe_protocol: Option<&str>,
maybe_endpoint: Option<&str>,
pub(crate) fn infer_protocol_from_env(
protocol_key: &str,
endpoint_key: &str,
endpoint_path: &str,
) -> Option<String> {
let (maybe_protocol, maybe_endpoint) =
read_protocol_and_endpoint_from_env(protocol_key, endpoint_key, endpoint_path);
infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref())
}

#[allow(unused_mut)]
fn infer_protocol(maybe_protocol: Option<&str>, maybe_endpoint: Option<&str>) -> Option<String> {
let mut maybe_protocol = match (maybe_protocol, maybe_endpoint) {
(Some(protocol), _) => Some(protocol.to_string()),
(None, Some(endpoint)) => {
Expand All @@ -91,6 +99,44 @@ pub(crate) fn infer_protocol(
maybe_protocol
}

pub fn debug_env() {
const SENSITIVE_KEYS: &[&str] = &[
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_CERTIFICATE",
];
std::env::vars()
.filter(|(k, _)| k.starts_with("OTEL_"))
.for_each(|(k, v)| {
let display_value = if SENSITIVE_KEYS.iter().any(|s| k == *s) {
"[redacted]"
} else {
&v
};
tracing::debug!(target: "otel::setup::env", key = %k, value = %display_value);
});
}

fn read_protocol_and_endpoint_from_env(
protocol_key: &str,
endpoint_key: &str,
endpoint_path: &str,
) -> (Option<String>, Option<String>) {
let maybe_protocol = std::env::var(protocol_key)
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
.ok();
let maybe_endpoint = std::env::var(endpoint_key)
.or_else(|_| {
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol {
Some(protocol) if protocol.contains("http") => {
format!("{endpoint}/{endpoint_path}")
}
_ => endpoint,
})
})
.ok();
(maybe_protocol, maybe_endpoint)
}

#[cfg(test)]
mod tests {
use assert2::assert;
Expand Down
35 changes: 8 additions & 27 deletions init-tracing-opentelemetry/src/otlp/traces.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::infer_protocol;
use super::infer_protocol_from_env;
use opentelemetry_otlp::{ExporterBuildError, SpanExporter};
use opentelemetry_sdk::{Resource, trace::SdkTracerProvider, trace::TracerProviderBuilder};
#[cfg(feature = "tls")]
Expand All @@ -17,10 +17,14 @@ pub fn init_tracerprovider<F>(
where
F: FnOnce(TracerProviderBuilder) -> TracerProviderBuilder,
{
debug_env();
let (maybe_protocol, maybe_endpoint) = read_protocol_and_endpoint_from_env();
let protocol = infer_protocol(maybe_protocol.as_deref(), maybe_endpoint.as_deref());
super::debug_env();
let protocol = infer_protocol_from_env(
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"v1/traces",
);

// builders used the environment variables to determine the endpoint (but not to setup the protocol)
let exporter: Option<SpanExporter> = match protocol.as_deref() {
Some("http/protobuf") => Some(SpanExporter::builder().with_http().build()?),
#[cfg(feature = "tls")]
Expand Down Expand Up @@ -52,26 +56,3 @@ where
trace_provider = transform(trace_provider);
Ok(trace_provider.build())
}

pub fn debug_env() {
std::env::vars()
.filter(|(k, _)| k.starts_with("OTEL_"))
.for_each(|(k, v)| tracing::debug!(target: "otel::setup::env", key = %k, value = %v));
}

fn read_protocol_and_endpoint_from_env() -> (Option<String>, Option<String>) {
let maybe_protocol = std::env::var("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
.ok();
let maybe_endpoint = std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
.or_else(|_| {
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").map(|endpoint| match &maybe_protocol {
Some(protocol) if protocol.contains("http") => {
format!("{endpoint}/v1/traces")
}
_ => endpoint,
})
})
.ok();
(maybe_protocol, maybe_endpoint)
}
38 changes: 22 additions & 16 deletions init-tracing-opentelemetry/src/resource.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::borrow::Cow;

use opentelemetry::KeyValue;
// use opentelemetry_resource_detectors::OsResourceDetector;
use opentelemetry_sdk::{Resource, resource::ResourceDetector};
Expand All @@ -16,37 +18,43 @@ use opentelemetry_semantic_conventions::resource;
/// ```
#[derive(Debug, Default, Clone)]
pub struct DetectResource {
fallback_service_name: Option<&'static str>,
fallback_service_version: Option<&'static str>,
fallback_service_name: Option<Cow<'static, str>>,
fallback_service_version: Option<Cow<'static, str>>,
}

impl DetectResource {
/// `service.name` is first extracted from environment variables
/// (in this order) `OTEL_SERVICE_NAME`, `SERVICE_NAME`, `APP_NAME`.
/// But a default value can be provided with this method.
#[must_use]
pub fn with_fallback_service_name(mut self, fallback_service_name: &'static str) -> Self {
self.fallback_service_name = Some(fallback_service_name);
pub fn with_fallback_service_name(
mut self,
fallback_service_name: impl Into<Cow<'static, str>>,
) -> Self {
self.fallback_service_name = Some(fallback_service_name.into());
self
}

/// `service.name` is first extracted from environment variables
/// (in this order) `SERVICE_VERSION`, `APP_VERSION`.
/// But a default value can be provided with this method.
#[must_use]
pub fn with_fallback_service_version(mut self, fallback_service_version: &'static str) -> Self {
self.fallback_service_version = Some(fallback_service_version);
pub fn with_fallback_service_version(
mut self,
fallback_service_version: impl Into<Cow<'static, str>>,
) -> Self {
self.fallback_service_version = Some(fallback_service_version.into());
self
}

#[must_use]
pub fn build(&mut self) -> Resource {
pub fn build(self) -> Resource {
//Box::new(OsResourceDetector), //FIXME enable when available for opentelemetry >= 0.25
//Box::new(ProcessResourceDetector),
let rsrc = Resource::builder()
.with_detector(Box::new(ServiceInfoDetector {
fallback_service_name: self.fallback_service_name.take(),
fallback_service_version: self.fallback_service_version.take(),
fallback_service_name: self.fallback_service_name,
fallback_service_version: self.fallback_service_version,
}))
.build();
debug_resource(&rsrc);
Expand All @@ -62,8 +70,8 @@ pub fn debug_resource(rsrc: &Resource) {

#[derive(Debug)]
pub struct ServiceInfoDetector {
fallback_service_name: Option<&'static str>,
fallback_service_version: Option<&'static str>,
fallback_service_name: Option<Cow<'static, str>>,
fallback_service_version: Option<Cow<'static, str>>,
}

impl ResourceDetector for ServiceInfoDetector {
Expand All @@ -72,17 +80,15 @@ impl ResourceDetector for ServiceInfoDetector {
.or_else(|_| std::env::var("SERVICE_NAME"))
.or_else(|_| std::env::var("APP_NAME"))
.ok()
.or_else(|| {
self.fallback_service_name
.map(std::string::ToString::to_string)
})
.or_else(|| self.fallback_service_name.clone().map(|v| v.to_string()))
.map(|v| KeyValue::new(resource::SERVICE_NAME, v));
let service_version = std::env::var("SERVICE_VERSION")
.or_else(|_| std::env::var("APP_VERSION"))
.ok()
.or_else(|| {
self.fallback_service_version
.map(std::string::ToString::to_string)
.clone()
.map(std::borrow::Cow::into_owned)
})
.map(|v| KeyValue::new(resource::SERVICE_VERSION, v));
let mut resource = Resource::builder_empty();
Expand Down
Loading