log-search: Add a --timeline option to show gaps and overlaps.

This commit is contained in:
Alex Vandiver 2023-02-13 17:11:14 +00:00 committed by Tim Abbott
parent cfd9e56d1a
commit 38b7ecff68
1 changed files with 43 additions and 5 deletions

View File

@ -1,14 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import calendar
import gzip import gzip
import logging import logging
import os import os
import re import re
import signal import signal
import sys import sys
from datetime import datetime, timedelta
from enum import Enum, auto from enum import Enum, auto
from typing import List, Match, Set, TextIO, Tuple from typing import List, Match, Optional, Set, TextIO, Tuple
ZULIP_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ZULIP_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(ZULIP_PATH) sys.path.append(ZULIP_PATH)
@ -91,6 +93,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")
return parser return parser
@ -178,6 +181,8 @@ def main() -> None:
(filter_types, filter_funcs) = parse_filters(args) (filter_types, filter_funcs) = parse_filters(args)
logfile_names = parse_logfile_names(args) logfile_names = parse_logfile_names(args)
if args.timeline and args.nginx:
print("! nginx logs not suggested for timeline, due to imprecision", file=sys.stderr)
use_color = sys.stdout.isatty() use_color = sys.stdout.isatty()
try: try:
@ -342,24 +347,36 @@ def passes_filters(
return not args.no_other return not args.no_other
last_match_end: Optional[datetime] = None
month_lookup = {v: f"{k:02d}" for k, v in enumerate(calendar.month_abbr)}
def print_line( def print_line(
match: Match[str], match: Match[str],
args: argparse.Namespace, args: argparse.Namespace,
filter_types: Set[FilterType], filter_types: Set[FilterType],
use_color: bool,
) -> None: ) -> None:
global last_match_end
if args.full_line: if args.full_line:
print(match.group(0)) print(match.group(0))
return return
if args.nginx:
day_of_month, month_abbr, year = match["date"].split("/")
date = f"{year}-{month_lookup[month_abbr]}-{day_of_month}"
else:
date = match["date"]
if args.all_logs or args.log_files is not None and args.log_files > 1: if args.all_logs or args.log_files is not None and args.log_files > 1:
ts = match["date"] + ":" + match["time"] ts = date + " " + match["time"]
else: else:
ts = match["time"] ts = match["time"]
if match["duration"].endswith("ms"): if match["duration"].endswith("ms"):
duration = match["duration"][:-2] duration_ms = int(match["duration"][:-2])
else: else:
duration = str(int(float(match["duration"][:-1]) * 1000)) duration_ms = int(float(match["duration"][:-1]) * 1000)
code = int(match["code"]) code = int(match["code"])
indicator = " " indicator = " "
@ -400,9 +417,30 @@ def print_line(
if not args.nginx and match["user_id"] is not None: if not args.nginx and match["user_id"] is not None:
user_id = match["user_id"] + "@" user_id = match["user_id"] + "@"
if args.timeline:
logline_end = datetime.fromisoformat(date + " " + match["time"])
logline_start = logline_end - timedelta(milliseconds=duration_ms)
if last_match_end is not None:
gap_ms = int((logline_start - last_match_end) / timedelta(milliseconds=1))
if gap_ms > 5000:
print()
print(f"========== {int(gap_ms/1000):>4} second gap ==========")
print()
elif gap_ms > 1000:
print(f"============ {gap_ms:>5}ms gap ============")
elif gap_ms > 0:
print(f"------------ {gap_ms:>5}ms gap ------------")
else:
print(f"!!!!!!!!!! {abs(gap_ms):>5}ms overlap !!!!!!!!!!")
if args.all_logs or args.log_files is not None and args.log_files > 1:
print(logline_start.isoformat(" ", timespec="milliseconds") + " (start)")
else:
print(logline_start.time().isoformat(timespec="milliseconds") + " (start)")
last_match_end = logline_end
parts = [ parts = [
ts, ts,
f"{duration:>5}ms", f"{duration_ms:>5}ms",
f"{user_id:7}" if not args.nginx and FilterType.USER_ID not in filter_types else None, f"{user_id:7}" if not args.nginx and FilterType.USER_ID not in filter_types else None,
f"{match['ip']:39}" if FilterType.CLIENT_IP not in filter_types else None, f"{match['ip']:39}" if FilterType.CLIENT_IP not in filter_types else None,
indicator + match["code"], indicator + match["code"],