1use std::{borrow::Cow, net::SocketAddr, time::Instant};
2
3use junction_api::{backend::BackendId, http::Route, Name};
4use smol_str::{SmolStr, ToSmolStr};
5
6#[derive(Clone, Debug)]
7pub(crate) struct Trace {
8 start: Instant,
9 phase: TracePhase,
10 events: Vec<TraceEvent>,
11}
12
13#[derive(Clone, Debug)]
14pub(crate) struct TraceEvent {
15 pub(crate) phase: TracePhase,
16 pub(crate) kind: TraceEventKind,
17 pub(crate) at: Instant,
18 pub(crate) kv: Vec<TraceData>,
19}
20
21type TraceData = (&'static str, SmolStr);
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub(crate) enum TracePhase {
25 RouteResolution,
26 EndpointSelection(u8),
27}
28
29#[derive(Clone, Copy, Debug)]
30pub(crate) enum TraceEventKind {
31 RouteLookup,
32 RouteRuleMatched,
33 BackendSelected,
34 BackendLookup,
35 EndpointsLookup,
36 SelectAddr,
37}
38
39impl Trace {
40 pub(crate) fn new() -> Self {
41 Trace {
42 start: Instant::now(),
43 phase: TracePhase::RouteResolution,
44 events: Vec::new(),
45 }
46 }
47
48 pub(crate) fn events(&self) -> impl Iterator<Item = &TraceEvent> {
49 self.events.iter()
50 }
51
52 pub(crate) fn start(&self) -> Instant {
53 self.start
54 }
55
56 pub(crate) fn lookup_route(&mut self, route: &Route) {
57 debug_assert!(matches!(self.phase, TracePhase::RouteResolution));
58
59 self.events.push(TraceEvent {
60 kind: TraceEventKind::RouteLookup,
61 phase: TracePhase::RouteResolution,
62 at: Instant::now(),
63 kv: vec![("route", route.id.to_smolstr())],
64 })
65 }
66
67 pub(crate) fn matched_rule(&mut self, rule: usize, rule_name: Option<&Name>) {
68 debug_assert!(matches!(self.phase, TracePhase::RouteResolution));
69
70 let kv = match rule_name {
71 Some(name) => vec![("rule-name", name.to_smolstr())],
72 None => vec![("rule-idx", rule.to_smolstr())],
73 };
74
75 self.events.push(TraceEvent {
76 kind: TraceEventKind::RouteRuleMatched,
77 phase: TracePhase::RouteResolution,
78 at: Instant::now(),
79 kv,
80 })
81 }
82
83 pub(crate) fn select_backend(&mut self, backend: &BackendId) {
84 debug_assert!(matches!(self.phase, TracePhase::RouteResolution));
85
86 self.events.push(TraceEvent {
87 phase: self.phase,
88 kind: TraceEventKind::BackendSelected,
89 at: Instant::now(),
90 kv: vec![("name", backend.to_smolstr())],
91 });
92 }
93
94 pub(crate) fn start_endpoint_selection(&mut self) {
95 let next_phase = match self.phase {
96 TracePhase::RouteResolution => TracePhase::EndpointSelection(0),
97 TracePhase::EndpointSelection(n) => TracePhase::EndpointSelection(n + 1),
98 };
99 self.phase = next_phase;
100 }
101
102 pub(crate) fn lookup_backend(&mut self, backend: &BackendId) {
103 debug_assert!(matches!(self.phase, TracePhase::EndpointSelection(_)));
104
105 self.events.push(TraceEvent {
106 kind: TraceEventKind::BackendLookup,
107 phase: self.phase,
108 at: Instant::now(),
109 kv: vec![("backend-id", backend.to_smolstr())],
110 })
111 }
112
113 pub(crate) fn lookup_endpoints(&mut self, backend: &BackendId) {
114 debug_assert!(matches!(self.phase, TracePhase::EndpointSelection(_)));
115
116 self.events.push(TraceEvent {
117 kind: TraceEventKind::EndpointsLookup,
118 phase: self.phase,
119 at: Instant::now(),
120 kv: vec![("backend-id", backend.to_smolstr())],
121 })
122 }
123
124 pub(crate) fn load_balance(
125 &mut self,
126 lb_name: &'static str,
127 addr: Option<&SocketAddr>,
128 extra: Vec<TraceData>,
129 ) {
130 debug_assert!(matches!(self.phase, TracePhase::EndpointSelection(_)));
131
132 let mut kv = Vec::with_capacity(extra.len() + 2);
133 kv.push(("type", lb_name.to_smolstr()));
134 kv.push((
135 "addr",
136 addr.map(|a| a.to_smolstr())
137 .unwrap_or_else(|| "-".to_smolstr()),
138 ));
139 kv.extend(extra);
140
141 self.events.push(TraceEvent {
142 kind: TraceEventKind::SelectAddr,
143 phase: self.phase,
144 at: Instant::now(),
145 kv,
146 });
147 }
148}
149
150pub type Result<T> = std::result::Result<T, Error>;
152
153#[derive(Debug, thiserror::Error)]
155#[error("{inner}")]
156pub struct Error {
157 trace: Option<Trace>,
159
160 inner: Box<ErrorImpl>,
167}
168
169impl Error {
170 pub fn is_temporary(&self) -> bool {
175 matches!(*self.inner, ErrorImpl::NoReachableEndpoints { .. })
176 }
177}
178
179impl Error {
180 pub(crate) fn timed_out(message: &'static str, trace: Trace) -> Self {
183 let inner = ErrorImpl::TimedOut(Cow::from(message));
184 Self {
185 trace: Some(trace),
186 inner: Box::new(inner),
187 }
188 }
189
190 pub(crate) fn into_invalid_url(message: String) -> Self {
195 let inner = ErrorImpl::InvalidUrl(Cow::Owned(message));
196 Self {
197 trace: None,
198 inner: Box::new(inner),
199 }
200 }
201
202 pub(crate) fn invalid_url(message: &'static str) -> Self {
203 let inner = ErrorImpl::InvalidUrl(Cow::Borrowed(message));
204 Self {
205 trace: None,
206 inner: Box::new(inner),
207 }
208 }
209
210 pub(crate) fn no_route_matched(authority: String, trace: Trace) -> Self {
213 Self {
214 trace: Some(trace),
215 inner: Box::new(ErrorImpl::NoRouteMatched { authority }),
216 }
217 }
218
219 pub(crate) fn no_rule_matched(route: Name, trace: Trace) -> Self {
220 Self {
221 trace: Some(trace),
222 inner: Box::new(ErrorImpl::NoRuleMatched { route }),
223 }
224 }
225
226 pub(crate) fn invalid_route(
227 message: &'static str,
228 id: Name,
229 rule: usize,
230 trace: Trace,
231 ) -> Self {
232 Self {
233 trace: Some(trace),
234 inner: Box::new(ErrorImpl::InvalidRoute { id, message, rule }),
235 }
236 }
237
238 pub(crate) fn no_backend(backend: BackendId, trace: Trace) -> Self {
241 Self {
242 trace: Some(trace),
243 inner: Box::new(ErrorImpl::NoBackend { backend }),
244 }
245 }
246
247 pub(crate) fn no_reachable_endpoints(backend: BackendId, trace: Trace) -> Self {
248 Self {
249 trace: Some(trace),
250 inner: Box::new(ErrorImpl::NoReachableEndpoints { backend }),
251 }
252 }
253}
254
255#[derive(Debug, thiserror::Error)]
256enum ErrorImpl {
257 #[error("timed out: {0}")]
258 TimedOut(Cow<'static, str>),
259
260 #[error("invalid url: {0}")]
261 InvalidUrl(Cow<'static, str>),
262
263 #[error("invalid route configuration")]
264 InvalidRoute {
265 message: &'static str,
266 id: Name,
267 rule: usize,
268 },
269
270 #[error("no route matched: '{authority}'")]
271 NoRouteMatched { authority: String },
272
273 #[error("{route}: no rules matched the request")]
274 NoRuleMatched { route: Name },
275
276 #[error("{backend}: backend not found")]
277 NoBackend { backend: BackendId },
278
279 #[error("{backend}: no reachable endpoints")]
280 NoReachableEndpoints { backend: BackendId },
281}