feat(otel): respect standard OTel env vars for exporter selection (#7144)
Some checks failed
CI / changes (push) Waiting to run
CI / Check Rust Code Format (push) Blocked by required conditions
CI / Build and Test Rust Project (push) Blocked by required conditions
CI / Lint Rust Code (push) Blocked by required conditions
CI / Check OpenAPI Schema is Up-to-Date (push) Blocked by required conditions
CI / Test and Lint Electron Desktop App (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (Code Execution) (push) Blocked by required conditions
Live Provider Tests / check-fork (push) Waiting to run
Live Provider Tests / changes (push) Blocked by required conditions
Live Provider Tests / Build Binary (push) Blocked by required conditions
Live Provider Tests / Smoke Tests (push) Blocked by required conditions
Live Provider Tests / Compaction Tests (push) Blocked by required conditions
Live Provider Tests / goose server HTTP integration tests (push) Blocked by required conditions
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Canary / Prepare Version (push) Has been cancelled
Cargo Deny / deny (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / Release (push) Has been cancelled

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole 2026-02-14 09:03:11 +08:00 committed by GitHub
parent f00630ad20
commit ab5407508b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 621 additions and 595 deletions

226
Cargo.lock generated
View file

@ -593,7 +593,7 @@ dependencies = [
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.4",
"tower 0.5.3",
"tower",
"tracing",
]
@ -743,7 +743,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper 1.0.2",
"tokio",
"tower 0.5.3",
"tower",
"tower-layer",
"tower-service",
"tracing",
@ -780,7 +780,7 @@ dependencies = [
"sync_wrapper 1.0.2",
"tokio",
"tokio-tungstenite",
"tower 0.5.3",
"tower",
"tower-layer",
"tower-service",
"tracing",
@ -4180,10 +4180,11 @@ dependencies = [
"mockall",
"nanoid",
"once_cell",
"opentelemetry 0.27.1",
"opentelemetry",
"opentelemetry-appender-tracing",
"opentelemetry-otlp 0.27.0",
"opentelemetry_sdk 0.27.1",
"opentelemetry-otlp",
"opentelemetry-stdout",
"opentelemetry_sdk",
"paste",
"pctx_code_mode",
"posthog-rs",
@ -4396,7 +4397,7 @@ dependencies = [
"tokio-stream",
"tokio-tungstenite",
"tokio-util",
"tower 0.5.3",
"tower",
"tower-http",
"tracing",
"tracing-appender",
@ -6591,20 +6592,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7"
dependencies = [
"futures-core",
"futures-sink",
"js-sys",
"pin-project-lite",
"thiserror 1.0.69",
"tracing",
]
[[package]]
name = "opentelemetry"
version = "0.31.0"
@ -6621,29 +6608,16 @@ dependencies = [
[[package]]
name = "opentelemetry-appender-tracing"
version = "0.27.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5feffc321035ad94088a7e5333abb4d84a8726e54a802e736ce9dd7237e85b"
checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2"
dependencies = [
"opentelemetry 0.27.1",
"opentelemetry",
"tracing",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "opentelemetry-http"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80"
dependencies = [
"async-trait",
"bytes",
"http 1.4.0",
"opentelemetry 0.27.1",
"reqwest 0.12.28",
]
[[package]]
name = "opentelemetry-http"
version = "0.31.0"
@ -6653,31 +6627,10 @@ dependencies = [
"async-trait",
"bytes",
"http 1.4.0",
"opentelemetry 0.31.0",
"opentelemetry",
"reqwest 0.12.28",
]
[[package]]
name = "opentelemetry-otlp"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76"
dependencies = [
"async-trait",
"futures-core",
"http 1.4.0",
"opentelemetry 0.27.1",
"opentelemetry-http 0.27.0",
"opentelemetry-proto 0.27.0",
"opentelemetry_sdk 0.27.1",
"prost 0.13.5",
"reqwest 0.12.28",
"thiserror 1.0.69",
"tokio",
"tonic 0.12.3",
"tracing",
]
[[package]]
name = "opentelemetry-otlp"
version = "0.31.0"
@ -6685,62 +6638,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf"
dependencies = [
"http 1.4.0",
"opentelemetry 0.31.0",
"opentelemetry-http 0.31.0",
"opentelemetry-proto 0.31.0",
"opentelemetry_sdk 0.31.0",
"prost 0.14.3",
"opentelemetry",
"opentelemetry-http",
"opentelemetry-proto",
"opentelemetry_sdk",
"prost",
"reqwest 0.12.28",
"thiserror 2.0.18",
"tokio",
"tonic 0.14.3",
"tonic",
"tracing",
]
[[package]]
name = "opentelemetry-proto"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6"
dependencies = [
"opentelemetry 0.27.1",
"opentelemetry_sdk 0.27.1",
"prost 0.13.5",
"tonic 0.12.3",
]
[[package]]
name = "opentelemetry-proto"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
dependencies = [
"opentelemetry 0.31.0",
"opentelemetry_sdk 0.31.0",
"prost 0.14.3",
"tonic 0.14.3",
"opentelemetry",
"opentelemetry_sdk",
"prost",
"tonic",
"tonic-prost",
]
[[package]]
name = "opentelemetry_sdk"
version = "0.27.1"
name = "opentelemetry-stdout"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8"
checksum = "bc8887887e169414f637b18751487cce4e095be787d23fad13c454e2fb1b3811"
dependencies = [
"async-trait",
"futures-channel",
"futures-executor",
"futures-util",
"glob",
"opentelemetry 0.27.1",
"percent-encoding",
"rand 0.8.5",
"serde_json",
"thiserror 1.0.69",
"tokio",
"tokio-stream",
"tracing",
"chrono",
"opentelemetry",
"opentelemetry_sdk",
]
[[package]]
@ -6752,7 +6683,7 @@ dependencies = [
"futures-channel",
"futures-executor",
"futures-util",
"opentelemetry 0.31.0",
"opentelemetry",
"percent-encoding",
"rand 0.9.2",
"thiserror 2.0.18",
@ -6946,8 +6877,8 @@ dependencies = [
"http 1.4.0",
"indexmap 2.13.0",
"keyring",
"opentelemetry-otlp 0.31.0",
"opentelemetry_sdk 0.31.0",
"opentelemetry-otlp",
"opentelemetry_sdk",
"reqwest 0.12.28",
"rmcp 0.14.0",
"serde",
@ -6955,7 +6886,7 @@ dependencies = [
"shlex",
"thiserror 2.0.18",
"tokio",
"tonic 0.14.3",
"tonic",
"tracing",
"url",
]
@ -7375,16 +7306,6 @@ dependencies = [
"windows 0.62.2",
]
[[package]]
name = "prost"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive 0.13.5",
]
[[package]]
name = "prost"
version = "0.14.3"
@ -7392,20 +7313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568"
dependencies = [
"bytes",
"prost-derive 0.14.3",
]
[[package]]
name = "prost-derive"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.114",
"prost-derive",
]
[[package]]
@ -7940,7 +7848,7 @@ dependencies = [
"tokio",
"tokio-rustls 0.26.4",
"tokio-util",
"tower 0.5.3",
"tower",
"tower-http",
"tower-service",
"url",
@ -10555,36 +10463,6 @@ version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tonic"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum 0.7.9",
"base64 0.22.1",
"bytes",
"h2 0.4.13",
"http 1.4.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.1",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"prost 0.13.5",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tower 0.4.13",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic"
version = "0.14.3"
@ -10608,7 +10486,7 @@ dependencies = [
"sync_wrapper 1.0.2",
"tokio",
"tokio-stream",
"tower 0.5.3",
"tower",
"tower-layer",
"tower-service",
"tracing",
@ -10621,28 +10499,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6c55a2d6a14174563de34409c9f92ff981d006f56da9c6ecd40d9d4a31500b0"
dependencies = [
"bytes",
"prost 0.14.3",
"tonic 0.14.3",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"indexmap 1.9.3",
"pin-project",
"pin-project-lite",
"rand 0.8.5",
"slab",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
"prost",
"tonic",
]
[[package]]
@ -10688,7 +10546,7 @@ dependencies = [
"pin-project-lite",
"tokio",
"tokio-util",
"tower 0.5.3",
"tower",
"tower-layer",
"tower-service",
"tracing",
@ -10764,14 +10622,12 @@ dependencies = [
[[package]]
name = "tracing-opentelemetry"
version = "0.28.0"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a971f6058498b5c0f1affa23e7ea202057a7301dbff68e968b2d578bcbd053"
checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
dependencies = [
"js-sys",
"once_cell",
"opentelemetry 0.27.1",
"opentelemetry_sdk 0.27.1",
"opentelemetry",
"smallvec",
"tracing",
"tracing-core",

View file

@ -62,3 +62,9 @@ wiremock = "0.6"
serial_test = "3.2.0"
test-case = "3.3.1"
url = "2.5.8"
opentelemetry = "0.31"
opentelemetry_sdk = { version = "0.31", features = ["metrics"] }
opentelemetry-otlp = "0.31"
opentelemetry-appender-tracing = "0.31"
opentelemetry-stdout = { version = "0.31", features = ["trace", "metrics", "logs"] }
tracing-opentelemetry = "0.32"

View file

@ -6,7 +6,8 @@ use tracing_subscriber::{
Registry,
};
use goose::tracing::{langfuse_layer, otlp_layer};
use goose::otel::otlp;
use goose::tracing::langfuse_layer;
// Used to ensure we only set up tracing once
static INIT: Once = Once::new();
@ -68,25 +69,7 @@ fn setup_logging_internal(name: Option<&str>, force: bool) -> Result<()> {
];
if !force {
if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) =
otlp_layer::init_otlp()
{
layers.push(
otlp_tracing_layer
.with_filter(otlp_layer::create_otlp_tracing_filter())
.boxed(),
);
layers.push(
otlp_metrics_layer
.with_filter(otlp_layer::create_otlp_metrics_filter())
.boxed(),
);
layers.push(
otlp_logs_layer
.with_filter(otlp_layer::create_otlp_logs_filter())
.boxed(),
);
}
layers.extend(otlp::init_otlp_layers(goose::config::Config::global()));
}
if let Some(langfuse) = langfuse_layer::create_langfuse_observer() {

View file

@ -9,28 +9,9 @@ async fn main() -> Result<()> {
let result = cli().await;
// Only wait for telemetry flush if OTLP is configured
let should_wait = goose::config::Config::global()
.get_param::<String>("otel_exporter_otlp_endpoint")
.is_ok();
if should_wait {
// Use a shorter, dynamic wait with max timeout
let max_wait = tokio::time::Duration::from_millis(500);
let start = tokio::time::Instant::now();
// Give telemetry a chance to flush, but don't wait too long
while start.elapsed() < max_wait {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
// In future, we could check if there are pending spans/metrics here
// For now, we just do a quick wait to allow batch exports to complete
if start.elapsed() >= tokio::time::Duration::from_millis(200) {
break; // Most exports should complete within 200ms
}
}
goose::tracing::shutdown_otlp();
if goose::otel::otlp::is_otlp_initialized() {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
goose::otel::otlp::shutdown_otlp();
}
result

View file

@ -58,6 +58,12 @@ pub async fn run() -> Result<()> {
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;
if goose::otel::otlp::is_otlp_initialized() {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
goose::otel::otlp::shutdown_otlp();
}
info!("server shutdown complete");
Ok(())
}

View file

@ -5,7 +5,8 @@ use tracing_subscriber::{
Registry,
};
use goose::tracing::{langfuse_layer, otlp_layer};
use goose::otel::otlp;
use goose::tracing::langfuse_layer;
/// Sets up the logging infrastructure for the application.
/// This includes:
@ -54,23 +55,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
console_layer.with_filter(base_env_filter).boxed(),
];
if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = otlp_layer::init_otlp() {
layers.push(
otlp_tracing_layer
.with_filter(otlp_layer::create_otlp_tracing_filter())
.boxed(),
);
layers.push(
otlp_metrics_layer
.with_filter(otlp_layer::create_otlp_metrics_filter())
.boxed(),
);
layers.push(
otlp_logs_layer
.with_filter(otlp_layer::create_otlp_logs_filter())
.boxed(),
);
}
layers.extend(otlp::init_otlp_layers(goose::config::Config::global()));
if let Some(langfuse) = langfuse_layer::create_langfuse_observer() {
layers.push(langfuse.with_filter(LevelFilter::DEBUG).boxed());

View file

@ -52,14 +52,12 @@ webbrowser = { workspace = true }
lazy_static = "1.5.0"
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-opentelemetry = "0.28"
opentelemetry = "0.27"
opentelemetry-appender-tracing = "0.27"
opentelemetry_sdk = { version = "0.27", features = ["rt-tokio", "metrics"] }
opentelemetry-otlp = { version = "0.27", features = [
"http-proto",
"reqwest-client",
] }
tracing-opentelemetry = { workspace = true }
opentelemetry = { workspace = true }
opentelemetry-appender-tracing = { workspace = true }
opentelemetry_sdk = { workspace = true }
opentelemetry-otlp = { workspace = true }
opentelemetry-stdout = { workspace = true }
keyring = { version = "3.6.2", features = [
"apple-native",
"windows-native",

View file

@ -12,6 +12,7 @@ pub mod logging;
pub mod mcp_utils;
pub mod model;
pub mod oauth;
pub mod otel;
pub mod permission;
pub mod posthog;
pub mod prompt_template;

View file

@ -0,0 +1 @@
pub mod otlp;

View file

@ -0,0 +1,551 @@
use opentelemetry::trace::TracerProvider;
use opentelemetry::{global, KeyValue};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider};
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use opentelemetry_sdk::resource::{EnvResourceDetector, TelemetryResourceDetector};
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::Resource;
use std::env;
use std::sync::Mutex;
use tracing::{Level, Metadata};
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::filter::FilterFn;
use tracing_subscriber::Layer as _;
pub type OtlpTracingLayer =
OpenTelemetryLayer<tracing_subscriber::Registry, opentelemetry_sdk::trace::Tracer>;
pub type OtlpMetricsLayer = MetricsLayer<tracing_subscriber::Registry, SdkMeterProvider>;
pub type OtlpLogsLayer = OpenTelemetryTracingBridge<SdkLoggerProvider, SdkLogger>;
pub type OtlpResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
static TRACER_PROVIDER: Mutex<Option<SdkTracerProvider>> = Mutex::new(None);
static METER_PROVIDER: Mutex<Option<SdkMeterProvider>> = Mutex::new(None);
static LOGGER_PROVIDER: Mutex<Option<SdkLoggerProvider>> = Mutex::new(None);
#[derive(Debug, Clone, PartialEq)]
pub enum ExporterType {
Otlp,
Console,
None,
}
impl ExporterType {
pub fn from_env_value(value: &str) -> Self {
match value.to_lowercase().as_str() {
"" | "otlp" => ExporterType::Otlp,
"console" | "stdout" => ExporterType::Console,
_ => ExporterType::None,
}
}
}
/// Returns the exporter type for a signal, or None if disabled.
///
/// Checks in order:
/// 1. OTEL_SDK_DISABLED — disables everything
/// 2. OTEL_{SIGNAL}_EXPORTER — explicit exporter selection ("none" disables)
/// 3. OTEL_EXPORTER_OTLP_{SIGNAL}_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT — enables OTLP
pub fn signal_exporter(signal: &str) -> Option<ExporterType> {
if env::var("OTEL_SDK_DISABLED")
.ok()
.is_some_and(|v| v.eq_ignore_ascii_case("true"))
{
return None;
}
let exporter_var = format!("OTEL_{}_EXPORTER", signal.to_uppercase());
if let Ok(val) = env::var(&exporter_var) {
let typ = ExporterType::from_env_value(&val);
return if matches!(typ, ExporterType::None) {
None
} else {
Some(typ)
};
}
let signal_endpoint = format!("OTEL_EXPORTER_OTLP_{}_ENDPOINT", signal.to_uppercase());
let has_endpoint = env::var(&signal_endpoint)
.ok()
.is_some_and(|v| !v.is_empty())
|| env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
.ok()
.is_some_and(|v| !v.is_empty());
if has_endpoint {
Some(ExporterType::Otlp)
} else {
None
}
}
/// Promotes goose config-file OTel settings to env vars before exporter build.
pub fn promote_config_to_env(config: &crate::config::Config) {
if env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_err() {
if let Ok(endpoint) = config.get_param::<String>("otel_exporter_otlp_endpoint") {
env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint);
}
}
if env::var("OTEL_EXPORTER_OTLP_TIMEOUT").is_err() {
if let Ok(timeout) = config.get_param::<u64>("otel_exporter_otlp_timeout") {
env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", timeout.to_string());
}
}
}
fn create_resource() -> Resource {
let mut builder = Resource::builder_empty()
.with_attributes([
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
])
.with_detector(Box::new(EnvResourceDetector::new()))
.with_detector(Box::new(TelemetryResourceDetector));
// OTEL_SERVICE_NAME takes highest priority (skip SdkProvidedResourceDetector
// which would fall back to "unknown_service" when unset)
if let Ok(name) = std::env::var("OTEL_SERVICE_NAME") {
if !name.is_empty() {
builder = builder.with_service_name(name);
}
}
builder.build()
}
/// Initializes all OTLP signal layers (traces, metrics, logs) and propagation.
/// Returns boxed layers ready to add to a subscriber.
pub fn init_otlp_layers(
config: &crate::config::Config,
) -> Vec<Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync>> {
promote_config_to_env(config);
let mut layers: Vec<
Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync>,
> = Vec::new();
if let Ok(layer) = create_otlp_tracing_layer() {
layers.push(layer.with_filter(create_otlp_tracing_filter()).boxed());
}
if let Ok(layer) = create_otlp_metrics_layer() {
layers.push(layer.with_filter(create_otlp_metrics_filter()).boxed());
}
if let Ok(layer) = create_otlp_logs_layer() {
layers.push(layer.with_filter(create_otlp_logs_filter()).boxed());
}
if !layers.is_empty() {
global::set_text_map_propagator(TraceContextPropagator::new());
}
layers
}
fn create_otlp_tracing_layer() -> OtlpResult<OtlpTracingLayer> {
let exporter = signal_exporter("traces").ok_or("Traces not enabled")?;
let resource = create_resource();
let tracer_provider = match exporter {
ExporterType::Otlp => {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.build()?;
SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build()
}
ExporterType::Console => {
let exporter = opentelemetry_stdout::SpanExporter::default();
SdkTracerProvider::builder()
.with_simple_exporter(exporter)
.with_resource(resource)
.build()
}
ExporterType::None => return Err("Traces exporter set to none".into()),
};
global::set_tracer_provider(tracer_provider.clone());
let tracer = tracer_provider.tracer("goose");
*TRACER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(tracer_provider);
Ok(tracing_opentelemetry::layer().with_tracer(tracer))
}
fn create_otlp_metrics_layer() -> OtlpResult<OtlpMetricsLayer> {
let exporter = signal_exporter("metrics").ok_or("Metrics not enabled")?;
let resource = create_resource();
let meter_provider = match exporter {
ExporterType::Otlp => {
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.build()?;
SdkMeterProvider::builder()
.with_resource(resource)
.with_periodic_exporter(exporter)
.build()
}
ExporterType::Console => {
let exporter = opentelemetry_stdout::MetricExporter::default();
SdkMeterProvider::builder()
.with_resource(resource)
.with_periodic_exporter(exporter)
.build()
}
ExporterType::None => return Err("Metrics exporter set to none".into()),
};
global::set_meter_provider(meter_provider.clone());
*METER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(meter_provider.clone());
Ok(MetricsLayer::new(meter_provider))
}
fn create_otlp_logs_layer() -> OtlpResult<OtlpLogsLayer> {
let exporter = signal_exporter("logs").ok_or("Logs not enabled")?;
let resource = create_resource();
let logger_provider = match exporter {
ExporterType::Otlp => {
let exporter = opentelemetry_otlp::LogExporter::builder()
.with_http()
.build()?;
SdkLoggerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build()
}
ExporterType::Console => {
let exporter = opentelemetry_stdout::LogExporter::default();
SdkLoggerProvider::builder()
.with_simple_exporter(exporter)
.with_resource(resource)
.build()
}
ExporterType::None => return Err("Logs exporter set to none".into()),
};
let bridge = OpenTelemetryTracingBridge::new(&logger_provider);
*LOGGER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(logger_provider);
Ok(bridge)
}
pub fn is_otlp_initialized() -> bool {
TRACER_PROVIDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_some()
|| METER_PROVIDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_some()
|| LOGGER_PROVIDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_some()
}
/// Creates a custom filter for OTLP tracing that captures:
/// - All spans at INFO level and above
/// - Specific spans marked with "otel.trace" field
/// - Events from specific modules related to telemetry
fn create_otlp_tracing_filter() -> FilterFn<impl Fn(&Metadata<'_>) -> bool> {
FilterFn::new(|metadata: &Metadata<'_>| {
if metadata.level() <= &Level::INFO {
return true;
}
if metadata.level() == &Level::DEBUG {
let target = metadata.target();
if target.starts_with("goose::")
|| target.starts_with("opentelemetry")
|| target.starts_with("tracing_opentelemetry")
{
return true;
}
}
false
})
}
/// Creates a custom filter for OTLP metrics that captures:
/// - All events at INFO level and above
/// - Specific events marked with "otel.metric" field
/// - Events that should be converted to metrics
fn create_otlp_metrics_filter() -> FilterFn<impl Fn(&Metadata<'_>) -> bool> {
FilterFn::new(|metadata: &Metadata<'_>| {
if metadata.level() <= &Level::INFO {
return true;
}
if metadata.level() == &Level::DEBUG {
let target = metadata.target();
if target.starts_with("goose::telemetry")
|| target.starts_with("goose::metrics")
|| target.contains("metric")
{
return true;
}
}
false
})
}
/// Creates a custom filter for OTLP logs that captures:
/// - All events at WARN level and above
fn create_otlp_logs_filter() -> FilterFn<impl Fn(&Metadata<'_>) -> bool> {
FilterFn::new(|metadata: &Metadata<'_>| metadata.level() <= &Level::WARN)
}
/// Shutdown OTLP providers gracefully
pub fn shutdown_otlp() {
if let Some(provider) = TRACER_PROVIDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
let _ = provider.shutdown();
}
if let Some(provider) = METER_PROVIDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
let _ = provider.shutdown();
}
if let Some(provider) = LOGGER_PROVIDER
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
let _ = provider.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
use opentelemetry::metrics::{Meter, MeterProvider};
use opentelemetry::InstrumentationScope;
use std::sync::Arc;
use test_case::test_case;
// set_meter_provider requires P: MeterProvider, not Arc<dyn MeterProvider>
struct SavedMeterProvider(Arc<dyn MeterProvider + Send + Sync>);
impl MeterProvider for SavedMeterProvider {
fn meter_with_scope(&self, scope: InstrumentationScope) -> Meter {
self.0.meter_with_scope(scope)
}
}
struct OtelTestGuard {
_env: env_lock::EnvGuard<'static>,
prev_tracer: global::GlobalTracerProvider,
prev_meter: Arc<dyn MeterProvider + Send + Sync>,
}
impl Drop for OtelTestGuard {
fn drop(&mut self) {
global::set_tracer_provider(self.prev_tracer.clone());
global::set_meter_provider(SavedMeterProvider(self.prev_meter.clone()));
}
}
fn clear_otel_env(overrides: &[(&str, &str)]) -> OtelTestGuard {
let prev_tracer = global::tracer_provider();
let prev_meter = global::meter_provider();
let guard = env_lock::lock_env([
("OTEL_SDK_DISABLED", None::<&str>),
("OTEL_TRACES_EXPORTER", None),
("OTEL_METRICS_EXPORTER", None),
("OTEL_LOGS_EXPORTER", None),
("OTEL_EXPORTER_OTLP_ENDPOINT", None),
("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", None),
("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", None),
("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", None),
("OTEL_EXPORTER_OTLP_TIMEOUT", None),
("OTEL_SERVICE_NAME", None),
("OTEL_RESOURCE_ATTRIBUTES", None),
]);
for &(k, v) in overrides {
env::set_var(k, v);
}
OtelTestGuard {
_env: guard,
prev_tracer,
prev_meter,
}
}
#[test]
fn exporter_type_from_env_value() {
assert_eq!(ExporterType::from_env_value("otlp"), ExporterType::Otlp);
assert_eq!(ExporterType::from_env_value("OTLP"), ExporterType::Otlp);
assert_eq!(ExporterType::from_env_value(""), ExporterType::Otlp);
assert_eq!(
ExporterType::from_env_value("console"),
ExporterType::Console
);
assert_eq!(
ExporterType::from_env_value("stdout"),
ExporterType::Console
);
assert_eq!(ExporterType::from_env_value("none"), ExporterType::None);
assert_eq!(ExporterType::from_env_value("NONE"), ExporterType::None);
assert_eq!(ExporterType::from_env_value("unknown"), ExporterType::None);
}
#[test_case(&[("OTEL_SDK_DISABLED", "true")]; "OTEL_SDK_DISABLED disables all signals")]
#[test_case(&[]; "no env vars returns None")]
fn signal_exporter_disabled(env: &[(&str, &str)]) {
let _guard = clear_otel_env(env);
assert!(signal_exporter("traces").is_none());
assert!(signal_exporter("metrics").is_none());
assert!(signal_exporter("logs").is_none());
}
#[test_case("traces", &[("OTEL_TRACES_EXPORTER", "console")], Some(ExporterType::Console); "OTEL_TRACES_EXPORTER=console")]
#[test_case("traces", &[("OTEL_TRACES_EXPORTER", "none")], None; "OTEL_TRACES_EXPORTER=none")]
#[test_case("traces", &[("OTEL_TRACES_EXPORTER", "otlp")], Some(ExporterType::Otlp); "OTEL_TRACES_EXPORTER=otlp")]
#[test_case("metrics", &[("OTEL_METRICS_EXPORTER", "console")], Some(ExporterType::Console); "OTEL_METRICS_EXPORTER=console")]
#[test_case("logs", &[("OTEL_LOGS_EXPORTER", "none")], None; "OTEL_LOGS_EXPORTER=none")]
fn signal_exporter_by_var(signal: &str, env: &[(&str, &str)], expected: Option<ExporterType>) {
let _guard = clear_otel_env(env);
assert_eq!(signal_exporter(signal), expected);
}
#[test_case("traces", &[("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")], Some(ExporterType::Otlp); "generic endpoint enables traces")]
#[test_case("traces", &[("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4318")], Some(ExporterType::Otlp); "signal-specific endpoint enables traces")]
#[test_case("metrics", &[("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")], Some(ExporterType::Otlp); "signal-specific endpoint enables metrics")]
#[test_case("traces", &[("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")], None; "metrics endpoint does not enable traces")]
#[test_case("traces", &[("OTEL_TRACES_EXPORTER", "none"), ("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")], None; "OTEL_TRACES_EXPORTER=none overrides endpoint")]
#[test_case("traces", &[("OTEL_EXPORTER_OTLP_ENDPOINT", "")], None; "empty endpoint returns None")]
fn signal_exporter_endpoints(
signal: &str,
env: &[(&str, &str)],
expected: Option<ExporterType>,
) {
let _guard = clear_otel_env(env);
assert_eq!(signal_exporter(signal), expected);
}
#[test_case("console"; "console")]
#[test_case("otlp"; "otlp")]
fn test_all_layers_ok(exporter: &str) {
let rt = tokio::runtime::Runtime::new().unwrap();
let _guard = rt.enter();
let _env = clear_otel_env(&[
("OTEL_TRACES_EXPORTER", exporter),
("OTEL_METRICS_EXPORTER", exporter),
("OTEL_LOGS_EXPORTER", exporter),
("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"),
]);
assert!(create_otlp_tracing_layer().is_ok());
assert!(create_otlp_metrics_layer().is_ok());
assert!(create_otlp_logs_layer().is_ok());
shutdown_otlp();
}
#[test_case(
&[],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.build();
"no env vars uses goose defaults"
)]
#[test_case(
&[("OTEL_SERVICE_NAME", "custom")],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.with_service_name("custom")
.build();
"OTEL_SERVICE_NAME overrides service.name"
)]
#[test_case(
&[("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.with_attribute(KeyValue::new("deployment.environment", "prod"))
.build();
"OTEL_RESOURCE_ATTRIBUTES adds custom attributes"
)]
#[test_case(
&[("OTEL_SERVICE_NAME", "custom"), ("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.with_service_name("custom")
.with_attribute(KeyValue::new("deployment.environment", "prod"))
.build();
"OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES combine"
)]
fn test_create_resource(env: &[(&str, &str)], expected: Resource) {
let _guard = clear_otel_env(env);
assert_eq!(create_resource(), expected);
}
fn test_config(
params: &[(&str, &str)],
) -> (
crate::config::Config,
tempfile::NamedTempFile,
tempfile::NamedTempFile,
) {
let config_file = tempfile::NamedTempFile::new().unwrap();
let secrets_file = tempfile::NamedTempFile::new().unwrap();
let yaml: String = params.iter().map(|(k, v)| format!("{k}: {v}\n")).collect();
std::fs::write(config_file.path(), yaml).unwrap();
let config =
crate::config::Config::new_with_file_secrets(config_file.path(), secrets_file.path())
.unwrap();
(config, config_file, secrets_file)
}
#[test_case(
&[],
&[("otel_exporter_otlp_endpoint", "http://config:4318"), ("otel_exporter_otlp_timeout", "5000")],
Some("http://config:4318"), Some("5000");
"config promotes to env when unset"
)]
#[test_case(
&[("OTEL_EXPORTER_OTLP_ENDPOINT", "http://env:4318"), ("OTEL_EXPORTER_OTLP_TIMEOUT", "3000")],
&[("otel_exporter_otlp_endpoint", "http://config:4318"), ("otel_exporter_otlp_timeout", "5000")],
Some("http://env:4318"), Some("3000");
"env var takes precedence over config"
)]
#[test_case(
&[],
&[],
None, None;
"no config leaves env unset"
)]
fn test_promote_config_to_env(
env_overrides: &[(&str, &str)],
cfg: &[(&str, &str)],
expect_endpoint: Option<&str>,
expect_timeout: Option<&str>,
) {
let _guard = clear_otel_env(env_overrides);
let (config, _cf, _sf) = test_config(cfg);
promote_config_to_env(&config);
assert_eq!(
env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok().as_deref(),
expect_endpoint
);
assert_eq!(
env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok().as_deref(),
expect_timeout
);
}
}

View file

@ -1,16 +1,11 @@
pub mod langfuse_layer;
mod observation_layer;
pub mod otlp_layer;
pub mod rate_limiter;
pub use langfuse_layer::{create_langfuse_observer, LangfuseBatchManager};
pub use observation_layer::{
flatten_metadata, map_level, BatchManager, ObservationLayer, SpanData, SpanTracker,
};
pub use otlp_layer::{
create_otlp_metrics_filter, create_otlp_tracing_filter, create_otlp_tracing_layer,
init_otlp_metrics, init_otlp_tracing, init_otlp_tracing_only, shutdown_otlp, OtlpConfig,
};
pub use rate_limiter::{
MetricData, RateLimitedTelemetrySender, SpanData as RateLimitedSpanData, TelemetryEvent,
};

View file

@ -1,337 +0,0 @@
use opentelemetry::trace::TracerProvider;
use opentelemetry::{global, KeyValue};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::logs::{Logger, LoggerProvider};
use opentelemetry_sdk::trace::{self, RandomIdGenerator, Sampler};
use opentelemetry_sdk::{runtime, Resource};
use std::time::Duration;
use tracing::{Level, Metadata};
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::filter::FilterFn;
pub type OtlpTracingLayer =
OpenTelemetryLayer<tracing_subscriber::Registry, opentelemetry_sdk::trace::Tracer>;
pub type OtlpMetricsLayer = MetricsLayer<tracing_subscriber::Registry>;
pub type OtlpLogsLayer = OpenTelemetryTracingBridge<LoggerProvider, Logger>;
pub type OtlpLayers = (OtlpTracingLayer, OtlpMetricsLayer, OtlpLogsLayer);
pub type OtlpResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
#[derive(Debug, Clone)]
pub struct OtlpConfig {
pub endpoint: String,
pub timeout: Duration,
}
impl Default for OtlpConfig {
fn default() -> Self {
Self {
endpoint: "http://localhost:4318".to_string(),
timeout: Duration::from_secs(10),
}
}
}
impl OtlpConfig {
pub fn from_config() -> Option<Self> {
let config = crate::config::Config::global();
// Try to get the endpoint from config (checks OTEL_EXPORTER_OTLP_ENDPOINT env var first)
let endpoint = config
.get_param::<String>("otel_exporter_otlp_endpoint")
.ok()?;
let mut otlp_config = Self {
endpoint,
timeout: Duration::from_secs(10),
};
// Try to get timeout from config (checks OTEL_EXPORTER_OTLP_TIMEOUT env var first)
if let Ok(timeout_ms) = config.get_param::<u64>("otel_exporter_otlp_timeout") {
otlp_config.timeout = Duration::from_millis(timeout_ms);
}
Some(otlp_config)
}
}
pub fn init_otlp_tracing(config: &OtlpConfig) -> OtlpResult<()> {
let resource = Resource::new(vec![
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
]);
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(&config.endpoint)
.with_timeout(config.timeout)
.build()?;
let tracer_provider = trace::TracerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.with_resource(resource.clone())
.with_id_generator(RandomIdGenerator::default())
.with_sampler(Sampler::AlwaysOn)
.build();
global::set_tracer_provider(tracer_provider);
Ok(())
}
pub fn init_otlp_metrics(config: &OtlpConfig) -> OtlpResult<()> {
let resource = Resource::new(vec![
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
]);
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.with_endpoint(&config.endpoint)
.with_timeout(config.timeout)
.build()?;
let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
.with_resource(resource)
.with_reader(
opentelemetry_sdk::metrics::PeriodicReader::builder(exporter, runtime::Tokio)
.with_interval(Duration::from_secs(3))
.build(),
)
.build();
global::set_meter_provider(meter_provider);
Ok(())
}
pub fn create_otlp_tracing_layer() -> OtlpResult<OtlpTracingLayer> {
let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?;
let resource = Resource::new(vec![
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
]);
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(&config.endpoint)
.with_timeout(config.timeout)
.build()?;
let tracer_provider = trace::TracerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.with_max_events_per_span(2048)
.with_max_attributes_per_span(512)
.with_max_links_per_span(512)
.with_resource(resource)
.with_id_generator(RandomIdGenerator::default())
.with_sampler(Sampler::TraceIdRatioBased(0.1))
.build();
let tracer = tracer_provider.tracer("goose");
Ok(tracing_opentelemetry::layer().with_tracer(tracer))
}
pub fn create_otlp_metrics_layer() -> OtlpResult<OtlpMetricsLayer> {
let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?;
let resource = Resource::new(vec![
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
]);
let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.with_endpoint(&config.endpoint)
.with_timeout(config.timeout)
.build()?;
let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
.with_resource(resource)
.with_reader(
opentelemetry_sdk::metrics::PeriodicReader::builder(exporter, runtime::Tokio)
.with_interval(Duration::from_millis(2000))
.build(),
)
.build();
global::set_meter_provider(meter_provider.clone());
Ok(tracing_opentelemetry::MetricsLayer::new(meter_provider))
}
pub fn create_otlp_logs_layer() -> OtlpResult<OpenTelemetryTracingBridge<LoggerProvider, Logger>> {
let config = OtlpConfig::from_config().ok_or("OTEL_EXPORTER_OTLP_ENDPOINT not configured")?;
let resource = Resource::new(vec![
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
]);
let exporter = opentelemetry_otlp::LogExporter::builder()
.with_http()
.with_endpoint(&config.endpoint)
.with_timeout(config.timeout)
.build()?;
let logger_provider = LoggerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.with_resource(resource)
.build();
Ok(OpenTelemetryTracingBridge::new(&logger_provider))
}
pub fn init_otlp() -> OtlpResult<OtlpLayers> {
let tracing_layer = create_otlp_tracing_layer()?;
let metrics_layer = create_otlp_metrics_layer()?;
let logs_layer = create_otlp_logs_layer()?;
Ok((tracing_layer, metrics_layer, logs_layer))
}
pub fn init_otlp_tracing_only() -> OtlpResult<OtlpTracingLayer> {
create_otlp_tracing_layer()
}
/// Creates a custom filter for OTLP tracing that captures:
/// - All spans at INFO level and above
/// - Specific spans marked with "otel.trace" field
/// - Events from specific modules related to telemetry
pub fn create_otlp_tracing_filter() -> FilterFn<impl Fn(&Metadata<'_>) -> bool> {
FilterFn::new(|metadata: &Metadata<'_>| {
if metadata.level() <= &Level::INFO {
return true;
}
if metadata.level() == &Level::DEBUG {
let target = metadata.target();
if target.starts_with("goose::")
|| target.starts_with("opentelemetry")
|| target.starts_with("tracing_opentelemetry")
{
return true;
}
}
false
})
}
/// Creates a custom filter for OTLP metrics that captures:
/// - All events at INFO level and above
/// - Specific events marked with "otel.metric" field
/// - Events that should be converted to metrics
pub fn create_otlp_metrics_filter() -> FilterFn<impl Fn(&Metadata<'_>) -> bool> {
FilterFn::new(|metadata: &Metadata<'_>| {
if metadata.level() <= &Level::INFO {
return true;
}
if metadata.level() == &Level::DEBUG {
let target = metadata.target();
if target.starts_with("goose::telemetry")
|| target.starts_with("goose::metrics")
|| target.contains("metric")
{
return true;
}
}
false
})
}
/// Creates a custom filter for OTLP metrics that captures:
/// - All events at WARN level and above
pub fn create_otlp_logs_filter() -> FilterFn<impl Fn(&Metadata<'_>) -> bool> {
FilterFn::new(|metadata: &Metadata<'_>| {
if metadata.level() <= &Level::WARN {
return true;
}
false
})
}
/// Shutdown OTLP providers gracefully
pub fn shutdown_otlp() {
// Shutdown the tracer provider and flush any pending spans
global::shutdown_tracer_provider();
// Force flush of metrics by waiting a bit
// The meter provider doesn't have a direct shutdown method in the current SDK,
// but we can give it time to export any pending metrics
std::thread::sleep(std::time::Duration::from_millis(500));
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_otlp_config_default() {
let config = OtlpConfig::default();
assert_eq!(config.endpoint, "http://localhost:4318");
assert_eq!(config.timeout, Duration::from_secs(10));
}
#[test]
fn test_otlp_config_from_config() {
use tempfile::NamedTempFile;
// Save original env vars
let original_endpoint = env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok();
let original_timeout = env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok();
// Clear env vars to ensure we're testing config file
env::remove_var("OTEL_EXPORTER_OTLP_ENDPOINT");
env::remove_var("OTEL_EXPORTER_OTLP_TIMEOUT");
// Create a test config file
let temp_file = NamedTempFile::new().unwrap();
let test_config = crate::config::Config::new(temp_file.path(), "test-otlp").unwrap();
// Set values in config
test_config
.set_param("otel_exporter_otlp_endpoint", "http://config:4318")
.unwrap();
test_config
.set_param("otel_exporter_otlp_timeout", 3000)
.unwrap();
// Test that from_config reads from the config file
// Note: We can't easily test from_config() directly since it uses Config::global()
// But we can test that the config system works with our keys
let endpoint: String = test_config
.get_param("otel_exporter_otlp_endpoint")
.unwrap();
assert_eq!(endpoint, "http://config:4318");
let timeout: u64 = test_config.get_param("otel_exporter_otlp_timeout").unwrap();
assert_eq!(timeout, 3000);
// Test env var override still works
env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "http://env:4317");
let endpoint: String = test_config
.get_param("otel_exporter_otlp_endpoint")
.unwrap();
assert_eq!(endpoint, "http://env:4317");
// Restore original env vars
match original_endpoint {
Some(val) => env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", val),
None => env::remove_var("OTEL_EXPORTER_OTLP_ENDPOINT"),
}
match original_timeout {
Some(val) => env::set_var("OTEL_EXPORTER_OTLP_TIMEOUT", val),
None => env::remove_var("OTEL_EXPORTER_OTLP_TIMEOUT"),
}
}
}