Docs/telemetryv1.0.0-rc.1

Telemetry — Anomaly Detection

Overview

RollingAnomalyDetector implements statistical anomaly detection using a rolling window of recent observations and a z-score calculation. It is designed for streaming metric data where the baseline shifts over time. Each metric tracked by the detector maintains its own independent window.

The detector is used internally by AIObservabilityWorker when an alert is configured with mode: 'anomaly', but it can also be used standalone to inspect anomaly results directly.

Key Concepts

  • Rolling window — a FIFO buffer of the last windowSize values for each metric. When the buffer is full, the oldest value is dropped before the new one is added.
  • Z-score — measures how many standard deviations a value is from the window mean: zScore = (value - mean) / stdDev. A z-score of 0 means the value is exactly average.
  • Sensitivity — the z-score magnitude threshold. A value is flagged as an anomaly when |zScore| >= sensitivity.
  • minSamples — the minimum number of values the window must contain before the detector will flag anything. This prevents false positives on startup when the window is nearly empty and statistics are unreliable.

API Reference

RollingAnomalyDetector

import { RollingAnomalyDetector } from '@tagna/udiot/telemetry';

const detector = new RollingAnomalyDetector(options?);

Constructor options: AnomalyDetectionOptions

Field Type Default Description
windowSize number 50 Maximum number of samples retained per metric
minSamples number 8 Minimum samples required before anomalies are flagged
sensitivity number 2.5 Z-score threshold; values with `

Constructor options serve as instance defaults. They can be overridden per call via the options argument on .detect().


.detect(metricName, value, options?): AnomalyDetectionResult

Feed a new observation into the detector and receive a result.

Parameter Type Description
metricName string Identifies which rolling window to use
value number The observed metric value
options Partial<AnomalyDetectionOptions> Per-call overrides for windowSize, minSamples, sensitivity

The value is added to the window before statistics are computed. If the window exceeds windowSize, the oldest entry is removed first.

.reset(metricName?): void

  • Called with no argument: clears all windows for all metrics.
  • Called with a metricName: clears only that metric's window.

AnomalyDetectionResult

Field Type Description
metricName string The metric name passed to .detect()
value number The observed value
mean number Rolling window mean at the time of detection
stdDev number Rolling window standard deviation
zScore number (value - mean) / stdDev; 0 when stdDev === 0
sensitivity number The z-score threshold that was applied
sampleSize number Number of values currently in the window (after adding this value)
timestamp number Unix timestamp in ms when .detect() was called
isAnomaly boolean true when `sampleSize >= minSamples &&

How the rolling window works

Initial window (windowSize=5):  []

Feed: 10  → [10]
Feed: 11  → [10, 11]
Feed: 10  → [10, 11, 10]
Feed: 12  → [10, 11, 10, 12]
Feed: 11  → [10, 11, 10, 12, 11]  ← window full

Feed: 13  → [11, 10, 12, 11, 13]  ← 10 (oldest) dropped

The mean and stdDev are recalculated from scratch over the current window on every call — no incremental update is performed.

Z-score formula

mean   = sum(window) / windowSize
stdDev = sqrt( sum( (x - mean)^2 ) / windowSize )
zScore = (value - mean) / stdDev

When stdDev === 0 (all values in the window are identical), zScore is set to 0 to avoid division-by-zero. A spike on a perfectly flat baseline will only be detected once a second distinct value enters the window.

Tuning sensitivity

Sensitivity Detection behavior
2.0 More sensitive — flags values that are 2σ away from the mean. Higher false-positive rate.
2.5 Default. Good general-purpose balance.
3.0 Stricter — only flags clear outliers (3σ). Fewer false positives on noisy metrics.
> 3.5 Very strict. Only catches extreme spikes.

Reduce sensitivity if you are getting too many false positives on a naturally noisy metric. Raise it if genuine anomalies are being missed.

Example: detecting a memory spike

import { RollingAnomalyDetector } from '@tagna/udiot/telemetry';

const detector = new RollingAnomalyDetector({
  windowSize: 50,
  minSamples: 8,
  sensitivity: 2.5,
});

// Simulate 50 normal memory readings (around 512 MB)
for (let i = 0; i < 50; i++) {
  const normalValue = 512 + (Math.random() * 20 - 10); // 502–522 MB
  const result = detector.detect('process.memory_mb', normalValue);
  console.log(`value=${normalValue.toFixed(1)}, isAnomaly=${result.isAnomaly}`);
  // isAnomaly: false for all normal readings
}

// Inject a spike
const spikeResult = detector.detect('process.memory_mb', 950);
console.log(spikeResult);
// {
//   metricName: 'process.memory_mb',
//   value: 950,
//   mean: ~512,
//   stdDev: ~6,
//   zScore: ~73,   ← far above sensitivity of 2.5
//   isAnomaly: true,
//   sampleSize: 50,
//   sensitivity: 2.5,
//   timestamp: 1711094400000
// }

Integration with AIObservabilityWorker

When an alert rule has mode: 'anomaly', AIObservabilityWorker calls anomalyDetector.detect(metricName, metricValue, alertOptions) on each evaluation cycle. The windowSize, minSamples, and sensitivity from AIAlertOptions are forwarded as per-call overrides. The full AnomalyDetectionResult is attached to the AlertNotification as the anomaly field.

You can inject a custom AnomalyDetector implementation into the worker:

import { AIObservabilityWorker, RollingAnomalyDetector } from '@tagna/udiot/telemetry';

const worker = new AIObservabilityWorker({
  anomalyDetector: new RollingAnomalyDetector({
    windowSize: 100,
    minSamples: 20,
    sensitivity: 3.0,
  }),
});

Common Patterns

Per-metric windows — the detector automatically maintains a separate window for each unique metricName. You do not need to create separate detector instances per metric.

Resetting after deployment — call detector.reset() when you deploy a new version that significantly changes a metric's baseline. Without resetting, old values from before the deployment will remain in the window and may trigger spurious anomalies during the transition.

Testing — use a small windowSize (e.g., 5) and minSamples (e.g., 3) in unit tests so you can warm up the detector with a few values without needing 50 data points.

Pitfalls

  • isAnomaly is always false until the window has at least minSamples entries. The result is still returned with valid zScore and mean fields — you can use those for inspection even before the threshold is met.
  • The detector uses population variance (divides by n, not n-1). For small windows this slightly underestimates the true variance. The default minSamples: 8 mitigates this.
  • Values are stored in an in-process array. On process restart, the window is empty. If you need persistent baselines across restarts, you must implement your own AnomalyDetector backed by a database or cache.
From the canonical UDIOT public documentation.Try it in the playground ↗