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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions rust/observability/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ thiserror = "2"
# opentelemetry-http is pulled with no features — we only need the `HttpClient`
# trait to implement it ourselves (the `reqwest` feature, which impls it for
# reqwest::Client, is no longer used).
opentelemetry = { version = "0.32", features = ["trace", "metrics"] }
opentelemetry = { version = "0.32", features = ["trace", "metrics", "logs"] }
# The two `experimental_*_with_async_runtime` features expose the async-runtime
# batch span processor + periodic metric reader used in `otel.rs`. They are
# REQUIRED for correctness, not a nicety: the default (thread-based) processors
Expand All @@ -59,11 +59,16 @@ opentelemetry = { version = "0.32", features = ["trace", "metrics"] }
opentelemetry_sdk = { version = "0.32", features = [
"trace",
"metrics",
"logs",
"rt-tokio",
"experimental_trace_batch_span_processor_with_async_runtime",
"experimental_metrics_periodicreader_with_async_runtime",
# Same reactor-panic rationale as traces/metrics (SMOODEV-2045): the DEFAULT
# `BatchLogProcessor` drives export with `block_on` on a bare OS thread where
# our reqwest-backed exporter has no Tokio reactor and aborts the process.
"experimental_logs_batch_log_processor_with_async_runtime",
] }
opentelemetry-otlp = { version = "0.32", features = ["trace", "metrics", "http-json"], default-features = false }
opentelemetry-otlp = { version = "0.32", features = ["trace", "metrics", "logs", "http-json"], default-features = false }
opentelemetry-http = { version = "0.32", default-features = false }
opentelemetry-semantic-conventions = "0.32"

Expand All @@ -77,6 +82,14 @@ pin-project-lite = { version = "0.2", optional = true }
# 0.5 supports reqwest 0.13 (this crate's pin).
reqwest-middleware = { version = "0.5", optional = true }

# The STANDARD tracing→OTel-log bridge: a `tracing_subscriber::Layer` that turns
# every `tracing` event into an OTel `LogRecord` on our `SdkLoggerProvider`. The
# SDK logger enriches each record's trace_id/span_id from the ACTIVE
# `opentelemetry::Context` at emit time, so a log fired inside one of this crate's
# spans (tower/gen_ai/reqwest, all OTel-native) is correlated automatically. The
# host installs the layer via `OtelSdkHandle::tracing_appender_layer()`.
opentelemetry-appender-tracing = "0.32"

[features]
default = []
# OTel server-span layer for Tower/Axum services.
Expand All @@ -89,3 +102,9 @@ reqwest-middleware = ["dep:reqwest-middleware", "dep:reqwest"]
[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "test-util"] }
wiremock = "0.6"
# Logs signal tests: emit `tracing` events through the bridge and assert on the
# captured records. `testing` unlocks `InMemoryLogExporter`; the feature unifies
# onto the normal `opentelemetry_sdk` dep only when building test targets.
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["registry", "std"] }
opentelemetry_sdk = { version = "0.32", features = ["testing"] }
16 changes: 13 additions & 3 deletions rust/observability/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
//! ## Env vars (identical names to the TS SDK)
//!
//! - `SMOOAI_OBSERVABILITY_ENDPOINT` — base ingest URL (e.g. `https://api.smoo.ai`).
//! `/v1/traces` and `/v1/metrics` are appended. Per-signal overrides via the
//! standard `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `_METRICS_ENDPOINT`.
//! `/v1/traces`, `/v1/metrics`, and `/v1/logs` are appended. Per-signal
//! overrides via the standard `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` /
//! `_METRICS_ENDPOINT` / `_LOGS_ENDPOINT`.
//! - Auth (pick one; pre-minted token wins):
//! - `SMOOAI_OBSERVABILITY_TOKEN` — pre-minted Bearer JWT (not refreshed).
//! - `SMOOAI_OBSERVABILITY_AUTH_URL` + `_CLIENT_ID` + `_CLIENT_SECRET` —
Expand All @@ -36,6 +37,7 @@ pub struct BootstrapEnv {
pub endpoint: Option<String>,
pub traces_endpoint: Option<String>,
pub metrics_endpoint: Option<String>,
pub logs_endpoint: Option<String>,
pub token: Option<String>,
pub auth_url: Option<String>,
pub client_id: Option<String>,
Expand All @@ -54,6 +56,7 @@ impl BootstrapEnv {
endpoint: env::var("SMOOAI_OBSERVABILITY_ENDPOINT").ok(),
traces_endpoint: env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").ok(),
metrics_endpoint: env::var("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT").ok(),
logs_endpoint: env::var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT").ok(),
token: env::var("SMOOAI_OBSERVABILITY_TOKEN").ok(),
auth_url: env::var("SMOOAI_OBSERVABILITY_AUTH_URL").ok(),
client_id: env::var("SMOOAI_OBSERVABILITY_CLIENT_ID").ok(),
Expand Down Expand Up @@ -127,6 +130,11 @@ async fn build(env: BootstrapEnv) -> BootstrapResult {
.as_ref()
.map(|e| format!("{}/v1/metrics", strip_trailing_slash(e)))
});
let logs_endpoint = env.logs_endpoint.clone().or_else(|| {
env.endpoint
.as_ref()
.map(|e| format!("{}/v1/logs", strip_trailing_slash(e)))
});

// Auth: static token wins; otherwise build a per-request TokenProvider and
// warm it so the first export doesn't pay the round-trip.
Expand Down Expand Up @@ -157,10 +165,12 @@ async fn build(env: BootstrapEnv) -> BootstrapResult {
warn("no auth configured (set SMOOAI_OBSERVABILITY_TOKEN or _AUTH_URL/_CLIENT_ID/_CLIENT_SECRET); OTLP exports will be unauthenticated");
}

let otel = if traces_endpoint.is_some() || metrics_endpoint.is_some() {
let otel = if traces_endpoint.is_some() || metrics_endpoint.is_some() || logs_endpoint.is_some()
{
let mut opts = SetupOtelOptions::new(service_name);
opts.otlp_traces_endpoint = traces_endpoint;
opts.otlp_metrics_endpoint = metrics_endpoint;
opts.otlp_logs_endpoint = logs_endpoint;
opts.otlp_headers = static_headers;
opts.environment = environment.clone();
opts.release = release.clone();
Expand Down
Loading
Loading