1use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5
6use serde::{Deserialize, Serialize};
7
8pub const METRIC_RECORD_MODEL_FORMAT_VERSION: u16 = 1;
9pub const METRIC_POINT_FORMAT_VERSION: u16 = 1;
10pub const METRIC_SOURCE_FORMAT_VERSION: u16 = 1;
11
12pub const MAX_METRIC_RESOURCE_ATTRIBUTES: usize = 64;
13pub const MAX_METRIC_SCOPE_ATTRIBUTES: usize = 32;
14pub const MAX_METRIC_SERIES_ATTRIBUTES: usize = 128;
15pub const MAX_METRIC_GROUPING_ATTRIBUTES: usize = 16;
16pub const MAX_METRIC_EXEMPLAR_ATTRIBUTES: usize = 16;
17pub const MAX_METRIC_ATTRIBUTE_KEY_BYTES: usize = 256;
18pub const MAX_METRIC_ATTRIBUTE_STRING_BYTES: usize = 4_096;
19pub const MAX_METRIC_ATTRIBUTE_BYTES: usize = 64 * 1_024;
20pub const MAX_METRIC_EXEMPLARS: usize = 8;
21pub const MAX_METRIC_EXPLICIT_BOUNDS: usize = 4_096;
22pub const MAX_METRIC_EXPONENTIAL_BUCKETS: usize = 4_096;
23pub const MIN_METRIC_EXPONENTIAL_SCALE: i32 = -10;
24pub const MAX_METRIC_EXPONENTIAL_SCALE: i32 = 20;
25pub const MAX_METRIC_POINT_BYTES: usize = 1_048_576;
26
27const RESOURCE_GROUPING_PREFIX: &str = "resource.";
28const SCOPE_GROUPING_PREFIX: &str = "scope.";
29const SERIES_GROUPING_PREFIX: &str = "series.";
30
31#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
33pub enum IndexRecordModel {
34 #[default]
35 Document,
36 MetricV1 {
37 format_version: u16,
38 grouping_attributes: Vec<String>,
39 },
40 RelationshipV1 {
41 format_version: u16,
42 },
43 ProfileV1 {
44 format_version: u16,
45 privacy: crate::ProfilePrivacyPolicyV1,
46 },
47}
48
49impl IndexRecordModel {
50 pub fn validate(&self, mode: crate::IndexMode) -> Result<(), MetricRecordError> {
51 match self {
52 Self::Document => Ok(()),
53 Self::MetricV1 {
54 format_version,
55 grouping_attributes,
56 } => {
57 if *format_version != METRIC_RECORD_MODEL_FORMAT_VERSION {
58 return Err(MetricRecordError::UnsupportedRecordModelVersion(*format_version));
59 }
60 if mode != crate::IndexMode::AppendLog {
61 return Err(MetricRecordError::MetricModelRequiresAppendLog);
62 }
63 validate_grouping_selectors(
64 grouping_attributes,
65 MAX_METRIC_GROUPING_ATTRIBUTES,
66 "grouping attributes",
67 )
68 }
69 Self::RelationshipV1 { format_version } => {
70 crate::validate_relationship_record_model_v1(*format_version, mode)
71 }
72 Self::ProfileV1 {
73 format_version,
74 privacy,
75 } => crate::validate_profile_record_model_v1(*format_version, privacy, mode),
76 }
77 }
78
79 #[must_use]
80 pub fn metric_grouping_attributes(&self) -> Option<&[String]> {
81 match self {
82 Self::Document => None,
83 Self::MetricV1 {
84 grouping_attributes, ..
85 } => Some(grouping_attributes),
86 Self::RelationshipV1 { .. } => None,
87 Self::ProfileV1 { .. } => None,
88 }
89 }
90
91 #[must_use]
92 pub fn semantic_digest_v1(&self) -> [u8; 32] {
93 let mut hasher = blake3::Hasher::new();
94 hasher.update(b"lodestone/index-record-model/v1\0");
95 match self {
96 Self::Document => {
97 hasher.update(b"document\0");
98 }
99 Self::MetricV1 {
100 format_version,
101 grouping_attributes,
102 } => {
103 hasher.update(b"metric_v1\0");
104 hasher.update(&format_version.to_be_bytes());
105 hasher.update(&(grouping_attributes.len() as u64).to_be_bytes());
106 for key in grouping_attributes {
107 hasher.update(&(key.len() as u64).to_be_bytes());
108 hasher.update(key.as_bytes());
109 }
110 }
111 Self::RelationshipV1 { format_version } => {
112 hasher.update(b"relationship_v1\0");
113 hasher.update(&format_version.to_be_bytes());
114 }
115 Self::ProfileV1 {
116 format_version,
117 privacy,
118 } => {
119 hasher.update(b"profile_v1\0");
120 hasher.update(&format_version.to_be_bytes());
121 hasher.update(&privacy.semantic_digest_v1());
122 }
123 }
124 *hasher.finalize().as_bytes()
125 }
126}
127
128pub fn metric_grouping_attribute_v1(
133 point: &MetricPointV1,
134 selector: &str,
135) -> Result<Option<MetricAttributeV1>, MetricRecordError> {
136 let (attributes, key) = if let Some(key) = selector.strip_prefix(RESOURCE_GROUPING_PREFIX) {
137 (&point.resource_attributes, key)
138 } else if let Some(key) = selector.strip_prefix(SCOPE_GROUPING_PREFIX) {
139 (&point.instrumentation_scope.attributes, key)
140 } else if let Some(key) = selector.strip_prefix(SERIES_GROUPING_PREFIX) {
141 (&point.series_attributes, key)
142 } else {
143 return Err(MetricRecordError::InvalidGroupingSelector);
144 };
145 validate_key(key)?;
146 Ok(attributes
147 .binary_search_by(|attribute| attribute.key.as_str().cmp(key))
148 .ok()
149 .and_then(|position| attributes.get(position))
150 .map(|attribute| MetricAttributeV1 {
151 key: selector.to_owned(),
152 value: attribute.value.clone(),
153 }))
154}
155
156#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
157#[serde(deny_unknown_fields)]
158pub struct MetricAttributeV1 {
159 pub key: String,
160 pub value: MetricAttributeValueV1,
161}
162
163#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
164#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
165pub enum MetricAttributeValueV1 {
166 String { value: String },
167 Boolean { value: bool },
168 I64 { value: i64 },
169 U64 { value: u64 },
170 F64Bits { value: u64 },
171}
172
173impl MetricAttributeValueV1 {
174 fn validate(&self) -> Result<(), MetricRecordError> {
175 match self {
176 Self::String { value } if value.len() > MAX_METRIC_ATTRIBUTE_STRING_BYTES => {
177 Err(MetricRecordError::AttributeStringTooLong)
178 }
179 Self::F64Bits { value } if !f64::from_bits(*value).is_finite() => Err(MetricRecordError::NonFiniteNumber),
180 Self::String { .. } | Self::Boolean { .. } | Self::I64 { .. } | Self::U64 { .. } | Self::F64Bits { .. } => {
181 Ok(())
182 }
183 }
184 }
185}
186
187#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
188#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
189pub enum MetricNumberV1 {
190 I64 { value: i64 },
191 F64Bits { value: u64 },
192}
193
194impl MetricNumberV1 {
195 pub fn validate(self) -> Result<(), MetricRecordError> {
196 match self {
197 Self::F64Bits { value } if !f64::from_bits(value).is_finite() => Err(MetricRecordError::NonFiniteNumber),
198 Self::I64 { .. } | Self::F64Bits { .. } => Ok(()),
199 }
200 }
201
202 #[must_use]
203 pub const fn kind(self) -> MetricNumberKindV1 {
204 match self {
205 Self::I64 { .. } => MetricNumberKindV1::I64,
206 Self::F64Bits { .. } => MetricNumberKindV1::F64,
207 }
208 }
209
210 #[must_use]
211 pub const fn as_i64(self) -> Option<i64> {
212 match self {
213 Self::I64 { value } => Some(value),
214 Self::F64Bits { .. } => None,
215 }
216 }
217
218 #[must_use]
219 pub fn as_f64(self) -> f64 {
220 match self {
221 Self::I64 { value } => value as f64,
222 Self::F64Bits { value } => f64::from_bits(value),
223 }
224 }
225}
226
227#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
228pub enum MetricNumberKindV1 {
229 I64,
230 F64,
231}
232
233#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
234#[serde(rename_all = "snake_case")]
235pub enum MetricTemporalityV1 {
236 Cumulative,
237 Delta,
238}
239
240#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
241#[serde(deny_unknown_fields)]
242pub struct MetricExponentialBucketsV1 {
243 pub offset: i32,
244 pub counts: Vec<u64>,
245}
246
247#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
248#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
249pub enum MetricDataV1 {
250 Gauge {
251 value: MetricNumberV1,
252 },
253 Sum {
254 value: MetricNumberV1,
255 temporality: MetricTemporalityV1,
256 monotonic: bool,
257 },
258 ExplicitHistogram {
259 temporality: MetricTemporalityV1,
260 count: u64,
261 sum: Option<MetricNumberV1>,
262 bounds_f64_bits: Vec<u64>,
263 bucket_counts: Vec<u64>,
264 },
265 ExponentialHistogram {
266 temporality: MetricTemporalityV1,
267 count: u64,
268 sum: Option<MetricNumberV1>,
269 scale: i32,
270 zero_count: u64,
271 positive: MetricExponentialBucketsV1,
272 negative: MetricExponentialBucketsV1,
273 },
274}
275
276impl MetricDataV1 {
277 #[must_use]
278 pub const fn kind(&self) -> MetricPointKindV1 {
279 match self {
280 Self::Gauge { .. } => MetricPointKindV1::Gauge,
281 Self::Sum { .. } => MetricPointKindV1::Sum,
282 Self::ExplicitHistogram { .. } => MetricPointKindV1::ExplicitHistogram,
283 Self::ExponentialHistogram { .. } => MetricPointKindV1::ExponentialHistogram,
284 }
285 }
286
287 #[must_use]
288 pub const fn temporality(&self) -> Option<MetricTemporalityV1> {
289 match self {
290 Self::Gauge { .. } => None,
291 Self::Sum { temporality, .. }
292 | Self::ExplicitHistogram { temporality, .. }
293 | Self::ExponentialHistogram { temporality, .. } => Some(*temporality),
294 }
295 }
296
297 #[must_use]
298 pub const fn monotonic(&self) -> Option<bool> {
299 match self {
300 Self::Sum { monotonic, .. } => Some(*monotonic),
301 Self::Gauge { .. } | Self::ExplicitHistogram { .. } | Self::ExponentialHistogram { .. } => None,
302 }
303 }
304
305 fn validate(&self) -> Result<(), MetricRecordError> {
306 match self {
307 Self::Gauge { value } => value.validate(),
308 Self::Sum {
309 value, monotonic: true, ..
310 } => {
311 value.validate()?;
312 if value.as_f64() < 0.0 {
313 return Err(MetricRecordError::NegativeMonotonicValue);
314 }
315 Ok(())
316 }
317 Self::Sum {
318 value,
319 monotonic: false,
320 ..
321 } => value.validate(),
322 Self::ExplicitHistogram {
323 count,
324 sum,
325 bounds_f64_bits,
326 bucket_counts,
327 ..
328 } => {
329 validate_optional_number(*sum)?;
330 if bounds_f64_bits.len() > MAX_METRIC_EXPLICIT_BOUNDS
331 || bucket_counts.len() > MAX_METRIC_EXPLICIT_BOUNDS + 1
332 {
333 return Err(MetricRecordError::HistogramBucketBudgetExceeded);
334 }
335 if bucket_counts.len() != bounds_f64_bits.len() + 1 {
336 return Err(MetricRecordError::InvalidExplicitHistogramLayout);
337 }
338 let mut previous = None;
339 for bits in bounds_f64_bits {
340 let bound = f64::from_bits(*bits);
341 if !bound.is_finite() || previous.is_some_and(|prior: f64| bound <= prior) {
342 return Err(MetricRecordError::InvalidExplicitHistogramBounds);
343 }
344 previous = Some(bound);
345 }
346 let observed = checked_count(bucket_counts.iter().copied())?;
347 if observed != *count {
348 return Err(MetricRecordError::HistogramCountMismatch);
349 }
350 Ok(())
351 }
352 Self::ExponentialHistogram {
353 count,
354 sum,
355 scale,
356 zero_count,
357 positive,
358 negative,
359 ..
360 } => {
361 validate_optional_number(*sum)?;
362 if positive.counts.len() > MAX_METRIC_EXPONENTIAL_BUCKETS
363 || negative.counts.len() > MAX_METRIC_EXPONENTIAL_BUCKETS
364 {
365 return Err(MetricRecordError::HistogramBucketBudgetExceeded);
366 }
367 if !(MIN_METRIC_EXPONENTIAL_SCALE..=MAX_METRIC_EXPONENTIAL_SCALE).contains(scale) {
368 return Err(MetricRecordError::InvalidExponentialHistogramLayout);
369 }
370 let observed = checked_count(
371 positive
372 .counts
373 .iter()
374 .copied()
375 .chain(negative.counts.iter().copied())
376 .chain(std::iter::once(*zero_count)),
377 )?;
378 if observed != *count {
379 return Err(MetricRecordError::HistogramCountMismatch);
380 }
381 Ok(())
382 }
383 }
384 }
385}
386
387fn validate_optional_number(value: Option<MetricNumberV1>) -> Result<(), MetricRecordError> {
388 match value {
389 Some(value) => value.validate(),
390 None => Ok(()),
391 }
392}
393
394fn checked_count(values: impl IntoIterator<Item = u64>) -> Result<u64, MetricRecordError> {
395 values.into_iter().try_fold(0_u64, |total, value| {
396 total.checked_add(value).ok_or(MetricRecordError::CountOverflow)
397 })
398}
399
400#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
401#[serde(rename_all = "snake_case")]
402pub enum MetricPointKindV1 {
403 Gauge,
404 Sum,
405 ExplicitHistogram,
406 ExponentialHistogram,
407}
408
409#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
410#[serde(deny_unknown_fields)]
411pub struct MetricInstrumentationScopeV1 {
412 pub name: String,
413 pub version: Option<String>,
414 pub schema_url: Option<String>,
415 pub attributes: Vec<MetricAttributeV1>,
416}
417
418#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
419#[serde(deny_unknown_fields)]
420pub struct MetricExemplarV1 {
421 pub timestamp_unix_nanos: u64,
422 pub value: MetricNumberV1,
423 pub trace_id: Option<String>,
424 pub span_id: Option<String>,
425 pub filtered_attributes: Vec<MetricAttributeV1>,
426}
427
428#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
429#[serde(deny_unknown_fields)]
430pub struct MetricPointV1 {
431 pub format_version: u16,
432 pub resource_attributes: Vec<MetricAttributeV1>,
433 pub instrumentation_scope: MetricInstrumentationScopeV1,
434 pub metric_name: String,
435 pub description: Option<String>,
436 pub unit: String,
437 pub series_attributes: Vec<MetricAttributeV1>,
438 pub timestamp_unix_nanos: u64,
439 pub start_timestamp_unix_nanos: Option<u64>,
440 pub data: MetricDataV1,
441 pub exemplars: Vec<MetricExemplarV1>,
442}
443
444impl MetricPointV1 {
445 pub fn validate(&self, model: &IndexRecordModel) -> Result<(), MetricRecordError> {
446 if self.format_version != METRIC_POINT_FORMAT_VERSION {
447 return Err(MetricRecordError::UnsupportedPointVersion(self.format_version));
448 }
449 if model.metric_grouping_attributes().is_none() {
450 return Err(MetricRecordError::RecordModelMismatch);
451 }
452 model.validate(crate::IndexMode::AppendLog)?;
453 validate_name(&self.metric_name, "metric name")?;
454 validate_bounded_string(&self.unit)?;
455 validate_optional_string(self.description.as_deref())?;
456 validate_name(&self.instrumentation_scope.name, "instrumentation scope name")?;
457 validate_optional_string(self.instrumentation_scope.version.as_deref())?;
458 validate_optional_string(self.instrumentation_scope.schema_url.as_deref())?;
459 validate_attributes(
460 &self.resource_attributes,
461 MAX_METRIC_RESOURCE_ATTRIBUTES,
462 "resource attributes",
463 )?;
464 validate_attributes(
465 &self.instrumentation_scope.attributes,
466 MAX_METRIC_SCOPE_ATTRIBUTES,
467 "scope attributes",
468 )?;
469 validate_attributes(
470 &self.series_attributes,
471 MAX_METRIC_SERIES_ATTRIBUTES,
472 "series attributes",
473 )?;
474 if self
475 .start_timestamp_unix_nanos
476 .is_some_and(|start| start > self.timestamp_unix_nanos)
477 {
478 return Err(MetricRecordError::StartAfterObservation);
479 }
480 self.data.validate()?;
481 if self.exemplars.len() > MAX_METRIC_EXEMPLARS {
482 return Err(MetricRecordError::TooManyExemplars);
483 }
484 for exemplar in &self.exemplars {
485 exemplar.value.validate()?;
486 validate_hex_id(exemplar.trace_id.as_deref(), 32)?;
487 validate_hex_id(exemplar.span_id.as_deref(), 16)?;
488 validate_attributes(
489 &exemplar.filtered_attributes,
490 MAX_METRIC_EXEMPLAR_ATTRIBUTES,
491 "exemplar attributes",
492 )?;
493 }
494 let attribute_bytes = serde_json::to_vec(&(
495 &self.resource_attributes,
496 &self.instrumentation_scope.attributes,
497 &self.series_attributes,
498 self.exemplars
499 .iter()
500 .map(|exemplar| &exemplar.filtered_attributes)
501 .collect::<Vec<_>>(),
502 ))
503 .map_err(|error| MetricRecordError::Codec(error.to_string()))?
504 .len();
505 if attribute_bytes > MAX_METRIC_ATTRIBUTE_BYTES {
506 return Err(MetricRecordError::AttributeBudgetExceeded(attribute_bytes));
507 }
508 let encoded = serde_json::to_vec(self).map_err(|error| MetricRecordError::Codec(error.to_string()))?;
509 if encoded.len() > MAX_METRIC_POINT_BYTES {
510 return Err(MetricRecordError::PointBudgetExceeded(encoded.len()));
511 }
512 Ok(())
513 }
514
515 pub fn series_id_v1(&self, model: &IndexRecordModel) -> Result<[u8; 32], MetricRecordError> {
516 self.validate(model)?;
517 let identity = MetricSeriesIdentityV1 {
518 format_version: METRIC_POINT_FORMAT_VERSION,
519 resource_attributes: &self.resource_attributes,
520 instrumentation_scope: &self.instrumentation_scope,
521 metric_name: &self.metric_name,
522 unit: &self.unit,
523 point_kind: self.data.kind(),
524 temporality: self.data.temporality(),
525 monotonic: self.data.monotonic(),
526 series_attributes: &self.series_attributes,
527 };
528 let encoded = serde_json::to_vec(&identity).map_err(|error| MetricRecordError::Codec(error.to_string()))?;
529 let mut hasher = blake3::Hasher::new();
530 hasher.update(b"lodestone/metric-series/v1\0");
531 hasher.update(&(encoded.len() as u64).to_be_bytes());
532 hasher.update(&encoded);
533 Ok(*hasher.finalize().as_bytes())
534 }
535}
536
537#[derive(Serialize)]
538struct MetricSeriesIdentityV1<'a> {
539 format_version: u16,
540 resource_attributes: &'a [MetricAttributeV1],
541 instrumentation_scope: &'a MetricInstrumentationScopeV1,
542 metric_name: &'a str,
543 unit: &'a str,
544 point_kind: MetricPointKindV1,
545 temporality: Option<MetricTemporalityV1>,
546 monotonic: Option<bool>,
547 series_attributes: &'a [MetricAttributeV1],
548}
549
550#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
551#[serde(deny_unknown_fields)]
552pub struct CanonicalMetricSourceV1 {
553 pub format_version: u16,
554 pub series_id: String,
555 pub point: MetricPointV1,
556}
557
558impl CanonicalMetricSourceV1 {
559 pub fn new(point: MetricPointV1, model: &IndexRecordModel) -> Result<Self, MetricRecordError> {
560 let series_id = encode_lower_hex(point.series_id_v1(model)?);
561 Ok(Self {
562 format_version: METRIC_SOURCE_FORMAT_VERSION,
563 series_id,
564 point,
565 })
566 }
567
568 pub fn validate(&self, model: &IndexRecordModel) -> Result<[u8; 32], MetricRecordError> {
569 if self.format_version != METRIC_SOURCE_FORMAT_VERSION {
570 return Err(MetricRecordError::UnsupportedSourceVersion(self.format_version));
571 }
572 let series_id = self.point.series_id_v1(model)?;
573 if self.series_id != encode_lower_hex(series_id) {
574 return Err(MetricRecordError::SeriesIdentityMismatch);
575 }
576 Ok(series_id)
577 }
578}
579
580pub fn canonical_metric_source_v1(
581 point: MetricPointV1,
582 model: &IndexRecordModel,
583) -> Result<(Vec<u8>, [u8; 32]), MetricRecordError> {
584 let source = CanonicalMetricSourceV1::new(point, model)?;
585 let series_id = source.validate(model)?;
586 let encoded = serde_json::to_vec(&source).map_err(|error| MetricRecordError::Codec(error.to_string()))?;
587 if encoded.len() > MAX_METRIC_POINT_BYTES {
588 return Err(MetricRecordError::PointBudgetExceeded(encoded.len()));
589 }
590 Ok((encoded, series_id))
591}
592
593pub fn decode_canonical_metric_source_v1(
594 bytes: &[u8],
595 model: &IndexRecordModel,
596) -> Result<(CanonicalMetricSourceV1, [u8; 32]), MetricRecordError> {
597 if bytes.len() > MAX_METRIC_POINT_BYTES {
598 return Err(MetricRecordError::PointBudgetExceeded(bytes.len()));
599 }
600 let source: CanonicalMetricSourceV1 =
601 serde_json::from_slice(bytes).map_err(|error| MetricRecordError::Codec(error.to_string()))?;
602 let series_id = source.validate(model)?;
603 let canonical = serde_json::to_vec(&source).map_err(|error| MetricRecordError::Codec(error.to_string()))?;
604 if canonical != bytes {
605 return Err(MetricRecordError::NonCanonicalSource);
606 }
607 Ok((source, series_id))
608}
609
610#[must_use]
611pub fn encode_lower_hex(bytes: [u8; 32]) -> String {
612 const HEX: &[u8; 16] = b"0123456789abcdef";
613 let mut output = String::with_capacity(64);
614 for byte in bytes {
615 output.push(char::from(HEX[usize::from(byte >> 4)]));
616 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
617 }
618 output
619}
620
621fn validate_attributes(
622 values: &[MetricAttributeV1],
623 maximum: usize,
624 section: &'static str,
625) -> Result<(), MetricRecordError> {
626 if values.len() > maximum {
627 return Err(MetricRecordError::TooManyAttributes(section));
628 }
629 let mut previous = None;
630 for attribute in values {
631 validate_key(&attribute.key)?;
632 if previous.is_some_and(|value: &str| value >= attribute.key.as_str()) {
633 return Err(MetricRecordError::AttributesNotSorted(section));
634 }
635 attribute.value.validate()?;
636 previous = Some(attribute.key.as_str());
637 }
638 Ok(())
639}
640
641fn validate_grouping_selectors(
642 values: &[String],
643 maximum: usize,
644 section: &'static str,
645) -> Result<(), MetricRecordError> {
646 if values.len() > maximum {
647 return Err(MetricRecordError::TooManyAttributes(section));
648 }
649 let mut previous = None;
650 for selector in values {
651 if previous.is_some_and(|prior: &str| prior >= selector.as_str()) {
652 return Err(MetricRecordError::AttributesNotSorted(section));
653 }
654 let key = selector
655 .strip_prefix(RESOURCE_GROUPING_PREFIX)
656 .or_else(|| selector.strip_prefix(SCOPE_GROUPING_PREFIX))
657 .or_else(|| selector.strip_prefix(SERIES_GROUPING_PREFIX))
658 .ok_or(MetricRecordError::InvalidGroupingSelector)?;
659 validate_key(key)?;
660 previous = Some(selector.as_str());
661 }
662 Ok(())
663}
664
665fn validate_key(value: &str) -> Result<(), MetricRecordError> {
666 if value.is_empty()
667 || value.len() > MAX_METRIC_ATTRIBUTE_KEY_BYTES
668 || value.bytes().any(|byte| byte <= 0x1f || byte == 0x7f)
669 {
670 return Err(MetricRecordError::InvalidAttributeKey);
671 }
672 Ok(())
673}
674
675fn validate_name(value: &str, section: &'static str) -> Result<(), MetricRecordError> {
676 if value.is_empty() {
677 return Err(MetricRecordError::MissingName(section));
678 }
679 validate_bounded_string(value)
680}
681
682fn validate_optional_string(value: Option<&str>) -> Result<(), MetricRecordError> {
683 match value {
684 Some(value) => validate_bounded_string(value),
685 None => Ok(()),
686 }
687}
688
689fn validate_bounded_string(value: &str) -> Result<(), MetricRecordError> {
690 if value.len() > MAX_METRIC_ATTRIBUTE_STRING_BYTES || value.bytes().any(|byte| byte == 0) {
691 return Err(MetricRecordError::AttributeStringTooLong);
692 }
693 Ok(())
694}
695
696fn validate_hex_id(value: Option<&str>, length: usize) -> Result<(), MetricRecordError> {
697 if value.is_some_and(|value| {
698 value.len() != length
699 || value
700 .bytes()
701 .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
702 }) {
703 return Err(MetricRecordError::InvalidCorrelationIdentity);
704 }
705 Ok(())
706}
707
708#[derive(Clone, Debug, Eq, PartialEq)]
709pub enum MetricRecordError {
710 UnsupportedRecordModelVersion(u16),
711 UnsupportedPointVersion(u16),
712 UnsupportedSourceVersion(u16),
713 MetricModelRequiresAppendLog,
714 RelationshipModelRequiresMutableOrAppendLog,
715 ProfileModelRequiresAppendLog,
716 InvalidProfilePrivacyPolicy,
717 RecordModelMismatch,
718 InvalidGroupingSelector,
719 TooManyAttributes(&'static str),
720 AttributesNotSorted(&'static str),
721 InvalidAttributeKey,
722 AttributeStringTooLong,
723 AttributeBudgetExceeded(usize),
724 MissingName(&'static str),
725 StartAfterObservation,
726 NonFiniteNumber,
727 NegativeMonotonicValue,
728 TooManyExemplars,
729 HistogramBucketBudgetExceeded,
730 InvalidCorrelationIdentity,
731 InvalidExplicitHistogramLayout,
732 InvalidExplicitHistogramBounds,
733 InvalidExponentialHistogramLayout,
734 HistogramCountMismatch,
735 CountOverflow,
736 PointBudgetExceeded(usize),
737 SeriesIdentityMismatch,
738 NonCanonicalSource,
739 Codec(String),
740}
741
742impl Display for MetricRecordError {
743 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
744 write!(formatter, "metric record: {self:?}")
745 }
746}
747
748impl Error for MetricRecordError {}
749
750#[cfg(test)]
751mod tests {
752 use std::error::Error;
753
754 use super::*;
755
756 fn model() -> IndexRecordModel {
757 IndexRecordModel::MetricV1 {
758 format_version: METRIC_RECORD_MODEL_FORMAT_VERSION,
759 grouping_attributes: vec!["series.host".to_owned()],
760 }
761 }
762
763 fn point() -> MetricPointV1 {
764 MetricPointV1 {
765 format_version: METRIC_POINT_FORMAT_VERSION,
766 resource_attributes: vec![MetricAttributeV1 {
767 key: "service.name".to_owned(),
768 value: MetricAttributeValueV1::String {
769 value: "checkout".to_owned(),
770 },
771 }],
772 instrumentation_scope: MetricInstrumentationScopeV1 {
773 name: "http".to_owned(),
774 version: Some("1".to_owned()),
775 schema_url: None,
776 attributes: Vec::new(),
777 },
778 metric_name: "requests".to_owned(),
779 description: Some("request count".to_owned()),
780 unit: "{request}".to_owned(),
781 series_attributes: vec![MetricAttributeV1 {
782 key: "host".to_owned(),
783 value: MetricAttributeValueV1::String { value: "a".to_owned() },
784 }],
785 timestamp_unix_nanos: 20,
786 start_timestamp_unix_nanos: Some(10),
787 data: MetricDataV1::Sum {
788 value: MetricNumberV1::I64 { value: 4 },
789 temporality: MetricTemporalityV1::Cumulative,
790 monotonic: true,
791 },
792 exemplars: Vec::new(),
793 }
794 }
795
796 #[test]
797 fn metric_source_is_canonical_and_series_identity_ignores_description() -> Result<(), Box<dyn Error>> {
798 let model = model();
799 let (encoded, series_id) = canonical_metric_source_v1(point(), &model)?;
800 let (decoded, decoded_id) = decode_canonical_metric_source_v1(&encoded, &model)?;
801 assert_eq!(decoded_id, series_id);
802 let mut changed = decoded.point;
803 changed.description = Some("changed".to_owned());
804 assert_eq!(changed.series_id_v1(&model)?, series_id);
805 changed.unit = "ms".to_owned();
806 assert_ne!(changed.series_id_v1(&model)?, series_id);
807 Ok(())
808 }
809
810 #[test]
811 fn metric_validation_rejects_unsorted_attributes_and_bad_histograms() {
812 let model = model();
813 let mut value = point();
814 value.series_attributes = vec![
815 MetricAttributeV1 {
816 key: "z".to_owned(),
817 value: MetricAttributeValueV1::Boolean { value: true },
818 },
819 MetricAttributeV1 {
820 key: "a".to_owned(),
821 value: MetricAttributeValueV1::Boolean { value: true },
822 },
823 ];
824 assert_eq!(
825 value.validate(&model),
826 Err(MetricRecordError::AttributesNotSorted("series attributes"))
827 );
828
829 let mut value = point();
830 value.data = MetricDataV1::ExplicitHistogram {
831 temporality: MetricTemporalityV1::Delta,
832 count: 2,
833 sum: None,
834 bounds_f64_bits: vec![1.0_f64.to_bits()],
835 bucket_counts: vec![1],
836 };
837 assert_eq!(
838 value.validate(&model),
839 Err(MetricRecordError::InvalidExplicitHistogramLayout)
840 );
841 }
842
843 #[test]
844 fn record_model_is_mode_bound_immutable_and_digest_sensitive() -> Result<(), Box<dyn Error>> {
845 let first = model();
846 assert_eq!(first.validate(crate::IndexMode::AppendLog), Ok(()));
847 assert_eq!(
848 first.validate(crate::IndexMode::Mutable),
849 Err(MetricRecordError::MetricModelRequiresAppendLog)
850 );
851 let second = IndexRecordModel::MetricV1 {
852 format_version: METRIC_RECORD_MODEL_FORMAT_VERSION,
853 grouping_attributes: vec!["series.zone".to_owned()],
854 };
855 assert_ne!(first.semantic_digest_v1(), second.semantic_digest_v1());
856 assert_ne!(
857 first.semantic_digest_v1(),
858 IndexRecordModel::Document.semantic_digest_v1()
859 );
860 let invalid = IndexRecordModel::MetricV1 {
861 format_version: METRIC_RECORD_MODEL_FORMAT_VERSION,
862 grouping_attributes: vec!["host".to_owned()],
863 };
864 assert_eq!(
865 invalid.validate(crate::IndexMode::AppendLog),
866 Err(MetricRecordError::InvalidGroupingSelector)
867 );
868 let newer = IndexRecordModel::MetricV1 {
869 format_version: METRIC_RECORD_MODEL_FORMAT_VERSION + 1,
870 grouping_attributes: vec!["series.host".to_owned()],
871 };
872 assert_eq!(
873 point().validate(&newer),
874 Err(MetricRecordError::UnsupportedRecordModelVersion(
875 METRIC_RECORD_MODEL_FORMAT_VERSION + 1
876 ))
877 );
878 assert_eq!(
879 metric_grouping_attribute_v1(&point(), "resource.service.name")?,
880 Some(MetricAttributeV1 {
881 key: "resource.service.name".to_owned(),
882 value: MetricAttributeValueV1::String {
883 value: "checkout".to_owned(),
884 },
885 })
886 );
887 Ok(())
888 }
889
890 #[test]
891 fn malformed_numbers_counts_correlation_and_noncanonical_sources_refuse() -> Result<(), Box<dyn Error>> {
892 let model = model();
893 let mut value = point();
894 value.data = MetricDataV1::Gauge {
895 value: MetricNumberV1::F64Bits {
896 value: f64::NAN.to_bits(),
897 },
898 };
899 assert_eq!(value.validate(&model), Err(MetricRecordError::NonFiniteNumber));
900
901 let mut value = point();
902 value.data = MetricDataV1::Sum {
903 value: MetricNumberV1::I64 { value: -1 },
904 temporality: MetricTemporalityV1::Delta,
905 monotonic: true,
906 };
907 assert_eq!(value.validate(&model), Err(MetricRecordError::NegativeMonotonicValue));
908
909 let mut value = point();
910 value.data = MetricDataV1::ExponentialHistogram {
911 temporality: MetricTemporalityV1::Delta,
912 count: 2,
913 sum: None,
914 scale: 0,
915 zero_count: 0,
916 positive: MetricExponentialBucketsV1 {
917 offset: 0,
918 counts: vec![1],
919 },
920 negative: MetricExponentialBucketsV1 {
921 offset: 0,
922 counts: Vec::new(),
923 },
924 };
925 assert_eq!(value.validate(&model), Err(MetricRecordError::HistogramCountMismatch));
926
927 let (canonical, _) = canonical_metric_source_v1(point(), &model)?;
928 let mut padded = canonical.clone();
929 padded.push(b' ');
930 assert_eq!(
931 decode_canonical_metric_source_v1(&padded, &model),
932 Err(MetricRecordError::NonCanonicalSource)
933 );
934 Ok(())
935 }
936}