first run at unique safe fingerprinting

This commit is contained in:
Michael Neale
2025-12-05 17:20:03 +11:00
parent bb9f0ff94b
commit 2f4e3cb74a
5 changed files with 113 additions and 27 deletions
Generated
+2 -1
View File
@@ -2025,7 +2025,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -2659,6 +2659,7 @@ dependencies = [
"etcetera",
"fs2",
"futures",
"hex",
"ignore",
"include_dir",
"indexmap 2.12.0",
+1
View File
@@ -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]
@@ -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
View File
@@ -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,
+22 -26
View File
@@ -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>;
@@ -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()
@@ -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()
@@ -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()
@@ -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()
@@ -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()