obs_core/envelope/projection.rs
1//! `EventSchema` — the trait codegen targets when emitting per-event
2//! impls. Generic and `Sized`, so it carries associated `const`s and
3//! `encode_payload` / `project` methods that do not require object
4//! safety. The object-safe complement is
5//! [`crate::registry::EventSchemaErased`].
6
7use bytes::BytesMut;
8use obs_proto::obs::v1::ObsEnvelope;
9use obs_types::{Cardinality, Classification, FieldKind, Severity, Tier};
10
11/// Compile-time field metadata emitted by codegen alongside each
12/// `EventSchema` impl. Mirrors the proto-side `obs.v1.FieldMeta`.
13#[derive(Debug, Clone, Copy)]
14pub struct FieldMeta {
15 /// Proto field name (`route`, `latency_ms`, …).
16 pub name: &'static str,
17 /// Proto field number.
18 pub number: u32,
19 /// Codegen-classified role.
20 pub role: FieldRole,
21 /// `LABEL` cardinality cap; `Unspecified` for non-labels.
22 pub cardinality: Cardinality,
23 /// Classification (drives redaction / SECRET strip).
24 pub classification: Classification,
25}
26
27impl FieldMeta {
28 /// Construct a [`FieldMeta`]. Used by codegen and tests.
29 #[must_use]
30 pub const fn new(
31 name: &'static str,
32 number: u32,
33 role: FieldRole,
34 cardinality: Cardinality,
35 classification: Classification,
36 ) -> Self {
37 Self {
38 name,
39 number,
40 role,
41 cardinality,
42 classification,
43 }
44 }
45}
46
47/// Codegen-derived classification of a field. Decoupled from the
48/// `FieldKind` enum so codegen can stamp this from the descriptor
49/// without re-parsing.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum FieldRole {
53 /// `kind: LABEL`.
54 Label,
55 /// `kind: ATTRIBUTE`.
56 Attribute,
57 /// `kind: MEASUREMENT`.
58 Measurement,
59 /// `kind: TRACE_ID`.
60 TraceId,
61 /// `kind: SPAN_ID`.
62 SpanId,
63 /// `kind: PARENT_SPAN_ID`.
64 ParentSpanId,
65 /// `kind: TIMESTAMP_NS`.
66 TimestampNs,
67 /// `kind: DURATION_NS`.
68 DurationNs,
69 /// `kind: FORENSIC`.
70 Forensic,
71}
72
73impl From<FieldKind> for FieldRole {
74 fn from(k: FieldKind) -> Self {
75 match k {
76 FieldKind::Label => Self::Label,
77 FieldKind::Attribute => Self::Attribute,
78 FieldKind::Measurement => Self::Measurement,
79 FieldKind::TraceId => Self::TraceId,
80 FieldKind::SpanId => Self::SpanId,
81 FieldKind::ParentSpanId => Self::ParentSpanId,
82 FieldKind::TimestampNs => Self::TimestampNs,
83 FieldKind::DurationNs => Self::DurationNs,
84 FieldKind::Forensic => Self::Forensic,
85 // FieldKind is #[non_exhaustive]; defensively map any
86 // unrecognised future variant + Unspecified to Attribute.
87 _ => Self::Attribute,
88 }
89 }
90}
91
92/// Trait implemented by every `Obs*` event type.
93///
94/// Codegen emits one impl per `.proto` message or `#[derive(Event)]`
95/// struct. Sinks consume the object-safe sibling
96/// [`crate::registry::EventSchemaErased`]; this trait is for the
97/// monomorphised emit path.
98pub trait EventSchema: Send + Sync + Sized + 'static {
99 /// Fully qualified event name (`myapp.v1.ObsXxx`).
100 const FULL_NAME: &'static str;
101 /// Tier the schema declares.
102 const TIER: Tier;
103 /// Default severity the schema declares.
104 const DEFAULT_SEV: Severity;
105 /// Per-field metadata table.
106 const FIELDS: &'static [FieldMeta];
107 /// First 8 bytes of BLAKE3 over the canonical descriptor; baked at
108 /// build time. See spec 10 § 6 + spec 12 § 3.5.
109 const SCHEMA_HASH: u64;
110
111 /// Encode this event's payload using buffa's encoder into a reused
112 /// buffer. The codegen impl forwards to `buffa::Message::encode`.
113 fn encode_payload(&self, buf: &mut BytesMut);
114
115 /// Project labels and lift trace/span ids onto the envelope.
116 /// Generated; never hand-written. See spec 11 pipeline § 4.1.
117 fn project(&self, env: &mut ObsEnvelope);
118
119 /// For `MEASUREMENT`-annotated fields, emit metric data points.
120 /// Phase-1 default impl is a no-op so MEASUREMENT-bearing schemas
121 /// authored in Phase 1 do not error in metric sinks; Phase 2
122 /// codegen overrides this. Spec 12 § 3.2.
123 fn project_metrics(&self, sink: &mut dyn crate::metric::MetricEmitter) {
124 let _ = sink;
125 }
126
127 /// When this event participates in a Started/Completed pair, the
128 /// schema's full name of its sibling event. The OTLP trace sink
129 /// matches paired events on this constant rather than suffix
130 /// sniffing. Schemas that are not paired return `None`.
131 /// Spec 20 § 2.5 B / spec 93 P1-7.
132 const SPANS_PAIRED_WITH: Option<&'static str> = None;
133}