rs_zero/observability/
correlation.rs1use http::HeaderMap;
2
3use crate::observability::{
4 current_span_id, current_trace_id, request_id_from_headers, span_id_from_traceparent,
5 trace_id_from_traceparent, traceparent_from_headers,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct CorrelationContext {
11 service: String,
12 transport: &'static str,
13 route: String,
14 method: String,
15 request_id: Option<String>,
16 traceparent: Option<String>,
17 trace_id: Option<String>,
18 span_id: Option<String>,
19 status: Option<String>,
20}
21
22impl CorrelationContext {
23 pub fn from_http_headers(
28 service: Option<&str>,
29 method: impl Into<String>,
30 route: Option<&str>,
31 headers: &HeaderMap,
32 ) -> Self {
33 let traceparent = traceparent_from_headers(headers);
34 Self::new(
35 service.unwrap_or("unknown"),
36 "http",
37 route.unwrap_or("unknown"),
38 method,
39 )
40 .with_request_id(request_id_from_headers(headers))
41 .with_traceparent(traceparent)
42 .with_current_span_context()
43 }
44
45 #[cfg(feature = "rpc")]
47 pub fn from_rpc_metadata(
48 service: impl Into<String>,
49 method: impl Into<String>,
50 metadata: &tonic::metadata::MetadataMap,
51 ) -> Self {
52 let traceparent = crate::observability::traceparent_from_metadata(metadata);
53 Self::new(service, "grpc", "rpc", method)
54 .with_request_id(crate::observability::request_id_from_metadata(metadata))
55 .with_traceparent(traceparent)
56 .with_current_span_context()
57 }
58
59 pub fn from_rpc_parts(
61 service: impl Into<String>,
62 method: impl Into<String>,
63 request_id: Option<&str>,
64 traceparent: Option<&str>,
65 ) -> Self {
66 Self::new(service, "grpc", "rpc", method)
67 .with_request_id(request_id.map(ToOwned::to_owned))
68 .with_traceparent(traceparent.map(ToOwned::to_owned))
69 .with_current_span_context()
70 }
71
72 pub fn new(
74 service: impl Into<String>,
75 transport: &'static str,
76 route: impl Into<String>,
77 method: impl Into<String>,
78 ) -> Self {
79 Self {
80 service: service.into(),
81 transport,
82 route: route.into(),
83 method: method.into(),
84 request_id: None,
85 traceparent: None,
86 trace_id: None,
87 span_id: None,
88 status: None,
89 }
90 }
91
92 pub fn with_status(mut self, status: impl Into<String>) -> Self {
94 self.status = Some(status.into());
95 self
96 }
97
98 pub fn route(&self) -> &str {
100 &self.route
101 }
102
103 pub fn traceparent(&self) -> Option<&str> {
105 self.traceparent.as_deref()
106 }
107
108 pub fn request_id(&self) -> Option<&str> {
110 self.request_id.as_deref()
111 }
112
113 pub fn trace_id(&self) -> Option<&str> {
115 self.trace_id.as_deref()
116 }
117
118 pub fn span_id(&self) -> Option<&str> {
120 self.span_id.as_deref()
121 }
122
123 #[cfg(feature = "core")]
125 pub fn into_log_fields(self) -> crate::core::logging::LogFields {
126 use crate::core::logging::LogFields;
127
128 let mut fields = LogFields::new(self.service)
129 .with_transport(self.transport)
130 .with_route(self.route)
131 .with_method(self.method);
132 if let Some(request_id) = self.request_id {
133 fields = fields.with_request_id(request_id);
134 }
135 if let Some(trace_id) = self.trace_id {
136 fields = fields.with_trace_id(trace_id);
137 }
138 if let Some(span_id) = self.span_id {
139 fields = fields.with_span_id(span_id);
140 }
141 if let Some(status) = self.status {
142 fields = fields.with_status(status);
143 }
144 fields
145 }
146
147 pub fn as_pairs(&self) -> Vec<(String, String)> {
149 let mut pairs = vec![("service".to_string(), self.service.clone())];
150 push_optional(&mut pairs, "transport", Some(self.transport));
151 push_optional(&mut pairs, "route", Some(&self.route));
152 push_optional(&mut pairs, "method", Some(&self.method));
153 push_optional(&mut pairs, "request_id", self.request_id.as_deref());
154 push_optional(&mut pairs, "trace_id", self.trace_id.as_deref());
155 push_optional(&mut pairs, "span_id", self.span_id.as_deref());
156 push_optional(&mut pairs, "status", self.status.as_deref());
157 pairs
158 }
159
160 fn with_request_id(mut self, request_id: Option<String>) -> Self {
161 self.request_id = request_id;
162 self
163 }
164
165 fn with_traceparent(mut self, traceparent: Option<String>) -> Self {
166 if let Some(value) = traceparent {
167 self.trace_id = trace_id_from_traceparent(&value).map(ToOwned::to_owned);
168 self.span_id = span_id_from_traceparent(&value).map(ToOwned::to_owned);
169 self.traceparent = Some(value);
170 }
171 self
172 }
173
174 fn with_current_span_context(mut self) -> Self {
175 if let Some(trace_id) = current_trace_id() {
176 self.trace_id = Some(trace_id);
177 }
178 if let Some(span_id) = current_span_id() {
179 self.span_id = Some(span_id);
180 }
181 self
182 }
183}
184
185fn push_optional(pairs: &mut Vec<(String, String)>, key: &str, value: Option<&str>) {
186 if let Some(value) = value
187 && !value.is_empty()
188 {
189 pairs.push((key.to_string(), value.to_string()));
190 }
191}