opentelemetry_application_insights/
metrics.rs

1use crate::{
2    convert::time_to_string,
3    models::{Data, DataPoint, DataPointType, Envelope, MetricData, Properties},
4    tags::get_tags_for_metric,
5    Exporter,
6};
7use async_trait::async_trait;
8use opentelemetry::KeyValue;
9use opentelemetry_http::HttpClient;
10use opentelemetry_sdk::{
11    error::OTelSdkResult,
12    metrics::{
13        data::{ExponentialHistogram, Gauge, Histogram, Metric, ResourceMetrics, Sum},
14        exporter::PushMetricExporter,
15        Temporality,
16    },
17};
18use std::{
19    convert::TryInto,
20    sync::Arc,
21    time::{Duration, SystemTime},
22};
23
24#[cfg_attr(docsrs, doc(cfg(feature = "metrics")))]
25#[async_trait]
26impl<C> PushMetricExporter for Exporter<C>
27where
28    C: HttpClient + 'static,
29{
30    fn export(
31        &self,
32        metrics: &ResourceMetrics,
33    ) -> impl std::future::Future<Output = OTelSdkResult> + Send {
34        let client = Arc::clone(&self.client);
35        let endpoint = Arc::clone(&self.track_endpoint);
36
37        let mut envelopes = Vec::new();
38        for scope_metrics in metrics.scope_metrics() {
39            for metric in scope_metrics.metrics() {
40                let data_points = map_metric(metric);
41                for data in data_points {
42                    let tags =
43                        get_tags_for_metric(metrics.resource(), scope_metrics.scope(), &data.attrs);
44                    let properties: Properties = metrics
45                        .resource()
46                        .iter()
47                        .chain(
48                            scope_metrics
49                                .scope()
50                                .attributes()
51                                .map(|kv| (&kv.key, &kv.value)),
52                        )
53                        .chain(data.attrs.iter().map(|kv| (&kv.key, &kv.value)))
54                        .map(|(k, v)| (k.as_str().into(), v.into()))
55                        .collect();
56                    envelopes.push(Envelope {
57                        name: "Microsoft.ApplicationInsights.Metric",
58                        time: time_to_string(data.time).into(),
59                        sample_rate: None,
60                        i_key: Some(self.instrumentation_key.clone().into()),
61                        tags: Some(tags).filter(|x| !x.is_empty()),
62                        data: Some(Data::Metric(MetricData {
63                            ver: 2,
64                            metrics: vec![data.data],
65                            properties: Some(properties).filter(|x| !x.is_empty()),
66                        })),
67                    });
68                }
69            }
70        }
71
72        async move {
73            crate::uploader::send(
74                client.as_ref(),
75                endpoint.as_ref(),
76                envelopes,
77                self.retry_notify.clone(),
78            )
79            .await
80            .map_err(Into::into)
81        }
82    }
83
84    fn force_flush(&self) -> OTelSdkResult {
85        Ok(())
86    }
87
88    fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult {
89        Ok(())
90    }
91
92    fn temporality(&self) -> Temporality {
93        // Application Insights only supports Delta temporality as defined in the spec:
94        //
95        // > Choose Delta aggregation temporality for Counter, Asynchronous Counter and Histogram
96        // > instrument kinds, choose Cumulative aggregation for UpDownCounter and Asynchronous
97        // > UpDownCounter instrument kinds.
98        //
99        // See:
100        // - https://github.com/open-telemetry/opentelemetry-specification/blob/58bfe48eabe887545198d66c43f44071b822373f/specification/metrics/sdk_exporters/otlp.md?plain=1#L46-L47
101        // - https://github.com/frigus02/opentelemetry-application-insights/issues/74#issuecomment-2108488385
102        Temporality::Delta
103    }
104}
105
106struct EnvelopeData {
107    time: SystemTime,
108    data: DataPoint,
109    attrs: Vec<KeyValue>,
110}
111
112trait ToF64Lossy {
113    fn to_f64_lossy(&self) -> f64;
114}
115
116impl ToF64Lossy for i64 {
117    fn to_f64_lossy(&self) -> f64 {
118        *self as f64
119    }
120}
121
122impl ToF64Lossy for u64 {
123    fn to_f64_lossy(&self) -> f64 {
124        *self as f64
125    }
126}
127
128impl ToF64Lossy for f64 {
129    fn to_f64_lossy(&self) -> f64 {
130        *self
131    }
132}
133
134fn map_metric(metric: &Metric) -> Vec<EnvelopeData> {
135    use opentelemetry_sdk::metrics::data::{AggregatedMetrics::*, MetricData};
136    match metric.data() {
137        F64(MetricData::Gauge(data)) => map_gauge(metric, data),
138        U64(MetricData::Gauge(data)) => map_gauge(metric, data),
139        I64(MetricData::Gauge(data)) => map_gauge(metric, data),
140        F64(MetricData::Sum(data)) => map_sum(metric, data),
141        U64(MetricData::Sum(data)) => map_sum(metric, data),
142        I64(MetricData::Sum(data)) => map_sum(metric, data),
143        F64(MetricData::Histogram(data)) => map_histogram(metric, data),
144        U64(MetricData::Histogram(data)) => map_histogram(metric, data),
145        I64(MetricData::Histogram(data)) => map_histogram(metric, data),
146        F64(MetricData::ExponentialHistogram(data)) => map_exponential_histogram(metric, data),
147        U64(MetricData::ExponentialHistogram(data)) => map_exponential_histogram(metric, data),
148        I64(MetricData::ExponentialHistogram(data)) => map_exponential_histogram(metric, data),
149    }
150}
151
152fn map_gauge<T: Copy + ToF64Lossy>(metric: &Metric, gauge: &Gauge<T>) -> Vec<EnvelopeData> {
153    gauge
154        .data_points()
155        .map(|data_point| {
156            let time = gauge.time();
157            let data = DataPoint {
158                ns: None,
159                name: metric.name().into(),
160                kind: Some(DataPointType::Measurement),
161                value: data_point.value().to_f64_lossy(),
162            };
163            let attrs = data_point.attributes().cloned().collect();
164            EnvelopeData { time, data, attrs }
165        })
166        .collect()
167}
168
169fn map_histogram<T: Copy + ToF64Lossy>(
170    metric: &Metric,
171    histogram: &Histogram<T>,
172) -> Vec<EnvelopeData> {
173    histogram
174        .data_points()
175        .map(|data_point| {
176            let time = histogram.time();
177            let data = DataPoint {
178                ns: None,
179                name: metric.name().into(),
180                kind: Some(DataPointType::Aggregation {
181                    count: Some(data_point.count().try_into().unwrap_or_default()),
182                    min: data_point.min().as_ref().map(ToF64Lossy::to_f64_lossy),
183                    max: data_point.max().as_ref().map(ToF64Lossy::to_f64_lossy),
184                    std_dev: None,
185                }),
186                value: data_point.sum().to_f64_lossy(),
187            };
188            let attrs = data_point.attributes().cloned().collect();
189            EnvelopeData { time, data, attrs }
190        })
191        .collect()
192}
193
194fn map_exponential_histogram<T: Copy + ToF64Lossy>(
195    metric: &Metric,
196    exp_histogram: &ExponentialHistogram<T>,
197) -> Vec<EnvelopeData> {
198    exp_histogram
199        .data_points()
200        .map(|data_point| {
201            let time = exp_histogram.time();
202            let data = DataPoint {
203                ns: None,
204                name: metric.name().into(),
205                kind: Some(DataPointType::Aggregation {
206                    count: Some(data_point.count().try_into().unwrap_or_default()),
207                    min: data_point.min().as_ref().map(ToF64Lossy::to_f64_lossy),
208                    max: data_point.max().as_ref().map(ToF64Lossy::to_f64_lossy),
209                    std_dev: None,
210                }),
211                value: data_point.sum().to_f64_lossy(),
212            };
213            let attrs = data_point.attributes().cloned().collect();
214            EnvelopeData { time, data, attrs }
215        })
216        .collect()
217}
218
219fn map_sum<T: Copy + ToF64Lossy>(metric: &Metric, sum: &Sum<T>) -> Vec<EnvelopeData> {
220    sum.data_points()
221        .map(|data_point| {
222            let time = sum.time();
223            let data = DataPoint {
224                ns: None,
225                name: metric.name().into(),
226                kind: Some(DataPointType::Aggregation {
227                    count: None,
228                    min: None,
229                    max: None,
230                    std_dev: None,
231                }),
232                value: data_point.value().to_f64_lossy(),
233            };
234            let attrs = data_point.attributes().cloned().collect();
235            EnvelopeData { time, data, attrs }
236        })
237        .collect()
238}