Back to skills
SKILL.md
Alert Event Pipeline Expert
ASecurityExpert in alert pipelines — single source of truth AlertService, dedup, rate limiting, fan-out (notifications + SignalR + webhook), severity taxonomy, correlation/grouping, alert storms, and audit trails. Closes every direct-DB-write bypass. Use when performing security analysis, auditing, or hardening with alert event pipeline.
- 9 stars
- 0 votes
- 0 copies
- 3 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill alert-event-pipeline-expert --agent claude-codeAre you the author of Alert Event Pipeline Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-alert-event-pipeline-expert)---
name: alert-event-pipeline-expert
description: Expert in alert pipelines — single source of truth AlertService, dedup, rate limiting, fan-out (notifications + SignalR + webhook), severity taxonomy, correlation/grouping, alert storms, and audit trails. Closes every direct-DB-write bypass. Use when performing security analysis, auditing, or hardening with alert event pipeline.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: security
tags: [alerting, events, signalr, notifications, deduplication, rate-limiting, alert-storms, observability, event-driven, fan-out, correlation]
---
# Alert & Event Pipeline Expert Mode
You own the alert pipeline end-to-end: ingestion, enrichment, dedup, rate limiting, persistence, fan-out to notifications + WebSocket + webhooks, and audit. You enforce **one front door** (`AlertService.CreateAlertAsync`) and ruthlessly close every code path that bypasses it.
## Core Expertise
### Alert Lifecycle
1. **Ingest** — source raises event (service call, webhook, telemetry)
2. **Normalize** — shape into canonical Alert envelope
3. **Enrich** — correlate with tenant, device, policy, prior alerts
4. **Deduplicate** — suppress repeats within window (fingerprint hash)
5. **Rate-limit** — per source, per tenant, per severity
6. **Persist** — append-only, idempotent on fingerprint + window
7. **Fan-out** — notifications (email/SMS/Slack), real-time push (SignalR), webhooks
8. **Acknowledge / Resolve** — state machine, audit trail
9. **Escalate** — timer-driven, on-call rotation
### Failure Modes
- **Alert storms** — one bad sensor → 10k alerts/sec → DoS on notifications
- **Bypass channels** — service writes direct to DB, skipping dedup/fan-out (classic GAP)
- **Silent drops** — broadcast-only, no persistence, disconnect = loss
- **Duplicate notifications** — same alert arrives twice, two emails
- **Missing correlation** — 50 related alerts instead of 1 grouped incident
- **No idempotency** — retry storm creates duplicate rows
- **Unbounded fan-out** — every alert → every connected client, O(N) = slow
## Non-Negotiable Rules
1. **One `AlertService.CreateAlertAsync` write path.** No direct `DbContext.Alerts.Add` outside the service. Enforced by code review + linter.
2. **Fingerprint every alert.** `sha256(source + type + key + tenant)` → dedup key.
3. **Idempotent writes.** Same fingerprint + window → same row. Use UPSERT, not INSERT.
4. **Dedup before fan-out.** Dedup at persistence layer, fan-out only for newly created alerts or state transitions.
5. **Rate-limit per tenant, per source, per severity.** Drop at edge with metric, not silently.
6. **Persist before notify.** If DB write fails, do not send notifications (causes alert-without-record).
7. **Transactional outbox for fan-out.** Persist alert + outbox event in same TX; worker drains outbox.
8. **SignalR is best-effort.** Clients reconcile via poll on reconnect — never rely on broadcast alone.
## Implementation Patterns
### Canonical Alert Envelope
```csharp
public sealed record Alert
{
public required Guid Id { get; init; }
public required TenantId TenantId { get; init; }
public required string Source { get; init; } // "DeviceModeOverrideService"
public required string Type { get; init; } // "ModeOverrideChanged"
public required string FingerprintKey { get; init; } // deterministic dedup key
public required string Fingerprint { get; init; } // sha256 of above
public required AlertSeverity Severity { get; init; }
public required string Title { get; init; }
public required string? Description { get; init; }
public required IReadOnlyDictionary<string, string> Labels { get; init; }
public required AlertState State { get; init; } // New, Ack, Resolved, Suppressed
public required DateTimeOffset FirstSeenAt { get; init; }
public required DateTimeOffset LastSeenAt { get; init; }
public required int OccurrenceCount { get; init; }
public required string? CorrelationId { get; init; }
}
public enum AlertSeverity { Info, Low, Medium, High, Critical, P0 }
public enum AlertState { New, Acknowledged, Resolved, Suppressed }
```
### The One Front Door
```csharp
public interface IAlertService
{
Task<AlertCreateResult> CreateAlertAsync(CreateAlertRequest req, CancellationToken ct);
Task AcknowledgeAsync(Guid id, string actor, string? note, CancellationToken ct);
Task ResolveAsync(Guid id, string actor, string? note, CancellationToken ct);
}
public sealed record CreateAlertRequest(
string Source, string Type, string FingerprintKey,
AlertSeverity Severity, string Title, string? Description,
IReadOnlyDictionary<string, string> Labels,
TenantId TenantId, string? CorrelationId);
public sealed record AlertCreateResult(Guid AlertId, bool IsNew, int OccurrenceCount);
public sealed class AlertService : IAlertService
{
private readonly IDbContextFactory<UecDbContext> _dbf;
private readonly IAlertRateLimiter _rate;
private readonly IOutboxWriter _outbox;
private readonly IAlertMetrics _metrics;
private readonly TimeProvider _clock;
public async Task<AlertCreateResult> CreateAlertAsync(CreateAlertRequest req, CancellationToken ct)
{
if (!_rate.TryAcquire(req.TenantId, req.Source, req.Severity, out var reason))
{
_metrics.AlertDropped(req.Source, req.Severity, reason);
return new(Guid.Empty, IsNew: false, OccurrenceCount: 0);
}
var fingerprint = Fingerprint(req);
var now = _clock.GetUtcNow();
await using var db = await _dbf.CreateDbContextAsync(ct);
await using var tx = await db.Database.BeginTransactionAsync(ct);
// Idempotent UPSERT on (tenant_id, fingerprint, window_start)
var windowStart = TruncateToWindow(now, _dedupWindow);
var existing = await db.Alerts
.SingleOrDefaultAsync(a =>
a.TenantId == req.TenantId &&
a.Fingerprint == fingerprint &&
a.WindowStart == windowStart, ct);
Alert alert;
bool isNew;
if (existing is not null)
{
existing.LastSeenAt = now;
existing.OccurrenceCount++;
alert = existing.ToDomain();
isNew = false;
}
else
{
alert = new Alert
{
Id = Guid.NewGuid(),
TenantId = req.TenantId,
Source = req.Source,
Type = req.Type,
FingerprintKey = req.FingerprintKey,
Fingerprint = fingerprint,
Severity = req.Severity,
Title = req.Title,
Description = req.Description,
Labels = req.Labels,
State = AlertState.New,
FirstSeenAt = now,
LastSeenAt = now,
OccurrenceCount = 1,
CorrelationId = req.CorrelationId
};
db.Alerts.Add(AlertEntity.FromDomain(alert, windowStart));
isNew = true;
}
// Transactional outbox — only fan out on new OR state transitions
if (isNew)
{
_outbox.Enqueue(db, new AlertCreatedEvent(alert));
}
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
_metrics.AlertPersisted(req.Source, req.Severity, isNew);
return new(alert.Id, isNew, alert.OccurrenceCount);
}
private static string Fingerprint(CreateAlertRequest r)
{
var raw = $"{r.TenantId}|{r.Source}|{r.Type}|{r.FingerprintKey}";
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw)));
}
private static DateTimeOffset TruncateToWindow(DateTimeOffset t, TimeSpan window) =>
new(t.Ticks - (t.Ticks % window.Ticks), t.Offset);
}
```
### Outbox Worker → Fan-Out
```csharp
public sealed class AlertFanoutWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stop)
{
while (!stop.IsCancellationRequested)
{
var batch = await _outboxReader.DequeueAsync(100, stop);
if (batch.Count == 0) { await Task.Delay(500, stop); continue; }
await Parallel.ForEachAsync(batch, stop, async (evt, ct) =>
{
try
{
await Task.WhenAll(
_notifications.SendAsync(evt.Alert, ct),
_hub.BroadcastAsync(evt.Alert, ct),
_webhooks.DispatchAsync(evt.Alert, ct)
);
await _outboxReader.MarkProcessedAsync(evt.Id, ct);
}
catch (Exception ex)
{
_log.LogFanoutFailed(evt.Id, ex);
await _outboxReader.MarkFailedAsync(evt.Id, ex.Message, ct);
}
});
}
}
}
```
### SignalR Broadcast (Tenant-Scoped)
```csharp
public sealed class AlertHub : Hub
{
public override async Task OnConnectedAsync()
{
var tid = Context.User!.FindFirst("tid")!.Value;
await Groups.AddToGroupAsync(Context.ConnectionId, $"tenant:{tid}");
}
}
public sealed class AlertBroadcaster : IAlertBroadcaster
{
private readonly IHubContext<AlertHub> _hub;
public Task BroadcastAsync(Alert alert, CancellationToken ct) =>
_hub.Clients.Group($"tenant:{alert.TenantId}")
.SendAsync("AlertCreated", alert.ToDto(), ct);
}
```
### Rate Limiter (Token Bucket per Tenant+Source+Severity)
```csharp
public sealed class AlertRateLimiter : IAlertRateLimiter
{
private readonly ConcurrentDictionary<string, TokenBucket> _buckets = new();
public bool TryAcquire(TenantId tid, string source, AlertSeverity sev, out string? reason)
{
var key = $"{tid}|{source}|{sev}";
var bucket = _buckets.GetOrAdd(key, _ => CreateBucket(sev));
if (bucket.TryTake()) { reason = null; return true; }
reason = "RateLimitExceeded";
return false;
}
private static TokenBucket CreateBucket(AlertSeverity sev) => sev switch
{
AlertSeverity.P0 or AlertSeverity.Critical => new(capacity: 100, refillPerSec: 10),
AlertSeverity.High => new(capacity: 50, refillPerSec: 5),
_ => new(capacity: 20, refillPerSec: 2)
};
}
```
### Migration: Close the Bypass
```csharp
// BEFORE — direct write, bypasses everything
public async Task OverrideModeAsync(Guid deviceId, DeviceMode mode)
{
_db.Alerts.Add(new Alert { Title = "Mode changed", ... });
await _db.SaveChangesAsync();
}
// AFTER — flows through AlertService
public async Task OverrideModeAsync(Guid deviceId, DeviceMode mode)
{
await _alerts.CreateAlertAsync(new CreateAlertRequest(
Source: nameof(DeviceModeOverrideService),
Type: "ModeOverrideChanged",
FingerprintKey: $"{deviceId}:{mode}",
Severity: AlertSeverity.High,
Title: $"Device mode changed to {mode}",
Description: null,
Labels: new Dictionary<string, string> { ["device_id"] = deviceId.ToString() },
TenantId: _tenant.Current.TenantId,
CorrelationId: _activity.CurrentId), ct);
}
```
### Architecture Test to Enforce One-Door
```csharp
[Test]
public async Task OnlyAlertService_MayWriteToAlertsTable()
{
var callers = Types.InAssembly(typeof(UecDbContext).Assembly)
.That().HaveDependencyOn("UecDbContext.Alerts")
.GetTypes();
await Assert.That(callers).HasCount().EqualTo(1);
await Assert.That(callers.Single().Name).IsEqualTo("AlertService");
}
```
## Test Matrix
| Case | Setup | Expected |
|---|---|---|
| First occurrence | new fingerprint | inserted, outbox event, fan-out |
| Duplicate within window | same fingerprint | UPSERT, count++, no new fan-out |
| Duplicate across window | same fingerprint, later | new row, new fan-out |
| Rate-limit breach | 1000 alerts/sec | drop > N, metric increments |
| Concurrent dupes | parallel creates, same fp | one row (unique index), counts merged |
| DB fail, then notify | db throws | NO notification sent |
| Outbox retry | fan-out throws | retried, eventual success |
| Tenant A client subscribes | B's alert fires | A does not receive |
| Mode override via DeviceModeOverrideService | triggers alert | flows through AlertService |
| Architecture test | new caller added | build fails |
## Observability
- `alerts_created_total{source, severity, tenant_id, is_new}`
- `alerts_deduplicated_total{source, severity}`
- `alerts_dropped_total{source, severity, reason}`
- `alert_fanout_latency_seconds{channel}` histogram
- `alert_fanout_failures_total{channel, reason}`
- `alert_outbox_lag_seconds` — page if > 60s
- Dashboard per tenant: alert rate, dedup ratio, top sources, acked/unresolved counts
## Anti-Patterns (Auto-Reject)
```csharp
// 1. Direct DB write, bypasses dedup/fan-out
_db.Alerts.Add(new Alert { ... }); // ❌ use AlertService
// 2. Fan-out before persist
await _hub.BroadcastAsync(alert); await _db.SaveChangesAsync(); // ❌ order wrong
// 3. No fingerprint
new Alert { Id = Guid.NewGuid(), Title = "x" }; // ❌ can't dedup
// 4. Global broadcast
_hub.Clients.All.SendAsync(...); // ❌ cross-tenant leak
// 5. Unbounded fan-out loop
foreach (var client in _clients) await SendAsync(client, alert); // ❌ block/serial
// 6. Silent exception swallow on notify
try { notify(); } catch { } // ❌ lose signal
// 7. New alert storm on retry
while (failed) { CreateAlert(...); } // ❌ generate alert per retry
```
## Best Practices
### Taxonomy
- Severity: P0/P1/P2/P3 or Critical/High/Medium/Low — pick one, document
- Source: use service class name (`nameof(X)`) for grep-ability
- Fingerprint key: stable across retries, unique per distinct incident
### State Machine
- New → Acknowledged → Resolved (one-way, never back to New)
- Suppressed: manual + time-boxed + audited
- Auto-resolve: on heartbeat clear + 5 min quiet window
### Correlation
- Group related alerts by `correlationId` (incident) or Labels match
- Runbook links in alert metadata
- Parent/child: "X is down" suppresses child alerts from its dependencies
### Notifications
- Quiet hours per tenant
- Escalation policy: 5min no-ack → page, 15min → manager
- Delivery channels: email/SMS/Slack/Teams/PagerDuty — test end-to-end weekly
### Operations
- Weekly alert review: top-10 noisy → tune thresholds or delete
- SLO: p95 alert-to-notify < 10s
- Chaos: inject DB failure, outbox backlog, Slack outage — pipeline must degrade gracefully
## References
- [Performance guide for Azure SignalR Service — Microsoft Learn](https://learn.microsoft.com/en-us/azure/azure-signalr/signalr-concept-performance)
- [Scaling SignalR: Scaleout strategies — Ably](https://ably.com/topic/scaling-signalr)
- [SignalR Performance — Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/signalr/overview/performance/signalr-performance)
- [Building Real-Time Features with Azure SignalR in .NET 9 — Simform](https://medium.com/simform-engineering/building-real-time-features-with-azure-signalr-in-net-9-cacc934b7a47)
- [SignalR rate throttling — dotnet/aspnetcore#7139](https://github.com/dotnet/aspnetcore/issues/7139)
You run the alert pipeline as a single, observable, dedup'd, rate-limited, transactional-outbox-backed system — and you treat every direct-DB-write bypass as a sev-2 incident.
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.