log-search: Add --stats flag.

This commit is contained in:
Alex Vandiver
2025-07-15 15:07:27 +00:00
committed by Tim Abbott
parent 2616b7d030
commit e9a2ee56c3

View File

@@ -7,6 +7,7 @@ import logging
import os import os
import re import re
import signal import signal
import statistics
import sys import sys
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from enum import Enum, auto from enum import Enum, auto
@@ -114,6 +115,7 @@ def parser() -> argparse.ArgumentParser:
output = parser.add_argument_group("Output") output = parser.add_argument_group("Output")
output.add_argument("--full-line", "-F", help="Show full matching line", action="store_true") output.add_argument("--full-line", "-F", help="Show full matching line", action="store_true")
output.add_argument("--timeline", "-T", help="Show start, end, and gaps", action="store_true") output.add_argument("--timeline", "-T", help="Show start, end, and gaps", action="store_true")
output.add_argument("--stats", "-S", help="Compute and show statistics", action="store_true")
return parser return parser
@@ -207,6 +209,10 @@ def main() -> None:
use_color = sys.stdout.isatty() use_color = sys.stdout.isatty()
lowered_terms = [term.lower() for term in substr_terms] lowered_terms = [term.lower() for term in substr_terms]
if args.stats:
durations: list[int] | None = []
else:
durations = None
try: try:
for logfile_name in reversed(logfile_names): for logfile_name in reversed(logfile_names):
with maybe_gzip(logfile_name) as logfile: with maybe_gzip(logfile_name) as logfile:
@@ -232,6 +238,7 @@ def main() -> None:
args, args,
filter_types=filter_types, filter_types=filter_types,
use_color=use_color, use_color=use_color,
durations=durations,
) )
except BrokenPipeError: except BrokenPipeError:
# Python flushes standard streams on exit; redirect remaining output # Python flushes standard streams on exit; redirect remaining output
@@ -242,6 +249,19 @@ def main() -> None:
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit(signal.SIGINT + 128) sys.exit(signal.SIGINT + 128)
if durations is not None:
# Prepend [0] to make the percentiles 1-indexed, instead of 0-indexed
percentiles = [0, *statistics.quantiles(durations, n=100)]
print()
print(f"Total requests: {len(durations)}")
print(f"Min duration: {min(durations):>5}ms")
print(f"p50 duration: {int(percentiles[50]):>5}ms")
print(f"p75 duration: {int(percentiles[75]):>5}ms")
print(f"p90 duration: {int(percentiles[90]):>5}ms")
print(f"p95 duration: {int(percentiles[95]):>5}ms")
print(f"p99 duration: {int(percentiles[99]):>5}ms")
print(f"Max duration: {max(durations):>5}ms")
def parse_logfile_names(args: argparse.Namespace) -> list[str]: def parse_logfile_names(args: argparse.Namespace) -> list[str]:
if args.nginx: if args.nginx:
@@ -437,9 +457,17 @@ def print_line(
args: argparse.Namespace, args: argparse.Namespace,
filter_types: set[FilterType], filter_types: set[FilterType],
use_color: bool, use_color: bool,
durations: list[int] | None = None,
) -> None: ) -> None:
global last_match_end global last_match_end
if match["duration"].endswith("ms"):
duration_ms = int(match["duration"].removesuffix("ms"))
else:
duration_ms = int(float(match["duration"].removesuffix("s")) * 1000)
if durations is not None:
durations.append(duration_ms)
if args.full_line: if args.full_line:
print(match.group(0)) print(match.group(0))
return return