use crate::request_ids::add_duplicate_completed_request_id_warning;
use crate::{
collector::generate_run_id, unix_time_ms, BuildError, CaptureLimits, CaptureMode,
EffectiveCoreConfig, EffectiveTokioSamplerConfig, InFlightSnapshot, QueueEvent, RequestEvent,
Run, RunEndReason, RunMetadata, RuntimeSnapshot, StageEvent, UnfinishedRequests,
};
use core::fmt;
#[derive(Debug, Clone)]
pub struct RunBuilderOptions {
service_name: String,
service_version: Option<String>,
run_id: Option<String>,
mode: Option<CaptureMode>,
capture_limits: Option<CaptureLimits>,
strict_lifecycle: bool,
started_at_unix_ms: Option<u64>,
finished_at_unix_ms: Option<u64>,
finalized_at_unix_ms: Option<u64>,
host: Option<String>,
pid: Option<u32>,
run_end_reason: Option<RunEndReason>,
}
impl RunBuilderOptions {
#[must_use]
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
service_version: None,
run_id: None,
mode: None,
capture_limits: None,
strict_lifecycle: false,
started_at_unix_ms: None,
finished_at_unix_ms: None,
finalized_at_unix_ms: None,
host: None,
pid: None,
run_end_reason: None,
}
}
#[must_use]
pub fn service_version(mut self, service_version: impl Into<String>) -> Self {
self.service_version = Some(service_version.into());
self
}
#[must_use]
pub fn run_id(mut self, run_id: impl Into<String>) -> Self {
self.run_id = Some(run_id.into());
self
}
#[must_use]
pub fn mode(mut self, mode: CaptureMode) -> Self {
self.mode = Some(mode);
self
}
#[must_use]
pub fn capture_limits(mut self, capture_limits: CaptureLimits) -> Self {
self.capture_limits = Some(capture_limits);
self
}
#[must_use]
pub const fn strict_lifecycle(mut self, strict_lifecycle: bool) -> Self {
self.strict_lifecycle = strict_lifecycle;
self
}
#[must_use]
pub const fn started_at_unix_ms(mut self, started_at_unix_ms: u64) -> Self {
self.started_at_unix_ms = Some(started_at_unix_ms);
self
}
#[must_use]
pub const fn finished_at_unix_ms(mut self, finished_at_unix_ms: u64) -> Self {
self.finished_at_unix_ms = Some(finished_at_unix_ms);
self
}
#[must_use]
pub const fn finalized_at_unix_ms(mut self, finalized_at_unix_ms: u64) -> Self {
self.finalized_at_unix_ms = Some(finalized_at_unix_ms);
self
}
#[must_use]
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
#[must_use]
pub const fn pid(mut self, pid: u32) -> Self {
self.pid = Some(pid);
self
}
#[must_use]
pub const fn run_end_reason(mut self, run_end_reason: RunEndReason) -> Self {
self.run_end_reason = Some(run_end_reason);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunBuilderEventError {
InvalidEvent {
event: &'static str,
field: &'static str,
reason: String,
},
}
impl fmt::Display for RunBuilderEventError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEvent {
event,
field,
reason,
} => {
write!(f, "invalid {event}.{field}: {reason}")
}
}
}
}
impl std::error::Error for RunBuilderEventError {}
#[derive(Debug)]
pub struct RunBuilder {
run: Run,
capture_limits: CaptureLimits,
}
impl RunBuilder {
pub fn new(options: RunBuilderOptions) -> Result<Self, BuildError> {
if options.service_name.trim().is_empty() {
return Err(BuildError::EmptyServiceName);
}
let mode = options.mode.unwrap_or(CaptureMode::Light);
let capture_limits = options
.capture_limits
.unwrap_or_else(|| mode.core_defaults());
let ts = unix_time_ms();
let started_at_unix_ms = options.started_at_unix_ms.unwrap_or(ts);
let finished_at_unix_ms = options.finished_at_unix_ms.unwrap_or(ts);
let finalized_at_unix_ms_value =
options.finalized_at_unix_ms.unwrap_or(finished_at_unix_ms);
let finalized_at_unix_ms = Some(finalized_at_unix_ms_value);
if finished_at_unix_ms < started_at_unix_ms {
return Err(BuildError::InvalidRunTimeBounds {
started_at_unix_ms,
finished_at_unix_ms,
});
}
if finalized_at_unix_ms_value < finished_at_unix_ms {
return Err(BuildError::InvalidFinalizationTime {
finished_at_unix_ms,
finalized_at_unix_ms: finalized_at_unix_ms_value,
});
}
Ok(Self {
run: Run::new(RunMetadata {
run_id: options.run_id.unwrap_or_else(generate_run_id),
service_name: options.service_name,
service_version: options.service_version,
started_at_unix_ms,
finished_at_unix_ms,
finalized_at_unix_ms,
mode,
effective_core_config: Some(EffectiveCoreConfig {
mode,
capture_limits,
strict_lifecycle: options.strict_lifecycle,
}),
effective_tokio_sampler_config: None,
host: options.host,
pid: options.pid,
lifecycle_warnings: Vec::new(),
unfinished_requests: UnfinishedRequests::default(),
run_end_reason: options.run_end_reason,
}),
capture_limits,
})
}
pub fn push_request(&mut self, event: RequestEvent) -> Result<(), RunBuilderEventError> {
validate_request_event(&event)?;
let _ = crate::retention::push_request_bounded(&mut self.run, self.capture_limits, event);
Ok(())
}
pub fn push_stage(&mut self, event: StageEvent) -> Result<(), RunBuilderEventError> {
validate_stage_event(&event)?;
let _ = crate::retention::push_stage_bounded(&mut self.run, self.capture_limits, event);
Ok(())
}
pub fn push_queue(&mut self, event: QueueEvent) -> Result<(), RunBuilderEventError> {
validate_queue_event(&event)?;
let _ = crate::retention::push_queue_bounded(&mut self.run, self.capture_limits, event);
Ok(())
}
pub fn push_inflight_snapshot(
&mut self,
snapshot: InFlightSnapshot,
) -> Result<(), RunBuilderEventError> {
validate_inflight_snapshot(&snapshot)?;
let _ = crate::retention::push_inflight_snapshot_bounded(
&mut self.run,
self.capture_limits,
snapshot,
);
Ok(())
}
pub fn push_runtime_snapshot(
&mut self,
snapshot: RuntimeSnapshot,
) -> Result<(), RunBuilderEventError> {
let _ = crate::retention::push_runtime_snapshot_bounded(
&mut self.run,
self.capture_limits,
snapshot,
);
Ok(())
}
pub fn add_lifecycle_warning(&mut self, warning: impl Into<String>) {
self.run.metadata.lifecycle_warnings.push(warning.into());
}
pub fn set_unfinished_requests(&mut self, unfinished: UnfinishedRequests) {
self.run.metadata.unfinished_requests = unfinished;
}
pub fn set_effective_tokio_sampler_config(&mut self, config: EffectiveTokioSamplerConfig) {
self.run.metadata.effective_tokio_sampler_config = Some(config);
}
pub fn set_run_end_reason_if_absent(&mut self, reason: RunEndReason) {
if self.run.metadata.run_end_reason.is_none() {
self.run.metadata.run_end_reason = Some(reason);
}
}
#[must_use]
pub fn finish(mut self) -> Run {
add_duplicate_completed_request_id_warning(&mut self.run);
self.run
}
}
fn invalid_event(
event: &'static str,
field: &'static str,
reason: impl Into<String>,
) -> RunBuilderEventError {
RunBuilderEventError::InvalidEvent {
event,
field,
reason: reason.into(),
}
}
fn validate_request_event(event: &RequestEvent) -> Result<(), RunBuilderEventError> {
if event.request_id.trim().is_empty() {
return Err(invalid_event(
"RequestEvent",
"request_id",
"must not be empty",
));
}
if event.route.trim().is_empty() {
return Err(invalid_event("RequestEvent", "route", "must not be empty"));
}
if event.finished_at_unix_ms < event.started_at_unix_ms {
return Err(invalid_event(
"RequestEvent",
"finished_at_unix_ms",
"must be >= started_at_unix_ms",
));
}
if let (Some(started_at_run_us), Some(finished_at_run_us)) =
(event.started_at_run_us, event.finished_at_run_us)
{
if finished_at_run_us < started_at_run_us {
return Err(invalid_event(
"RequestEvent",
"finished_at_run_us",
"must be >= started_at_run_us",
));
}
}
if event.outcome.trim().is_empty() {
return Err(invalid_event(
"RequestEvent",
"outcome",
"must not be empty",
));
}
Ok(())
}
fn validate_stage_event(event: &StageEvent) -> Result<(), RunBuilderEventError> {
if event.request_id.trim().is_empty() {
return Err(invalid_event(
"StageEvent",
"request_id",
"must not be empty",
));
}
if event.stage.trim().is_empty() {
return Err(invalid_event("StageEvent", "stage", "must not be empty"));
}
if event.finished_at_unix_ms < event.started_at_unix_ms {
return Err(invalid_event(
"StageEvent",
"finished_at_unix_ms",
"must be >= started_at_unix_ms",
));
}
if let (Some(started_at_run_us), Some(finished_at_run_us)) =
(event.started_at_run_us, event.finished_at_run_us)
{
if finished_at_run_us < started_at_run_us {
return Err(invalid_event(
"StageEvent",
"finished_at_run_us",
"must be >= started_at_run_us",
));
}
}
Ok(())
}
fn validate_queue_event(event: &QueueEvent) -> Result<(), RunBuilderEventError> {
if event.request_id.trim().is_empty() {
return Err(invalid_event(
"QueueEvent",
"request_id",
"must not be empty",
));
}
if event.queue.trim().is_empty() {
return Err(invalid_event("QueueEvent", "queue", "must not be empty"));
}
if event.waited_until_unix_ms < event.waited_from_unix_ms {
return Err(invalid_event(
"QueueEvent",
"waited_until_unix_ms",
"must be >= waited_from_unix_ms",
));
}
if let (Some(waited_from_run_us), Some(waited_until_run_us)) =
(event.waited_from_run_us, event.waited_until_run_us)
{
if waited_until_run_us < waited_from_run_us {
return Err(invalid_event(
"QueueEvent",
"waited_until_run_us",
"must be >= waited_from_run_us",
));
}
}
Ok(())
}
fn validate_inflight_snapshot(snapshot: &InFlightSnapshot) -> Result<(), RunBuilderEventError> {
if snapshot.gauge.trim().is_empty() {
return Err(invalid_event(
"InFlightSnapshot",
"gauge",
"must not be empty",
));
}
Ok(())
}