feat(telemetry): add OpenTelemetry tracing with optional OTLP export to Tempo

This commit is contained in:
Hibryda 2026-03-08 20:34:19 +01:00
parent 3f1638c98b
commit fd9f55faff
9 changed files with 601 additions and 2 deletions

View file

@ -0,0 +1,27 @@
services:
tempo:
image: grafana/tempo:latest
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./tempo.yaml:/etc/tempo.yaml:ro
- tempo-data:/var/tempo
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "3200:3200" # Tempo query API
grafana:
image: grafana/grafana:latest
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_DISABLE_LOGIN_FORM=true
volumes:
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro
ports:
- "9715:3000" # Grafana UI (project port convention)
depends_on:
- tempo
volumes:
tempo-data:

View file

@ -0,0 +1,11 @@
apiVersion: 1
datasources:
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
isDefault: true
jsonData:
tracesToLogsV2:
datasourceUid: ''

19
docker/tempo/tempo.yaml Normal file
View file

@ -0,0 +1,19 @@
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal

369
v2/Cargo.lock generated
View file

@ -152,6 +152,17 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "atk"
version = "0.18.2"
@ -297,6 +308,9 @@ dependencies = [
"futures-util",
"log",
"notify",
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
"rfd",
"rusqlite",
"serde",
@ -309,6 +323,9 @@ dependencies = [
"tempfile",
"tokio",
"tokio-tungstenite",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
"uuid",
]
@ -935,6 +952,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embed-resource"
version = "3.0.6"
@ -1194,6 +1217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -1973,6 +1997,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.17"
@ -2259,6 +2292,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.10"
@ -2447,6 +2489,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-conv"
version = "0.2.0"
@ -2685,6 +2736,86 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426"
dependencies = [
"futures-core",
"futures-sink",
"js-sys",
"pin-project-lite",
"thiserror 2.0.18",
"tracing",
]
[[package]]
name = "opentelemetry-http"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253"
dependencies = [
"async-trait",
"bytes",
"http",
"opentelemetry",
"reqwest 0.12.28",
"tracing",
]
[[package]]
name = "opentelemetry-otlp"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91"
dependencies = [
"async-trait",
"futures-core",
"http",
"opentelemetry",
"opentelemetry-http",
"opentelemetry-proto",
"opentelemetry_sdk",
"prost",
"reqwest 0.12.28",
"thiserror 2.0.18",
"tracing",
]
[[package]]
name = "opentelemetry-proto"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d"
dependencies = [
"opentelemetry",
"opentelemetry_sdk",
"prost",
"tonic",
]
[[package]]
name = "opentelemetry_sdk"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570"
dependencies = [
"async-trait",
"futures-channel",
"futures-executor",
"futures-util",
"glob",
"opentelemetry",
"percent-encoding",
"rand 0.8.5",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tracing",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@ -2893,6 +3024,26 @@ dependencies = [
"siphasher 1.0.2",
]
[[package]]
name = "pin-project"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@ -3087,6 +3238,29 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
@ -3328,6 +3502,40 @@ dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "reqwest"
version = "0.13.2"
@ -3565,6 +3773,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
@ -3792,6 +4006,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_with"
version = "3.17.0"
@ -3919,6 +4145,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shared_library"
version = "0.1.9"
@ -4256,7 +4491,7 @@ dependencies = [
"percent-encoding",
"plist",
"raw-window-handle",
"reqwest",
"reqwest 0.13.2",
"serde",
"serde_json",
"serde_repr",
@ -4435,7 +4670,7 @@ dependencies = [
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest",
"reqwest 0.13.2",
"rustls",
"semver",
"serde",
@ -4625,6 +4860,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.47"
@ -4731,6 +4975,17 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
@ -4863,6 +5118,27 @@ 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-trait",
"base64 0.22.1",
"bytes",
"http",
"http-body",
"http-body-util",
"percent-encoding",
"pin-project",
"prost",
"tokio-stream",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower"
version = "0.5.3"
@ -4915,9 +5191,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
@ -4925,6 +5213,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-opentelemetry"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36"
dependencies = [
"js-sys",
"once_cell",
"opentelemetry",
"opentelemetry_sdk",
"smallvec",
"tracing",
"tracing-core",
"tracing-log",
"tracing-subscriber",
"web-time",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
@ -5113,6 +5462,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "value-bag"
version = "1.12.0"
@ -5322,6 +5677,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webkit2gtk"
version = "2.0.2"

View file

@ -33,6 +33,12 @@ uuid = { version = "1", features = ["v4"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
tokio = { version = "1", features = ["full"] }
futures-util = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
opentelemetry = "0.28"
opentelemetry_sdk = { version = "0.28", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.28", features = ["http-proto", "reqwest-client"] }
tracing-opentelemetry = "0.29"
[dev-dependencies]
tempfile = "3"

View file

@ -5,6 +5,7 @@ mod pty;
mod remote;
mod sidecar;
mod session;
mod telemetry;
mod watcher;
use ctx::CtxDb;
@ -25,11 +26,13 @@ struct AppState {
file_watcher: Arc<FileWatcherManager>,
ctx_db: Arc<CtxDb>,
remote_manager: Arc<RemoteManager>,
_telemetry: telemetry::TelemetryGuard,
}
// --- PTY commands ---
#[tauri::command]
#[tracing::instrument(skip(state), fields(shell = ?options.shell))]
fn pty_spawn(
state: State<'_, AppState>,
options: PtyOptions,
@ -53,6 +56,7 @@ fn pty_resize(
}
#[tauri::command]
#[tracing::instrument(skip(state))]
fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.pty_manager.kill(&id)
}
@ -60,6 +64,7 @@ fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), String> {
// --- Agent/sidecar commands ---
#[tauri::command]
#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))]
fn agent_query(
state: State<'_, AppState>,
options: AgentQueryOptions,
@ -68,6 +73,7 @@ fn agent_query(
}
#[tauri::command]
#[tracing::instrument(skip(state))]
fn agent_stop(state: State<'_, AppState>, session_id: String) -> Result<(), String> {
state.sidecar_manager.stop_session(&session_id)
}
@ -78,6 +84,7 @@ fn agent_ready(state: State<'_, AppState>) -> bool {
}
#[tauri::command]
#[tracing::instrument(skip(state))]
fn agent_restart(state: State<'_, AppState>) -> Result<(), String> {
state.sidecar_manager.restart()
}
@ -470,6 +477,19 @@ fn cli_get_group() -> Option<String> {
None
}
// --- Frontend telemetry bridge ---
#[tauri::command]
fn frontend_log(level: String, message: String, context: Option<serde_json::Value>) {
match level.as_str() {
"error" => tracing::error!(source = "frontend", ?context, "{message}"),
"warn" => tracing::warn!(source = "frontend", ?context, "{message}"),
"info" => tracing::info!(source = "frontend", ?context, "{message}"),
"debug" => tracing::debug!(source = "frontend", ?context, "{message}"),
_ => tracing::trace!(source = "frontend", ?context, "{message}"),
}
}
// --- Remote machine commands ---
#[tauri::command]
@ -488,26 +508,31 @@ async fn remote_remove(state: State<'_, AppState>, machine_id: String) -> Result
}
#[tauri::command]
#[tracing::instrument(skip(app, state))]
async fn remote_connect(app: tauri::AppHandle, state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
state.remote_manager.connect(&app, &machine_id).await
}
#[tauri::command]
#[tracing::instrument(skip(state))]
async fn remote_disconnect(state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
state.remote_manager.disconnect(&machine_id).await
}
#[tauri::command]
#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))]
async fn remote_agent_query(state: State<'_, AppState>, machine_id: String, options: AgentQueryOptions) -> Result<(), String> {
state.remote_manager.agent_query(&machine_id, &options).await
}
#[tauri::command]
#[tracing::instrument(skip(state))]
async fn remote_agent_stop(state: State<'_, AppState>, machine_id: String, session_id: String) -> Result<(), String> {
state.remote_manager.agent_stop(&machine_id, &session_id).await
}
#[tauri::command]
#[tracing::instrument(skip(state), fields(shell = ?options.shell))]
async fn remote_pty_spawn(state: State<'_, AppState>, machine_id: String, options: PtyOptions) -> Result<String, String> {
state.remote_manager.pty_spawn(&machine_id, &options).await
}
@ -532,6 +557,9 @@ pub fn run() {
// Force dark GTK theme for native dialogs (file chooser, etc.)
std::env::set_var("GTK_THEME", "Adwaita:dark");
// Initialize tracing + optional OTLP export (before any tracing macros)
let telemetry_guard = telemetry::init();
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
pty_spawn,
@ -588,6 +616,7 @@ pub fn run() {
project_agent_state_load,
cli_get_group,
pick_directory,
frontend_log,
])
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
@ -652,6 +681,7 @@ pub fn run() {
file_watcher,
ctx_db,
remote_manager,
_telemetry: telemetry_guard,
});
Ok(())

View file

@ -0,0 +1,101 @@
// OpenTelemetry telemetry — tracing spans + OTLP export to Tempo/Grafana
//
// Controlled by BTERMINAL_OTLP_ENDPOINT env var:
// - Set (e.g. "http://localhost:4318") → export traces via OTLP/HTTP + console
// - Absent → console-only (no network calls)
use opentelemetry::trace::TracerProvider;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
/// Holds the tracer provider and shuts it down on drop.
/// Store this in Tauri's managed state so it lives for the app lifetime.
pub struct TelemetryGuard {
provider: Option<SdkTracerProvider>,
}
impl Drop for TelemetryGuard {
fn drop(&mut self) {
if let Some(provider) = self.provider.take() {
if let Err(e) = provider.shutdown() {
eprintln!("OTEL shutdown error: {e}");
}
}
}
}
/// Initialize tracing with optional OTLP export.
/// Call once at app startup, before any tracing macros fire.
pub fn init() -> TelemetryGuard {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("bterminal=info,bterminal_lib=info,bterminal_core=info"));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.compact();
match std::env::var("BTERMINAL_OTLP_ENDPOINT") {
Ok(endpoint) if !endpoint.is_empty() => {
match build_otlp_provider(&endpoint) {
Ok(provider) => {
let otel_layer = tracing_opentelemetry::layer()
.with_tracer(provider.tracer("bterminal"));
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(otel_layer)
.init();
log::info!("Telemetry: OTLP export enabled → {endpoint}");
TelemetryGuard { provider: Some(provider) }
}
Err(e) => {
// Fall back to console-only if OTLP setup fails
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
log::warn!("Telemetry: OTLP setup failed ({e}), console-only fallback");
TelemetryGuard { provider: None }
}
}
}
_ => {
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
log::info!("Telemetry: console-only (BTERMINAL_OTLP_ENDPOINT not set)");
TelemetryGuard { provider: None }
}
}
}
fn build_otlp_provider(endpoint: &str) -> Result<SdkTracerProvider, Box<dyn std::error::Error>> {
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::Resource;
use opentelemetry::KeyValue;
let exporter = SpanExporter::builder()
.with_http()
.with_endpoint(endpoint)
.build()?;
let resource = Resource::builder()
.with_attributes([
KeyValue::new("service.name", "bterminal"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
])
.build();
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build();
Ok(provider)
}

View file

@ -0,0 +1,26 @@
// Telemetry bridge — routes frontend events to Rust tracing via IPC
// No browser OTEL SDK needed (WebKit2GTK incompatible)
import { invoke } from '@tauri-apps/api/core';
type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
/** Emit a structured log event to the Rust tracing layer */
export function telemetryLog(
level: LogLevel,
message: string,
context?: Record<string, unknown>,
): void {
invoke('frontend_log', { level, message, context: context ?? null }).catch(() => {
// Swallow IPC errors — telemetry must never break the app
});
}
/** Convenience wrappers */
export const tel = {
error: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('error', msg, ctx),
warn: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('warn', msg, ctx),
info: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('info', msg, ctx),
debug: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('debug', msg, ctx),
trace: (msg: string, ctx?: Record<string, unknown>) => telemetryLog('trace', msg, ctx),
};

View file

@ -22,6 +22,7 @@ import {
saveAgentMessages,
type AgentMessageRecord,
} from './adapters/groups-bridge';
import { tel } from './adapters/telemetry-bridge';
let unlistenMsg: (() => void) | null = null;
let unlistenExit: (() => void) | null = null;
@ -66,6 +67,7 @@ export async function startAgentDispatcher(): Promise<void> {
switch (msg.type) {
case 'agent_started':
updateAgentStatus(sessionId, 'running');
tel.info('agent_started', { sessionId });
break;
case 'agent_event':
@ -74,11 +76,13 @@ export async function startAgentDispatcher(): Promise<void> {
case 'agent_stopped':
updateAgentStatus(sessionId, 'done');
tel.info('agent_stopped', { sessionId });
notify('success', `Agent ${sessionId.slice(0, 8)} completed`);
break;
case 'agent_error':
updateAgentStatus(sessionId, 'error', msg.message);
tel.error('agent_error', { sessionId, error: msg.message });
notify('error', `Agent error: ${msg.message ?? 'Unknown'}`);
break;
@ -89,6 +93,7 @@ export async function startAgentDispatcher(): Promise<void> {
unlistenExit = await onSidecarExited(async () => {
sidecarAlive = false;
tel.error('sidecar_crashed', { restartAttempts });
// Guard against re-entrant exit handler (double-restart race)
if (restarting) return;
@ -176,6 +181,15 @@ function handleAgentEvent(sessionId: string, event: Record<string, unknown>): vo
numTurns: cost.numTurns,
durationMs: cost.durationMs,
});
tel.info('agent_cost', {
sessionId,
costUsd: cost.totalCostUsd,
inputTokens: cost.inputTokens,
outputTokens: cost.outputTokens,
numTurns: cost.numTurns,
durationMs: cost.durationMs,
isError: cost.isError,
});
if (cost.isError) {
updateAgentStatus(sessionId, 'error', cost.errors?.join('; '));
notify('error', `Agent failed: ${cost.errors?.[0] ?? 'Unknown error'}`);