osproxy_observe/trace.rs
1//! The per-request causal trace, **shape-only by construction**.
2//!
3//! [`RequestTrace`] accumulates what happened to one request as it crosses each
4//! stage. Its setters accept *only* identifier newtypes, compile-time `&'static
5//! str` shape labels, and numeric sizes/counts, never a `String`/`&str` taken
6//! from request data and never a JSON value. There is therefore **no API path**
7//! by which a document field value, query literal, or secret can enter a trace
8//! (`docs/05` §7); the guarantee is structural, not redaction after the fact.
9
10use osproxy_core::{
11 ClusterId, EndpointKind, Epoch, ErrorContext, FieldName, IndexName, PartitionId, TraceContext,
12};
13
14/// The `ingress` span: how the connection was framed (`docs/05` §2).
15#[derive(Clone, PartialEq, Eq, Debug)]
16pub struct IngressInfo {
17 /// Wire protocol label, e.g. `"h1"`.
18 pub protocol: &'static str,
19 /// Negotiated TLS suite label, if the connection was TLS.
20 pub tls_suite: Option<&'static str>,
21 /// Whether the TLS session was resumed.
22 pub tls_reused: Option<bool>,
23}
24
25/// The `classify` span: how the request path was categorized.
26#[derive(Clone, PartialEq, Eq, Debug)]
27pub struct ClassifyInfo {
28 /// The endpoint classification.
29 pub endpoint: EndpointKind,
30 /// The logical index from the path (a name, never a value).
31 pub logical_index: IndexName,
32}
33
34/// The `spi.resolve` span: the routing decision and its inputs.
35#[derive(Clone, PartialEq, Eq, Debug)]
36pub struct ResolveInfo {
37 /// The resolved partition (an id).
38 pub partition: PartitionId,
39 /// The placement mode label, e.g. `"shared_index"`.
40 pub placement_kind: &'static str,
41 /// The target cluster.
42 pub cluster: ClusterId,
43 /// The target index.
44 pub index: IndexName,
45 /// The placement epoch the decision was derived from.
46 pub epoch: Epoch,
47 /// The names of fields injected (names only, never values).
48 pub inject_fields: Vec<FieldName>,
49 /// Whether `_routing` was set.
50 pub routing: bool,
51 /// The partition's migration phase at resolve time, e.g. `"settled"` /
52 /// `"draining"` / `"cutover"`, so an operator sees where a migration is
53 /// without reading values (`docs/06` §5).
54 pub migration: &'static str,
55}
56
57/// The `rewrite` span: what the body transform did (in shapes).
58#[derive(Clone, PartialEq, Eq, Debug)]
59pub struct RewriteInfo {
60 /// The transform kind label, e.g. `"inject+construct_id"`.
61 pub transform_kind: &'static str,
62 /// The transformed body size in bytes (a size, never the bytes).
63 pub body_bytes: usize,
64}
65
66/// The `dispatch` span: the upstream call outcome.
67#[derive(Clone, PartialEq, Eq, Debug)]
68pub struct DispatchInfo {
69 /// The cluster the request was sent to.
70 pub cluster: ClusterId,
71 /// The upstream HTTP status.
72 pub upstream_status: u16,
73 /// Whether a pooled connection was reused.
74 pub pool_reuse: bool,
75}
76
77/// The `egress` span: what was returned to the client.
78#[derive(Clone, PartialEq, Eq, Debug)]
79pub struct EgressInfo {
80 /// The status returned to the client.
81 pub status: u16,
82 /// The response size in bytes.
83 pub response_bytes: usize,
84}
85
86/// The accumulated causal trace for one request, filled stage by stage.
87///
88/// Constructed with the [`RequestId`](osproxy_core::RequestId) and populated via
89/// the `record_*` setters; assembled into a `/debug/explain` document by
90/// [`crate::explain_json`].
91#[derive(Clone, PartialEq, Eq, Debug, Default)]
92pub struct RequestTrace {
93 /// The distributed-trace identity (W3C) this request continues or minted,
94 /// the trace/span ids that correlate `/debug/explain` and the emitted OTLP
95 /// span with the wider trace.
96 pub(crate) context: Option<TraceContext>,
97 pub(crate) ingress: Option<IngressInfo>,
98 pub(crate) classify: Option<ClassifyInfo>,
99 pub(crate) resolve: Option<ResolveInfo>,
100 pub(crate) rewrite: Option<RewriteInfo>,
101 pub(crate) dispatch: Option<DispatchInfo>,
102 pub(crate) egress: Option<EgressInfo>,
103 pub(crate) error: Option<ErrorContext>,
104}
105
106impl RequestTrace {
107 /// A new, empty trace.
108 #[must_use]
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 /// Records the request's W3C trace context (trace/span ids).
114 pub fn record_context(&mut self, context: TraceContext) {
115 self.context = Some(context);
116 }
117
118 /// The request's trace context, once recorded.
119 #[must_use]
120 pub fn context(&self) -> Option<&TraceContext> {
121 self.context.as_ref()
122 }
123
124 /// The partition the request resolved to, once routing has run, so a
125 /// tenant-targeted diagnostics directive can be evaluated against it.
126 #[must_use]
127 pub fn resolved_partition(&self) -> Option<&PartitionId> {
128 self.resolve.as_ref().map(|r| &r.partition)
129 }
130
131 /// Records the `ingress` span.
132 pub fn record_ingress(&mut self, info: IngressInfo) {
133 self.ingress = Some(info);
134 }
135
136 /// Records the `classify` span.
137 pub fn record_classify(&mut self, info: ClassifyInfo) {
138 self.classify = Some(info);
139 }
140
141 /// Records the `spi.resolve` span.
142 pub fn record_resolve(&mut self, info: ResolveInfo) {
143 self.resolve = Some(info);
144 }
145
146 /// Records the `rewrite` span.
147 pub fn record_rewrite(&mut self, info: RewriteInfo) {
148 self.rewrite = Some(info);
149 }
150
151 /// Records the `dispatch` span.
152 pub fn record_dispatch(&mut self, info: DispatchInfo) {
153 self.dispatch = Some(info);
154 }
155
156 /// Records the `egress` span.
157 pub fn record_egress(&mut self, info: EgressInfo) {
158 self.egress = Some(info);
159 }
160
161 /// Attaches the error context to the failing span.
162 pub fn record_error(&mut self, error: ErrorContext) {
163 self.error = Some(error);
164 }
165
166 /// Whether the request failed (carries an error context).
167 #[must_use]
168 pub fn failed(&self) -> bool {
169 self.error.is_some()
170 }
171}