1use crate::request_ids::add_duplicate_completed_request_id_warning;
2use crate::{
3 collector::generate_run_id, unix_time_ms, BuildError, CaptureLimits, CaptureMode,
4 EffectiveCoreConfig, EffectiveTokioSamplerConfig, InFlightSnapshot, QueueEvent, RequestEvent,
5 Run, RunEndReason, RunMetadata, RuntimeSnapshot, StageEvent, UnfinishedRequests,
6};
7use core::fmt;
8
9#[derive(Debug, Clone)]
24pub struct RunBuilderOptions {
25 service_name: String,
26 service_version: Option<String>,
27 run_id: Option<String>,
28 mode: Option<CaptureMode>,
29 capture_limits: Option<CaptureLimits>,
30 strict_lifecycle: bool,
31 started_at_unix_ms: Option<u64>,
32 finished_at_unix_ms: Option<u64>,
33 finalized_at_unix_ms: Option<u64>,
34 host: Option<String>,
35 pid: Option<u32>,
36 run_end_reason: Option<RunEndReason>,
37}
38
39impl RunBuilderOptions {
40 #[must_use]
42 pub fn new(service_name: impl Into<String>) -> Self {
43 Self {
44 service_name: service_name.into(),
45 service_version: None,
46 run_id: None,
47 mode: None,
48 capture_limits: None,
49 strict_lifecycle: false,
50 started_at_unix_ms: None,
51 finished_at_unix_ms: None,
52 finalized_at_unix_ms: None,
53 host: None,
54 pid: None,
55 run_end_reason: None,
56 }
57 }
58
59 #[must_use]
61 pub fn service_version(mut self, service_version: impl Into<String>) -> Self {
62 self.service_version = Some(service_version.into());
63 self
64 }
65 #[must_use]
67 pub fn run_id(mut self, run_id: impl Into<String>) -> Self {
68 self.run_id = Some(run_id.into());
69 self
70 }
71 #[must_use]
73 pub fn mode(mut self, mode: CaptureMode) -> Self {
74 self.mode = Some(mode);
75 self
76 }
77 #[must_use]
79 pub fn capture_limits(mut self, capture_limits: CaptureLimits) -> Self {
80 self.capture_limits = Some(capture_limits);
81 self
82 }
83 #[must_use]
85 pub const fn strict_lifecycle(mut self, strict_lifecycle: bool) -> Self {
86 self.strict_lifecycle = strict_lifecycle;
87 self
88 }
89 #[must_use]
91 pub const fn started_at_unix_ms(mut self, started_at_unix_ms: u64) -> Self {
92 self.started_at_unix_ms = Some(started_at_unix_ms);
93 self
94 }
95 #[must_use]
97 pub const fn finished_at_unix_ms(mut self, finished_at_unix_ms: u64) -> Self {
98 self.finished_at_unix_ms = Some(finished_at_unix_ms);
99 self
100 }
101 #[must_use]
103 pub const fn finalized_at_unix_ms(mut self, finalized_at_unix_ms: u64) -> Self {
104 self.finalized_at_unix_ms = Some(finalized_at_unix_ms);
105 self
106 }
107 #[must_use]
109 pub fn host(mut self, host: impl Into<String>) -> Self {
110 self.host = Some(host.into());
111 self
112 }
113 #[must_use]
115 pub const fn pid(mut self, pid: u32) -> Self {
116 self.pid = Some(pid);
117 self
118 }
119 #[must_use]
121 pub const fn run_end_reason(mut self, run_end_reason: RunEndReason) -> Self {
122 self.run_end_reason = Some(run_end_reason);
123 self
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum RunBuilderEventError {
130 InvalidEvent {
132 event: &'static str,
134 field: &'static str,
136 reason: String,
138 },
139}
140
141impl fmt::Display for RunBuilderEventError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 Self::InvalidEvent {
145 event,
146 field,
147 reason,
148 } => {
149 write!(f, "invalid {event}.{field}: {reason}")
150 }
151 }
152 }
153}
154
155impl std::error::Error for RunBuilderEventError {}
156
157#[derive(Debug)]
171pub struct RunBuilder {
172 run: Run,
173 capture_limits: CaptureLimits,
174}
175
176impl RunBuilder {
177 pub fn new(options: RunBuilderOptions) -> Result<Self, BuildError> {
189 if options.service_name.trim().is_empty() {
190 return Err(BuildError::EmptyServiceName);
191 }
192
193 let mode = options.mode.unwrap_or(CaptureMode::Light);
194 let capture_limits = options
195 .capture_limits
196 .unwrap_or_else(|| mode.core_defaults());
197 let ts = unix_time_ms();
198 let started_at_unix_ms = options.started_at_unix_ms.unwrap_or(ts);
199 let finished_at_unix_ms = options.finished_at_unix_ms.unwrap_or(ts);
200 let finalized_at_unix_ms_value =
201 options.finalized_at_unix_ms.unwrap_or(finished_at_unix_ms);
202 let finalized_at_unix_ms = Some(finalized_at_unix_ms_value);
203
204 if finished_at_unix_ms < started_at_unix_ms {
205 return Err(BuildError::InvalidRunTimeBounds {
206 started_at_unix_ms,
207 finished_at_unix_ms,
208 });
209 }
210
211 if finalized_at_unix_ms_value < finished_at_unix_ms {
212 return Err(BuildError::InvalidFinalizationTime {
213 finished_at_unix_ms,
214 finalized_at_unix_ms: finalized_at_unix_ms_value,
215 });
216 }
217
218 Ok(Self {
219 run: Run::new(RunMetadata {
220 run_id: options.run_id.unwrap_or_else(generate_run_id),
221 service_name: options.service_name,
222 service_version: options.service_version,
223 started_at_unix_ms,
224 finished_at_unix_ms,
225 finalized_at_unix_ms,
226 mode,
227 effective_core_config: Some(EffectiveCoreConfig {
228 mode,
229 capture_limits,
230 strict_lifecycle: options.strict_lifecycle,
231 }),
232 effective_tokio_sampler_config: None,
233 host: options.host,
234 pid: options.pid,
235 lifecycle_warnings: Vec::new(),
236 unfinished_requests: UnfinishedRequests::default(),
237 run_end_reason: options.run_end_reason,
238 }),
239 capture_limits,
240 })
241 }
242
243 pub fn push_request(&mut self, event: RequestEvent) -> Result<(), RunBuilderEventError> {
252 validate_request_event(&event)?;
253 let _ = crate::retention::push_request_bounded(&mut self.run, self.capture_limits, event);
254 Ok(())
255 }
256 pub fn push_stage(&mut self, event: StageEvent) -> Result<(), RunBuilderEventError> {
265 validate_stage_event(&event)?;
266 let _ = crate::retention::push_stage_bounded(&mut self.run, self.capture_limits, event);
267 Ok(())
268 }
269 pub fn push_queue(&mut self, event: QueueEvent) -> Result<(), RunBuilderEventError> {
278 validate_queue_event(&event)?;
279 let _ = crate::retention::push_queue_bounded(&mut self.run, self.capture_limits, event);
280 Ok(())
281 }
282 pub fn push_inflight_snapshot(
292 &mut self,
293 snapshot: InFlightSnapshot,
294 ) -> Result<(), RunBuilderEventError> {
295 validate_inflight_snapshot(&snapshot)?;
296 let _ = crate::retention::push_inflight_snapshot_bounded(
297 &mut self.run,
298 self.capture_limits,
299 snapshot,
300 );
301 Ok(())
302 }
303 pub fn push_runtime_snapshot(
316 &mut self,
317 snapshot: RuntimeSnapshot,
318 ) -> Result<(), RunBuilderEventError> {
319 let _ = crate::retention::push_runtime_snapshot_bounded(
320 &mut self.run,
321 self.capture_limits,
322 snapshot,
323 );
324 Ok(())
325 }
326 pub fn add_lifecycle_warning(&mut self, warning: impl Into<String>) {
328 self.run.metadata.lifecycle_warnings.push(warning.into());
329 }
330 pub fn set_unfinished_requests(&mut self, unfinished: UnfinishedRequests) {
332 self.run.metadata.unfinished_requests = unfinished;
333 }
334 pub fn set_effective_tokio_sampler_config(&mut self, config: EffectiveTokioSamplerConfig) {
336 self.run.metadata.effective_tokio_sampler_config = Some(config);
337 }
338 pub fn set_run_end_reason_if_absent(&mut self, reason: RunEndReason) {
340 if self.run.metadata.run_end_reason.is_none() {
341 self.run.metadata.run_end_reason = Some(reason);
342 }
343 }
344 #[must_use]
349 pub fn finish(mut self) -> Run {
350 add_duplicate_completed_request_id_warning(&mut self.run);
351 self.run
352 }
353}
354
355fn invalid_event(
356 event: &'static str,
357 field: &'static str,
358 reason: impl Into<String>,
359) -> RunBuilderEventError {
360 RunBuilderEventError::InvalidEvent {
361 event,
362 field,
363 reason: reason.into(),
364 }
365}
366
367fn validate_request_event(event: &RequestEvent) -> Result<(), RunBuilderEventError> {
368 if event.request_id.trim().is_empty() {
369 return Err(invalid_event(
370 "RequestEvent",
371 "request_id",
372 "must not be empty",
373 ));
374 }
375 if event.route.trim().is_empty() {
376 return Err(invalid_event("RequestEvent", "route", "must not be empty"));
377 }
378 if event.finished_at_unix_ms < event.started_at_unix_ms {
379 return Err(invalid_event(
380 "RequestEvent",
381 "finished_at_unix_ms",
382 "must be >= started_at_unix_ms",
383 ));
384 }
385 if let (Some(started_at_run_us), Some(finished_at_run_us)) =
386 (event.started_at_run_us, event.finished_at_run_us)
387 {
388 if finished_at_run_us < started_at_run_us {
389 return Err(invalid_event(
390 "RequestEvent",
391 "finished_at_run_us",
392 "must be >= started_at_run_us",
393 ));
394 }
395 }
396 if event.outcome.trim().is_empty() {
397 return Err(invalid_event(
398 "RequestEvent",
399 "outcome",
400 "must not be empty",
401 ));
402 }
403 Ok(())
404}
405fn validate_stage_event(event: &StageEvent) -> Result<(), RunBuilderEventError> {
406 if event.request_id.trim().is_empty() {
407 return Err(invalid_event(
408 "StageEvent",
409 "request_id",
410 "must not be empty",
411 ));
412 }
413 if event.stage.trim().is_empty() {
414 return Err(invalid_event("StageEvent", "stage", "must not be empty"));
415 }
416 if event.finished_at_unix_ms < event.started_at_unix_ms {
417 return Err(invalid_event(
418 "StageEvent",
419 "finished_at_unix_ms",
420 "must be >= started_at_unix_ms",
421 ));
422 }
423 if let (Some(started_at_run_us), Some(finished_at_run_us)) =
424 (event.started_at_run_us, event.finished_at_run_us)
425 {
426 if finished_at_run_us < started_at_run_us {
427 return Err(invalid_event(
428 "StageEvent",
429 "finished_at_run_us",
430 "must be >= started_at_run_us",
431 ));
432 }
433 }
434 Ok(())
435}
436fn validate_queue_event(event: &QueueEvent) -> Result<(), RunBuilderEventError> {
437 if event.request_id.trim().is_empty() {
438 return Err(invalid_event(
439 "QueueEvent",
440 "request_id",
441 "must not be empty",
442 ));
443 }
444 if event.queue.trim().is_empty() {
445 return Err(invalid_event("QueueEvent", "queue", "must not be empty"));
446 }
447 if event.waited_until_unix_ms < event.waited_from_unix_ms {
448 return Err(invalid_event(
449 "QueueEvent",
450 "waited_until_unix_ms",
451 "must be >= waited_from_unix_ms",
452 ));
453 }
454 if let (Some(waited_from_run_us), Some(waited_until_run_us)) =
455 (event.waited_from_run_us, event.waited_until_run_us)
456 {
457 if waited_until_run_us < waited_from_run_us {
458 return Err(invalid_event(
459 "QueueEvent",
460 "waited_until_run_us",
461 "must be >= waited_from_run_us",
462 ));
463 }
464 }
465 Ok(())
466}
467fn validate_inflight_snapshot(snapshot: &InFlightSnapshot) -> Result<(), RunBuilderEventError> {
468 if snapshot.gauge.trim().is_empty() {
469 return Err(invalid_event(
470 "InFlightSnapshot",
471 "gauge",
472 "must not be empty",
473 ));
474 }
475 Ok(())
476}