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
431 lines
16 KiB
Markdown
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) |