Files
comet-ml--opik/apps/opik-backend/docs/webhook-event-debouncing-implementation.md
wehub-resource-sync 5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:44 +08:00

431 lines
16 KiB
Markdown

# Webhook Alert Debouncing Implementation
## Overview
This document describes the implementation of webhook alert debouncing in the Opik backend. The system aggregates multiple alert events within a configurable time window and sends consolidated webhook notifications.
## Architecture
### Core Components
```
┌────────────────────────────────────────────────────────────────────────┐
│ Alert Evaluation │
│ (Checks if events match alert config and adds to bucket) │
└─────────────────────────────────┬──────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ AlertBucketService │
│ (Manages alert event buckets in Redis) │
│ │
│ • addEventToBucket() - Adds event to Redis bucket │
│ • getBucketsReadyToProcess() - Returns buckets past debounce window │
│ • getBucketEventIds() - Retrieves aggregated event IDs │
│ • deleteBucket() - Removes processed bucket │
└─────────────────────────────────┬──────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ AlertJob │
│ (Scheduled job runs every 5 seconds) │
│ │
│ 1. Check buckets ready to process │
│ 2. For each ready bucket: │
│ • Retrieve bucket data (event IDs, workspace ID) │
│ • Fetch alert configuration from AlertService │
│ • Create payload with alert and event data │
│ • Publish webhook via WebhookPublisher │
│ • Delete bucket │
└─────────────────────────────────┬──────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────┐
│ WebhookPublisher │
│ (Publishes webhooks to message queue) │
│ │
│ • publishWebhookEvent() - Publishes event to webhook queue │
└────────────────────────────────────────────────────────────────────────┘
```
### Separation of Concerns
| Component | Responsibility |
|-----------|---------------|
| **Alert Evaluation** | Checks if event matches alert config, adds to bucket via AlertBucketService |
| **AlertBucketService** | Stores and retrieves alert event buckets in Redis with workspace context |
| **AlertJob** | Scheduled job that checks buckets, retrieves alert config from Alert Service, creates payload, publishes webhooks |
| **WebhookPublisher** | **ONLY publishes webhook events** - no knowledge of aggregation |
## Data Flow
### 1. Event Occurs
```java
// Alert evaluation logic (to be implemented)
if (eventMatchesAlertConfig(event, alert)) {
alertBucketService.addEventToBucket(
alert.getId(),
workspaceId,
eventType,
event.getId()
).subscribe();
}
```
### 2. Event Storage in Redis
**Bucket Key Format:** `alert_bucket:{alertId}:{eventType}`
**Bucket Data Structure:**
```json
{
"eventIds": "[\"event-1\", \"event-2\", \"event-3\"]",
"firstSeen": "1704067200000",
"windowSize": "60000",
"workspaceId": "workspace-123"
}
```
**Note:** The bucket stores:
- `windowSize`: The debouncing window (in milliseconds) active when the bucket was created
- `workspaceId`: The workspace ID for retrieving alert configuration without RequestContext
These fields ensure that configuration changes do not affect existing buckets and allow background jobs to access alert data.
### 3. Background Processing
**AlertJob** (runs every 5 seconds):
```java
@Every("5s")
public class AlertJob extends Job {
@Override
public void doJob(JobExecutionContext context) {
// Check buckets ready to process
bucketService.getBucketsReadyToProcess()
.flatMap(this::processBucket)
.blockLast();
}
}
```
### 4. Webhook Notification
When a bucket is ready:
```java
private Mono<Void> processBucket(String bucketKey) {
// 1. Parse alert ID and event type
// 2. Retrieve alert configuration from AlertService
// 3. Get aggregated event IDs from bucket
// 4. Create consolidated webhook event
// 5. Send via WebhookSubscriber
// 6. Delete bucket
}
```
## Configuration
### Debouncing Settings
```yaml
webhook:
debouncing:
enabled: true
windowSize: 60 seconds # Time to wait before sending consolidated notification
bucketTtl: 3 minutes # Bucket expiration time (safety cleanup)
```
### Alert Configuration
```java
Alert {
id: UUID
name: String
enabled: Boolean
webhook: {
url: String
headers: Map<String, String>
secretToken: String
}
triggers: List<AlertTrigger> {
eventType: AlertEventType
triggerConfigs: List<TriggerConfig>
}
}
```
## Implementation Details
### Redis Bucket Management
**AlertBucketService:**
```java
public Mono<Void> addEventToBucket(
UUID alertId,
String workspaceId,
AlertEventType eventType,
String eventId) {
String bucketKey = "alert_bucket:" + alertId + ":" + eventType.getValue();
RMapReactive<String, String> bucket = redissonClient.getMap(bucketKey);
long currentWindowSizeMillis = webhookConfig.getDebouncing().getWindowSize().toMilliseconds();
// Add event ID to set
// If first event in bucket:
// - Store firstSeen timestamp
// - Store windowSize (preserves config at creation time)
// - Store workspaceId (enables background job to retrieve alert)
// - Set bucket TTL (ONLY on first event, NOT refreshed on subsequent events)
// Subsequent events:
// - DO NOT update windowSize or workspaceId (preserves original config and context)
// - DO NOT refresh TTL (bucket expires based on original TTL)
}
public Flux<String> getBucketsReadyToProcess() {
// Scan all alert_bucket:* keys
// For each bucket:
// - Retrieve firstSeen timestamp AND windowSize from bucket
// - If (now - firstSeen) >= stored windowSize
// - Return bucket key
// Note: Uses the windowSize stored in the bucket, not current config
}
public Mono<BucketData> getBucketData(String bucketKey) {
// Retrieves complete bucket data including:
// - eventIds: Set of aggregated event IDs
// - firstSeen: Timestamp when first event was added
// - windowSize: Debouncing window size (milliseconds)
// - workspaceId: Workspace ID for accessing alert configuration
}
```
### Timestamp-Based Debouncing
```
Event Timeline:
t0: First event arrives → Store firstSeen = t0, windowSize = 60000ms, Set TTL = 3 minutes
t1: Second event arrives → Keep firstSeen = t0, Keep windowSize = 60000ms, Keep TTL (not refreshed)
t2: Third event arrives → Keep firstSeen = t0, Keep windowSize = 60000ms, Keep TTL (not refreshed)
...
t60: AlertJob runs → (t60 - t0) >= 60s → Send consolidated webhook
t60+: Bucket deleted by AlertJob after processing
(If not deleted by AlertJob, Redis automatically expires bucket after TTL)
```
**TTL Behavior:**
- TTL is set ONLY when the first event is added to a new bucket
- Subsequent events to the same bucket do NOT refresh the TTL
- This prevents buckets from living indefinitely if events keep arriving
- The `bucketTtl` configuration (default: 3 minutes) is the safety cleanup mechanism
- After processing, buckets are explicitly deleted by AlertJob
### Handling Configuration Changes
When the debouncing `windowSize` configuration is changed, the system handles existing and new buckets correctly:
**Scenario:**
1. Alert has debouncing window = 60 seconds
2. Bucket A is created at t0 with events
3. Configuration is changed to debouncing window = 120 seconds
4. New events for same alert arrive
**Behavior:**
- **Bucket A** (created with 60s window):
- Continues to use its stored windowSize = 60000ms
- Will be processed when (now - firstSeen) >= 60s
- Not affected by configuration change
- **New Bucket B** (created after config change):
- Stores new windowSize = 120000ms
- Will be processed when (now - firstSeen) >= 120s
- Uses the new configuration
**Key Implementation Details:**
1. `addEventToBucket()` stores `windowSize` only for the first event in a bucket
2. Subsequent events to the same bucket preserve the original `windowSize`
3. `getBucketsReadyToProcess()` reads the `windowSize` from each bucket (not from current config)
4. This ensures existing buckets are processed with their original configuration
**Test Coverage:**
See `AlertBucketServiceTest.java` for comprehensive tests covering:
- Window size storage with first event
- Window size preservation with subsequent events
- Multiple buckets with different window sizes being processed independently
- Configuration changes creating new buckets while existing ones continue with original settings
### Consolidated Webhook Payload
```json
{
"id": "alert-{alertId}-{uuid}",
"eventType": "alert.fired",
"alertId": "{alertId}",
"alertName": "High Error Rate",
"workspaceId": "{workspaceId}",
"userName": "system",
"url": "{webhookUrl}",
"payload": {
"alertId": "{alertId}",
"alertName": "High Error Rate",
"eventType": "trace:errors",
"eventIds": ["event-1", "event-2", "event-3"],
"eventCount": 3,
"aggregationType": "consolidated",
"message": "Alert 'High Error Rate': 3 trace:errors events aggregated"
},
"headers": {
"X-Custom-Header": "value"
}
}
```
## Job Management
### AlertJob Schedule
- **Frequency**: Every 5 seconds
- **Concurrency**: Disabled (`@DisallowConcurrentExecution`)
- **Purpose**: Check buckets and trigger webhooks for events past debouncing window
### Job Execution Flow
```java
1. getBucketsReadyToProcess() Flux<String> of bucket keys
2. For each bucketKey:
a. Parse alertId and eventType
b. Retrieve Alert configuration
c. Get aggregated event IDs
d. Create consolidated webhook event
e. Send via WebhookSubscriber
f. Delete bucket
3. Error handling: Continue on error, log failures
```
## Error Handling
### Bucket Processing Errors
```java
bucketService.getBucketsReadyToProcess()
.flatMap(this::processBucket)
.onErrorContinue((throwable, bucketKey) -> {
log.error("Failed to process bucket '{}': {}",
bucketKey, throwable.getMessage(), throwable);
})
.blockLast();
```
### Alert Configuration Errors
```java
// If alert is disabled
if (!alert.enabled()) {
log.warn("Alert '{}' is disabled, skipping webhook", alert.id());
return Mono.empty();
}
// If webhook configuration is missing
if (alert.webhook() == null || alert.webhook().url() == null) {
log.error("Alert '{}' has no webhook configuration, skipping", alert.id());
return Mono.empty();
}
```
### Webhook Sending Errors
```java
return webhookSubscriber.sendWebhook(webhookEvent)
.doOnSuccess(__ -> log.info("Successfully sent webhook for alert '{}'", alert.id()))
.doOnError(error -> log.error("Failed to send webhook for alert '{}': {}",
alert.id(), error.getMessage(), error));
```
## Testing
### Unit Tests
```java
@Test
void shouldAddEventToBucket() {
// Test event addition to Redis bucket
}
@Test
void shouldReturnBucketsReadyToProcess() {
// Test bucket readiness based on firstSeen timestamp
}
@Test
void shouldProcessBucketAndSendWebhook() {
// Test AlertJob processing and webhook sending
}
@Test
void shouldHandleDisabledAlert() {
// Test skipping webhooks for disabled alerts
}
```
### Integration Tests
- Test complete flow from event to webhook
- Test bucket TTL and cleanup
- Test concurrent event additions to same bucket
- Test AlertJob execution with multiple ready buckets
## Future Enhancements
1. **Alert Configuration Management**: Integration with Alert CRUD operations
2. **Metrics**: Track aggregation stats, publish latency, success/failure rates
3. **Rate Limiting**: Implement per-alert webhook rate limits
4. **Batch Size Limits**: Configurable maximum events per consolidated webhook
## Key Files
### Core Implementation
- `AlertBucketService.java` - Redis bucket management
- `AlertJob.java` - Scheduled job for processing buckets
- `WebhookPublisher.java` - Webhook event publisher
- `WebhookConfig.java` - Debouncing configuration
- `AlertService.java` - Alert configuration service (includes `getByIdAndWorkspace`)
### Data Models
- `Alert.java` - Alert configuration
- `AlertTrigger.java` - Alert trigger configuration
- `WebhookEvent.java` - Webhook event structure
- `WebhookEventTypes.java` - Event type enum (includes `ALERT_FIRED`)
### Services
- `AlertService.java` - Alert CRUD operations
- `AlertDAO.java` - Alert database access
## Version History
### Version 1.2 (Current)
- **Workspace Context**: Added `workspaceId` storage in buckets for background job access
- AlertJob can now retrieve alert configuration without RequestContext
- Added `getByIdAndWorkspace()` method to AlertService
- **AlertJob Updates**: Uses `getBucketData()` to retrieve complete bucket information
- Fetches workspace ID from bucket data
- Retrieves alert configuration using workspace context
- **Documentation**: Updated to use `WebhookPublisher` instead of `WebhookSubscriber`
### Version 1.1
- **Configuration Change Handling**: Buckets now store their creation-time `windowSize`
- Existing buckets continue to use their original debouncing window
- New buckets created after configuration changes use the updated window
- Added comprehensive test coverage in `AlertBucketServiceTest.java`
- Enhanced bucket data structure to include `windowSize` field
- Modified `getBucketsReadyToProcess()` to use stored window size instead of current config
### Version 1.0 (2024-01-01)
- Initial implementation
- Timestamp-based debouncing
- 5-second AlertJob schedule
- WebhookPublisher only publishes events to queue
- AlertBucketService manages Redis storage with workspace context
- AlertJob orchestrates bucket processing, fetches alert data, and publishes webhooks
- Alert data is retrieved using workspaceId from bucket (no RequestContext needed)