laceBaseline¶
Response timing and size baseline monitoring and spike detection for Lace probes. Bundled with every executor as builtin:laceBaseline.
Purpose¶
laceBaseline tracks rolling averages of HTTP response metrics (six timings plus response size) across probe runs. When a metric deviates significantly from its baseline average, the extension dispatches a structured() notification via laceNotifications.
This extension demonstrates extension composition -- it depends on laceNotifications and calls its exposed pushNotification() function to emit spike alerts. All spike data flows through actions.notifications; the extension does not declare its own action array.
Activation¶
Or per run: --enable-extension laceNotifications --enable-extension laceBaseline.
Both extensions must be active. laceBaseline declares require = ["laceNotifications"] and will fail startup if laceNotifications is absent.
Tracked metrics¶
Seven fields from the response object -- six timings and the response size. These are the only metrics the extension keeps sums for:
| Metric | Description |
|---|---|
responseTimeMs |
Total response time |
dnsMs |
DNS resolution |
connectMs |
TCP connection |
tlsMs |
TLS handshake (0 for plain HTTP) |
ttfbMs |
Time to first byte |
transferMs |
Body transfer |
sizeBytes |
Response body size |
Stats model¶
Stats are stored in result.runVars["laceBaseline.stats"] and carried across runs via the prev mechanism:
{
"count": 10,
"sums": {
"responseTimeMs": 1450.0,
"dnsMs": 120.0,
"connectMs": 340.0,
"tlsMs": 280.0,
"ttfbMs": 980.0,
"transferMs": 470.0,
"sizeBytes": 51200.0
}
}
The average for any metric is sums[metric] / count. The model stores count and sums rather than averages to allow incremental updates without precision loss.
The backend persists the full result after each run and passes it as prev on the next run. The extension reads prev.runVars["laceBaseline.stats"] for the accumulated baseline and emits updated stats at the end of each run.
Spike detection¶
On each successful call (the detect_spikes_per_call rule, registered as on = ["call after laceNotifications"]):
- Read baseline stats from
prev.runVars["laceBaseline.stats"]. - If
count < min_entries, skip detection (not enough data yet). - For each tracked metric, compute
average = sum / count. - If
actual > average * spike_multiplierandaverage > 0, dispatch astructured()notification vialaceNotifications.pushNotification(). A null metric value is skipped.
The average > 0 guard prevents false positives for metrics that are normally zero (e.g. tlsMs for plain HTTP).
Spike detection rule¶
when call.outcome eq "success"
when not is_null(call.response)
let $stats = prev.runVars["laceBaseline.stats"]
when not is_null($stats)
when not is_null($stats.count)
when $stats.count gte config.min_entries
let $mult = config.spike_multiplier
let $resp = call.response
check_spike($stats, "responseTimeMs", $resp.responseTimeMs, call.index, $mult)
check_spike($stats, "dnsMs", $resp.dnsMs, call.index, $mult)
check_spike($stats, "connectMs", $resp.connectMs, call.index, $mult)
check_spike($stats, "tlsMs", $resp.tlsMs, call.index, $mult)
check_spike($stats, "ttfbMs", $resp.ttfbMs, call.index, $mult)
check_spike($stats, "transferMs", $resp.transferMs, call.index, $mult)
check_spike($stats, "sizeBytes", $resp.sizeBytes, call.index, $mult)
Stats accumulation¶
At the end of each run (the update_stats rule on the script hook):
- Read previous stats from
prev.runVars["laceBaseline.stats"](or initialize from zeros). - Iterate all calls from the current run.
- For each successful call with a response, add its metric values to the rolling sums (a null value counts as
0.0) and increment count. - Emit updated stats to
result.runVars["laceBaseline.stats"].
The accumulate_stats function (params prev_stats, calls, spike_action, spike_multiplier) uses the set statement for in-loop accumulation. Abridged to one metric -- the bundled body repeats each line for all seven:
let $count = is_null(prev_stats) ? 0 : (is_null(prev_stats.count) ? 0 : prev_stats.count)
let $s_rt = is_null(prev_stats) ? 0.0 : (is_null(prev_stats.sums) ? 0.0 : (is_null(prev_stats.sums.responseTimeMs) ? 0.0 : prev_stats.sums.responseTimeMs))
# ... one initializer per metric
for $call in calls:
when $call.outcome eq "success":
when not is_null($call.response):
let $skip = spike_action eq "skip" ? check_any_spike(prev_stats, $call.response, spike_multiplier) : false
when not $skip:
set $count = $count + 1
set $s_rt = $s_rt + (is_null($call.response.responseTimeMs) ? 0.0 : $call.response.responseTimeMs)
# ... one set per metric
return {
count: $count,
sums: {
responseTimeMs: $s_rt
# ... one entry per metric
}
}
When spike_action = "skip", calls where any metric would spike are excluded from the rolling average, preventing one-off anomalies from polluting the baseline.
Configuration¶
| Key | Type | Default | Description |
|---|---|---|---|
min_entries |
int | 5 |
Minimum data points before spike detection activates |
spike_multiplier |
float | 3.0 |
Metric must exceed average x multiplier to be a spike |
spike_action |
string | "include" |
"include" or "skip" -- whether spiking calls are included in the rolling average |
Override in lace.config:
[executor]
extensions = ["laceNotifications", "laceBaseline"]
[extensions.laceBaseline]
min_entries = 10
spike_multiplier = 2.5
spike_action = "skip"
Exposed functions¶
check_spike(stats, metric_name, actual, call_index, multiplier)¶
Checks a single metric against the baseline and dispatches a notification if the spike threshold is exceeded. Other extensions that require = ["laceBaseline"] can call it -- for example to check with a different multiplier:
let $stats = prev.runVars["laceBaseline.stats"]
laceBaseline.check_spike($stats, "ttfbMs", call.response?.ttfbMs, call.index, 2.0)
metric_name must be one of the seven tracked metrics above. The stats only hold sums for those, so any other name (a custom metric) finds no sum and never detects a spike.
Returns true if a spike was detected and a notification dispatched, null otherwise (including when actual or stats is null, count is below 1, or the average is not positive).
Notification format¶
Spike notifications use structured() and are emitted into actions.notifications with trigger: "baseline_spike":
{
"callIndex": 0,
"conditionIndex": -1,
"trigger": "baseline_spike",
"scope": "responseTimeMs",
"notification": {
"tag": "structured",
"data": {
"metric": "responseTimeMs",
"actual": 1500,
"average": 145.0,
"threshold": 435.0,
"multiplier": 3.0
}
}
}
The backend receives these as regular notification events and can format, group, and deliver them alongside other notifications.