1mod batch_metadata;
7mod file;
8mod resolve;
9
10#[cfg(feature = "telemetry-datadog")]
11pub mod datadog;
12#[cfg(feature = "telemetry-dev")]
13mod dev;
14#[cfg(feature = "telemetry-otlp")]
15mod otlp;
16#[cfg(feature = "telemetry-posthog")]
17mod posthog;
18
19use crate::core::config::{ExporterConfig, TelemetryConfig};
20use crate::sync::IngestExportBatch;
21use anyhow::Result;
22use std::path::Path;
23use std::sync::Arc;
24
25pub use batch_metadata::telemetry_file_line;
26pub use file::{FileExporter, default_ndjson_path, resolve_file_exporter_path};
27
28pub use resolve::DatadogResolved;
29pub use resolve::OtlpResolved;
30pub use resolve::PostHogResolved;
31
32pub trait TelemetryExporter: Send + Sync {
34 fn name(&self) -> &str;
35 fn export(&self, batch: &IngestExportBatch) -> Result<()>;
36}
37
38pub struct ExporterRegistry {
40 exporters: Vec<Arc<dyn TelemetryExporter>>,
41}
42
43impl ExporterRegistry {
44 pub fn empty() -> Self {
45 Self {
46 exporters: Vec::new(),
47 }
48 }
49
50 pub fn is_empty(&self) -> bool {
51 self.exporters.is_empty()
52 }
53
54 pub fn from_vec(exporters: Vec<Arc<dyn TelemetryExporter>>) -> Self {
55 Self { exporters }
56 }
57
58 pub fn fan_out(&self, fail_open: bool, batch: &IngestExportBatch) -> Result<()> {
60 for e in &self.exporters {
61 let r = e.export(batch);
62 if let Err(err) = r {
63 tracing::warn!(exporter = e.name(), error = %err, "telemetry exporter");
64 if !fail_open {
65 return Err(err);
66 }
67 }
68 }
69 Ok(())
70 }
71
72 pub fn exporter_names(&self) -> Vec<String> {
74 self.exporters
75 .iter()
76 .map(|e| e.name().to_string())
77 .collect()
78 }
79
80 pub fn export_one(&self, name: &str, batch: &IngestExportBatch) -> Result<()> {
82 let exp = self
83 .exporters
84 .iter()
85 .find(|e| e.name() == name)
86 .ok_or_else(|| anyhow::anyhow!("no exporter named `{name}`"))?;
87 exp.export(batch)
88 }
89}
90
91pub fn load_exporters(cfg: &TelemetryConfig, workspace: &Path) -> ExporterRegistry {
94 let mut v: Vec<Arc<dyn TelemetryExporter>> = Vec::new();
95 for entry in &cfg.exporters {
96 if let Some(exp) = build_exporter(entry, workspace) {
97 v.push(exp);
98 }
99 }
100 ExporterRegistry::from_vec(v)
101}
102
103fn build_exporter(c: &ExporterConfig, workspace: &Path) -> Option<Arc<dyn TelemetryExporter>> {
104 if !c.is_enabled() {
105 return None;
106 }
107 match c {
108 ExporterConfig::None => None,
109 ExporterConfig::File { path, .. } => {
110 let p = file::resolve_file_exporter_path(path.as_deref(), workspace);
111 Some(Arc::new(file::FileExporter::new(p)) as _)
112 }
113 ExporterConfig::Dev { .. } => {
114 #[cfg(feature = "telemetry-dev")]
115 {
116 Some(Arc::new(dev::DevExporter) as _)
117 }
118 #[cfg(not(feature = "telemetry-dev"))]
119 {
120 tracing::warn!(
121 "telemetry `dev` exporter configured but `telemetry-dev` is not enabled"
122 );
123 None
124 }
125 }
126 ExporterConfig::PostHog { .. } => {
127 let r = PostHogResolved::from_config(c)?;
128 #[cfg(feature = "telemetry-posthog")]
129 {
130 Some(Arc::new(posthog::PostHogExporter::new(&r.host, &r.project_api_key)) as _)
131 }
132 #[cfg(not(feature = "telemetry-posthog"))]
133 {
134 let _ = &r;
135 tracing::warn!(
136 "PostHog configured but the `telemetry-posthog` feature is not enabled"
137 );
138 None
139 }
140 }
141 ExporterConfig::Datadog { .. } => {
142 let r = DatadogResolved::from_config(c)?;
143 #[cfg(feature = "telemetry-datadog")]
144 {
145 Some(Arc::new(datadog::DatadogExporter::new(&r.site, &r.api_key)) as _)
146 }
147 #[cfg(not(feature = "telemetry-datadog"))]
148 {
149 let _ = &r;
150 tracing::warn!(
151 "Datadog configured but the `telemetry-datadog` feature is not enabled"
152 );
153 None
154 }
155 }
156 ExporterConfig::Otlp { .. } => {
157 let r = OtlpResolved::from_config(c)?;
158 #[cfg(feature = "telemetry-otlp")]
159 {
160 Some(Arc::new(otlp::OtlpExporter::new(&r.endpoint)) as _)
161 }
162 #[cfg(not(feature = "telemetry-otlp"))]
163 {
164 let _ = &r;
165 tracing::warn!("OTLP configured but the `telemetry-otlp` feature is not enabled");
166 None
167 }
168 }
169 }
170}