From ecfd759d7ff41246a7174a996d0e85057081c285 Mon Sep 17 00:00:00 2001 From: Vlad Doloman Date: Mon, 22 Jun 2026 18:41:22 +0300 Subject: [PATCH] feat: log line preprocessing (ts strip + router/child classify) --- log_parser.py | 26 ++++++++++++++++++++++ tests/test_log_parser.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 log_parser.py create mode 100644 tests/test_log_parser.py diff --git a/log_parser.py b/log_parser.py new file mode 100644 index 0000000..edbd20b --- /dev/null +++ b/log_parser.py @@ -0,0 +1,26 @@ +import re +import json +from dataclasses import dataclass, field, asdict +from datetime import datetime +from typing import Optional + +# ── Preprocessing ──────────────────────────────────────────────────────────── + +_TS_RE = re.compile(r'^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\s*') +_CHILD_RE = re.compile(r'^\[(\d+)\]\s*') + + +def strip_ts(line: str) -> str: + """Remove optional `ts`-injected timestamp prefix from a log line.""" + return _TS_RE.sub('', line.strip()) + + +def classify_line(line: str) -> tuple[str, Optional[int], str]: + """ + Return (kind, port, content) where kind is 'router' or 'child'. + `line` must already have the ts prefix stripped. + """ + m = _CHILD_RE.match(line) + if m: + return ('child', int(m.group(1)), line[m.end():]) + return ('router', None, line) diff --git a/tests/test_log_parser.py b/tests/test_log_parser.py new file mode 100644 index 0000000..a4b2843 --- /dev/null +++ b/tests/test_log_parser.py @@ -0,0 +1,47 @@ +import pytest +from log_parser import strip_ts, classify_line + +# ── strip_ts ──────────────────────────────────────────────────────────────── + +def test_strip_ts_with_prefix(): + line = "[2026-06-22 14:29:49] 0.00.044.403 I srv init: running without SSL" + assert strip_ts(line) == "0.00.044.403 I srv init: running without SSL" + +def test_strip_ts_without_prefix(): + line = "0.00.044.403 I srv init: running without SSL" + assert strip_ts(line) == "0.00.044.403 I srv init: running without SSL" + +def test_strip_ts_child_line(): + line = "[2026-06-22 14:31:59] [41803] 0.00.041.673 I log_info: verbosity = 3" + assert strip_ts(line) == "[41803] 0.00.041.673 I log_info: verbosity = 3" + +def test_strip_ts_cmd_line(): + line = '[2026-06-22 14:31:59] [41803] cmd_child_to_router:state:{"state":"loading"}' + assert strip_ts(line) == '[41803] cmd_child_to_router:state:{"state":"loading"}' + +# ── classify_line ──────────────────────────────────────────────────────────── + +def test_classify_router_line(): + kind, port, content = classify_line("0.00.044.403 I srv init: running without SSL") + assert kind == "router" + assert port is None + assert content == "0.00.044.403 I srv init: running without SSL" + +def test_classify_child_line(): + kind, port, content = classify_line("[41803] 0.00.041.673 I log_info: verbosity = 3") + assert kind == "child" + assert port == 41803 + assert content == "0.00.041.673 I log_info: verbosity = 3" + +def test_classify_child_cmd_line(): + payload = '{"state":"loading","payload":null}' + kind, port, content = classify_line(f"[41803] cmd_child_to_router:state:{payload}") + assert kind == "child" + assert port == 41803 + assert content == f"cmd_child_to_router:state:{payload}" + +def test_classify_router_proxy_line(): + line = "2.10.286.889 I srv proxy_reques: proxying request to model Qwen3 on port 41803" + kind, port, content = classify_line(line) + assert kind == "router" + assert port is None