The last detail of the alert path — and a lesson about boundaries. ~9 min.
The alert-processor (L15) calls
notifyClient.Send(event) to deliver an alert. Open pkg/notification expecting
Slack/email/PagerDuty routing… and you find one small file. That's the lesson. The actual channels
live in a separate service; this package is just a resilient HTTP client to it. The final thing
to understand about risk-graph-indexer is where it stops.
AlertEvent.pkg/notification is the seam. Its entire job is to hand a correctly-shaped message across
that wall over HTTP. The code even says so: NotificationRequest "matches the ChainWorkerMessage
schema from forta-attester … fields and JSON tags must match exactly for the notification service to accept it."
Send doesn't just forward the event — it translates the system's internal
AlertEvent into the external service's schema (client.go):
| Internal (our model) | → External (their schema) |
|---|---|
event.Severity ("high"/"medium"/"low") | Risk uint8 (100 / 50 / 25) |
NodeID, AlertType, Timestamp | FFRId = sha256(NodeID:AlertType:Timestamp) |
event.Details, Label, AlertType | Failure.Metadata (from/addresses/label/detection_module…) |
This translation layer is an anti-corruption layer: the external service's wire format never leaks into the risk engine, and a change on either side is absorbed here. The two domains stay decoupled.
FFRId = sha256(NodeID:AlertType:Timestamp) — a stable, content-derived ID for the
alert. It lets the external service dedup too. This is the same discipline you've now seen four times: the
write path's MERGE (L4), graphwrite's IdemKey (L9), the alert-processor's doc-id = msg-id
(L15), and now FFRId across the system boundary. "Make replays safe with a stable key" is woven
through the entire codebase — even at its edge.
The send is small but production-hardened — a compact tour of the patterns you've met:
// pkg/notification/client.go — Send (the retry loop) for attempt := range c.maxRetries { if ctx.Err() != nil { return false } // honour cancellation/shutdown if err := c.doPost(ctx, body); err == nil { return true // delivered → L15 marks Delivered=true } if attempt < c.maxRetries-1 { backoff := time.Duration(1<<uint(attempt)) * time.Second // 1s, 2s, 4s, 8s… select { case <-time.After(backoff): // wait… case <-ctx.Done(): return false // …unless cancelled mid-wait } } } return false // all retries exhausted → L15 marks Delivered=false (recoverable)
| Detail | What it gives you (and where you saw it) |
|---|---|
Exponential backoff 1<<attempt s | Don't hammer a struggling service — 1s, 2s, 4s… (the resilience instinct of L5/L7). |
Context-cancellable select{…ctx.Done()} | A shutdown signal aborts a backoff wait instantly — graceful shutdown done right (the proper Go ctx idiom). |
Returns bool | Feeds L15's Delivered flag; false persists as undelivered → queryable + re-sendable. No alert lost. |
| Instrumented client + 10s timeout + bearer auth | telemetry.InstrumentedHTTPClient — even the outbound call is traced/metered (L11) and authenticated. |
pkg/notification actually contain?Send translating AlertEvent → NotificationRequest (severity→risk number, etc.) is an example of…FFRId = sha256(NodeID:AlertType:Timestamp) is which now-familiar pattern?select { case <-time.After(backoff): case <-ctx.Done(): } achieve?Send returning false (all retries exhausted) matters because…1<<attempt seconds produces…pkg/notification is just the resilient HTTP seam.Send as an adapter / anti-corruption layer, and FFRId as a cross-boundary idempotency key.Send's bool to L15's Delivered flag.Grounded in: pkg/notification/client.go (NotificationRequest matches forta-attester ChainWorkerMessage; Send adapter: severity→risk, FFRId=sha256, metadata mapping; exponential-backoff + ctx-cancellable retry; InstrumentedHTTPClient + bearer auth + 10s timeout; bool → L15 Delivered). Verify against source — the code is the truth.