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 opentelemetry::global::set_meter_provider(meter_provider.clone());
65 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);