use std::path::{Path, PathBuf};
use sipx_call::SignallingCounts;
use crate::Args;
use crate::output::{Format, Report};
const BESIDE_CAPTURE: &str = ".counters.json";
pub(crate) fn destination(args: &Args<'_>) -> Option<PathBuf> {
if let Some(path) = args.value("counters") {
return Some(PathBuf::from(path));
}
args.value("capture")
.map(|capture| PathBuf::from(format!("{capture}{BESIDE_CAPTURE}")))
}
pub(crate) fn report(counts: &SignallingCounts) -> Report {
let transport = &counts.transport;
let mut report = Report::new()
.boolean("any_loss", counts.any_loss())
.number("messages_in", cast(transport.messages_in()))
.number("messages_out", cast(transport.messages_out()))
.number("parse_failures", cast(transport.parse_failures()))
.number("shed_requests", cast(transport.shed.requests))
.number("shed_acks", cast(transport.shed.acks))
.number("shed_unmatched", cast(transport.shed.unmatched))
.number("unmatched_responses", cast(transport.unmatched_responses))
.number("retransmissions_sent", cast(transport.retransmissions_sent))
.number("timeout_b", cast(transport.timeouts.b))
.number("timeout_f", cast(transport.timeouts.f))
.number("timeout_h", cast(transport.timeouts.h))
.number(
"discard_transaction_events",
cast(transport.discards.transaction_events),
)
.number("discard_unanswered", cast(transport.discards.unanswered))
.number(
"discard_no_destination",
cast(transport.discards.no_destination),
)
.number(
"discard_send_failures",
cast(transport.discards.send_failures),
)
.number(
"discard_stun_unmatched",
cast(transport.discards.stun_unmatched),
)
.number("unsent_invite", cast(transport.unsent.invite))
.number("unsent_ack", cast(transport.unsent.ack))
.number("unsent_bye", cast(transport.unsent.bye))
.number("unsent_cancel", cast(transport.unsent.cancel))
.number("unsent_other", cast(transport.unsent.other))
.number("capture_records", cast(transport.capture.records))
.number("capture_dropped", cast(transport.capture.dropped))
.number("capture_errors", cast(transport.capture.errors))
.boolean("dispatch_measured", counts.dispatch.is_some());
if let Some(dispatch) = counts.dispatch {
report = report
.number("dispatch_shed", cast(dispatch.shed))
.number("dispatch_acks", cast(dispatch.acks))
.number("dispatch_unmatched", cast(dispatch.unmatched))
.number("dispatch_unsupported", cast(dispatch.unsupported))
.number("dispatch_malformed", cast(dispatch.malformed))
.number("dispatch_merged", cast(dispatch.merged));
}
report
}
pub(crate) struct Export {
destination: Option<PathBuf>,
endpoint: sipx_transport::Handle,
written: bool,
}
impl Export {
pub(crate) fn arm(args: &Args<'_>, endpoint: &sipx_transport::Handle) -> Self {
Self {
destination: destination(args),
endpoint: endpoint.clone(),
written: false,
}
}
pub(crate) fn into_report(mut self, report: Report) -> Result<Report, String> {
let Some(path) = self.destination.clone() else {
return Ok(report);
};
write(&path, &SignallingCounts::of(&self.endpoint))?;
self.written = true;
Ok(report.text("counters", path.display().to_string()))
}
}
impl Drop for Export {
fn drop(&mut self) {
if self.written {
return;
}
let Some(path) = &self.destination else {
return;
};
match write(path, &SignallingCounts::of(&self.endpoint)) {
Ok(()) => tracing::info!(counters = %path.display(), "wrote the signalling counters"),
Err(message) => tracing::error!("{message}"),
}
}
}
fn cast(value: u64) -> i64 {
i64::try_from(value).unwrap_or(i64::MAX)
}
pub(crate) fn write(path: &Path, counts: &SignallingCounts) -> Result<(), String> {
let body = format!("{}\n", report(counts).render(Format::Json));
std::fs::write(path, body).map_err(|error| format!("counters {}: {error}", path.display()))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn args(items: &[&str]) -> Vec<String> {
items.iter().map(|item| (*item).to_owned()).collect()
}
#[test]
fn a_capture_implies_a_counters_file_beside_it() {
let raw = args(&["dial", "--capture", "/tmp/run/signalling.pcapng", "sip:a@b"]);
let parsed = Args::new(&raw).expect("well formed");
assert_eq!(
destination(&parsed),
Some(PathBuf::from("/tmp/run/signalling.pcapng.counters.json")),
"an operator assembling a bug report should not have to ask twice"
);
}
#[test]
fn counters_alone_needs_no_capture() {
let raw = args(&["dial", "--counters", "/tmp/run/counts.json", "sip:a@b"]);
let parsed = Args::new(&raw).expect("well formed");
assert_eq!(
destination(&parsed),
Some(PathBuf::from("/tmp/run/counts.json"))
);
}
#[test]
fn an_explicit_path_beats_the_capture_sibling() {
let raw = args(&[
"dial",
"--capture",
"/tmp/run/signalling.pcapng",
"--counters",
"/tmp/elsewhere/counts.json",
"sip:a@b",
]);
let parsed = Args::new(&raw).expect("well formed");
assert_eq!(
destination(&parsed),
Some(PathBuf::from("/tmp/elsewhere/counts.json"))
);
}
#[test]
fn neither_flag_writes_nothing() {
let raw = args(&["dial", "sip:a@b"]);
let parsed = Args::new(&raw).expect("well formed");
assert_eq!(destination(&parsed), None);
}
}