Skip to main content

kaizen/telemetry/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Optional pluggable sinks that receive the same redacted [`IngestExportBatch`] as Kaizen sync.
3//! Fan-out runs in parallel with the primary `POST` (see `sync::engine`); outbox is committed only
4//! when the primary succeeds (and, when `fail_open` is `false`, when the fan-out completes `Ok`).
5
6mod resolve;
7
8#[cfg(feature = "telemetry-datadog")]
9mod datadog;
10#[cfg(feature = "telemetry-dev")]
11mod dev;
12#[cfg(feature = "telemetry-otlp")]
13mod otlp;
14#[cfg(feature = "telemetry-posthog")]
15mod posthog;
16
17use crate::core::config::{ExporterConfig, TelemetryConfig};
18use crate::sync::IngestExportBatch;
19use anyhow::Result;
20use std::sync::Arc;
21
22pub use resolve::DatadogResolved;
23pub use resolve::OtlpResolved;
24pub use resolve::PostHogResolved;
25
26/// Third-party and OTel sinks use the same batch types as the HTTP ingest.
27pub trait TelemetryExporter: Send + Sync {
28    fn name(&self) -> &str;
29    fn export(&self, batch: &IngestExportBatch) -> Result<()>;
30}
31
32/// Built from `TelemetryConfig` via [`load_exporters`]. Empty is a no-op.
33pub struct ExporterRegistry {
34    exporters: Vec<Arc<dyn TelemetryExporter>>,
35}
36
37impl ExporterRegistry {
38    pub fn empty() -> Self {
39        Self {
40            exporters: Vec::new(),
41        }
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.exporters.is_empty()
46    }
47
48    pub fn from_vec(exporters: Vec<Arc<dyn TelemetryExporter>>) -> Self {
49        Self { exporters }
50    }
51
52    /// When `fail_open` is `true`, log each exporter error and continue. If `false`, return the first error.
53    pub fn fan_out(&self, fail_open: bool, batch: &IngestExportBatch) -> Result<()> {
54        for e in &self.exporters {
55            let r = e.export(batch);
56            if let Err(err) = r {
57                tracing::warn!(exporter = e.name(), error = %err, "telemetry exporter");
58                if !fail_open {
59                    return Err(err);
60                }
61            }
62        }
63        Ok(())
64    }
65}
66
67/// Build exporters from TOML + environment. Missing creds for a sink log a warning and skip it.
68pub fn load_exporters(cfg: &TelemetryConfig) -> ExporterRegistry {
69    let mut v: Vec<Arc<dyn TelemetryExporter>> = Vec::new();
70    for entry in &cfg.exporters {
71        if let Some(exp) = build_exporter(entry) {
72            v.push(exp);
73        }
74    }
75    ExporterRegistry::from_vec(v)
76}
77
78fn build_exporter(c: &ExporterConfig) -> Option<Arc<dyn TelemetryExporter>> {
79    if !c.is_enabled() {
80        return None;
81    }
82    match c {
83        ExporterConfig::None => None,
84        ExporterConfig::Dev { .. } => {
85            #[cfg(feature = "telemetry-dev")]
86            {
87                Some(Arc::new(dev::DevExporter) as _)
88            }
89            #[cfg(not(feature = "telemetry-dev"))]
90            {
91                tracing::warn!(
92                    "telemetry `dev` exporter configured but `telemetry-dev` is not enabled"
93                );
94                None
95            }
96        }
97        ExporterConfig::PostHog { .. } => {
98            let r = PostHogResolved::from_config(c)?;
99            #[cfg(feature = "telemetry-posthog")]
100            {
101                Some(Arc::new(posthog::PostHogExporter::new(&r.host, &r.project_api_key)) as _)
102            }
103            #[cfg(not(feature = "telemetry-posthog"))]
104            {
105                let _ = &r;
106                tracing::warn!(
107                    "PostHog configured but the `telemetry-posthog` feature is not enabled"
108                );
109                None
110            }
111        }
112        ExporterConfig::Datadog { .. } => {
113            let r = DatadogResolved::from_config(c)?;
114            #[cfg(feature = "telemetry-datadog")]
115            {
116                Some(Arc::new(datadog::DatadogExporter::new(&r.site, &r.api_key)) as _)
117            }
118            #[cfg(not(feature = "telemetry-datadog"))]
119            {
120                let _ = &r;
121                tracing::warn!(
122                    "Datadog configured but the `telemetry-datadog` feature is not enabled"
123                );
124                None
125            }
126        }
127        ExporterConfig::Otlp { .. } => {
128            let r = OtlpResolved::from_config(c)?;
129            #[cfg(feature = "telemetry-otlp")]
130            {
131                Some(Arc::new(otlp::OtlpExporter::new(&r.endpoint)) as _)
132            }
133            #[cfg(not(feature = "telemetry-otlp"))]
134            {
135                let _ = &r;
136                tracing::warn!("OTLP configured but the `telemetry-otlp` feature is not enabled");
137                None
138            }
139        }
140    }
141}