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.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill alert-event-pipeline-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Alert Event Pipeline Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-alert-event-pipeline-expert)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!