1use super::attrs::attrs_to_jsonb;
11use crate::lakehouse::{
12 block_partition_spec::BlockProcessor, partition_source_data::PartitionSourceBlock,
13 write_partition::PartitionRowSet,
14};
15use crate::metadata::ProcessMetadata;
16use crate::payload::fetch_block_payload;
17use crate::time::TimeRange;
18use anyhow::{Context, Result};
19use async_trait::async_trait;
20use chrono::DateTime;
21use datafusion::arrow::array::{
22 BinaryDictionaryBuilder, PrimitiveBuilder, StringDictionaryBuilder,
23};
24use datafusion::arrow::datatypes::{Float64Type, Int32Type, TimestampNanosecondType};
25use datafusion::arrow::record_batch::RecordBatch;
26use jsonb::Value as JsonbValue;
27use micromegas_telemetry::blob_storage::BlobStorage;
28use micromegas_tracing::prelude::*;
29use opentelemetry_proto::tonic::metrics::v1::{
30 DataPointFlags, NumberDataPoint, ResourceMetrics, SummaryDataPoint, metric::Data,
31 number_data_point,
32};
33use prost::Message;
34use std::borrow::Cow;
35use std::sync::Arc;
36
37fn nonzero_time_nanos(kind: &str, metric_name: &str, time_unix_nano: u64) -> Option<i64> {
41 let time_nanos = time_unix_nano as i64;
42 if time_nanos == 0 {
43 debug!("OTel {kind} data point for {metric_name} dropped (time_unix_nano=0)");
44 return None;
45 }
46 Some(time_nanos)
47}
48
49#[derive(Debug)]
50pub struct OtelMetricsBlockProcessor {}
51
52#[async_trait]
53impl BlockProcessor for OtelMetricsBlockProcessor {
54 #[span_fn]
55 async fn process(
56 &self,
57 blob_storage: Arc<BlobStorage>,
58 src_block: Arc<PartitionSourceBlock>,
59 ) -> Result<Option<PartitionRowSet>> {
60 let payload = fetch_block_payload(
61 blob_storage,
62 sqlx::types::Uuid::from_bytes(*src_block.block.process_id.as_bytes()),
63 sqlx::types::Uuid::from_bytes(*src_block.block.stream_id.as_bytes()),
64 sqlx::types::Uuid::from_bytes(*src_block.block.block_id.as_bytes()),
65 )
66 .await
67 .with_context(|| "fetch_block_payload")?;
68
69 let resource_metrics = ResourceMetrics::decode(payload.objects.as_slice())
70 .with_context(|| "decoding ResourceMetrics proto")?;
71
72 let insert_time_nanos = src_block
73 .block
74 .insert_time
75 .timestamp_nanos_opt()
76 .with_context(|| "block.insert_time → nanos")?;
77 let mut builder = MeasuresRowBuilder::new(
78 src_block.process.process_id.to_string(),
79 src_block.block.stream_id.to_string(),
80 src_block.block.block_id.to_string(),
81 insert_time_nanos,
82 src_block.process.clone(),
83 );
84
85 for scope_metrics in &resource_metrics.scope_metrics {
86 let scope = scope_metrics.scope.as_ref();
87 let scope_name = scope.map(|s| s.name.clone()).unwrap_or_default();
88
89 for metric in &scope_metrics.metrics {
90 match metric.data.as_ref() {
91 Some(Data::Sum(sum)) => {
92 let extras = [
93 (
94 "otel.metric.aggregation_temporality".to_string(),
95 JsonbValue::Number(jsonb::Number::Int64(
96 sum.aggregation_temporality as i64,
97 )),
98 ),
99 (
100 "otel.metric.is_monotonic".to_string(),
101 JsonbValue::Bool(sum.is_monotonic),
102 ),
103 (
104 "otel.metric.kind".to_string(),
105 JsonbValue::String(Cow::Borrowed("sum")),
106 ),
107 ];
108 for dp in &sum.data_points {
109 builder.append(&scope_name, &metric.name, &metric.unit, dp, &extras)?;
110 }
111 }
112 Some(Data::Gauge(gauge)) => {
113 let extras = [(
114 "otel.metric.kind".to_string(),
115 JsonbValue::String(Cow::Borrowed("gauge")),
116 )];
117 for dp in &gauge.data_points {
118 builder.append(&scope_name, &metric.name, &metric.unit, dp, &extras)?;
119 }
120 }
121 Some(Data::Histogram(h)) => {
122 debug!(
123 "OTel histogram dropped (deferred to v2): name={} unit={} points={}",
124 metric.name,
125 metric.unit,
126 h.data_points.len()
127 );
128 }
129 Some(Data::ExponentialHistogram(h)) => {
130 debug!(
131 "OTel exponential_histogram dropped (deferred to v2): name={} unit={} points={}",
132 metric.name,
133 metric.unit,
134 h.data_points.len()
135 );
136 }
137 Some(Data::Summary(s)) => {
138 for dp in &s.data_points {
139 builder.append_summary(&scope_name, &metric.name, &metric.unit, dp)?;
140 }
141 }
142 None => {}
143 }
144 }
145 }
146
147 builder.finish()
148 }
149}
150
151struct MeasuresRowBuilder {
154 process_ids: StringDictionaryBuilder<Int32Type>,
155 stream_ids: StringDictionaryBuilder<Int32Type>,
156 block_ids: StringDictionaryBuilder<Int32Type>,
157 insert_times: PrimitiveBuilder<TimestampNanosecondType>,
158 exes: StringDictionaryBuilder<Int32Type>,
159 usernames: StringDictionaryBuilder<Int32Type>,
160 computers: StringDictionaryBuilder<Int32Type>,
161 times: PrimitiveBuilder<TimestampNanosecondType>,
162 targets: StringDictionaryBuilder<Int32Type>,
163 names: StringDictionaryBuilder<Int32Type>,
164 units: StringDictionaryBuilder<Int32Type>,
165 values: PrimitiveBuilder<Float64Type>,
166 properties: BinaryDictionaryBuilder<Int32Type>,
167 process_properties: BinaryDictionaryBuilder<Int32Type>,
168 min_time: i64,
169 max_time: i64,
170 nb_appended: usize,
171 process_id_str: String,
172 stream_id_str: String,
173 block_id_str: String,
174 insert_time_nanos: i64,
175 process: Arc<ProcessMetadata>,
176}
177
178impl MeasuresRowBuilder {
179 fn new(
180 process_id_str: String,
181 stream_id_str: String,
182 block_id_str: String,
183 insert_time_nanos: i64,
184 process: Arc<ProcessMetadata>,
185 ) -> Self {
186 Self {
187 process_ids: StringDictionaryBuilder::new(),
188 stream_ids: StringDictionaryBuilder::new(),
189 block_ids: StringDictionaryBuilder::new(),
190 insert_times: PrimitiveBuilder::new(),
191 exes: StringDictionaryBuilder::new(),
192 usernames: StringDictionaryBuilder::new(),
193 computers: StringDictionaryBuilder::new(),
194 times: PrimitiveBuilder::new(),
195 targets: StringDictionaryBuilder::new(),
196 names: StringDictionaryBuilder::new(),
197 units: StringDictionaryBuilder::new(),
198 values: PrimitiveBuilder::new(),
199 properties: BinaryDictionaryBuilder::new(),
200 process_properties: BinaryDictionaryBuilder::new(),
201 min_time: i64::MAX,
202 max_time: i64::MIN,
203 nb_appended: 0,
204 process_id_str,
205 stream_id_str,
206 block_id_str,
207 insert_time_nanos,
208 process,
209 }
210 }
211
212 fn append_row(
218 &mut self,
219 scope_name: &str,
220 metric_name: &str,
221 unit: &str,
222 time_nanos: i64,
223 value: f64,
224 props_jsonb: &[u8],
225 ) -> Result<()> {
226 self.min_time = self.min_time.min(time_nanos);
227 self.max_time = self.max_time.max(time_nanos);
228
229 self.process_ids.append(&self.process_id_str)?;
230 self.stream_ids.append(&self.stream_id_str)?;
231 self.block_ids.append(&self.block_id_str)?;
232 self.insert_times.append_value(self.insert_time_nanos);
233 self.exes.append(&self.process.exe)?;
234 self.usernames.append(&self.process.username)?;
235 self.computers.append(&self.process.computer)?;
236 self.times.append_value(time_nanos);
237 self.targets.append(scope_name)?;
238 self.names.append(metric_name)?;
239 self.units.append(unit)?;
240 self.values.append_value(value);
241 self.properties.append(props_jsonb)?;
242 self.process_properties.append(&**self.process.properties)?;
243
244 self.nb_appended += 1;
245 Ok(())
246 }
247
248 fn append(
249 &mut self,
250 scope_name: &str,
251 metric_name: &str,
252 unit: &str,
253 dp: &NumberDataPoint,
254 extras: &[(String, JsonbValue<'static>)],
255 ) -> Result<()> {
256 let Some(time_nanos) = nonzero_time_nanos("metric", metric_name, dp.time_unix_nano) else {
257 return Ok(());
258 };
259
260 let value = match dp.value.as_ref() {
261 Some(number_data_point::Value::AsDouble(d)) => *d,
262 Some(number_data_point::Value::AsInt(i)) => *i as f64,
263 None => {
264 debug!("OTel data point for {metric_name} has no value, skipping");
265 return Ok(());
266 }
267 };
268
269 self.append_row(
270 scope_name,
271 metric_name,
272 unit,
273 time_nanos,
274 value,
275 &attrs_to_jsonb(&dp.attributes, extras),
276 )
277 }
278
279 fn append_summary(
290 &mut self,
291 scope_name: &str,
292 metric_name: &str,
293 unit: &str,
294 dp: &SummaryDataPoint,
295 ) -> Result<()> {
296 if dp.flags & DataPointFlags::NoRecordedValueMask as u32 != 0 {
297 debug!("OTel summary data point dropped (NO_RECORDED_VALUE): name={metric_name}");
298 return Ok(());
299 }
300
301 let Some(time_nanos) = nonzero_time_nanos("summary", metric_name, dp.time_unix_nano) else {
302 return Ok(());
303 };
304
305 let props_jsonb = attrs_to_jsonb(&dp.attributes, &[]);
307
308 self.append_row(
309 scope_name,
310 &format!("{metric_name}_count"),
311 "",
312 time_nanos,
313 dp.count as f64,
314 &props_jsonb,
315 )?;
316 self.append_row(
317 scope_name,
318 &format!("{metric_name}_sum"),
319 unit,
320 time_nanos,
321 dp.sum,
322 &props_jsonb,
323 )?;
324
325 let mut min_emitted = false;
326 let mut max_emitted = false;
327 for q in &dp.quantile_values {
328 if q.quantile == 0.0 {
329 if min_emitted {
330 debug!(
331 "OTel summary quantile dropped (duplicate q=0.0, _min already emitted): \
332 name={metric_name}"
333 );
334 continue;
335 }
336 min_emitted = true;
337 self.append_row(
338 scope_name,
339 &format!("{metric_name}_min"),
340 unit,
341 time_nanos,
342 q.value,
343 &props_jsonb,
344 )?;
345 } else if q.quantile == 1.0 {
346 if max_emitted {
347 debug!(
348 "OTel summary quantile dropped (duplicate q=1.0, _max already emitted): \
349 name={metric_name}"
350 );
351 continue;
352 }
353 max_emitted = true;
354 self.append_row(
355 scope_name,
356 &format!("{metric_name}_max"),
357 unit,
358 time_nanos,
359 q.value,
360 &props_jsonb,
361 )?;
362 } else {
363 debug!(
364 "OTel summary quantile dropped (only count/sum/min/max are materialized): \
365 name={metric_name} quantile={}",
366 q.quantile
367 );
368 }
369 }
370 Ok(())
371 }
372
373 fn finish(mut self) -> Result<Option<PartitionRowSet>> {
374 if self.nb_appended == 0 {
375 return Ok(None);
376 }
377 let schema = Arc::new(crate::metrics_table::metrics_table_schema());
378 let batch = RecordBatch::try_new(
379 schema,
380 vec![
381 Arc::new(self.process_ids.finish()),
382 Arc::new(self.stream_ids.finish()),
383 Arc::new(self.block_ids.finish()),
384 Arc::new(self.insert_times.finish().with_timezone_utc()),
385 Arc::new(self.exes.finish()),
386 Arc::new(self.usernames.finish()),
387 Arc::new(self.computers.finish()),
388 Arc::new(self.times.finish().with_timezone_utc()),
389 Arc::new(self.targets.finish()),
390 Arc::new(self.names.finish()),
391 Arc::new(self.units.finish()),
392 Arc::new(self.values.finish()),
393 Arc::new(self.properties.finish()),
394 Arc::new(self.process_properties.finish()),
395 ],
396 )
397 .with_context(|| "building OTel measures batch")?;
398
399 Ok(Some(PartitionRowSet::new(
400 TimeRange::new(
401 DateTime::from_timestamp_nanos(self.min_time),
402 DateTime::from_timestamp_nanos(self.max_time),
403 ),
404 batch,
405 )))
406 }
407}