Skip to main content

obs_types/
field_kind.rs

1//! [`FieldKind`] — role of a field on a wide event.
2
3use std::str::FromStr;
4
5use buffa::Enumeration;
6use serde::{Deserialize, Serialize};
7
8use crate::UnknownVariant;
9
10/// Role of a field on a wide event. Drives codegen, OTel mapping, and lints.
11///
12/// See [10-data-model.md § 4](../../specs/10-data-model.md#4-field-roles).
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15#[repr(i32)]
16#[non_exhaustive]
17pub enum FieldKind {
18    /// Never appears in a well-formed schema.
19    #[default]
20    Unspecified = 0,
21    /// Bounded dimension; safe as metric/span attribute.
22    Label = 1,
23    /// Free-form; never a metric dimension; in log/span body.
24    Attribute = 2,
25    /// Numeric; emitted as a metric data point.
26    Measurement = 3,
27    /// Lifted to envelope `trace_id`.
28    TraceId = 4,
29    /// Lifted to envelope `span_id`.
30    SpanId = 5,
31    /// Lifted to envelope `parent_span_id`.
32    ParentSpanId = 6,
33    /// Overrides envelope `ts_ns`.
34    TimestampNs = 7,
35    /// Drives span start/end derivation.
36    DurationNs = 8,
37    /// Opaque blob; never indexed; size-capped.
38    Forensic = 9,
39}
40
41impl FieldKind {
42    /// Stable string label.
43    #[must_use]
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::Unspecified => "unspecified",
47            Self::Label => "label",
48            Self::Attribute => "attribute",
49            Self::Measurement => "measurement",
50            Self::TraceId => "trace_id",
51            Self::SpanId => "span_id",
52            Self::ParentSpanId => "parent_span_id",
53            Self::TimestampNs => "timestamp_ns",
54            Self::DurationNs => "duration_ns",
55            Self::Forensic => "forensic",
56        }
57    }
58
59    /// True if a value of this kind is lifted from the typed payload to a
60    /// dedicated envelope slot (per [10-data-model.md § 6](
61    /// ../../specs/10-data-model.md#6-envelope)).
62    #[must_use]
63    pub const fn is_envelope_lifted(self) -> bool {
64        matches!(
65            self,
66            Self::TraceId | Self::SpanId | Self::ParentSpanId | Self::TimestampNs
67        )
68    }
69}
70
71impl Enumeration for FieldKind {
72    fn from_i32(value: i32) -> Option<Self> {
73        match value {
74            0 => Some(Self::Unspecified),
75            1 => Some(Self::Label),
76            2 => Some(Self::Attribute),
77            3 => Some(Self::Measurement),
78            4 => Some(Self::TraceId),
79            5 => Some(Self::SpanId),
80            6 => Some(Self::ParentSpanId),
81            7 => Some(Self::TimestampNs),
82            8 => Some(Self::DurationNs),
83            9 => Some(Self::Forensic),
84            _ => None,
85        }
86    }
87
88    fn to_i32(&self) -> i32 {
89        *self as i32
90    }
91
92    fn proto_name(&self) -> &'static str {
93        match self {
94            Self::Unspecified => "FIELD_KIND_UNSPECIFIED",
95            Self::Label => "LABEL",
96            Self::Attribute => "ATTRIBUTE",
97            Self::Measurement => "MEASUREMENT",
98            Self::TraceId => "TRACE_ID",
99            Self::SpanId => "SPAN_ID",
100            Self::ParentSpanId => "PARENT_SPAN_ID",
101            Self::TimestampNs => "TIMESTAMP_NS",
102            Self::DurationNs => "DURATION_NS",
103            Self::Forensic => "FORENSIC",
104        }
105    }
106
107    fn from_proto_name(name: &str) -> Option<Self> {
108        match name {
109            "FIELD_KIND_UNSPECIFIED" => Some(Self::Unspecified),
110            "LABEL" => Some(Self::Label),
111            "ATTRIBUTE" => Some(Self::Attribute),
112            "MEASUREMENT" => Some(Self::Measurement),
113            "TRACE_ID" => Some(Self::TraceId),
114            "SPAN_ID" => Some(Self::SpanId),
115            "PARENT_SPAN_ID" => Some(Self::ParentSpanId),
116            "TIMESTAMP_NS" => Some(Self::TimestampNs),
117            "DURATION_NS" => Some(Self::DurationNs),
118            "FORENSIC" => Some(Self::Forensic),
119            _ => None,
120        }
121    }
122
123    fn values() -> &'static [Self] {
124        &[
125            Self::Unspecified,
126            Self::Label,
127            Self::Attribute,
128            Self::Measurement,
129            Self::TraceId,
130            Self::SpanId,
131            Self::ParentSpanId,
132            Self::TimestampNs,
133            Self::DurationNs,
134            Self::Forensic,
135        ]
136    }
137}
138
139impl FromStr for FieldKind {
140    type Err = UnknownVariant;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        match s.to_ascii_lowercase().as_str() {
144            "label" => Ok(Self::Label),
145            "attribute" => Ok(Self::Attribute),
146            "measurement" => Ok(Self::Measurement),
147            "trace_id" => Ok(Self::TraceId),
148            "span_id" => Ok(Self::SpanId),
149            "parent_span_id" => Ok(Self::ParentSpanId),
150            "timestamp_ns" => Ok(Self::TimestampNs),
151            "duration_ns" => Ok(Self::DurationNs),
152            "forensic" => Ok(Self::Forensic),
153            _ => Err(UnknownVariant {
154                kind: "FieldKind",
155                value: s.to_string(),
156            }),
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_should_round_trip_via_i32() {
167        for v in FieldKind::values() {
168            assert_eq!(FieldKind::from_i32(v.to_i32()), Some(*v));
169        }
170    }
171
172    #[test]
173    fn test_should_identify_envelope_lifted() {
174        assert!(FieldKind::TraceId.is_envelope_lifted());
175        assert!(FieldKind::SpanId.is_envelope_lifted());
176        assert!(!FieldKind::Label.is_envelope_lifted());
177        assert!(!FieldKind::Measurement.is_envelope_lifted());
178    }
179}