use anyhow::{Context, Result};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fs;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use super::*;
pub(crate) fn read_runtime_log_tail(path: &Path, max_bytes: usize) -> Result<Vec<u8>> {
let mut file =
fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let len = file
.metadata()
.with_context(|| format!("failed to inspect {}", path.display()))?
.len();
let start = len.saturating_sub(max_bytes as u64);
file.seek(SeekFrom::Start(start))
.with_context(|| format!("failed to seek {}", path.display()))?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.with_context(|| format!("failed to read {}", path.display()))?;
if start > 0
&& let Some(position) = buffer.iter().position(|byte| *byte == b'\n')
{
buffer.drain(..=position);
}
Ok(buffer)
}
pub(crate) fn summarize_runtime_log_tail(tail: &[u8]) -> RuntimeDoctorSummary {
let text = String::from_utf8_lossy(tail);
let mut summary = RuntimeDoctorSummary::default();
for line in text.lines() {
summary.line_count += 1;
if let Some(timestamp) = runtime_doctor_line_timestamp(line) {
if summary.first_timestamp.is_none() {
summary.first_timestamp = Some(timestamp.clone());
}
summary.last_timestamp = Some(timestamp);
}
if let Some(marker) = runtime_doctor_marker_name(line) {
*summary.marker_counts.entry(marker).or_insert(0) += 1;
summary.last_marker_line = Some(runtime_doctor_truncate_line(line, 160));
let fields = runtime_doctor_parse_fields(line);
if marker == "previous_response_not_found" {
if let Some(route) = fields.get("route").cloned() {
*summary
.previous_response_not_found_by_route
.entry(route)
.or_insert(0) += 1;
}
if let Some(transport) = fields.get("transport").cloned() {
*summary
.previous_response_not_found_by_transport
.entry(transport)
.or_insert(0) += 1;
}
}
for facet in RUNTIME_DOCTOR_FACETS {
if let Some(value) = fields.get(*facet).cloned() {
*summary
.facet_counts
.entry((*facet).to_string())
.or_default()
.entry(value)
.or_insert(0) += 1;
}
}
if !fields.is_empty() {
summary.marker_last_fields.insert(marker, fields);
}
}
}
diagnosis::runtime_doctor_finalize_log_summary(&mut summary);
summary
}
fn runtime_doctor_line_timestamp(line: &str) -> Option<String> {
if let Some(value) = runtime_doctor_json_line_value(line) {
return value
.get("timestamp")
.or_else(|| value.get("ts"))
.and_then(serde_json::Value::as_str)
.map(ToString::to_string);
}
let end = line.find("] ")?;
line.strip_prefix('[')
.and_then(|trimmed| trimmed.get(..end.saturating_sub(1)))
.map(ToString::to_string)
}
fn runtime_doctor_json_line_value(line: &str) -> Option<serde_json::Value> {
let trimmed = line.trim();
if !trimmed.starts_with('{') {
return None;
}
serde_json::from_str(trimmed).ok()
}
fn runtime_doctor_line_message<'a>(line: &'a str) -> Cow<'a, str> {
if let Some(value) = runtime_doctor_json_line_value(line)
&& let Some(message) = value.get("message").and_then(serde_json::Value::as_str)
{
return Cow::Owned(message.to_string());
}
Cow::Borrowed(
line.split_once("] ")
.map(|(_, message)| message)
.unwrap_or(line)
.trim(),
)
}
fn runtime_doctor_parse_fields(line: &str) -> BTreeMap<String, String> {
if let Some(value) = runtime_doctor_json_line_value(line)
&& let Some(fields) = value.get("fields").and_then(serde_json::Value::as_object)
{
let mut parsed = BTreeMap::new();
for (key, value) in fields {
let string_value = match value {
serde_json::Value::String(value) => value.clone(),
serde_json::Value::Number(value) => value.to_string(),
serde_json::Value::Bool(value) => value.to_string(),
_ => continue,
};
parsed.insert(key.clone(), string_value);
}
if !parsed.is_empty() {
return parsed;
}
}
let message = runtime_doctor_line_message(line);
let mut fields = BTreeMap::new();
for token in message.split_whitespace() {
let Some((key, value)) = token.split_once('=') else {
continue;
};
if key.is_empty() || value.is_empty() {
continue;
}
fields.insert(key.to_string(), value.trim_matches('"').to_string());
}
fields
}
fn runtime_doctor_marker_name(line: &str) -> Option<&'static str> {
let message = runtime_doctor_line_message(line);
RUNTIME_DOCTOR_MARKERS
.iter()
.copied()
.find(|marker| message.contains(marker))
}
fn runtime_doctor_truncate_line(line: &str, limit: usize) -> String {
let trimmed = line.trim();
let count = trimmed.chars().count();
if count <= limit {
return trimmed.to_string();
}
trimmed
.chars()
.take(limit.saturating_sub(1))
.collect::<String>()
+ "…"
}