Parser — Drain3¶
Drain3 is the log parser that clusters raw messages into templates using an online tree-based algorithm. It runs as the first processing stage in the pipeline.
What is Drain3?¶
Drain3 (Python port of IBM's Drain algorithm) parses unstructured logs into templates (patterns with variable placeholders <*>). It builds an online search tree for real-time clustering.
%%{init: {'theme':'default','themeVariables':{'primaryColor':'#1a73e8'}}}%%
flowchart TD
MSG[Raw Message] --> TOKENS[Tokenize]
TOKENS --> TREE[Search Tree by Length]
TREE --> SIM[Similarity Check<br/>sim_th=0.4]
SIM -->|Match| EXIST[Existing Cluster]
SIM -->|No Match| NEW[New Cluster]
EXIST --> TMPL[Template: "User <*> logged in from <*>"]
NEW --> TMPL2[New Template]
Algorithm¶
- Tokenize message by whitespace/special chars
- Tree Level 1: Branch by token count (length)
- Tree Level 2+: Branch by token at each position
- Leaf: Compare new log to existing cluster templates using token similarity
- Threshold (
sim_th=0.4): If similarity ≥ threshold → assign to cluster; else → create new cluster
Key Parameters:
| Parameter | Value | Effect |
|-----------|-------|--------|
| sim_th | 0.4 | Similarity threshold (lower = more aggressive clustering) |
| depth | 4 | Tree depth (higher = more precise) |
| max_clusters | 100,000 | Hard limit on template count |
| max_children | 100 | Max branches per node |
Version & Config¶
# services/pipeline/parser.py
from drain3 import TemplateMiner
from drain3.persistence import AsyncFilePersistence
parser = LogParser(
state_path="/app/state/drain3_state.json",
max_clusters=100000,
)
# Internally uses:
TemplateMiner(
persistence=AsyncFilePersistence("/app/state/drain3_state.json"),
config=TemplateMinerConfig(
sim_th=0.4,
depth=4,
max_clusters=100000,
max_children=100,
)
)
Version: drain3>=0.9.11 (PyPI)
Input / Output¶
| Input | Output |
|---|---|
message: str (raw log line) |
template_id: str (e.g., "123")template_text: str (e.g., "User <> logged in from <>") |
# Usage in pipeline
parse_result = parser.parse(raw_message)
event.template_id = parse_result.template_id
event_dict["_template_text"] = parse_result.template_text
Persistence¶
File: /app/state/drain3_state.json (mounted volume ./state:/app/state)
- AsyncFilePersistence: Writes every 30 seconds (non-blocking)
- Size: ~300 KB typical (thousands of templates)
- Survives restarts: Templates learned are retained
Why Drain3?¶
| Approach | Pros | Cons |
|---|---|---|
| Drain3 | Online, real-time, persisted, low CPU | Fixed similarity threshold |
| Regex/Grok | Precise | Manual maintenance, brittle |
| LLM | Semantic understanding | High latency, cost, non-deterministic |
| Spell/Logmine | Batch, academic | Not streaming, no Python lib |
Drain3 sweet spot: Real-time, zero-config, template-level ML (1000× fewer inferences).
Template Examples¶
| Raw Message | Template |
|---|---|
User 4923 logged in from 10.0.0.5 |
User <*> logged in from <*> |
ERROR: Connection to db-primary failed (timeout=30s) |
ERROR: Connection to <*> failed (timeout=<*>) |
2026-01-15 10:30:00 INFO [main] Starting service on port 8080 |
<*> INFO [<*>] Starting service on port <*> |
Memory usage: 85% (threshold 80%) |
Memory usage: <*> (threshold <*>) |
Integration with ML¶
Templates are the unit of classification:
- Parser extracts
template_id - Cache checked:
template_cache.get(template_id) - If miss → RoBERTa classifies
template_text - Result cached:
template_cache.set(template_id, priority, confidence)
→ 1000× fewer ML inferences vs per-message classification.
Troubleshooting¶
| Issue | Check |
|---|---|
| Too many templates (>50k) | Increase sim_th to 0.5, check for high-cardinality logs (request IDs) |
| State file growing | Normal; check max_clusters limit |
| Restart loses templates | Verify volume mount ./state:/app/state |
| High CPU | Reduce max_clusters, check log volume |