Initial solution: add parse_log.py
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
"""CLI tool to parse raw log lines into typed ApiEvent objects using LangChain structured output.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python parse_log.py [--log FILE]
|
||||||
|
|
||||||
|
If --log is omitted, a built‑in example log is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable, List
|
||||||
|
|
||||||
|
from langchain_core.output_parsers import PydanticOutputParser
|
||||||
|
from langchain_core.prompts import ChatPromptTemplate
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
from models import ApiEvent
|
||||||
|
|
||||||
|
# Example log with mixed OK and error events
|
||||||
|
EXAMPLE_LOG = """
|
||||||
|
GET /api/users 200 123ms
|
||||||
|
POST /api/login 404 Not Found
|
||||||
|
GET /api/data 200 456ms
|
||||||
|
GET /api/health 500 Internal Server Error
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prompt template that instructs LLM to output a single ApiEvent in JSON
|
||||||
|
PROMPT = ChatPromptTemplate.from_messages(
|
||||||
|
[
|
||||||
|
("system", "You are a helpful assistant that parses a single log line into a structured event.")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a parser for the ApiEvent union
|
||||||
|
parser = PydanticOutputParser(pydantic_object=ApiEvent)
|
||||||
|
|
||||||
|
# LLM model – replace with your own key or use a local model
|
||||||
|
llm = ChatOpenAI(temperature=0, model="gpt-4o-mini")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_line(line: str) -> ApiEvent:
|
||||||
|
"""Parse a single log line using the LLM and the structured output parser."""
|
||||||
|
# Build a prompt that includes the line and asks for JSON
|
||||||
|
prompt = PROMPT.format_messages(user=line)
|
||||||
|
# Get raw response
|
||||||
|
raw = llm.invoke(prompt)
|
||||||
|
# Parse into ApiEvent
|
||||||
|
return parser.parse(raw.content)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_log(lines: Iterable[str]) -> List[ApiEvent]:
|
||||||
|
return [parse_line(line) for line in lines if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser_cli = argparse.ArgumentParser(description="Parse raw log into typed events")
|
||||||
|
parser_cli.add_argument("--log", type=Path, help="Path to a log file; if omitted, example log is used")
|
||||||
|
args = parser_cli.parse_args()
|
||||||
|
|
||||||
|
if args.log and args.log.exists():
|
||||||
|
raw_lines = args.log.read_text().splitlines()
|
||||||
|
else:
|
||||||
|
raw_lines = EXAMPLE_LOG.strip().splitlines()
|
||||||
|
|
||||||
|
events = parse_log(raw_lines)
|
||||||
|
|
||||||
|
# Print each event as JSON
|
||||||
|
for ev in events:
|
||||||
|
print(ev.model_dump_json(indent=2))
|
||||||
|
|
||||||
|
# Simple table
|
||||||
|
print("\nParsed events table:\n")
|
||||||
|
print("{:<6} {:<20} {:<6} {}".format("kind", "path", "status", "details"))
|
||||||
|
for ev in events:
|
||||||
|
if ev.kind == "ok":
|
||||||
|
print(f"{ev.kind:<6} {ev.path:<20} {ev.status:<6} duration={ev.duration_ms}ms")
|
||||||
|
else:
|
||||||
|
print(f"{ev.kind:<6} {ev.path:<20} {ev.status:<6} error={ev.error_message}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user