feat(tracing): provider-agnostic exporters with first-class PostHog support (#972)

* feat(tracing): add provider-agnostic exporters with first-class PostHog support

* chore(config): regenerate full reference rendered config for exporters

* refactor(tracing): drop posthog exporter 'enabled' flag per review
This commit is contained in:
Musa 2026-06-25 10:33:46 -07:00 committed by GitHub
parent ff4f2b95d6
commit cdde1adf0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 725 additions and 12 deletions

View file

@ -251,6 +251,11 @@ pub struct Tracing {
pub random_sampling: Option<u32>,
pub opentracing_grpc_endpoint: Option<String>,
pub span_attributes: Option<SpanAttributes>,
/// Provider-agnostic telemetry export destinations. Each entry is tagged by
/// its `type` (e.g. `posthog`) so new backends can be added without breaking
/// existing configs. LLM spans are translated into each backend's native
/// event format and streamed in addition to any `opentracing_grpc_endpoint`.
pub exporters: Option<Vec<Exporter>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@ -260,6 +265,36 @@ pub struct SpanAttributes {
pub static_attributes: Option<HashMap<String, String>>,
}
/// A telemetry export destination configured under `tracing.exporters`.
///
/// The list is provider-agnostic; each variant is internally tagged by its
/// `type` field (e.g. `type: posthog`). Additional backends (datadog, raw
/// otlp, ...) can be added as new variants without breaking existing configs.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Exporter {
/// PostHog AI observability. LLM spans are converted into PostHog
/// `$ai_generation` events and POSTed to the configured `url`.
Posthog(PosthogExporter),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PosthogExporter {
/// PostHog host, e.g. `https://us.i.posthog.com`. The `/batch/` capture
/// path is appended automatically.
pub url: String,
/// PostHog project API key (token). Supports `$ENV_VAR` expansion at render
/// time, e.g. `$POSTHOG_API_KEY`.
pub api_key: String,
/// Optional request header whose value is used as the PostHog `distinct_id`.
/// When unset (or the header is missing on a request) events are captured
/// anonymously.
pub distinct_id_header: Option<String>,
/// When true, include the truncated user message preview as `$ai_input`.
/// Defaults to `false` to avoid sending prompt content off-box.
pub capture_messages: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub enum GatewayMode {
#[serde(rename = "llm")]
@ -865,4 +900,47 @@ disable_signals: false
let overrides: super::Overrides = serde_yaml::from_str(yaml_missing).unwrap();
assert_eq!(overrides.disable_signals, None);
}
#[test]
fn test_tracing_posthog_exporter_deserialize() {
let yaml = r#"
random_sampling: 100
exporters:
- type: posthog
url: https://us.i.posthog.com
api_key: phc_secret
distinct_id_header: x-user-id
capture_messages: true
"#;
let tracing: super::Tracing = serde_yaml::from_str(yaml).unwrap();
let exporters = tracing.exporters.expect("exporters should be parsed");
assert_eq!(exporters.len(), 1);
match &exporters[0] {
super::Exporter::Posthog(posthog) => {
assert_eq!(posthog.url, "https://us.i.posthog.com");
assert_eq!(posthog.api_key, "phc_secret");
assert_eq!(posthog.distinct_id_header.as_deref(), Some("x-user-id"));
assert_eq!(posthog.capture_messages, Some(true));
}
}
}
#[test]
fn test_tracing_posthog_exporter_minimal() {
let yaml = r#"
exporters:
- type: posthog
url: https://eu.i.posthog.com
api_key: phc_eu
"#;
let tracing: super::Tracing = serde_yaml::from_str(yaml).unwrap();
let exporters = tracing.exporters.unwrap();
match &exporters[0] {
super::Exporter::Posthog(posthog) => {
assert_eq!(posthog.url, "https://eu.i.posthog.com");
assert_eq!(posthog.distinct_id_header, None);
assert_eq!(posthog.capture_messages, None);
}
}
}
}