use crate::service_index_found::WaitNotes;
use crate::journald_native::{Delivery, MESSAGE_LIMIT, Priority, Sink, StreamWriter, cut, one_line};
use crate::lfs_output_mask::mask_userinfo_text;
use crate::quiet_ledger::longest_healthy;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
pub const LOG_PREFIX: &str = "cli-git-service: ";
#[derive(Clone, Copy, Debug)]
pub struct Arrival {
pub accepted: Instant,
pub read: Duration,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LandTiming {
pub lock_wait: Duration,
pub lock_holder: Option<String>,
pub lock_notes: WaitNotes,
pub objects: Duration,
pub journal: Duration,
pub ref_update: Duration,
pub index_update: Duration,
pub hold: Duration,
pub attempts: usize,
}
impl LandTiming {
pub fn reads(&self, landing: Duration) -> Duration {
return landing.saturating_sub(self.lock_wait + self.objects + self.journal + self.ref_update + self.index_update);
}
}
pub struct Answered<'record> {
pub request: &'record str,
pub reference: &'record str,
pub outcome: &'record str,
pub batch: u64,
pub size: usize,
pub position: usize,
pub arrival: Arrival,
pub queued: Duration,
pub settle: Duration,
pub landing: &'record LandTiming,
pub land_total: Duration,
pub answer: Duration,
pub total: Duration,
pub quiet: &'record [(String, Duration)],
}
fn quiet_steps(figures: &[(String, Duration)]) -> String {
let words: Vec<String> = figures.iter().map(|(step, gap)| { return format!("{step}={:.3}", gap.as_micros() as f64 / 1000.0); }).collect();
return words.join(";");
}
fn holder_field(who: &String) -> String {
return format!(" lock_holder={who:?}");
}
pub fn lock_notes_fields(notes: &WaitNotes) -> String {
if notes.looks == 0 {
return format!(" lock_stage_ms={} lock_stage_kind={} lock_place_ms={}", notes.staging.as_millis(), notes.stage_kind, notes.placing.as_millis());
}
let first: &str = notes.found.as_deref().unwrap_or("none");
let last: &str = notes.last.as_deref().unwrap_or("none");
return format!(
" lock_stage_ms={} lock_stage_kind={} lock_place_ms={} lock_looks={} lock_found={first:?} lock_last={last:?}",
notes.staging.as_millis(),
notes.stage_kind,
notes.placing.as_millis(),
notes.looks
);
}
pub const SLOW_LOCK_STEP: Duration = Duration::from_secs(1);
pub fn wait_line(seq: u64, total: Duration, notes: &WaitNotes, taken: bool) -> Option<String> {
if notes.looks == 0 && notes.staging < SLOW_LOCK_STEP && total < SLOW_LOCK_STEP {
return None;
}
return Some(format!(
"waited for the index lock batch={seq} result={} lock_wait_ms={}{}",
if taken { "taken" } else { "refused" },
total.as_millis(),
lock_notes_fields(notes)
));
}
const LEGACY_INDEX_UPDATE_KEY: &str = "sync_ms";
pub fn index_update_ms_of(line: &str) -> Option<u64> {
let value_of = |key: &str| -> Option<u64> {
let word: String = format!(" {key}=");
let from: usize = line.find(word.as_str())? + word.len();
let digits: String = line[from..].chars().take_while(char::is_ascii_digit).collect();
return digits.parse::<u64>().ok();
};
return value_of("index_update_ms").or_else(|| { return value_of(LEGACY_INDEX_UPDATE_KEY); });
}
pub fn answered_line(answered: &Answered<'_>) -> String {
let mut holder: String = answered.landing.lock_holder.as_ref().map_or_else(String::new, holder_field);
holder.push_str(lock_notes_fields(&answered.landing.lock_notes).as_str());
return format!(
"answered request={:?} branch={:?} outcome={} batch={} size={} position={} read_ms={} queued_ms={} settle_ms={} \
lock_wait_ms={}{holder} lock_hold_ms={} reads_ms={} objects_ms={} journal_ms={} ref_ms={} index_update_ms={} attempts={} landing_ms={} \
answer_ms={} total_ms={} quiet_ms={} quiet_steps={:?}",
answered.request,
answered.reference,
answered.outcome,
answered.batch,
answered.size,
answered.position,
answered.arrival.read.as_millis(),
answered.queued.as_millis(),
answered.settle.as_millis(),
answered.landing.lock_wait.as_millis(),
answered.landing.hold.as_millis(),
answered.landing.reads(answered.land_total).as_millis(),
answered.landing.objects.as_millis(),
answered.landing.journal.as_millis(),
answered.landing.ref_update.as_millis(),
answered.landing.index_update.as_millis(),
answered.landing.attempts,
answered.land_total.as_millis(),
answered.answer.as_millis(),
answered.total.as_millis(),
longest_healthy(answered.quiet),
quiet_steps(answered.quiet),
);
}
pub fn started_line(release: &str, protocol: u32, instance: Option<&str>) -> String {
return format!("started release={release:?} protocol={protocol} pid={} instance={}", std::process::id(), instance.unwrap_or("any"));
}
pub fn retire_line(release: &str) -> String {
return format!("retire requested by a client; this service is release {release:?} and exits after its batch");
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExitReason {
Idle,
Retire,
Error,
}
impl ExitReason {
pub fn word(self) -> &'static str {
return match self {
ExitReason::Idle => "idle",
ExitReason::Retire => "retire",
ExitReason::Error => "error",
};
}
}
pub fn exit_line(reason: ExitReason, uptime: Duration) -> String {
return format!("exiting reason={} uptime_ms={}", reason.word(), uptime.as_millis());
}
pub fn wall_millis() -> u128 {
return std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |elapsed| { return elapsed.as_millis(); });
}
pub fn stamped_line(line: &str, at_ms: u128) -> String {
return format!("{LOG_PREFIX}{line} at_ms={at_ms}\n");
}
pub fn log(line: &str) {
log_entry(Priority::Info, line, &[]);
}
pub fn in_service() -> bool {
#[cfg(test)]
if capture::active() {
return true;
}
return CONTEXT.get().is_some();
}
static CONTEXT: OnceLock<Vec<(&'static str, String)>> = OnceLock::new();
static SINK: OnceLock<Sink> = OnceLock::new();
pub fn sink() -> &'static Sink {
return SINK.get_or_init(Sink::from_environment);
}
pub fn set_context(release: &str, instance: Option<&str>) {
let mut fields: Vec<(&'static str, String)> = vec![("CLI_GIT_RELEASE", release.to_string())];
if let Some(name) = instance {
fields.push(("CLI_GIT_INSTANCE", name.to_string()));
}
let _first = CONTEXT.set(fields);
}
pub fn log_entry(priority: Priority, line: &str, fields: &[(&str, String)]) -> Delivery {
let at_ms: u128 = wall_millis();
let masked: String = mask_userinfo_text(line);
let mut all: Vec<(&str, String)> = CONTEXT.get().map_or_else(Vec::new, |context| { return context.iter().map(|(name, value)| { return (*name, value.clone()); }).collect(); });
all.push(("CLI_GIT_AT_MS", at_ms.to_string()));
all.extend(fields.iter().map(|(name, value)| { return (*name, mask_userinfo_text(value.as_str())); }));
let message: String = format!("{} at_ms={at_ms}", cut(masked.as_str(), MESSAGE_LIMIT));
#[cfg(test)]
if capture::record(priority, message.as_str(), all.as_slice()) {
return Delivery::Stderr;
}
let text: String = stamped_line(one_line(masked.as_str(), MESSAGE_LIMIT).as_str(), at_ms);
if sink().on_stream() {
return sink().deliver(priority, message.as_str(), all.as_slice(), text.as_str(), &mut StreamWriter::stderr());
}
return sink().deliver(priority, message.as_str(), all.as_slice(), text.as_str(), &mut std::io::stderr());
}
#[macro_export]
macro_rules! service_warn {
($($argument:tt)*) => {
{
let _delivered = $crate::service_timing::log_entry($crate::journald_native::Priority::Warning, format!($($argument)*).as_str(), &[]);
}
};
}
#[macro_export]
macro_rules! service_error {
($($argument:tt)*) => {
{
let _delivered = $crate::service_timing::log_entry($crate::journald_native::Priority::Error, format!($($argument)*).as_str(), &[]);
}
};
}
#[macro_export]
macro_rules! service_log {
($($argument:tt)*) => {
$crate::service_timing::log(format!($($argument)*).as_str())
};
}
#[cfg(test)]
pub mod capture {
use super::Priority;
use std::cell::RefCell;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Captured {
pub priority: Priority,
pub message: String,
pub fields: Vec<(String, String)>,
}
thread_local! {
static ENTRIES: RefCell<Option<Vec<Captured>>> = const { RefCell::new(None) };
}
pub fn active() -> bool {
return ENTRIES.with(|entries| { return entries.borrow().is_some(); });
}
pub fn record(priority: Priority, message: &str, fields: &[(&str, String)]) -> bool {
return ENTRIES.with(|entries| {
return entries.borrow_mut().as_mut().is_some_and(|taken| {
taken.push(Captured { priority, message: message.to_string(), fields: fields.iter().map(|(name, value)| { return ((*name).to_string(), value.clone()); }).collect() });
return true;
});
});
}
pub fn entries_of(body: impl FnOnce()) -> Vec<Captured> {
ENTRIES.with(|entries| { *entries.borrow_mut() = Some(Vec::new()); });
body();
return ENTRIES.with(|entries| { return entries.borrow_mut().take().unwrap_or_default(); });
}
}
#[cfg(test)]
#[path = "service_timing_tests.rs"]
mod tests;