Opsgenie vs PagerDuty for Audit Alert Routing
Teams choosing an incident platform for audit alerts usually expect the decision to matter more than it does. Both Opsgenie and PagerDuty accept an event, group it by an identifier you supply, escalate through a schedule, and close when told to — and every hard part of the integration stays on the audit side regardless. This page is part of Integrating Audit Alerts with Incident Management and compares the two where they genuinely differ.
Comparison Table
| Concern | Opsgenie | PagerDuty |
|---|---|---|
| Credential scope | API key, per integration | Routing key, per service |
| Deduplication field | alias |
dedup_key |
| Open / update semantics | Same alias updates the open alert | Same dedup key updates the open incident |
| Close semantics | Close action against the alias | event_action: "resolve" with the same key |
| Ownership model | Responder team, with its own schedules | Service, with its own escalation policy |
| Extra context | details map plus tags |
custom_details object |
| Priority levels | P1 to P5 | critical, error, warning, info |
| Rate limiting | Per integration | Per routing key |
| Terraform support | Official provider | Official provider |
| Acknowledged-but-open state | Yes | Yes |
The mapping is close enough that a well-built emitter is portable by renaming four fields. What is not portable is an emitter whose identity field was assembled carelessly — that breaks the same way on both sides.
One emitter, two adapters
#!/usr/bin/env python3
# /opt/audit/incidents/emit.py
# The judgement stays here; the adapter only renames fields.
from __future__ import annotations
import hashlib
import os
from dataclasses import dataclass
import httpx
@dataclass(frozen=True)
class Breach:
metric: str
segment: str
environment: str
severity: str # critical | error | warning | info
value: float
run_id: str
def identity(b: Breach) -> str:
"""Stable across runs by construction: nothing here varies per run.
The value and the run id are deliberately excluded."""
raw = f"{b.metric}|{b.segment}|{b.environment}"
return hashlib.sha256(raw.encode()).hexdigest()[:32]
class PagerDutyAdapter:
URL = "https://events.pagerduty.com/v2/enqueue"
def send(self, b: Breach, *, action: str) -> int:
payload = {
"routing_key": os.environ["PD_ROUTING_KEY"],
"event_action": action, # trigger | resolve
"dedup_key": identity(b),
"payload": {
"summary": f"{b.metric} on {b.segment} ({b.environment})",
"severity": b.severity,
"source": "site-health-audit",
"custom_details": {"value": b.value, "run_id": b.run_id},
},
}
return httpx.post(self.URL, json=payload, timeout=10).status_code
class OpsgenieAdapter:
BASE = "https://api.opsgenie.com/v2/alerts"
PRIORITY = {"critical": "P1", "error": "P2",
"warning": "P3", "info": "P4"}
def send(self, b: Breach, *, action: str) -> int:
headers = {"Authorization": f"GenieKey {os.environ['OG_API_KEY']}"}
if action == "resolve":
url = f"{self.BASE}/{identity(b)}/close?identifierType=alias"
return httpx.post(url, headers=headers, json={}, timeout=10).status_code
payload = {
"message": f"{b.metric} on {b.segment} ({b.environment})",
"alias": identity(b),
"priority": self.PRIORITY[b.severity],
"responders": [{"name": os.environ["OG_TEAM"], "type": "team"}],
"details": {"value": str(b.value), "run_id": b.run_id},
"tags": [b.environment, b.segment],
}
return httpx.post(self.BASE, headers=headers, json=payload, timeout=10).status_code
The shape is the point. identity is shared, defined once, and excludes everything that varies between runs — which is the property both platforms depend on and neither can supply. The adapters contain no judgement at all: they rename fields and map a severity vocabulary, and swapping one for the other is a configuration change.
Verification
set -euo pipefail
export ADAPTER="${1:-pagerduty}"
# 1. A trigger opens exactly one incident.
python3 -m audit.incidents.emit --adapter "$ADAPTER" --action trigger --metric lcp_p75 --segment /checkout/ --env production --severity critical
sleep 5
# 2. An identical trigger updates rather than duplicating.
python3 -m audit.incidents.emit --adapter "$ADAPTER" --action trigger --metric lcp_p75 --segment /checkout/ --env production --severity critical
sleep 5
# 3. A resolve with the same identity closes it.
python3 -m audit.incidents.emit --adapter "$ADAPTER" --action resolve --metric lcp_p75 --segment /checkout/ --env production --severity critical
echo "Now confirm in the platform UI: exactly one incident, now closed."
Expected result is one incident that opened, updated once, and closed — on either adapter, with an identical sequence of commands. Running the same script against both platforms is the cheapest possible portability test, and it is the one that catches an identity field that is not actually stable.
Failure Modes
A 202 or a 200 arrives and nobody is paged
Both platforms acknowledge acceptance of an event separately from routing it, so a successful status code confirms only that the event was queued. On PagerDuty the usual cause is an escalation policy with no active schedule; on Opsgenie it is a responder team with an empty on-call rotation. Verify in the platform's own incident list rather than by status code, on both.
Migration breaks the close path first
When switching platforms, triggers keep working and resolves silently stop, because the close semantics differ more than the open ones — a resolve on PagerDuty is another event to the same endpoint, while on Opsgenie it is a different endpoint keyed by alias. Test the close path explicitly before cutting over, or the first week of the migration accumulates open incidents nobody notices.
Alert volume looks different after a migration
Usually a severity-mapping artefact rather than a change in the underlying findings: error and warning map onto P2 and P3, and if the escalation policies attached to those priorities differ from the previous service, the same events produce a different number of pages. Compare policies, not just severities, and keep the confirmation and cooldown logic described in the parent guide unchanged across the cutover so the emitter is not a variable.
FAQ
Does the choice of incident platform matter much for audit alerting?
Less than most teams expect. The concepts map one to one — a routing key is an API key, a dedup key is an alias, a resolve event is a close action — and every genuinely hard part of the integration stays on the audit side regardless: deciding whether a breach is real, building a stable identity, choosing severity, and emitting the close when the metric recovers. Pick on the basis of what the organisation already runs, and spend the effort on the emitter.
What breaks first during a migration between the two?
The close path. Triggers translate almost directly, so they keep working, while resolves differ more: on PagerDuty a resolve is another event posted to the same endpoint, and on Opsgenie it is a separate endpoint keyed by alias. The result is a migration where alerts still open correctly and quietly stop closing, so the first week accumulates open incidents that nobody attributes to the cutover. Test the close path explicitly before switching.
Why does a successful API response not mean someone was paged?
Because both platforms separate accepting an event from routing it. A 202 or 200 confirms the event was queued for processing, not that it reached a service or team with an active on-call schedule attached. The common causes differ in name and not in substance: an escalation policy with no schedule on PagerDuty, a responder team with an empty rotation on Opsgenie. Verify in the platform incident list during setup rather than trusting the status code.
Related
- Integrating Audit Alerts with Incident Management — the parent guide covering confirmation, dedup keys, auto-resolve and retention
- Routing Audit Alerts to PagerDuty — the concrete PagerDuty wiring, including the trigger and resolve pair
- Configuring Alert Thresholds and Routing — the severity tiers and cooldown logic that feed whichever platform is chosen