Skip to main content

diode_base/
metrics.rs

1use std::time::Duration;
2
3use diode::{AppContext, StdError};
4use duration_str::deserialize_option_duration;
5use opentelemetry::KeyValue;
6use opentelemetry_otlp::WithExportConfig;
7use opentelemetry_sdk::metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider};
8use opentelemetry_sdk::{Resource, runtime};
9use serde::{Deserialize, Serialize};
10
11use crate::{Config, ConfigSection};
12
13pub struct Metrics {
14    meter_provider: SdkMeterProvider,
15}
16
17impl Metrics {
18    pub fn build(ctx: &AppContext) -> Result<(), StdError> {
19        if ctx.has_component::<Self>() {
20            return Ok(());
21        }
22        let config = match ctx
23            .get_component_ref::<Config>()
24            .unwrap()
25            .get::<Option<MetricsConfig>>("metrics")?
26        {
27            Some(v) => v,
28            None => return Ok(()),
29        };
30        let meter_provider = {
31            if let Some(otlp_exporter) = config.otlp_exporter {
32                let exporter = opentelemetry_otlp::MetricExporter::builder()
33                    .with_tonic()
34                    .with_endpoint(
35                        otlp_exporter
36                            .endpoint
37                            .unwrap_or(DEFAULT_OTLP_EXPORTER_ENDPOINT.into()),
38                    )
39                    .with_timeout(
40                        otlp_exporter
41                            .timeout
42                            .unwrap_or(DEFAULT_OTLP_EXPORTER_TIMEOUT),
43                    )
44                    .build()?;
45                let reader = PeriodicReader::builder(exporter, runtime::Tokio)
46                    .with_interval(
47                        otlp_exporter
48                            .interval
49                            .unwrap_or(DEFAULT_OTLP_EXPORTER_INTERVAL),
50                    )
51                    .build();
52                MeterProviderBuilder::default()
53                    .with_resource(Resource::new(vec![KeyValue::new(
54                        "service.name",
55                        otlp_exporter.service_name.unwrap_or("unknown".into()),
56                    )]))
57                    .with_reader(reader)
58                    .build()
59            } else {
60                MeterProviderBuilder::default().build()
61            }
62        };
63        // Setup meter provider.
64        opentelemetry::global::set_meter_provider(meter_provider.clone());
65        // Add app components.
66        ctx.add_component(Self { meter_provider });
67        Ok(())
68    }
69}
70
71impl Drop for Metrics {
72    fn drop(&mut self) {
73        if let Err(err) = self.meter_provider.shutdown() {
74            tracing::error!("Cannot shutdown meter provider: {err}");
75        }
76    }
77}
78
79#[derive(Serialize, Deserialize)]
80pub struct MetricsConfig {
81    #[serde(default)]
82    pub otlp_exporter: Option<MetricsOtlpExporterConfig>,
83}
84
85#[derive(Serialize, Deserialize)]
86pub struct MetricsOtlpExporterConfig {
87    #[serde(default)]
88    pub service_name: Option<String>,
89    #[serde(default)]
90    pub endpoint: Option<String>,
91    #[serde(default, deserialize_with = "deserialize_option_duration")]
92    pub timeout: Option<Duration>,
93    #[serde(default, deserialize_with = "deserialize_option_duration")]
94    pub interval: Option<Duration>,
95}
96
97impl ConfigSection for MetricsConfig {
98    fn key() -> &'static str {
99        "metrics"
100    }
101}
102
103const DEFAULT_OTLP_EXPORTER_ENDPOINT: &str = "https://localhost:4317/v1/metrics";
104const DEFAULT_OTLP_EXPORTER_TIMEOUT: Duration = Duration::from_secs(10);
105const DEFAULT_OTLP_EXPORTER_INTERVAL: Duration = Duration::from_secs(10);