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 audiences: StringDictionaryBuilder<Int32Type>,
169 min_time: i64,
170 max_time: i64,
171 nb_appended: usize,
172 process_id_str: String,
173 stream_id_str: String,
174 block_id_str: String,
175 insert_time_nanos: i64,
176 process: Arc<ProcessMetadata>,
177}
178
179impl MeasuresRowBuilder {
180 fn new(
181 process_id_str: String,
182 stream_id_str: String,
183 block_id_str: String,
184 insert_time_nanos: i64,
185 process: Arc<ProcessMetadata>,
186 ) -> Self {
187 Self {
188 process_ids: StringDictionaryBuilder::new(),
189 stream_ids: StringDictionaryBuilder::new(),
190 block_ids: StringDictionaryBuilder::new(),
191 insert_times: PrimitiveBuilder::new(),
192 exes: StringDictionaryBuilder::new(),
193 usernames: StringDictionaryBuilder::new(),
194 computers: StringDictionaryBuilder::new(),
195 times: PrimitiveBuilder::new(),
196 targets: StringDictionaryBuilder::new(),
197 names: StringDictionaryBuilder::new(),
198 units: StringDictionaryBuilder::new(),
199 values: PrimitiveBuilder::new(),
200 properties: BinaryDictionaryBuilder::new(),
201 process_properties: BinaryDictionaryBuilder::new(),
202 audiences: StringDictionaryBuilder::new(),
203 min_time: i64::MAX,
204 max_time: i64::MIN,
205 nb_appended: 0,
206 process_id_str,
207 stream_id_str,
208 block_id_str,
209 insert_time_nanos,
210 process,
211 }
212 }
213
214 fn append_row(
220 &mut self,
221 scope_name: &str,
222 metric_name: &str,
223 unit: &str,
224 time_nanos: i64,
225 value: f64,
226 props_jsonb: &[u8],
227 ) -> Result<()> {
228 self.min_time = self.min_time.min(time_nanos);
229 self.max_time = self.max_time.max(time_nanos);
230
231 self.process_ids.append(&self.process_id_str)?;
232 self.stream_ids.append(&self.stream_id_str)?;
233 self.block_ids.append(&self.block_id_str)?;
234 self.insert_times.append_value(self.insert_time_nanos);
235 self.exes.append(&self.process.exe)?;
236 self.usernames.append(&self.process.username)?;
237 self.computers.append(&self.process.computer)?;
238 self.times.append_value(time_nanos);
239 self.targets.append(scope_name)?;
240 self.names.append(metric_name)?;
241 self.units.append(unit)?;
242 self.values.append_value(value);
243 self.properties.append(props_jsonb)?;
244 self.process_properties.append(&**self.process.properties)?;
245 self.audiences.append(&*self.process.audience)?;
246
247 self.nb_appended += 1;
248 Ok(())
249 }
250
251 fn append(
252 &mut self,
253 scope_name: &str,
254 metric_name: &str,
255 unit: &str,
256 dp: &NumberDataPoint,
257 extras: &[(String, JsonbValue<'static>)],
258 ) -> Result<()> {
259 let Some(time_nanos) = nonzero_time_nanos("metric", metric_name, dp.time_unix_nano) else {
260 return Ok(());
261 };
262
263 let value = match dp.value.as_ref() {
264 Some(number_data_point::Value::AsDouble(d)) => *d,
265 Some(number_data_point::Value::AsInt(i)) => *i as f64,
266 None => {
267 debug!("OTel data point for {metric_name} has no value, skipping");
268 return Ok(());
269 }
270 };
271
272 self.append_row(
273 scope_name,
274 metric_name,
275 unit,
276 time_nanos,
277 value,
278 &attrs_to_jsonb(&dp.attributes, extras),
279 )
280 }
281
282 fn append_summary(
293 &mut self,
294 scope_name: &str,
295 metric_name: &str,
296 unit: &str,
297 dp: &SummaryDataPoint,
298 ) -> Result<()> {
299 if dp.flags & DataPointFlags::NoRecordedValueMask as u32 != 0 {
300 debug!("OTel summary data point dropped (NO_RECORDED_VALUE): name={metric_name}");
301 return Ok(());
302 }
303
304 let Some(time_nanos) = nonzero_time_nanos("summary", metric_name, dp.time_unix_nano) else {
305 return Ok(());
306 };
307
308 let props_jsonb = attrs_to_jsonb(&dp.attributes, &[]);
310
311 self.append_row(
312 scope_name,
313 &format!("{metric_name}_count"),
314 "",
315 time_nanos,
316 dp.count as f64,
317 &props_jsonb,
318 )?;
319 self.append_row(
320 scope_name,
321 &format!("{metric_name}_sum"),
322 unit,
323 time_nanos,
324 dp.sum,
325 &props_jsonb,
326 )?;
327
328 let mut min_emitted = false;
329 let mut max_emitted = false;
330 for q in &dp.quantile_values {
331 if q.quantile == 0.0 {
332 if min_emitted {
333 debug!(
334 "OTel summary quantile dropped (duplicate q=0.0, _min already emitted): \
335 name={metric_name}"
336 );
337 continue;
338 }
339 min_emitted = true;
340 self.append_row(
341 scope_name,
342 &format!("{metric_name}_min"),
343 unit,
344 time_nanos,
345 q.value,
346 &props_jsonb,
347 )?;
348 } else if q.quantile == 1.0 {
349 if max_emitted {
350 debug!(
351 "OTel summary quantile dropped (duplicate q=1.0, _max already emitted): \
352 name={metric_name}"
353 );
354 continue;
355 }
356 max_emitted = true;
357 self.append_row(
358 scope_name,
359 &format!("{metric_name}_max"),
360 unit,
361 time_nanos,
362 q.value,
363 &props_jsonb,
364 )?;
365 } else {
366 debug!(
367 "OTel summary quantile dropped (only count/sum/min/max are materialized): \
368 name={metric_name} quantile={}",
369 q.quantile
370 );
371 }
372 }
373 Ok(())
374 }
375
376 fn finish(mut self) -> Result<Option<PartitionRowSet>> {
377 if self.nb_appended == 0 {
378 return Ok(None);
379 }
380 let schema = Arc::new(crate::metrics_table::metrics_table_schema());
381 let batch = RecordBatch::try_new(
382 schema,
383 vec![
384 Arc::new(self.process_ids.finish()),
385 Arc::new(self.stream_ids.finish()),
386 Arc::new(self.block_ids.finish()),
387 Arc::new(self.insert_times.finish().with_timezone_utc()),
388 Arc::new(self.exes.finish()),
389 Arc::new(self.usernames.finish()),
390 Arc::new(self.computers.finish()),
391 Arc::new(self.times.finish().with_timezone_utc()),
392 Arc::new(self.targets.finish()),
393 Arc::new(self.names.finish()),
394 Arc::new(self.units.finish()),
395 Arc::new(self.values.finish()),
396 Arc::new(self.properties.finish()),
397 Arc::new(self.process_properties.finish()),
398 Arc::new(self.audiences.finish()),
399 ],
400 )
401 .with_context(|| "building OTel measures batch")?;
402
403 Ok(Some(PartitionRowSet::new(
404 TimeRange::new(
405 DateTime::from_timestamp_nanos(self.min_time),
406 DateTime::from_timestamp_nanos(self.max_time),
407 ),
408 batch,
409 None,
410 )))
411 }
412}