Skip to content

Commit 6e01958

Browse files
committed
adding loadgen for testing
1 parent b9dd54e commit 6e01958

4 files changed

Lines changed: 268 additions & 2 deletions

File tree

‎loadgen.py‎

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env python3
2+
"""
3+
loadgen.py — Log load generator for testing log-explainer.
4+
5+
Appends realistic log lines to a file for a given duration,
6+
with weighted severity distribution.
7+
8+
Usage:
9+
python3 loadgen.py # append to sample.log for 5s
10+
python3 loadgen.py --output /tmp/test.log # custom output file
11+
python3 loadgen.py --duration 30 # run for 30 seconds
12+
python3 loadgen.py --rate 5 # 5 lines per second
13+
"""
14+
15+
import argparse
16+
import random
17+
import time
18+
from datetime import datetime
19+
20+
# ── Log line pool (100 entries) ───────────────────────────────────────────────
21+
# Each entry is (severity, message)
22+
23+
LOG_LINES = [
24+
# INFO (40 entries)
25+
("INFO", "Starting application on port 8000"),
26+
("INFO", "Connected to PostgreSQL at postgres:5432"),
27+
("INFO", "Redis cache initialized successfully"),
28+
("INFO", "Worker pool started with 4 threads"),
29+
("INFO", "GET /api/users 200 OK (45ms)"),
30+
("INFO", "POST /api/orders 201 Created (112ms)"),
31+
("INFO", "GET /api/products 200 OK (23ms)"),
32+
("INFO", "PUT /api/users/42 200 OK (67ms)"),
33+
("INFO", "DELETE /api/sessions/8821 200 OK (18ms)"),
34+
("INFO", "Scheduled job 'cleanup_expired_tokens' started"),
35+
("INFO", "Scheduled job 'cleanup_expired_tokens' completed in 340ms"),
36+
("INFO", "Cache hit ratio: 87.4% over last 60s"),
37+
("INFO", "Database connection pool size: 10/20"),
38+
("INFO", "Listening on 0.0.0.0:8000"),
39+
("INFO", "Health check passed: all systems nominal"),
40+
("INFO", "Config reloaded from /etc/app/config.yaml"),
41+
("INFO", "TLS certificate valid, expires in 87 days"),
42+
("INFO", "New user registered: user_id=10482"),
43+
("INFO", "Session created for user_id=10482, ttl=3600s"),
44+
("INFO", "Metrics exported to Prometheus endpoint /metrics"),
45+
("INFO", "Graceful shutdown initiated, draining connections"),
46+
("INFO", "Background task 'send_digest_emails' queued"),
47+
("INFO", "S3 upload completed: reports/2026-04-12.csv (2.3MB)"),
48+
("INFO", "Kafka consumer group 'order-events' rebalanced, partitions=3"),
49+
("INFO", "Feature flag 'new_checkout_flow' enabled for 10% of traffic"),
50+
("INFO", "Rate limiter reset for IP 203.0.113.42"),
51+
("INFO", "Audit log written: admin user_id=1 deleted user_id=304"),
52+
("INFO", "gRPC server started on port 50051"),
53+
("INFO", "Dependency check passed: postgres=14.5, redis=7.0.5"),
54+
("INFO", "Request tracing enabled, trace_id=7f3a9c21b4e0"),
55+
("INFO", "CDN cache purged for path /static/app.js"),
56+
("INFO", "OAuth token refreshed for service account svc-worker"),
57+
("INFO", "Compaction triggered on table 'events' (2.1GB freed)"),
58+
("INFO", "Websocket connection established, client_id=ws-8821"),
59+
("INFO", "Batch job processed 1200 records in 4.2s"),
60+
("INFO", "Replica lag is 0ms, replication healthy"),
61+
("INFO", "Circuit breaker for payment-service is CLOSED"),
62+
("INFO", "Auto-scaling: added 1 worker node, total=5"),
63+
("INFO", "Log rotation completed, archived to /var/log/app/2026-04-11.gz"),
64+
("INFO", "Startup complete, ready to serve traffic"),
65+
66+
# WARN (30 entries)
67+
("WARNING", "Retrying database connection attempt 1 of 5"),
68+
("WARNING", "Retrying database connection attempt 2 of 5"),
69+
("WARNING", "Response time degraded: GET /api/reports took 2300ms"),
70+
("WARNING", "Memory usage at 78%, approaching threshold"),
71+
("WARNING", "Deprecated API endpoint /v1/users called by client 10.0.0.5"),
72+
("WARNING", "Redis cache miss rate exceeded 30% in last 60s"),
73+
("WARNING", "Disk usage at 82% on /var/data, consider cleanup"),
74+
("WARNING", "JWT token expiring soon for user_id=204, issuing refresh"),
75+
("WARNING", "Rate limit approaching for IP 198.51.100.7: 95/100 requests"),
76+
("WARNING", "Kafka consumer lag growing: topic=order-events lag=1500"),
77+
("WARNING", "Slow query detected: SELECT * FROM events took 4100ms"),
78+
("WARNING", "Connection pool saturation: 19/20 connections in use"),
79+
("WARNING", "TLS certificate expires in 14 days, renewal required"),
80+
("WARNING", "Retry attempt 1 for HTTP POST to payment-service"),
81+
("WARNING", "Retry attempt 2 for HTTP POST to payment-service"),
82+
("WARNING", "Background worker queue depth: 850 jobs pending"),
83+
("WARNING", "Config value 'MAX_UPLOAD_SIZE' not set, using default 10MB"),
84+
("WARNING", "Health check latency elevated: postgres responded in 850ms"),
85+
("WARNING", "Graceful shutdown timeout exceeded, forcing exit"),
86+
("WARNING", "S3 upload retrying due to transient error (attempt 2/3)"),
87+
("WARNING", "Feature flag service unreachable, falling back to defaults"),
88+
("WARNING", "CPU usage spike to 91% on worker-3"),
89+
("WARNING", "Replica lag increased to 250ms, monitoring"),
90+
("WARNING", "Circuit breaker for inventory-service is HALF-OPEN"),
91+
("WARNING", "Auto-vacuum running on table 'orders', may cause slowdown"),
92+
("WARNING", "Timeout waiting for lock on resource 'user:8821'"),
93+
("WARNING", "Websocket client ws-4421 disconnected unexpectedly"),
94+
("WARNING", "DNS resolution for cache.internal took 900ms"),
95+
("WARNING", "OpenTelemetry exporter dropped 12 spans due to buffer overflow"),
96+
("WARNING", "Rollout paused: error rate above 2% threshold"),
97+
98+
# ERROR (20 entries)
99+
("ERROR", "Connection refused to postgres:5432"),
100+
("ERROR", "Traceback (most recent call last): File 'db.py' line 42 in connect"),
101+
("ERROR", "psycopg2.OperationalError: could not connect to server"),
102+
("ERROR", "HTTP 500 Internal Server Error on POST /api/checkout"),
103+
("ERROR", "Failed to publish event to Kafka: topic=order-events"),
104+
("ERROR", "Redis SETNX failed: timeout after 5000ms"),
105+
("ERROR", "Unhandled exception in worker thread: ZeroDivisionError"),
106+
("ERROR", "S3 upload failed after 3 retries: AccessDenied"),
107+
("ERROR", "Payment service returned 503: Service Unavailable"),
108+
("ERROR", "Database transaction rolled back: deadlock detected"),
109+
("ERROR", "JWT verification failed: signature mismatch for user_id=8801"),
110+
("ERROR", "Failed to acquire connection from pool after 10s"),
111+
("ERROR", "gRPC call to inventory-service failed: UNAVAILABLE"),
112+
("ERROR", "Config file /etc/app/config.yaml not found, cannot reload"),
113+
("ERROR", "Batch job 'send_digest_emails' failed: SMTP connection refused"),
114+
("ERROR", "Disk write error on /var/data: No space left on device"),
115+
("ERROR", "Failed to parse request body: invalid JSON"),
116+
("ERROR", "Websocket write error for client ws-8821: broken pipe"),
117+
("ERROR", "Elasticsearch index 'logs-2026' is read-only: disk watermark exceeded"),
118+
("ERROR", "Circuit breaker for payment-service OPENED after 5 consecutive failures"),
119+
120+
# CRITICAL (10 entries)
121+
("CRITICAL", "FATAL: max connection retries exhausted, shutting down"),
122+
("CRITICAL", "OOMKilled: process exceeded memory limit of 512MB"),
123+
("CRITICAL", "PANIC: nil pointer dereference in handler.go:84"),
124+
("CRITICAL", "segfault at address 0x00000000, core dumped"),
125+
("CRITICAL", "FATAL: WAL log corrupted, database cannot start"),
126+
("CRITICAL", "OutOfMemory: JVM heap space exhausted, killing process"),
127+
("CRITICAL", "FATAL: SSL certificate validation failed, refusing to start"),
128+
("CRITICAL", "Data loss detected: replication stream broken for 120s"),
129+
("CRITICAL", "FATAL: etcd cluster quorum lost, all writes rejected"),
130+
("CRITICAL", "Kernel OOM killer terminated PID 8821 (app-worker)"),
131+
]
132+
133+
# ── Severity weights (must sum to 100) ───────────────────────────────────────
134+
# Mirrors realistic production log distribution
135+
SEVERITY_WEIGHTS = {
136+
"INFO": 40,
137+
"WARNING": 30,
138+
"ERROR": 20,
139+
"CRITICAL": 10,
140+
}
141+
142+
# Pre-bucket lines by severity for fast weighted sampling
143+
_BUCKETS = {}
144+
for sev, weight in SEVERITY_WEIGHTS.items():
145+
_BUCKETS[sev] = [msg for s, msg in LOG_LINES if s == sev]
146+
147+
_POPULATION = list(SEVERITY_WEIGHTS.keys())
148+
_WEIGHTS = list(SEVERITY_WEIGHTS.values())
149+
150+
151+
def pick_line() -> tuple[str, str]:
152+
severity = random.choices(_POPULATION, weights=_WEIGHTS, k=1)[0]
153+
message = random.choice(_BUCKETS[severity])
154+
return severity, message
155+
156+
157+
def write_line(f, severity: str, message: str):
158+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
159+
line = f"{ts} {severity:<8} {message}\n"
160+
f.write(line)
161+
f.flush()
162+
print(line, end="")
163+
164+
165+
def main():
166+
parser = argparse.ArgumentParser(
167+
description="Append realistic log lines to a file for testing log-explainer.",
168+
formatter_class=argparse.RawDescriptionHelpFormatter,
169+
epilog="""
170+
Examples:
171+
python3 loadgen.py
172+
python3 loadgen.py --output /tmp/test.log --duration 30
173+
python3 loadgen.py --rate 10 --duration 60
174+
175+
Then in another terminal:
176+
python3 log_parser.py sample.log --model qwen2.5-coder:1.5b
177+
""",
178+
)
179+
parser.add_argument("--output", default="sample.log", help="Log file to append to (default: sample.log)")
180+
parser.add_argument("--duration", default=5, type=int, help="How long to run in seconds (default: 5)")
181+
parser.add_argument("--rate", default=2, type=int, help="Log lines per second (default: 2)")
182+
args = parser.parse_args()
183+
184+
interval = 1.0 / args.rate
185+
end_time = time.time() + args.duration
186+
187+
print(f"Writing to '{args.output}' for {args.duration}s at {args.rate} lines/sec")
188+
print(f"Severity weights: {SEVERITY_WEIGHTS}")
189+
print("─" * 60)
190+
191+
count = 0
192+
with open(args.output, "a") as f:
193+
while time.time() < end_time:
194+
severity, message = pick_line()
195+
write_line(f, severity, message)
196+
count += 1
197+
time.sleep(interval)
198+
199+
print("─" * 60)
200+
print(f"Done. Wrote {count} lines to '{args.output}'.")
201+
202+
203+
if __name__ == "__main__":
204+
main()

‎log_parser.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,23 @@
3939

4040

4141
def classify_severity(line: str) -> str:
42+
# First pass: look for an explicit severity word anywhere in the line.
43+
# This avoids misclassifying lines like "WARNING ... lag=1500" as ERROR
44+
# just because a pattern keyword appears later in the message body.
45+
_EXPLICIT = {
46+
"CRITICAL": ["CRITICAL", "FATAL", "PANIC"],
47+
"ERROR": ["ERROR"],
48+
"WARN": ["WARNING", "WARN"],
49+
"INFO": ["INFO"],
50+
}
51+
upper = line.upper()
52+
for severity, keywords in _EXPLICIT.items():
53+
for kw in keywords:
54+
if kw in upper:
55+
return severity
56+
57+
# Second pass: fall back to full pattern matching for lines with no
58+
# explicit severity word (e.g. bare exception tracebacks, OOM messages)
4259
for severity, patterns in SEVERITY_PATTERNS.items():
4360
for pattern in patterns:
4461
if re.search(pattern, line):
@@ -204,7 +221,17 @@ def tail_file(filepath: str, model: str, context: str, min_severity: str): # pr
204221
color = SEVERITY_COLORS.get(severity, SEVERITY_COLORS["UNKNOWN"])
205222
ts = datetime.now().strftime("%H:%M:%S")
206223

207-
print(f"{DIM}{ts}{RESET} {color}[{severity:<8}]{RESET} {line}")
224+
# Strip leading timestamp from the raw line if present so we
225+
# don't print two timestamps side by side. Handles formats:
226+
# 2026-04-12 17:30:04,792 WARNING message
227+
# 2026-04-12T17:30:04 WARNING message
228+
display_line = re.sub(
229+
r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[,.]?\d*\s+",
230+
"",
231+
line,
232+
)
233+
234+
print(f"{DIM}{ts}{RESET} {color}[{severity:<8}]{RESET} {display_line}")
208235

209236
# Pattern spike detection
210237
repeat_count = detector.check(line)

‎sample.log‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,15 @@
1515
2024-03-15 10:01:12,000 CRITICAL FATAL: max connection retries exhausted, shutting down
1616
2026-04-12 10:01:02 CRITICAL FATAL: max retries exhausted
1717
2026-04-12 10:01:00 ERROR Connection refused to postgres:5432
18+
2026-04-12 11:57:46,093 WARNING TLS certificate expires in 14 days, renewal required
19+
2026-04-12 11:57:46,344 INFO Startup complete, ready to serve traffic
20+
2026-04-12 11:57:46,594 WARNING Timeout waiting for lock on resource 'user:8821'
21+
2026-04-12 11:57:46,845 INFO gRPC server started on port 50051
22+
2026-04-12 11:57:47,095 INFO Rate limiter reset for IP 203.0.113.42
23+
2026-04-12 11:57:47,346 WARNING Redis cache miss rate exceeded 30% in last 60s
24+
2026-04-12 11:57:47,597 INFO Scheduled job 'cleanup_expired_tokens' started
25+
2026-04-12 11:57:47,848 CRITICAL segfault at address 0x00000000, core dumped
26+
2026-04-12 11:57:48,099 WARNING Disk usage at 82% on /var/data, consider cleanup
27+
2026-04-12 11:57:48,351 WARNING Slow query detected: SELECT * FROM events took 4100ms
28+
2026-04-12 11:57:48,602 ERROR Unhandled exception in worker thread: ZeroDivisionError
29+
2026-04-12 11:57:48,852 INFO PUT /api/users/42 200 OK (67ms)

‎tests/test_parser.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,31 @@ def test_unknown(self):
2727
assert classify_severity("some random log line with no markers") == "UNKNOWN"
2828

2929
def test_case_sensitivity(self):
30-
assert classify_severity("error: something went wrong") == "UNKNOWN" # lowercase not matched
30+
# Explicit severity words are matched case-insensitively (upper() pass)
3131
assert classify_severity("ERROR: something went wrong") == "ERROR"
32+
assert classify_severity("error: something went wrong") == "ERROR"
33+
assert classify_severity("Warning: disk almost full") == "WARN"
34+
# A line with no severity keyword at all should be UNKNOWN
35+
assert classify_severity("something went wrong") == "UNKNOWN"
36+
37+
def test_explicit_severity_wins_over_pattern(self):
38+
# A WARNING line that also contains a pattern keyword (e.g. "lag")
39+
# should be classified as WARN, not ERROR
40+
line = "2026-04-12 17:30:06 WARNING Kafka consumer lag growing: topic=order-events lag=1500"
41+
assert classify_severity(line) == "WARN"
42+
43+
def test_explicit_severity_wins_for_full_loadgen_line(self):
44+
# Full loadgen-style line with timestamp prefix
45+
line = "2026-04-12 17:30:04,792 WARNING Auto-vacuum running on table 'orders', may cause slowdown"
46+
assert classify_severity(line) == "WARN"
47+
48+
def test_fallback_pattern_for_bare_traceback(self):
49+
# No explicit severity word — should fall back to pattern matching
50+
assert classify_severity("Traceback (most recent call last):") == "ERROR"
51+
52+
def test_fallback_pattern_for_oom(self):
53+
# OOMKilled has no explicit severity word — pattern match should catch it
54+
assert classify_severity("OOMKilled: container exceeded memory limit") == "CRITICAL"
3255

3356

3457
# ── Pattern detector ─────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)