micromegas_analytics/lakehouse/otel/
block_decoders.rs1use super::attrs::{attrs_to_jsonb_value, scope_extras};
12use crate::lakehouse::block_object_decoder::{BlockObjectDecoder, ObjectVisitor};
13use crate::metadata::StreamMetadata;
14use anyhow::{Context, Result};
15use jsonb::Value as JsonbValue;
16use micromegas_telemetry::block_wire_format::BlockPayload;
17use micromegas_tracing::prelude::*;
18use opentelemetry_proto::tonic::common::v1::KeyValue;
19use opentelemetry_proto::tonic::logs::v1::ResourceLogs;
20use opentelemetry_proto::tonic::metrics::v1::{
21 ExponentialHistogramDataPoint, HistogramDataPoint, NumberDataPoint, ResourceMetrics,
22 SummaryDataPoint, metric::Data,
23};
24use opentelemetry_proto::tonic::trace::v1::ResourceSpans;
25use prost::Message;
26use serde::Serialize;
27use std::borrow::Cow;
28use std::collections::BTreeMap;
29
30fn leaf_jsonb<T: Serialize>(
43 type_name: &str,
44 leaf: &T,
45 leaf_attrs: &[KeyValue],
46 resource: &JsonbValue<'static>,
47 scope: &JsonbValue<'static>,
48 metric_extras: &[(String, JsonbValue<'static>)],
49) -> Result<Vec<u8>> {
50 let json = serde_json::to_value(leaf).context("serializing OTLP leaf to JSON")?;
51 let mut map: BTreeMap<String, JsonbValue<'static>> = match JsonbValue::from(&json) {
52 JsonbValue::Object(map) => map,
53 other => {
57 let mut map = BTreeMap::new();
58 map.insert("__value".to_string(), other);
59 map
60 }
61 };
62
63 map.insert(
64 "__type".to_string(),
65 JsonbValue::String(Cow::Owned(type_name.to_string())),
66 );
67 map.insert(
68 "__attributes".to_string(),
69 attrs_to_jsonb_value(leaf_attrs, &[]),
70 );
71 map.insert("__resource".to_string(), resource.clone());
72 map.insert("__scope".to_string(), scope.clone());
73 if !metric_extras.is_empty() {
74 map.insert(
75 "__metric".to_string(),
76 JsonbValue::Object(metric_extras.iter().cloned().collect()),
77 );
78 }
79
80 let mut buf = Vec::new();
81 JsonbValue::Object(map).write_to_vec(&mut buf);
82 Ok(buf)
83}
84
85#[derive(Debug)]
88pub struct OtelLogsBlockDecoder;
89
90impl BlockObjectDecoder for OtelLogsBlockDecoder {
91 fn decode(
92 &self,
93 _stream: &StreamMetadata,
94 payload: &BlockPayload,
95 visitor: &mut dyn ObjectVisitor,
96 ) -> Result<()> {
97 let resource_logs = ResourceLogs::decode(payload.objects.as_slice())
100 .inspect_err(|e| error!("corrupt OTLP logs block payload: {e:?}"))
101 .context("decoding ResourceLogs proto")?;
102
103 let resource = resource_logs.resource.as_ref();
104 let resource_attrs: &[KeyValue] = resource.map_or(&[], |r| r.attributes.as_slice());
105 let resource_jsonb = attrs_to_jsonb_value(resource_attrs, &[]);
106 for scope_logs in &resource_logs.scope_logs {
107 let scope = scope_logs.scope.as_ref();
108 let scope_jsonb = JsonbValue::Object(
109 scope_extras(scope, &scope_logs.schema_url)
110 .into_iter()
111 .collect(),
112 );
113 for record in &scope_logs.log_records {
114 let bytes = leaf_jsonb(
115 "otlp.LogRecord",
116 record,
117 &record.attributes,
118 &resource_jsonb,
119 &scope_jsonb,
120 &[],
121 )?;
122 if !visitor.visit("otlp.LogRecord", &bytes)? {
123 return Ok(());
124 }
125 }
126 }
127 Ok(())
128 }
129}
130
131#[derive(Debug)]
134pub struct OtelTracesBlockDecoder;
135
136impl BlockObjectDecoder for OtelTracesBlockDecoder {
137 fn decode(
138 &self,
139 _stream: &StreamMetadata,
140 payload: &BlockPayload,
141 visitor: &mut dyn ObjectVisitor,
142 ) -> Result<()> {
143 let resource_spans = ResourceSpans::decode(payload.objects.as_slice())
144 .inspect_err(|e| error!("corrupt OTLP traces block payload: {e:?}"))
145 .context("decoding ResourceSpans proto")?;
146
147 let resource = resource_spans.resource.as_ref();
148 let resource_attrs: &[KeyValue] = resource.map_or(&[], |r| r.attributes.as_slice());
149 let resource_jsonb = attrs_to_jsonb_value(resource_attrs, &[]);
150 for scope_spans in &resource_spans.scope_spans {
151 let scope = scope_spans.scope.as_ref();
152 let scope_jsonb = JsonbValue::Object(
153 scope_extras(scope, &scope_spans.schema_url)
154 .into_iter()
155 .collect(),
156 );
157 for span in &scope_spans.spans {
158 let bytes = leaf_jsonb(
159 "otlp.Span",
160 span,
161 &span.attributes,
162 &resource_jsonb,
163 &scope_jsonb,
164 &[],
165 )?;
166 if !visitor.visit("otlp.Span", &bytes)? {
167 return Ok(());
168 }
169 }
170 }
171 Ok(())
172 }
173}
174
175trait DataPointAttrs {
179 fn attributes(&self) -> &[KeyValue];
180}
181
182impl DataPointAttrs for NumberDataPoint {
183 fn attributes(&self) -> &[KeyValue] {
184 &self.attributes
185 }
186}
187
188impl DataPointAttrs for HistogramDataPoint {
189 fn attributes(&self) -> &[KeyValue] {
190 &self.attributes
191 }
192}
193
194impl DataPointAttrs for ExponentialHistogramDataPoint {
195 fn attributes(&self) -> &[KeyValue] {
196 &self.attributes
197 }
198}
199
200impl DataPointAttrs for SummaryDataPoint {
201 fn attributes(&self) -> &[KeyValue] {
202 &self.attributes
203 }
204}
205
206fn emit_data_points<T: Serialize + DataPointAttrs>(
217 type_name: &str,
218 data_points: &[T],
219 resource: &JsonbValue<'static>,
220 scope: &JsonbValue<'static>,
221 metric_extras: &[(String, JsonbValue<'static>)],
222 visitor: &mut dyn ObjectVisitor,
223) -> Result<bool> {
224 for dp in data_points {
225 let bytes = leaf_jsonb(
226 type_name,
227 dp,
228 dp.attributes(),
229 resource,
230 scope,
231 metric_extras,
232 )?;
233 if !visitor.visit(type_name, &bytes)? {
234 return Ok(false);
235 }
236 }
237 Ok(true)
238}
239
240#[derive(Debug)]
252pub struct OtelMetricsBlockDecoder;
253
254impl BlockObjectDecoder for OtelMetricsBlockDecoder {
255 fn decode(
256 &self,
257 _stream: &StreamMetadata,
258 payload: &BlockPayload,
259 visitor: &mut dyn ObjectVisitor,
260 ) -> Result<()> {
261 let resource_metrics = ResourceMetrics::decode(payload.objects.as_slice())
262 .inspect_err(|e| error!("corrupt OTLP metrics block payload: {e:?}"))
263 .context("decoding ResourceMetrics proto")?;
264
265 let resource = resource_metrics.resource.as_ref();
266 let resource_attrs: &[KeyValue] = resource.map_or(&[], |r| r.attributes.as_slice());
267 let resource_jsonb = attrs_to_jsonb_value(resource_attrs, &[]);
268 for scope_metrics in &resource_metrics.scope_metrics {
269 let scope = scope_metrics.scope.as_ref();
270 let scope_jsonb = JsonbValue::Object(
271 scope_extras(scope, &scope_metrics.schema_url)
272 .into_iter()
273 .collect(),
274 );
275 for metric in &scope_metrics.metrics {
276 let Some(data) = metric.data.as_ref() else {
277 debug!(
282 "OTel metric '{}' has no data, skipping (no row emitted, no ordinal consumed)",
283 metric.name
284 );
285 continue;
286 };
287
288 let mut extras: Vec<(String, JsonbValue<'static>)> = vec![
292 (
293 "name".to_string(),
294 JsonbValue::String(Cow::Owned(metric.name.clone())),
295 ),
296 (
297 "unit".to_string(),
298 JsonbValue::String(Cow::Owned(metric.unit.clone())),
299 ),
300 (
301 "description".to_string(),
302 JsonbValue::String(Cow::Owned(metric.description.clone())),
303 ),
304 ];
305 if !metric.metadata.is_empty() {
306 extras.push((
307 "otel.metric.metadata".to_string(),
308 attrs_to_jsonb_value(&metric.metadata, &[]),
309 ));
310 }
311
312 match data {
313 Data::Sum(sum) => {
314 extras.push((
315 "otel.metric.kind".to_string(),
316 JsonbValue::String(Cow::Borrowed("sum")),
317 ));
318 extras.push((
319 "otel.metric.aggregation_temporality".to_string(),
320 JsonbValue::Number(jsonb::Number::Int64(
321 sum.aggregation_temporality as i64,
322 )),
323 ));
324 extras.push((
325 "otel.metric.is_monotonic".to_string(),
326 JsonbValue::Bool(sum.is_monotonic),
327 ));
328 if !emit_data_points(
329 "otlp.NumberDataPoint",
330 &sum.data_points,
331 &resource_jsonb,
332 &scope_jsonb,
333 &extras,
334 visitor,
335 )? {
336 return Ok(());
337 }
338 }
339 Data::Gauge(gauge) => {
340 extras.push((
341 "otel.metric.kind".to_string(),
342 JsonbValue::String(Cow::Borrowed("gauge")),
343 ));
344 if !emit_data_points(
345 "otlp.NumberDataPoint",
346 &gauge.data_points,
347 &resource_jsonb,
348 &scope_jsonb,
349 &extras,
350 visitor,
351 )? {
352 return Ok(());
353 }
354 }
355 Data::Histogram(h) => {
356 extras.push((
357 "otel.metric.kind".to_string(),
358 JsonbValue::String(Cow::Borrowed("histogram")),
359 ));
360 extras.push((
361 "otel.metric.aggregation_temporality".to_string(),
362 JsonbValue::Number(jsonb::Number::Int64(
363 h.aggregation_temporality as i64,
364 )),
365 ));
366 if !emit_data_points(
367 "otlp.HistogramDataPoint",
368 &h.data_points,
369 &resource_jsonb,
370 &scope_jsonb,
371 &extras,
372 visitor,
373 )? {
374 return Ok(());
375 }
376 }
377 Data::ExponentialHistogram(h) => {
378 extras.push((
379 "otel.metric.kind".to_string(),
380 JsonbValue::String(Cow::Borrowed("exponential_histogram")),
381 ));
382 extras.push((
383 "otel.metric.aggregation_temporality".to_string(),
384 JsonbValue::Number(jsonb::Number::Int64(
385 h.aggregation_temporality as i64,
386 )),
387 ));
388 if !emit_data_points(
389 "otlp.ExponentialHistogramDataPoint",
390 &h.data_points,
391 &resource_jsonb,
392 &scope_jsonb,
393 &extras,
394 visitor,
395 )? {
396 return Ok(());
397 }
398 }
399 Data::Summary(s) => {
400 extras.push((
401 "otel.metric.kind".to_string(),
402 JsonbValue::String(Cow::Borrowed("summary")),
403 ));
404 if !emit_data_points(
405 "otlp.SummaryDataPoint",
406 &s.data_points,
407 &resource_jsonb,
408 &scope_jsonb,
409 &extras,
410 visitor,
411 )? {
412 return Ok(());
413 }
414 }
415 }
416 }
417 }
418 Ok(())
419 }
420}