Skip to content
Draft
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
3 changes: 2 additions & 1 deletion Cargo.lock

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

1 change: 1 addition & 0 deletions crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ shellexpand = "3.1.1"
indexmap = "2.12.0"
ignore = "0.4.25"
which = "8.0.0"
hex = "0.4.3"


[target.'cfg(target_os = "windows")'.dependencies]
Expand Down
84 changes: 84 additions & 0 deletions crates/goose/src/tracing/client_fingerprint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! Privacy-respecting client fingerprinting for telemetry.
//!
//! Generates a stable, hashed client ID plus OS/arch info for OTLP resource attributes.
//! The raw UUID is stored locally; only the SHA-256 hash is sent.

use opentelemetry::KeyValue;
use sha2::{Digest, Sha256};
use uuid::Uuid;

use crate::config::Config;

const CLIENT_ID_CONFIG_KEY: &str = "goose_client_id";

/// Get or create the client ID, returning its SHA-256 hash.
pub fn get_client_id_hash() -> String {
let config = Config::global();

let client_id: String = match config.get_param(CLIENT_ID_CONFIG_KEY) {
Ok(id) => id,
Err(_) => {
let new_id = Uuid::new_v4().to_string();
let _ = config.set_param(CLIENT_ID_CONFIG_KEY, &new_id);
new_id
}
};

let mut hasher = Sha256::new();
hasher.update(client_id.as_bytes());
hex::encode(hasher.finalize())
}

pub fn get_os_type() -> &'static str {
#[cfg(target_os = "macos")]
return "macos";
#[cfg(target_os = "linux")]
return "linux";
#[cfg(target_os = "windows")]
return "windows";
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
return "unknown";
}

pub fn get_host_arch() -> &'static str {
#[cfg(target_arch = "x86_64")]
return "x86_64";
#[cfg(target_arch = "aarch64")]
return "aarch64";
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
return "unknown";
}

/// Create OTLP resource attributes for client fingerprinting.
pub fn create_fingerprint_attributes() -> Vec<KeyValue> {
vec![
KeyValue::new("client.id", get_client_id_hash()),
KeyValue::new("os.type", get_os_type()),
KeyValue::new("host.arch", get_host_arch()),
]
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_client_id_hash_is_stable() {
let hash1 = get_client_id_hash();
let hash2 = get_client_id_hash();
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 64);
}

#[test]
fn test_os_and_arch() {
assert!(["macos", "linux", "windows", "unknown"].contains(&get_os_type()));
assert!(["x86_64", "aarch64", "unknown"].contains(&get_host_arch()));
}

#[test]
fn test_fingerprint_attributes() {
let attrs = create_fingerprint_attributes();
assert_eq!(attrs.len(), 3);
}
}
4 changes: 4 additions & 0 deletions crates/goose/src/tracing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
pub mod client_fingerprint;
pub mod langfuse_layer;
mod observation_layer;
pub mod otlp_layer;
pub mod rate_limiter;

pub use client_fingerprint::{
create_fingerprint_attributes, get_client_id_hash, get_host_arch, get_os_type,
};
pub use langfuse_layer::{create_langfuse_observer, LangfuseBatchManager};
pub use observation_layer::{
flatten_metadata, map_level, BatchManager, ObservationLayer, SpanData, SpanTracker,
Expand Down
48 changes: 22 additions & 26 deletions crates/goose/src/tracing/otlp_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use tracing::{Level, Metadata};
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::filter::FilterFn;

use super::client_fingerprint::create_fingerprint_attributes;

pub type OtlpTracingLayer =
OpenTelemetryLayer<tracing_subscriber::Registry, opentelemetry_sdk::trace::Tracer>;
pub type OtlpMetricsLayer = MetricsLayer<tracing_subscriber::Registry>;
Expand Down Expand Up @@ -55,12 +57,25 @@ impl OtlpConfig {
}
}

pub fn init_otlp_tracing(config: &OtlpConfig) -> OtlpResult<()> {
let resource = Resource::new(vec![
/// Create an OpenTelemetry Resource with service info and client fingerprint.
///
/// Includes:
/// - service.name, service.version, service.namespace (standard OTLP attributes)
/// - client.id (hashed, privacy-safe unique identifier)
/// - os.type (macos, linux, windows)
/// - host.arch (x86_64, aarch64)
fn create_resource() -> Resource {
let mut attributes = vec![
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
]);
];
attributes.extend(create_fingerprint_attributes());
Resource::new(attributes)
}

pub fn init_otlp_tracing(config: &OtlpConfig) -> OtlpResult<()> {
let resource = create_resource();

let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
Expand All @@ -81,11 +96,7 @@ pub fn init_otlp_tracing(config: &OtlpConfig) -> OtlpResult<()> {
}

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 resource = create_resource();

let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
Expand All @@ -109,12 +120,7 @@ pub fn init_otlp_metrics(config: &OtlpConfig) -> OtlpResult<()> {

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 resource = create_resource();

let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
Expand All @@ -138,12 +144,7 @@ pub fn create_otlp_tracing_layer() -> OtlpResult<OtlpTracingLayer> {

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 resource = create_resource();

let exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
Expand All @@ -167,12 +168,7 @@ pub fn create_otlp_metrics_layer() -> OtlpResult<OtlpMetricsLayer> {

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 resource = create_resource();

let exporter = opentelemetry_otlp::LogExporter::builder()
.with_http()
Expand Down
Loading