1use std::sync::Arc;
16
17use auth::UserProviderRef;
18use common_error::ext::ErrorExt;
19use common_error::status_code::status_to_tonic_code;
20use common_telemetry::error;
21use futures::SinkExt;
22use otel_arrow_rust::Consumer;
23use otel_arrow_rust::proto::opentelemetry::arrow::v1::arrow_metrics_service_server::ArrowMetricsService;
24use otel_arrow_rust::proto::opentelemetry::arrow::v1::{
25 BatchArrowRecords, BatchStatus, StatusCode as ArrowStatusCode,
26};
27use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
28use otel_arrow_rust::proto::opentelemetry::metrics::v1::metric;
29use session::protocol_ctx::{OtlpMetricCtx, ProtocolCtx};
30use tonic::metadata::{Entry, MetadataValue};
31use tonic::service::Interceptor;
32use tonic::{Request, Response, Status, Streaming};
33
34use crate::error;
35use crate::grpc::context_auth;
36use crate::grpc::memory_limit::PreDecodeMemoryReservation;
37use crate::query_handler::{MetricsIngestOutcome, OpenTelemetryProtocolHandlerRef};
38use crate::request_memory_limiter::ServerMemoryLimiter;
39
40const EXPONENTIAL_HISTOGRAM_UNSUPPORTED: &str = "OTel Arrow exponential histograms are unsupported because the Arrow wire format omits zero_threshold";
41
42pub struct OtelArrowServiceHandler<T> {
43 handler: T,
44 user_provider: Option<UserProviderRef>,
45}
46
47impl<T> OtelArrowServiceHandler<T> {
48 pub fn new(handler: T, user_provider: Option<UserProviderRef>) -> Self {
49 Self {
50 handler,
51 user_provider,
52 }
53 }
54}
55
56fn remove_exponential_histograms(request: &mut ExportMetricsServiceRequest) -> bool {
58 let mut has_data_points = false;
59 for scope in request
60 .resource_metrics
61 .iter_mut()
62 .flat_map(|resource| &mut resource.scope_metrics)
63 {
64 scope.metrics.retain(|item| {
65 if let Some(metric::Data::ExponentialHistogram(histogram)) = &item.data {
66 has_data_points |= !histogram.data_points.is_empty();
67 false
68 } else {
69 true
70 }
71 });
72 }
73 has_data_points
74}
75
76fn batch_status(
77 batch_id: i64,
78 outcome: MetricsIngestOutcome,
79 has_exponential_histogram_data_points: bool,
80) -> BatchStatus {
81 let status_code = if outcome.accepted_data_points == 0
82 && (outcome.rejected_data_points > 0 || has_exponential_histogram_data_points)
83 {
84 ArrowStatusCode::InvalidArgument
85 } else {
86 ArrowStatusCode::Ok
87 };
88 let status_message = if has_exponential_histogram_data_points {
89 EXPONENTIAL_HISTOGRAM_UNSUPPORTED.to_string()
90 } else {
91 outcome.error_message.unwrap_or_default()
92 };
93 BatchStatus {
94 batch_id,
95 status_code: status_code as i32,
96 status_message,
97 }
98}
99
100#[async_trait::async_trait]
101impl ArrowMetricsService for OtelArrowServiceHandler<OpenTelemetryProtocolHandlerRef> {
102 type ArrowMetricsStream = futures::channel::mpsc::Receiver<Result<BatchStatus, Status>>;
103 async fn arrow_metrics(
104 &self,
105 request: Request<Streaming<BatchArrowRecords>>,
106 ) -> Result<Response<Self::ArrowMetricsStream>, Status> {
107 let (mut sender, receiver) = futures::channel::mpsc::channel(100);
108
109 let (headers, extensions, mut incoming_requests) = request.into_parts();
110
111 let query_ctx =
116 context_auth::create_query_context_from_grpc_metadata(&headers, &extensions)?;
117 context_auth::check_auth(self.user_provider.clone(), &headers, query_ctx.clone()).await?;
118 let query_ctx = {
119 let mut ctx = query_ctx.fork();
120 ctx.set_protocol_ctx(ProtocolCtx::OtlpMetric(OtlpMetricCtx::default()));
121 Arc::new(ctx)
122 };
123
124 let handler = self.handler.clone();
125 let memory_limiter = extensions.get::<ServerMemoryLimiter>().cloned();
126 let decode_reservation = extensions.get::<PreDecodeMemoryReservation>().cloned();
127
128 common_runtime::spawn_global(async move {
130 let mut consumer = Consumer::default();
131 while let Some(batch_res) = incoming_requests.message().await.transpose() {
132 let mut batch = match batch_res {
133 Ok(batch) => batch,
134 Err(e) => {
135 error!(
136 "Failed to receive batch from otel-arrow client, error: {}",
137 e
138 );
139 let _ = sender.send(Err(e)).await;
140 return;
141 }
142 };
143 let batch_id = batch.batch_id;
144 let mut request = match consumer.consume_metrics_batches(&mut batch).map_err(|e| {
145 error::HandleOtelArrowRequestSnafu {
146 err_msg: e.to_string(),
147 }
148 .build()
149 }) {
150 Ok(request) => request,
151 Err(e) => {
152 let _ = sender
153 .send(Err(Status::new(
154 status_to_tonic_code(e.status_code()),
155 e.to_string(),
156 )))
157 .await;
158 error!(e;
159 "Failed to consume batch from otel-arrow client"
160 );
161 return;
162 }
163 };
164 let has_exponential_histogram_data_points =
165 remove_exponential_histograms(&mut request);
166 let memory_limiter = memory_limiter.clone().map(|limiter| {
167 let held_bytes = decode_reservation
168 .as_ref()
169 .map_or(0, PreDecodeMemoryReservation::reserved_bytes);
170 (limiter, held_bytes)
171 });
172 let outcome = match handler
173 .metrics(request, query_ctx.clone(), memory_limiter)
174 .await
175 {
176 Ok(outcome) => outcome,
177 Err(error::Error::InvalidOtlpMetricInput { reason }) => {
178 let _ = sender
179 .send(Ok(BatchStatus {
180 batch_id,
181 status_code: ArrowStatusCode::InvalidArgument as i32,
182 status_message: reason,
183 }))
184 .await;
185 continue;
186 }
187 Err(e) => {
188 let _ = sender
189 .send(Err(Status::new(
190 status_to_tonic_code(e.status_code()),
191 e.to_string(),
192 )))
193 .await;
194 error!(e; "Failed to ingest metrics from otel-arrow");
195 return;
196 }
197 };
198 let batch_status =
199 batch_status(batch_id, outcome, has_exponential_histogram_data_points);
200 let _ = sender.send(Ok(batch_status)).await;
201 }
202 });
203 Ok(Response::new(receiver))
204 }
205}
206
207#[derive(Clone)]
209pub struct HeaderInterceptor;
210
211impl Interceptor for HeaderInterceptor {
212 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
213 if let Ok(Entry::Occupied(mut e)) = request.metadata_mut().entry("grpc-encoding") {
214 if e.get().as_bytes().starts_with(b"zstdarrow") {
216 e.insert(MetadataValue::from_static("zstd"));
217 }
218 }
219 Ok(request)
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn removes_arrow_exponential_histograms_and_preserves_other_metrics() {
229 use otel_arrow_rust::proto::opentelemetry::metrics::v1::{
230 ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, Metric, ResourceMetrics,
231 ScopeMetrics,
232 };
233
234 let mut request = ExportMetricsServiceRequest {
235 resource_metrics: vec![ResourceMetrics {
236 scope_metrics: vec![ScopeMetrics {
237 metrics: vec![
238 Metric {
239 data: Some(metric::Data::ExponentialHistogram(ExponentialHistogram {
240 data_points: vec![ExponentialHistogramDataPoint::default()],
241 ..Default::default()
242 })),
243 ..Default::default()
244 },
245 Metric {
246 data: Some(metric::Data::Gauge(Gauge::default())),
247 ..Default::default()
248 },
249 ],
250 ..Default::default()
251 }],
252 ..Default::default()
253 }],
254 };
255 assert!(remove_exponential_histograms(&mut request));
256 let metrics = &request.resource_metrics[0].scope_metrics[0].metrics;
257 assert_eq!(metrics.len(), 1);
258 assert!(matches!(metrics[0].data, Some(metric::Data::Gauge(_))));
259 assert!(!remove_exponential_histograms(&mut request));
260 }
261
262 #[test]
263 fn batch_status_explains_arrow_exponential_histogram_limit() {
264 for accepted_data_points in [0, 1] {
265 let status = batch_status(
266 7,
267 MetricsIngestOutcome {
268 accepted_data_points,
269 ..Default::default()
270 },
271 true,
272 );
273 assert_eq!(7, status.batch_id);
274 assert_eq!(
275 if accepted_data_points == 0 {
276 ArrowStatusCode::InvalidArgument
277 } else {
278 ArrowStatusCode::Ok
279 } as i32,
280 status.status_code
281 );
282 assert_eq!(EXPONENTIAL_HISTOGRAM_UNSUPPORTED, status.status_message);
283 }
284 }
285}