xiaohongshu
xiaohongshu
detecting-anomalies
Detect anomalies in metrics and time-series data using OPAL statistical methods. Use when you need to identify unusual patterns, spikes, drops, or outliers in observability data. Covers statistical outlier detection (Z-score, IQR), threshold-based alerts, rate-of-change detection with window func...
Full skill instructions
Detect anomalies in metrics and time-series data using OPAL statistical methods. This skill covers multiple detection patterns for different types of anomalies: statistical outliers, sudden spikes/drops, threshold violations, and deviations from moving baselines.
Use when you need to:
Statistical Methods (good for gradual changes):
Temporal Methods (good for sudden changes):
Threshold Methods (simple and interpretable):
What type of anomaly?
├─ Known threshold (e.g., "CPU > 90%")
│ └─> Threshold-Based Detection (Pattern 3)
│
├─ Statistical outliers (unusual values)
│ ├─ Normal distribution?
│ │ └─> Z-Score Method (Pattern 1)
│ │
│ └─ Skewed distribution?
│ └─> IQR Method (Pattern 2)
│
├─ Sudden spikes/drops
│ └─> Rate of Change (Pattern 4)
│
└─ Deviation from recent baseline
└─> Moving Average (Pattern 5)
Concept: Detect values beyond N standard deviations from the mean
When to use:
Query:
align 5m, metric_value:sum(m("span_call_count_5m"))
| aggregate avg_val:avg(metric_value),
stddev_val:stddev(metric_value),
current_val:sum(metric_value),
group_by(service_name)
| make_col z_score:(current_val - avg_val) / stddev_val
| make_col upper_bound:avg_val + (2 * stddev_val)
| make_col lower_bound:avg_val - (2 * stddev_val)
| make_col is_anomaly:if(z_score > 2 or z_score < -2, true, false)
| filter is_anomaly = true
| sort desc(z_score)
| limit 20
Threshold tuning:
z > 2 or z < -2: ~95% confidence (moderate sensitivity)z > 3 or z < -3: ~99.7% confidence (low false positives)z > 1.5 or z < -1.5: ~87% confidence (high sensitivity)Example result:
service_name: featureflagservice
avg_val: 11.5
stddev_val: 13.9
current_val: 46
z_score: 2.48
is_anomaly: true
Pros:
Cons:
Concept: Detect values beyond the interquartile range using Tukey's fences
When to use:
Query:
align 5m, metric_value:sum(m("span_call_count_5m"))
| aggregate p25:percentile(metric_value, 0.25),
p75:percentile(metric_value, 0.75),
current_val:sum(metric_value),
group_by(service_name)
| make_col iqr:p75 - p25
| make_col upper_fence:p75 + (1.5 * iqr)
| make_col lower_fence:p25 - (1.5 * iqr)
| make_col is_outlier:if(current_val > upper_fence or current_val < lower_fence, true, false)
| filter is_outlier = true
| sort desc(current_val)
| limit 20
Threshold tuning:
1.5 * IQR: Standard outliers (moderate sensitivity)3 * IQR: Extreme outliers (low false positives)1 * IQR: More sensitive detectionExample result:
service_name: featureflagservice
p25: 1.75
p75: 16.75
iqr: 15
upper_fence: 39.25
current_val: 46
is_outlier: true
Pros:
Cons:
Concept: Simple comparison against fixed or dynamic thresholds
When to use:
Static Threshold:
align options(bins: 1), total_calls:sum(m("span_call_count_5m"))
aggregate current_rate:sum(total_calls), group_by(service_name)
make_col threshold:100000
| make_col is_high:if(current_rate > threshold, true, false)
| filter is_high = true
| sort desc(current_rate)
Dynamic Threshold (baseline comparison):
align options(bins: 1), metric_value:sum(m("span_call_count_5m"))
aggregate baseline:avg(metric_value),
current:sum(metric_value),
group_by(service_name)
make_col threshold:baseline * 1.5
| make_col is_anomaly:if(current > threshold, true, false)
| filter is_anomaly = true
Threshold multiplier guidance:
1.5x: High sensitivity (more alerts)2x: Moderate sensitivity (balanced)3x: Low sensitivity (only major spikes)Pros:
Cons:
Concept: Detect sudden spikes or drops by comparing to previous time period
When to use:
Query:
align 5m, metric_value:sum(m("span_call_count_5m"))
| make_col previous_value:window(lag(metric_value, 1), group_by(service_name))
| make_col value_change:metric_value - previous_value
| make_col pct_change:if(previous_value > 0, (value_change / previous_value) * 100, 0)
| make_col is_spike:if(pct_change > 100 or pct_change < -50, true, false)
| filter is_spike = true
| sort desc(pct_change)
| limit 20
Critical syntax: Use window(lag(...), group_by(...)) NOT lag(...) over (partition by...)
Threshold examples:
pct_change > 100: 2x increase (doubling)pct_change > 200: 3x increasepct_change < -50: 50% droppct_change < -75: 75% dropExample result:
service_name: frontend
metric_value: 50
previous_value: 2
value_change: 48
pct_change: 2400
is_spike: true
Pros:
Cons:
Best practices:
Concept: Compare current value to recent moving average using sliding window
When to use:
Query:
align 5m, metric_value:sum(m("span_call_count_5m"))
| make_col moving_avg:window(avg(metric_value), group_by(service_name), frame(back:30m))
| make_col deviation:metric_value - moving_avg
| make_col pct_deviation:if(moving_avg > 0, (deviation / moving_avg) * 100, 0)
| make_col is_anomaly:if(pct_deviation > 50 or pct_deviation < -50, true, false)
| filter is_anomaly = true
| sort desc(pct_deviation)
| limit 20
Frame options:
frame(back:10m): Short-term baseline (10-minute average)frame(back:30m): Medium-term baseline (30-minute average)frame(back:1h): Longer-term baseline (1-hour average)Deviation thresholds:
> 50%: Moderate deviation from recent average> 100%: Doubling compared to recent average> 25%: More sensitive detectionPros:
Cons:
Best practices:
Concept: Compare current value to historical percentile (p95, p99)
When to use:
Query:
align 5m, metric_value:sum(m("span_call_count_5m"))
| aggregate p95:percentile(metric_value, 0.95),
p99:percentile(metric_value, 0.99),
current:sum(metric_value),
group_by(service_name)
| make_col is_anomaly:if(current > p95, true, false)
| filter is_anomaly = true
| sort desc(current)
Percentile choices:
p95: Detect top 5% unusual values (moderate sensitivity)p99: Detect top 1% extreme values (low false positives)p90: Detect top 10% (high sensitivity)Pros:
Cons:
Increase confidence by requiring multiple methods to agree:
align 5m, metric_value:sum(m("span_call_count_5m"))
| aggregate avg_val:avg(metric_value),
stddev_val:stddev(metric_value),
p95:percentile(metric_value, 0.95),
current:sum(metric_value),
group_by(service_name)
| make_col z_score:(current - avg_val) / stddev_val
| make_col is_zscore_anomaly:if(z_score > 2 or z_score < -2, true, false)
| make_col is_percentile_anomaly:if(current > p95, true, false)
| make_col is_anomaly:if(is_zscore_anomaly = true and is_percentile_anomaly = true, true, false)
| filter is_anomaly = true
Use case: Reduce false positives by requiring consensus
Detect anomalies across correlated metrics:
align options(bins: 1),
requests:sum(m("span_call_count_5m")),
errors:sum(m("span_error_count_5m"))
aggregate total_requests:sum(requests),
total_errors:sum(errors),
group_by(service_name)
make_col error_rate:if(total_requests > 0, (float64(total_errors) / float64(total_requests)) * 100, 0)
| make_col threshold:1.0
| make_col is_high_error:if(error_rate > threshold and total_requests > 100, true, false)
| filter is_high_error = true
Use case: Alert when error rate AND request volume both indicate issues
Track anomalies over time using timechart:
align 5m, metric_value:sum(m("span_call_count_5m"))
| aggregate avg_val:avg(metric_value),
stddev_val:stddev(metric_value),
current:sum(metric_value),
group_by(service_name)
| make_col z_score:(current - avg_val) / stddev_val
| make_col is_anomaly:if(z_score > 2 or z_score < -2, true, false)
| filter is_anomaly = true
Result: Multiple rows per service showing anomalies across time buckets
Use case: Visualize when and how often anomalies occur
CRITICAL: OPAL uses window() function, NOT SQL OVER clause!
✅ CORRECT Syntax:
make_col prev:window(lag(column, 1), group_by(dimension))
make_col next:window(lead(column, 1), group_by(dimension))
make_col moving_avg:window(avg(column), group_by(dimension), frame(back:30m))
❌ WRONG Syntax (SQL-style):
lag(column, 1) over (partition by dimension order by time)
Window function components:
lag(column, offset): Access previous row valuelead(column, offset): Access next row valuegroup_by(dimension): Partition by dimensionframe(back:duration): Sliding window lookback period❌ WRONG - Cannot reference derived column in same make_col:
make_col upper_bound:avg + (2 * stddev),
is_anomaly:if(value > upper_bound, true, false)
✅ CORRECT - Use separate make_col statements:
make_col upper_bound:avg + (2 * stddev)
| make_col is_anomaly:if(value > upper_bound, true, false)
Summary (one row per group):
align options(bins: 1), metric:sum(m("metric_name"))
aggregate result:sum(metric), group_by(dimension)
Note: No pipe | between align options(bins: 1) and aggregate!
Time-series (multiple rows per group):
align 5m, metric:sum(m("metric_name"))
| aggregate result:sum(metric), group_by(dimension)
Note: Pipe | required between align 5m and aggregate!
For comparing entire periods (e.g., "this hour" vs "exactly 1 hour ago"), use the timeshift + union pattern with subquery definitions.
Key Difference:
window(lag()): Compares adjacent buckets (5-min to 5-min, approximate)timeshift + union: Compares entire periods (exact time offset: 1h, 1d, 1w)Working Example (✅ works in all query contexts):
@current <- @ {
align rate:sum(m("span_call_count_5m"))
aggregate current_sum:sum(rate), group_by(service_name)
}
@previous <- @ {
timeshift 1h # Shift BEFORE align!
align rate:sum(m("span_call_count_5m"))
aggregate prev_sum:sum(rate), group_by(service_name)
}
@combined <- @current {
union @previous
aggregate current:any_not_null(current_sum),
previous:any_not_null(prev_sum),
group_by(service_name)
make_col change:current - previous
make_col pct_change:if(previous > 0, (change / previous) * 100, 0)
make_col abs_pct_change:if(pct_change < 0, -pct_change, pct_change)
}
<- @combined {
filter abs_pct_change > 50
sort desc(abs_pct_change)
limit 10
}
Critical Points:
@subquery <- @: Use @ alone to reference the primary input datasettimeshift BEFORE align: Operates on raw data, shifts timestamps before aggregationany_not_null() collapses union: Combines current/previous into single row per dimensionUse Cases:
timeshift 1d)timeshift 7d)timeshift 1h)timeshift 30d)Tested Results:
Comparison with window(lag()):
| Feature | window(lag(rate, N)) | timeshift + union |
|---|---|---|
| Time precision | Approximate (N buckets back) | Exact (fixed time offset) |
| Example | lag(rate, 12) ≈ 1 hour (if buckets are 5min) | timeshift 1h = exactly 60 minutes |
| Complexity | Simple, one query | More complex, subqueries + union |
| Use case | Real-time spike detection | Period-over-period reporting |
| Best for | "Current vs previous bucket" | "Current vs same time yesterday" |
When to use each:
window(lag()) for: Real-time alerts, simple spike detection, fast queriestimeshift + union for: Exact period comparison, day-over-day reports, SLA trackingCause: Using SQL window function syntax instead of OPAL syntax
Solution: Use window(lag(...), group_by(...)) instead of lag(...) over (...)
Example:
# WRONG
make_col prev:lag(value, 1) over (partition by service order by time)
# CORRECT
make_col prev:window(lag(value, 1), group_by(service))
Cause: Threshold too sensitive or baseline includes anomalies
Solutions:
Example with minimum value filter:
| make_col is_spike:if(pct_change > 100 and metric_value > 10, true, false)
Cause: Threshold too strict or wrong detection method for data type
Solutions:
Cause: Calculating percentage change when previous value is zero or very small
Solution: Add conditional check for minimum denominator:
make_col pct_change:if(previous_value > 5, (value_change / previous_value) * 100, 0)
Cause: First row in group has no previous value for lag()
Solution: This is expected behavior - first row will have null for lag(). Filter nulls or provide default:
make_col previous_value:window(lag(metric_value, 1), group_by(service_name))
| filter not is_null(previous_value)
Or use default value (though not directly supported in current lag syntax):
make_col pct_change:if(is_null(previous_value), 0, (value_change / previous_value) * 100)
Choose detection method based on anomaly type and data distribution
OPAL window functions use different syntax from SQL
window(lag(...), group_by(...)) NOT lag(...) over (...)Combine multiple methods to reduce false positives
Tune thresholds based on metric characteristics
Derived columns require separate make_col statements
Frame specification enables sliding window calculations
frame(back:30m) for 30-minute moving averageMetrics queries have two distinct patterns
options(bins: 1) for summary (no pipe before aggregate)align 5m for time-series (pipe required before aggregate)Statistical methods work best with sufficient historical data
Rate-of-change detection is powerful but requires careful tuning
Test detection patterns against historical data
Use detecting-anomalies skill when:
Cross-references:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience