Skip to main content

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}