1use std::fmt;
14#[allow(unused_imports)]
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18#[derive(Debug, Default)]
20pub(crate) struct SignalCounters {
21 pub(crate) offered: AtomicU64,
22 pub(crate) exported: AtomicU64,
23 pub(crate) failed: AtomicU64,
24 pub(crate) export_calls: AtomicU64,
25 pub(crate) export_failures: AtomicU64,
26}
27
28impl SignalCounters {
29 fn snapshot(&self) -> SignalStats {
30 let exported = self.exported.load(Ordering::Relaxed);
32 let failed = self.failed.load(Ordering::Relaxed);
33 let export_calls = self.export_calls.load(Ordering::Relaxed);
34 let export_failures = self.export_failures.load(Ordering::Relaxed);
35 let offered = self.offered.load(Ordering::Relaxed);
36 SignalStats {
37 offered,
38 exported,
39 failed,
40 unexported: offered.saturating_sub(exported + failed),
41 export_calls,
42 export_failures,
43 }
44 }
45
46 #[cfg_attr(not(feature = "otlp"), allow(dead_code))]
47 pub(crate) fn record_export(&self, items: u64, ok: bool) {
48 self.export_calls.fetch_add(1, Ordering::Relaxed);
49 if ok {
50 self.exported.fetch_add(items, Ordering::Relaxed);
51 } else {
52 self.failed.fetch_add(items, Ordering::Relaxed);
53 self.export_failures.fetch_add(1, Ordering::Relaxed);
54 }
55 }
56}
57
58#[derive(Debug, Default)]
60pub(crate) struct Counters {
61 pub(crate) traces: SignalCounters,
62 pub(crate) logs: SignalCounters,
63 pub(crate) metrics: SignalCounters,
64 #[cfg(feature = "logs")]
66 pub(crate) console_dropped: std::sync::OnceLock<tracing_appender::non_blocking::ErrorCounter>,
67}
68
69impl Counters {
70 pub(crate) fn snapshot(&self) -> TelemetryStats {
71 TelemetryStats {
72 traces: self.traces.snapshot(),
73 logs: self.logs.snapshot(),
74 metrics: self.metrics.snapshot(),
75 #[cfg(feature = "logs")]
76 console_lines_dropped: self
77 .console_dropped
78 .get()
79 .map_or(0, |c| c.dropped_lines() as u64),
80 #[cfg(not(feature = "logs"))]
81 console_lines_dropped: 0,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub struct SignalStats {
89 pub offered: u64,
92 pub exported: u64,
94 pub failed: u64,
96 pub unexported: u64,
100 pub export_calls: u64,
102 pub export_failures: u64,
104}
105
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
109pub struct TelemetryStats {
110 pub traces: SignalStats,
112 pub logs: SignalStats,
114 pub metrics: SignalStats,
117 pub console_lines_dropped: u64,
119}
120
121impl fmt::Display for TelemetryStats {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 for (name, s) in [
124 ("traces", self.traces),
125 ("logs", self.logs),
126 ("metrics", self.metrics),
127 ] {
128 writeln!(
129 f,
130 "{name}: offered={} exported={} failed={} unexported={} export_failures={}",
131 s.offered, s.exported, s.failed, s.unexported, s.export_failures
132 )?;
133 }
134 write!(f, "console lines dropped={}", self.console_lines_dropped)
135 }
136}
137
138#[cfg(all(feature = "tracing", feature = "otlp"))]
143pub(crate) mod traces {
144 use super::*;
145 use opentelemetry::Context;
146 use opentelemetry_sdk::Resource;
147 use opentelemetry_sdk::error::OTelSdkResult;
148 use opentelemetry_sdk::trace::{Span, SpanData, SpanExporter, SpanProcessor};
149 use std::time::Duration;
150
151 #[derive(Debug)]
153 pub(crate) struct CountingProcessor<P> {
154 pub(crate) inner: P,
155 pub(crate) counters: Arc<Counters>,
156 }
157
158 impl<P: SpanProcessor> SpanProcessor for CountingProcessor<P> {
159 fn on_start(&self, span: &mut Span, cx: &Context) {
160 self.inner.on_start(span, cx);
161 }
162 fn on_end(&self, span: SpanData) {
163 if span.span_context.is_sampled() {
164 self.counters.traces.offered.fetch_add(1, Ordering::Relaxed);
165 }
166 self.inner.on_end(span);
167 }
168 fn force_flush(&self) -> OTelSdkResult {
169 self.inner.force_flush()
170 }
171 fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
172 self.inner.shutdown_with_timeout(timeout)
173 }
174 fn set_resource(&mut self, resource: &Resource) {
175 self.inner.set_resource(resource);
176 }
177 }
178
179 #[derive(Debug)]
181 pub(crate) struct CountingExporter<E> {
182 pub(crate) inner: E,
183 pub(crate) counters: Arc<Counters>,
184 }
185
186 impl<E: SpanExporter> SpanExporter for CountingExporter<E> {
187 async fn export(&self, batch: Vec<SpanData>) -> OTelSdkResult {
188 let n = batch.len() as u64;
189 let result = self.inner.export(batch).await;
190 self.counters.traces.record_export(n, result.is_ok());
191 result
192 }
193 fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
194 self.inner.shutdown_with_timeout(timeout)
195 }
196 fn force_flush(&self) -> OTelSdkResult {
197 self.inner.force_flush()
198 }
199 fn set_resource(&mut self, resource: &Resource) {
200 self.inner.set_resource(resource);
201 }
202 }
203}
204
205#[cfg(all(feature = "logs", feature = "otlp"))]
206pub(crate) mod logs {
207 use super::*;
208 use opentelemetry::InstrumentationScope;
209 use opentelemetry::logs::Severity;
210 use opentelemetry_sdk::Resource;
211 use opentelemetry_sdk::error::OTelSdkResult;
212 use opentelemetry_sdk::logs::{LogBatch, LogExporter, LogProcessor, SdkLogRecord};
213 use std::time::Duration;
214
215 #[derive(Debug)]
216 pub(crate) struct CountingProcessor<P> {
217 pub(crate) inner: P,
218 pub(crate) counters: Arc<Counters>,
219 }
220
221 impl<P: LogProcessor> LogProcessor for CountingProcessor<P> {
222 fn emit(&self, data: &mut SdkLogRecord, instrumentation: &InstrumentationScope) {
223 self.counters.logs.offered.fetch_add(1, Ordering::Relaxed);
224 self.inner.emit(data, instrumentation);
225 }
226 fn force_flush(&self) -> OTelSdkResult {
227 self.inner.force_flush()
228 }
229 fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
230 self.inner.shutdown_with_timeout(timeout)
231 }
232 fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool {
233 self.inner.event_enabled(level, target, name)
234 }
235 fn set_resource(&mut self, resource: &Resource) {
236 self.inner.set_resource(resource);
237 }
238 }
239
240 #[derive(Debug)]
241 pub(crate) struct CountingExporter<E> {
242 pub(crate) inner: E,
243 pub(crate) counters: Arc<Counters>,
244 }
245
246 impl<E: LogExporter> LogExporter for CountingExporter<E> {
247 async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult {
248 let n = batch.iter().count() as u64;
249 let result = self.inner.export(batch).await;
250 self.counters.logs.record_export(n, result.is_ok());
251 result
252 }
253 fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
254 self.inner.shutdown_with_timeout(timeout)
255 }
256 fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool {
257 self.inner.event_enabled(level, target, name)
258 }
259 fn set_resource(&mut self, resource: &Resource) {
260 self.inner.set_resource(resource);
261 }
262 }
263}
264
265#[cfg(feature = "metrics")]
266pub(crate) mod metrics {
267 use super::*;
268 use opentelemetry_sdk::error::OTelSdkResult;
269 use opentelemetry_sdk::metrics::Temporality;
270 use opentelemetry_sdk::metrics::data::{AggregatedMetrics, MetricData, ResourceMetrics};
271 use opentelemetry_sdk::metrics::exporter::PushMetricExporter;
272 use std::time::Duration;
273
274 #[derive(Debug)]
275 #[cfg_attr(not(feature = "otlp"), allow(dead_code))]
276 pub(crate) struct CountingExporter<E> {
277 pub(crate) inner: E,
278 pub(crate) counters: Arc<Counters>,
279 }
280
281 #[cfg_attr(not(feature = "otlp"), allow(dead_code))]
282 fn points(metrics: &ResourceMetrics) -> u64 {
283 let mut n = 0u64;
284 for scope in metrics.scope_metrics() {
285 for metric in scope.metrics() {
286 macro_rules! count {
287 ($d:expr) => {
288 match $d {
289 MetricData::Sum(s) => s.data_points().count(),
290 MetricData::Gauge(g) => g.data_points().count(),
291 MetricData::Histogram(h) => h.data_points().count(),
292 MetricData::ExponentialHistogram(h) => h.data_points().count(),
293 }
294 };
295 }
296 n += match metric.data() {
297 AggregatedMetrics::F64(d) => count!(d),
298 AggregatedMetrics::U64(d) => count!(d),
299 AggregatedMetrics::I64(d) => count!(d),
300 } as u64;
301 }
302 }
303 n
304 }
305
306 impl<E: PushMetricExporter> PushMetricExporter for CountingExporter<E> {
307 async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult {
308 let n = points(metrics);
309 self.counters
310 .metrics
311 .offered
312 .fetch_add(n, Ordering::Relaxed);
313 let result = self.inner.export(metrics).await;
314 self.counters.metrics.record_export(n, result.is_ok());
315 result
316 }
317 fn force_flush(&self) -> OTelSdkResult {
318 self.inner.force_flush()
319 }
320 fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult {
321 self.inner.shutdown_with_timeout(timeout)
322 }
323 fn temporality(&self) -> Temporality {
324 self.inner.temporality()
325 }
326 }
327
328 pub(crate) fn register(meter: &opentelemetry::metrics::Meter, counters: &Arc<Counters>) {
330 use kerniq_telemetry::semconv::kerniq as k;
331 use opentelemetry::KeyValue;
332
333 let c = counters.clone();
334 meter
335 .u64_observable_counter(k::TELEMETRY_ITEMS)
336 .with_unit("{item}")
337 .with_description("Telemetry items by signal and outcome (offered, exported, failed).")
338 .with_callback(move |observer| {
339 let s = c.snapshot();
340 for (signal, stats) in [("traces", s.traces), ("logs", s.logs)] {
341 for (outcome, value) in [
342 ("offered", stats.offered),
343 ("exported", stats.exported),
344 ("failed", stats.failed),
345 ] {
346 observer.observe(
347 value,
348 &[
349 KeyValue::new(k::SIGNAL, signal),
350 KeyValue::new(k::OUTCOME, outcome),
351 ],
352 );
353 }
354 }
355 observer.observe(
356 s.console_lines_dropped,
357 &[
358 KeyValue::new(k::SIGNAL, "console_logs"),
359 KeyValue::new(k::OUTCOME, "dropped"),
360 ],
361 );
362 })
363 .build();
364 let c = counters.clone();
365 meter
366 .u64_observable_counter(k::TELEMETRY_EXPORTS)
367 .with_unit("{export}")
368 .with_description("Export calls by signal and outcome (success, failure).")
369 .with_callback(move |observer| {
370 let s = c.snapshot();
371 for (signal, stats) in [
372 ("traces", s.traces),
373 ("logs", s.logs),
374 ("metrics", s.metrics),
375 ] {
376 observer.observe(
377 stats.export_calls - stats.export_failures,
378 &[
379 KeyValue::new(k::SIGNAL, signal),
380 KeyValue::new(k::OUTCOME, "success"),
381 ],
382 );
383 observer.observe(
384 stats.export_failures,
385 &[
386 KeyValue::new(k::SIGNAL, signal),
387 KeyValue::new(k::OUTCOME, "failure"),
388 ],
389 );
390 }
391 })
392 .build();
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn unexported_is_never_negative() {
402 let c = SignalCounters::default();
403 c.offered.fetch_add(10, Ordering::Relaxed);
404 c.record_export(6, true);
405 c.record_export(2, false);
406 let s = c.snapshot();
407 assert_eq!(s.unexported, 2);
408 assert_eq!(s.export_calls, 2);
409 assert_eq!(s.export_failures, 1);
410 }
411}