mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
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
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:
parent
f00630ad20
commit
ab5407508b
12 changed files with 621 additions and 595 deletions
226
Cargo.lock
generated
226
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1
crates/goose/src/otel/mod.rs
Normal file
1
crates/goose/src/otel/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod otlp;
|
||||
551
crates/goose/src/otel/otlp.rs
Normal file
551
crates/goose/src/otel/otlp.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue