kmp_observability/
otlp_query_adapter.rs1use std::future::Future;
2use std::pin::Pin;
3
4use kmp_application::{
5 ObservabilityExemplar, ObservabilityProjection, ObservabilityQuery, ObservabilityQueryPort,
6 ObservabilitySeries,
7};
8use kmp_domain::PortError;
9
10#[derive(Debug, Clone, PartialEq)]
17pub struct OtlpQueryResponse {
18 pub series: Vec<ObservabilitySeries>,
19 pub exemplars: Vec<ObservabilityExemplar>,
20 pub missing: Vec<String>,
21 pub truncated: bool,
22}
23
24pub trait OtlpMetricsQueryClient: Send + Sync {
25 fn query_range<'a>(
26 &'a self,
27 query: &'a ObservabilityQuery,
28 ) -> Pin<Box<dyn Future<Output = Result<OtlpQueryResponse, PortError>> + Send + 'a>>;
29}
30
31#[derive(Debug, Clone)]
35pub struct OtlpObservabilityQueryAdapter<C> {
36 client: C,
37}
38
39impl<C> OtlpObservabilityQueryAdapter<C> {
40 pub fn new(client: C) -> Self {
41 Self { client }
42 }
43}
44
45impl<C> ObservabilityQueryPort for OtlpObservabilityQueryAdapter<C>
46where
47 C: OtlpMetricsQueryClient,
48{
49 fn query<'a>(
50 &'a self,
51 query: ObservabilityQuery,
52 ) -> Pin<Box<dyn Future<Output = Result<ObservabilityProjection, PortError>> + Send + 'a>> {
53 Box::pin(async move {
54 let response = self.client.query_range(&query).await?;
55 Ok(ObservabilityProjection {
56 contract: "kmp.observability.projection.v1".to_string(),
57 from_millis: query.from_millis,
58 to_millis: query.to_millis,
59 series: response.series,
60 exemplars: response.exemplars,
61 missing: response.missing,
62 truncated: response.truncated,
63 })
64 })
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 struct FakeClient;
73
74 impl OtlpMetricsQueryClient for FakeClient {
75 fn query_range<'a>(
76 &'a self,
77 _query: &'a ObservabilityQuery,
78 ) -> Pin<Box<dyn Future<Output = Result<OtlpQueryResponse, PortError>> + Send + 'a>>
79 {
80 Box::pin(async {
81 Ok(OtlpQueryResponse {
82 series: vec![ObservabilitySeries {
83 name: "rpc_duration".to_string(),
84 unit: "seconds".to_string(),
85 scope: "rpc".to_string(),
86 points: Vec::new(),
87 }],
88 exemplars: Vec::new(),
89 missing: Vec::new(),
90 truncated: false,
91 })
92 })
93 }
94 }
95
96 #[tokio::test]
97 async fn adapter_preserves_exact_metric_semantics() {
98 let adapter = OtlpObservabilityQueryAdapter::new(FakeClient);
99 let result = adapter
100 .query(ObservabilityQuery {
101 about: Some("project:kmp".to_string()),
102 from_millis: 10,
103 to_millis: 20,
104 series: vec!["rpc_duration".to_string()],
105 max_points: 100,
106 })
107 .await
108 .expect("backend query");
109 assert_eq!(result.series[0].unit, "seconds");
110 assert_eq!(result.series[0].scope, "rpc");
111 }
112}