Drain-style log template miner and anomaly detector for structured agent pipeline events — groups log lines into similarity clusters and produces scored anomaly reports.
The agentdrain package implements the Drain log template mining algorithm adapted for analyzing structured agent pipeline events. It is used for anomaly detection in agentic workflow runs.
The package also embeds default trained weights under data/, so callers can bootstrap a Coordinator with pre-trained clusters instead of starting from an empty model.
Drain is an online log parsing algorithm that groups log lines into clusters based on token similarity. Each cluster has a template — a tokenized log pattern where variable tokens are replaced with a wildcard (<*>). When a new log line arrives, Drain finds the most similar existing cluster or creates a new one.
In GitHub Agentic Workflows, agentdrain processes AgentEvent records emitted by pipeline stages (e.g. "plan", "tool_call", "finish") to:
- Build a model of normal behavior by training on events from successful runs.
- Detect anomalies in new runs by comparing events against the learned model.
Tuning parameters for the Drain miner.
| Field | Type | Default | Description |
|---|---|---|---|
Depth |
int |
4 |
Parse-tree depth |
SimThreshold |
float64 |
0.4 |
Minimum similarity score to match a cluster |
MaxChildren |
int |
100 |
Maximum children per tree node |
ParamToken |
string |
"<*>" |
Wildcard inserted at variable positions |
RareClusterThreshold |
int |
2 |
Clusters with Size ≤ this value are flagged as rare |
MaskRules |
[]MaskRule |
(see below) | Regex substitutions applied before tokenization |
ExcludeFields |
[]string |
["session_id", "trace_id", "span_id", "timestamp"] |
Event fields excluded from flattening |
Use DefaultConfig() for production-ready defaults.
A regex-based substitution applied to log lines before tokenization to normalize variable content.
type MaskRule struct {
Name string // Human-readable identifier
Pattern string // Regular expression
Replacement string // Substitution string
}Default mask rules normalize UUIDs, session IDs, numeric values, URLs, quoted strings, and timestamps.
A structured event from an agent pipeline stage.
type AgentEvent struct {
Stage string // e.g. "plan", "tool_call", "finish"
Fields map[string]string // Key-value pairs from the log line
}A group of log lines that share the same template.
type Cluster struct {
ID int // Unique identifier
Template []string // Tokenized template with wildcards
Size int // Number of lines assigned to this cluster
Stage string // Pipeline stage that generated this cluster
}Returned after processing a log line.
type MatchResult struct {
ClusterID int // Matched or newly created cluster ID
Template string // Space-joined template string
Params []string // Actual token values at wildcard positions
Similarity float64 // Fraction of non-wildcard tokens that matched exactly
Stage string // Pipeline stage of the matched cluster
}Describes anomalies detected for a log line.
type AnomalyReport struct {
IsNewTemplate bool // Line produced a brand-new log cluster
LowSimilarity bool // Best match score was below SimThreshold
RareCluster bool // Matched cluster has been seen ≤ RareClusterThreshold times
AnomalyScore float64 // Weighted composite score in [0, 1]
Reason string // Human-readable anomaly description
}The single-stage Drain miner. Processes one pipeline stage at a time.
cfg := agentdrain.DefaultConfig()
miner, err := agentdrain.NewMiner(cfg)
// Process a raw log line (training + matching in one step)
result, err := miner.Train(rawLogLine)
// Training phase — call for structured events from known-good runs
result, err := miner.TrainEvent(evt)
// Analysis phase — call for events to check for anomalies
result, report, err := miner.AnalyzeEvent(evt)
// Inspect clusters
clusters := miner.Clusters()// Save miner state to JSON
data, err := miner.SaveJSON()
// Restore miner state from JSON
err = miner.LoadJSON(data)Manages a separate Miner per pipeline stage, routing events to the correct miner.
stages := []string{"plan", "tool_call", "finish"}
coord, err := agentdrain.NewCoordinator(cfg, stages)
// Load default trained weights
err = coord.LoadDefaultWeights()
// Training phase — route events from known-good runs to stage miners
result, err := coord.TrainEvent(evt)
// Analyze an event
result, report, err := coord.AnalyzeEvent(evt)
// Access all clusters across all stages
allClusters := coord.AllClusters()
// Save/restore snapshots
snapshots, err := coord.SaveSnapshots()
err = coord.LoadSnapshots(snapshots)
// Save/restore coordinator weights as JSON
data, err := coord.SaveWeightsJSON()
err = coord.LoadWeightsJSON(data)Post-processes MatchResult values to produce an AnomalyReport.
Creates a new AnomalyDetector. Returns an error if simThreshold is outside [0,1] or rareClusterThreshold is negative.
Evaluates result and produces an AnomalyReport. isNew indicates that result created a brand-new cluster; cluster is the matched or newly created Cluster.
detector, err := agentdrain.NewAnomalyDetector(cfg.SimThreshold, cfg.RareClusterThreshold)
if err != nil {
panic(err)
}
report := detector.Analyze(result, isNew, cluster)Applies MaskRule substitutions to log lines before tokenization.
Applies all compiled mask rules to line in order, returning the normalized string.
masker, err := agentdrain.NewMasker(cfg.MaskRules)
masked := masker.Mask(rawLine)Converts an AgentEvent to a single string for tokenization, omitting fields listed in excludeFields. Fields are sorted for deterministic output.
Splits a log line into tokens on whitespace boundaries.
Returns a space-separated string of the stages from a slice of events (e.g. "plan tool_call tool_result finish"). Useful for summarizing pipeline execution paths.
Serializable representations of miner state used for persistence.
type Snapshot struct {
Config Config // Miner configuration
Clusters []SnapshotCluster // Serialized cluster list
NextID int // Next cluster ID counter
}
type SnapshotCluster struct {
ID int // Cluster identifier
Template []string // Tokenized template with wildcards
Size int // Number of lines assigned to cluster
Stage string // Pipeline stage
}These types are returned and consumed by SaveSnapshots / LoadSnapshots and are serialized as JSON.
import "github.com/github/gh-aw/pkg/agentdrain"
// Create a coordinator with default config
cfg := agentdrain.DefaultConfig()
stages := []string{"plan", "tool_call", "finish"}
coord, err := agentdrain.NewCoordinator(cfg, stages)
if err != nil {
panic(err)
}
// Load pre-trained weights
if err := coord.LoadDefaultWeights(); err != nil {
panic(err)
}
// Analyze an incoming agent event
evt := agentdrain.AgentEvent{
Stage: "tool_call",
Fields: map[string]string{"tool": "read_file", "path": "/workspace/main.go"},
}
result, report, err := coord.AnalyzeEvent(evt)
if err != nil {
panic(err)
}
if report.IsNewTemplate {
fmt.Printf("New behavior pattern detected (score=%.2f): %s\n",
report.AnomalyScore, report.Reason)
}Both Miner and Coordinator are safe for concurrent use from multiple goroutines. All mutating operations are protected by an internal sync.RWMutex:
Miner.Train,Miner.TrainEvent,Miner.AnalyzeEvent— acquire a write lockMiner.Clusters— acquires a read lockMiner.SaveJSON/Miner.LoadJSON— read and write lock respectivelyCoordinator.TrainEvent,Coordinator.AnalyzeEvent— delegate to per-stageMinerinstancesCoordinator.AllClusters,Coordinator.SaveSnapshots— acquire a read lock on the coordinator map
Internal:
github.com/github/gh-aw/pkg/logger— debug logginggithub.com/github/gh-aw/pkg/sliceutil— slice utilities for cluster management
The package embeds a set of default trained weights (in data/) via //go:embed. Call coord.LoadDefaultWeights() to initialize the coordinator with pre-trained cluster weights rather than starting cold.
Update embedded weights by running gh aw logs --train --output <dir> and copying the resulting drain3_weights.json to pkg/agentdrain/data/default_weights.json, then rebuilding the binary.
- The Drain algorithm is O(n·d) per event, where
nis the number of tokens anddisDepth. SimThresholdof0.4means at least 40% of tokens must match exactly (excluding wildcards) for a line to join an existing cluster.- The
Coordinatorroutes eachAgentEventto its stage-specificMinerso that templates from different stages do not interfere. SaveJSON/LoadJSONserialize the parse tree and cluster list to enable persistence across workflow runs.
This specification is automatically maintained by the spec-extractor workflow.