PagerDuty is a critical incident management platform used by DevOps and SRE teams. This guide covers how to programmatically trigger, manage, and resolve PagerDuty incidents from Java applications.
Project Setup and Dependencies
1. Maven Dependencies
<dependencies> <!-- PagerDuty REST API Client --> <dependency> <groupId>com.pagerduty</groupId> <artifactId>pagerduty-client</artifactId> <version>1.0.0</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <!-- Spring Boot Starter Web (Optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.3.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.35.0</version> <scope>test</scope> </dependency> </dependencies>
Core PagerDuty Integration
1. Configuration Classes
// PagerDutyConfig.java
@Configuration
@ConfigurationProperties(prefix = "pagerduty")
@Data
public class PagerDutyConfig {
private String apiKey;
private String integrationKey;
private String apiUrl = "https://api.pagerduty.com";
private int timeoutSeconds = 30;
private int maxRetries = 3;
private String fromEmail;
private String serviceId;
public void validate() {
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalStateException("PagerDuty API key is required");
}
if (integrationKey == null || integrationKey.trim().isEmpty()) {
throw new IllegalStateException("PagerDuty integration key is required");
}
}
}
// IncidentSeverity.java
public enum IncidentSeverity {
CRITICAL("critical"),
ERROR("error"),
WARNING("warning"),
INFO("info");
private final String value;
IncidentSeverity(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
// IncidentAction.java
public enum IncidentAction {
TRIGGER("trigger"),
ACKNOWLEDGE("acknowledge"),
RESOLVE("resolve");
private final String value;
IncidentAction(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
2. Request/Response DTOs
// PagerDutyIncidentRequest.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PagerDutyIncidentRequest {
@JsonProperty("routing_key")
private String routingKey;
@JsonProperty("event_action")
private String eventAction;
@JsonProperty("dedup_key")
private String dedupKey;
private Payload payload;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Payload {
private String summary;
private String source;
private String severity;
private String timestamp;
@JsonProperty("custom_details")
private Map<String, Object> customDetails;
@JsonProperty("component")
private String component;
@JsonProperty("group")
private String group;
@JsonProperty("class")
private String eventClass;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Image {
private String src;
private String href;
private String alt;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Link {
private String href;
private String text;
}
}
// PagerDutyIncidentResponse.java
@Data
public class PagerDutyIncidentResponse {
private String status;
private String message;
private String dedupKey;
@JsonProperty("incident_key")
private String incidentKey;
@JsonProperty("created_at")
private String createdAt;
private List<Error> errors;
@Data
public static class Error {
private String code;
private String message;
private String field;
}
}
// PagerDutyIncident.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PagerDutyIncident {
private String id;
private String title;
private String description;
private IncidentSeverity severity;
private String serviceId;
private String assignedTo;
private IncidentStatus status;
private ZonedDateTime createdAt;
private ZonedDateTime lastStatusChange;
private Map<String, Object> customDetails;
private List<String> escalationPolicies;
private Integer urgency;
public enum IncidentStatus {
TRIGGERED, ACKNOWLEDGED, RESOLVED
}
}
Core PagerDuty Service Implementation
1. Main PagerDuty Service
// PagerDutyService.java
@Service
@Slf4j
public class PagerDutyService {
private final PagerDutyConfig config;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private final PagerDutyEventV2Client eventClient;
public PagerDutyService(PagerDutyConfig config,
ObjectMapper objectMapper,
RestTemplate restTemplate) {
this.config = config;
this.objectMapper = objectMapper;
this.restTemplate = restTemplate;
this.config.validate();
// Initialize Events API v2 client
this.eventClient = new PagerDutyEventV2Client(config.getIntegrationKey());
}
/**
* Trigger a new incident in PagerDuty
*/
public IncidentTriggerResult triggerIncident(IncidentTriggerRequest request) {
try {
log.info("Triggering PagerDuty incident: {}", request.getSummary());
PagerDutyIncidentRequest incidentRequest = buildIncidentRequest(request);
String requestBody = objectMapper.writeValueAsString(incidentRequest);
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.postForEntity(
config.getApiUrl() + "/v2/enqueue",
entity,
String.class
);
if (response.getStatusCode().is2xxSuccessful()) {
PagerDutyIncidentResponse responseBody = objectMapper.readValue(
response.getBody(), PagerDutyIncidentResponse.class);
log.info("Successfully triggered incident with dedup key: {}",
responseBody.getDedupKey());
return IncidentTriggerResult.builder()
.success(true)
.incidentKey(responseBody.getDedupKey())
.message("Incident triggered successfully")
.build();
} else {
log.error("Failed to trigger incident. Status: {}, Response: {}",
response.getStatusCode(), response.getBody());
return IncidentTriggerResult.builder()
.success(false)
.message("Failed to trigger incident: " + response.getBody())
.build();
}
} catch (Exception e) {
log.error("Error triggering PagerDuty incident", e);
return IncidentTriggerResult.builder()
.success(false)
.message("Error triggering incident: " + e.getMessage())
.build();
}
}
/**
* Trigger incident using Events API v2 (Recommended)
*/
public IncidentTriggerResult triggerIncidentV2(IncidentTriggerRequest request) {
try {
EventResult result = eventClient.sendEvent(EventPayload.builder()
.summary(request.getSummary())
.source(request.getSource())
.severity(request.getSeverity().getValue())
.timestamp(request.getTimestamp() != null ?
request.getTimestamp() : Instant.now().toString())
.customDetails(request.getCustomDetails())
.component(request.getComponent())
.group(request.getGroup())
.eventClass(request.getEventClass())
.build());
if (result.isSuccess()) {
log.info("Successfully triggered incident via V2 API. Dedup key: {}",
result.getDedupKey());
return IncidentTriggerResult.builder()
.success(true)
.incidentKey(result.getDedupKey())
.message("Incident triggered successfully")
.build();
} else {
log.error("Failed to trigger incident via V2 API: {}", result.getMessage());
return IncidentTriggerResult.builder()
.success(false)
.message(result.getMessage())
.errors(result.getErrors())
.build();
}
} catch (Exception e) {
log.error("Error triggering PagerDuty incident via V2 API", e);
return IncidentTriggerResult.builder()
.success(false)
.message("Error triggering incident: " + e.getMessage())
.build();
}
}
/**
* Acknowledge an existing incident
*/
public boolean acknowledgeIncident(String incidentKey, String message) {
try {
PagerDutyIncidentRequest request = PagerDutyIncidentRequest.builder()
.routingKey(config.getIntegrationKey())
.eventAction(IncidentAction.ACKNOWLEDGE.getValue())
.dedupKey(incidentKey)
.payload(PagerDutyIncidentRequest.Payload.builder()
.summary(message)
.source(config.getFromEmail())
.build())
.build();
return sendEvent(request);
} catch (Exception e) {
log.error("Error acknowledging incident: {}", incidentKey, e);
return false;
}
}
/**
* Resolve an existing incident
*/
public boolean resolveIncident(String incidentKey, String resolutionMessage) {
try {
PagerDutyIncidentRequest request = PagerDutyIncidentRequest.builder()
.routingKey(config.getIntegrationKey())
.eventAction(IncidentAction.RESOLVE.getValue())
.dedupKey(incidentKey)
.payload(PagerDutyIncidentRequest.Payload.builder()
.summary(resolutionMessage)
.source(config.getFromEmail())
.build())
.build();
return sendEvent(request);
} catch (Exception e) {
log.error("Error resolving incident: {}", incidentKey, e);
return false;
}
}
/**
* Get incident details from PagerDuty REST API
*/
public Optional<PagerDutyIncident> getIncident(String incidentId) {
try {
HttpHeaders headers = createHeaders();
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
config.getApiUrl() + "/incidents/" + incidentId,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode().is2xxSuccessful()) {
JsonNode root = objectMapper.readTree(response.getBody());
JsonNode incidentNode = root.path("incident");
PagerDutyIncident incident = objectMapper.treeToValue(
incidentNode, PagerDutyIncident.class);
return Optional.of(incident);
}
} catch (Exception e) {
log.error("Error fetching incident: {}", incidentId, e);
}
return Optional.empty();
}
/**
* List incidents with filtering
*/
public List<PagerDutyIncident> listIncidents(IncidentFilter filter) {
try {
HttpHeaders headers = createHeaders();
HttpEntity<Void> entity = new HttpEntity<>(headers);
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(config.getApiUrl() + "/incidents")
.queryParam("limit", filter.getLimit() != null ? filter.getLimit() : 25)
.queryParam("offset", filter.getOffset() != null ? filter.getOffset() : 0);
if (filter.getStatuses() != null && !filter.getStatuses().isEmpty()) {
builder.queryParam("statuses", String.join(",", filter.getStatuses()));
}
if (filter.getServiceIds() != null && !filter.getServiceIds().isEmpty()) {
builder.queryParam("service_ids", String.join(",", filter.getServiceIds()));
}
if (filter.getSince() != null) {
builder.queryParam("since", filter.getSince());
}
if (filter.getUntil() != null) {
builder.queryParam("until", filter.getUntil());
}
ResponseEntity<String> response = restTemplate.exchange(
builder.toUriString(),
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode().is2xxSuccessful()) {
JsonNode root = objectMapper.readTree(response.getBody());
JsonNode incidentsNode = root.path("incidents");
List<PagerDutyIncident> incidents = new ArrayList<>();
for (JsonNode incidentNode : incidentsNode) {
PagerDutyIncident incident = objectMapper.treeToValue(
incidentNode, PagerDutyIncident.class);
incidents.add(incident);
}
return incidents;
}
} catch (Exception e) {
log.error("Error listing incidents", e);
}
return Collections.emptyList();
}
private PagerDutyIncidentRequest buildIncidentRequest(IncidentTriggerRequest request) {
return PagerDutyIncidentRequest.builder()
.routingKey(config.getIntegrationKey())
.eventAction(IncidentAction.TRIGGER.getValue())
.dedupKey(request.getDedupKey() != null ?
request.getDedupKey() : generateDedupKey(request))
.payload(PagerDutyIncidentRequest.Payload.builder()
.summary(request.getSummary())
.source(request.getSource())
.severity(request.getSeverity().getValue())
.timestamp(request.getTimestamp() != null ?
request.getTimestamp() : Instant.now().toString())
.customDetails(request.getCustomDetails())
.component(request.getComponent())
.group(request.getGroup())
.eventClass(request.getEventClass())
.build())
.build();
}
private HttpHeaders createHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Token token=" + config.getApiKey());
headers.set("Accept", "application/vnd.pagerduty+json;version=2");
headers.set("From", config.getFromEmail());
return headers;
}
private boolean sendEvent(PagerDutyIncidentRequest request) {
try {
String requestBody = objectMapper.writeValueAsString(request);
HttpHeaders headers = createHeaders();
HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.postForEntity(
config.getApiUrl() + "/v2/enqueue",
entity,
String.class
);
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
log.error("Error sending event to PagerDuty", e);
return false;
}
}
private String generateDedupKey(IncidentTriggerRequest request) {
return request.getSource() + "-" +
request.getComponent() + "-" +
Instant.now().toEpochMilli();
}
}
2. Supporting Classes
// IncidentTriggerRequest.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IncidentTriggerRequest {
@NotBlank
private String summary;
@NotBlank
private String source;
@NotNull
private IncidentSeverity severity;
private String component;
private String group;
private String eventClass;
private String timestamp;
private String dedupKey;
@Singular
private Map<String, Object> customDetails;
private List<PagerDutyIncidentRequest.Image> images;
private List<PagerDutyIncidentRequest.Link> links;
}
// IncidentTriggerResult.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IncidentTriggerResult {
private boolean success;
private String incidentKey;
private String message;
private List<String> errors;
public static IncidentTriggerResult success(String incidentKey) {
return IncidentTriggerResult.builder()
.success(true)
.incidentKey(incidentKey)
.message("Incident triggered successfully")
.build();
}
public static IncidentTriggerResult failure(String message) {
return IncidentTriggerResult.builder()
.success(false)
.message(message)
.build();
}
}
// IncidentFilter.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IncidentFilter {
private List<String> statuses;
private List<String> serviceIds;
private String since;
private String until;
private Integer limit;
private Integer offset;
private String sortBy;
}
Advanced Features
1. Incident Management with Retry Logic
// RetryablePagerDutyService.java
@Service
@Slf4j
public class RetryablePagerDutyService {
private final PagerDutyService pagerDutyService;
private final RetryTemplate retryTemplate;
public RetryablePagerDutyService(PagerDutyService pagerDutyService) {
this.pagerDutyService = pagerDutyService;
this.retryTemplate = createRetryTemplate();
}
/**
* Trigger incident with retry logic
*/
public IncidentTriggerResult triggerIncidentWithRetry(IncidentTriggerRequest request) {
return retryTemplate.execute(context -> {
log.info("Attempting to trigger incident (attempt {}): {}",
context.getRetryCount() + 1, request.getSummary());
IncidentTriggerResult result = pagerDutyService.triggerIncidentV2(request);
if (!result.isSuccess()) {
log.warn("Failed to trigger incident on attempt {}. Retrying...",
context.getRetryCount() + 1);
throw new IncidentTriggerException("Failed to trigger incident: " + result.getMessage());
}
return result;
});
}
/**
* Resolve incident with retry logic
*/
public boolean resolveIncidentWithRetry(String incidentKey, String resolutionMessage) {
return retryTemplate.execute(context -> {
log.info("Attempting to resolve incident {} (attempt {})",
incidentKey, context.getRetryCount() + 1);
boolean success = pagerDutyService.resolveIncident(incidentKey, resolutionMessage);
if (!success) {
log.warn("Failed to resolve incident {} on attempt {}. Retrying...",
incidentKey, context.getRetryCount() + 1);
throw new IncidentResolutionException("Failed to resolve incident: " + incidentKey);
}
return true;
});
}
private RetryTemplate createRetryTemplate() {
RetryTemplate template = new RetryTemplate();
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2);
backOffPolicy.setMaxInterval(10000);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
template.setBackOffPolicy(backOffPolicy);
template.setRetryPolicy(retryPolicy);
return template;
}
}
// Custom Exceptions
class IncidentTriggerException extends RuntimeException {
public IncidentTriggerException(String message) {
super(message);
}
}
class IncidentResolutionException extends RuntimeException {
public IncidentResolutionException(String message) {
super(message);
}
}
2. Event-Driven Incident Management
// IncidentEventPublisher.java
@Component
@Slf4j
public class IncidentEventPublisher {
private final ApplicationEventPublisher eventPublisher;
public IncidentEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void publishIncidentTriggered(IncidentTriggeredEvent event) {
log.info("Publishing incident triggered event: {}", event.getIncidentKey());
eventPublisher.publishEvent(event);
}
public void publishIncidentResolved(IncidentResolvedEvent event) {
log.info("Publishing incident resolved event: {}", event.getIncidentKey());
eventPublisher.publishEvent(event);
}
}
// Incident Events
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IncidentTriggeredEvent {
private String incidentKey;
private String summary;
private IncidentSeverity severity;
private String source;
private ZonedDateTime timestamp;
private Map<String, Object> customDetails;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IncidentResolvedEvent {
private String incidentKey;
private String resolutionMessage;
private ZonedDateTime resolvedAt;
private String resolvedBy;
}
// Incident Event Listeners
@Component
@Slf4j
public class IncidentEventListener {
private final PagerDutyService pagerDutyService;
private final NotificationService notificationService;
public IncidentEventListener(PagerDutyService pagerDutyService,
NotificationService notificationService) {
this.pagerDutyService = pagerDutyService;
this.notificationService = notificationService;
}
@EventListener
public void handleIncidentTriggered(IncidentTriggeredEvent event) {
log.info("Handling incident triggered event: {}", event.getIncidentKey());
// Send additional notifications
notificationService.sendIncidentNotification(event);
// Log to audit system
log.info("Incident {} triggered with severity {}",
event.getIncidentKey(), event.getSeverity());
}
@EventListener
public void handleIncidentResolved(IncidentResolvedEvent event) {
log.info("Handling incident resolved event: {}", event.getIncidentKey());
// Send resolution notifications
notificationService.sendResolutionNotification(event);
// Update internal tracking
log.info("Incident {} resolved at {}",
event.getIncidentKey(), event.getResolvedAt());
}
}
Spring Boot REST Controller
// IncidentController.java
@RestController
@RequestMapping("/api/v1/incidents")
@Slf4j
@Validated
public class IncidentController {
private final PagerDutyService pagerDutyService;
private final RetryablePagerDutyService retryableService;
private final IncidentEventPublisher eventPublisher;
public IncidentController(PagerDutyService pagerDutyService,
RetryablePagerDutyService retryableService,
IncidentEventPublisher eventPublisher) {
this.pagerDutyService = pagerDutyService;
this.retryableService = retryableService;
this.eventPublisher = eventPublisher;
}
@PostMapping("/trigger")
public ResponseEntity<ApiResponse<IncidentTriggerResult>> triggerIncident(
@Valid @RequestBody IncidentTriggerRequest request) {
log.info("Received incident trigger request: {}", request.getSummary());
try {
IncidentTriggerResult result = retryableService.triggerIncidentWithRetry(request);
if (result.isSuccess()) {
// Publish event for other systems
eventPublisher.publishIncidentTriggered(IncidentTriggeredEvent.builder()
.incidentKey(result.getIncidentKey())
.summary(request.getSummary())
.severity(request.getSeverity())
.source(request.getSource())
.timestamp(ZonedDateTime.now())
.customDetails(request.getCustomDetails())
.build());
return ResponseEntity.ok(ApiResponse.success(result));
} else {
return ResponseEntity.badRequest()
.body(ApiResponse.error(result.getMessage()));
}
} catch (Exception e) {
log.error("Failed to trigger incident", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("Failed to trigger incident: " + e.getMessage()));
}
}
@PostMapping("/{incidentKey}/acknowledge")
public ResponseEntity<ApiResponse<Void>> acknowledgeIncident(
@PathVariable String incidentKey,
@RequestParam(defaultValue = "Acknowledged via API") String message) {
log.info("Acknowledging incident: {}", incidentKey);
boolean success = pagerDutyService.acknowledgeIncident(incidentKey, message);
if (success) {
return ResponseEntity.ok(ApiResponse.success(null, "Incident acknowledged"));
} else {
return ResponseEntity.badRequest()
.body(ApiResponse.error("Failed to acknowledge incident"));
}
}
@PostMapping("/{incidentKey}/resolve")
public ResponseEntity<ApiResponse<Void>> resolveIncident(
@PathVariable String incidentKey,
@RequestParam String resolutionMessage) {
log.info("Resolving incident: {}", incidentKey);
try {
boolean success = retryableService.resolveIncidentWithRetry(incidentKey, resolutionMessage);
if (success) {
eventPublisher.publishIncidentResolved(IncidentResolvedEvent.builder()
.incidentKey(incidentKey)
.resolutionMessage(resolutionMessage)
.resolvedAt(ZonedDateTime.now())
.resolvedBy("API")
.build());
return ResponseEntity.ok(ApiResponse.success(null, "Incident resolved"));
} else {
return ResponseEntity.badRequest()
.body(ApiResponse.error("Failed to resolve incident"));
}
} catch (Exception e) {
log.error("Failed to resolve incident: {}", incidentKey, e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("Failed to resolve incident: " + e.getMessage()));
}
}
@GetMapping("/{incidentId}")
public ResponseEntity<ApiResponse<PagerDutyIncident>> getIncident(
@PathVariable String incidentId) {
Optional<PagerDutyIncident> incident = pagerDutyService.getIncident(incidentId);
return incident.map(inc -> ResponseEntity.ok(ApiResponse.success(inc)))
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<ApiResponse<List<PagerDutyIncident>>> listIncidents(
@ModelAttribute IncidentFilter filter) {
List<PagerDutyIncident> incidents = pagerDutyService.listIncidents(filter);
return ResponseEntity.ok(ApiResponse.success(incidents));
}
}
// ApiResponse.java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private ZonedDateTime timestamp;
private String requestId;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.data(data)
.timestamp(ZonedDateTime.now())
.build();
}
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(ZonedDateTime.now())
.build();
}
public static <T> ApiResponse<T> error(String message) {
return ApiResponse.<T>builder()
.success(false)
.message(message)
.timestamp(ZonedDateTime.now())
.build();
}
}
Testing
1. Unit Tests
// PagerDutyServiceTest.java
@ExtendWith(MockitoExtension.class)
class PagerDutyServiceTest {
@Mock
private RestTemplate restTemplate;
@Mock
private ObjectMapper objectMapper;
private PagerDutyService pagerDutyService;
private PagerDutyConfig config;
@BeforeEach
void setUp() {
config = new PagerDutyConfig();
config.setApiKey("test-api-key");
config.setIntegrationKey("test-integration-key");
config.setFromEmail("[email protected]");
config.setApiUrl("https://api.pagerduty.com");
pagerDutyService = new PagerDutyService(config, objectMapper, restTemplate);
}
@Test
void testTriggerIncident_Success() throws Exception {
// Given
IncidentTriggerRequest request = IncidentTriggerRequest.builder()
.summary("Test Incident")
.source("test-service")
.severity(IncidentSeverity.CRITICAL)
.build();
PagerDutyIncidentRequest expectedRequest = createExpectedRequest(request);
PagerDutyIncidentResponse mockResponse = new PagerDutyIncidentResponse();
mockResponse.setStatus("success");
mockResponse.setDedupKey("incident-123");
when(objectMapper.writeValueAsString(any())).thenReturn("request-body");
when(objectMapper.readValue(anyString(), eq(PagerDutyIncidentResponse.class)))
.thenReturn(mockResponse);
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(ResponseEntity.ok("response-body"));
// When
IncidentTriggerResult result = pagerDutyService.triggerIncident(request);
// Then
assertTrue(result.isSuccess());
assertEquals("incident-123", result.getIncidentKey());
verify(restTemplate).postForEntity(anyString(), any(HttpEntity.class), eq(String.class));
}
@Test
void testResolveIncident_Success() throws Exception {
// Given
String incidentKey = "incident-123";
String resolutionMessage = "Issue resolved";
when(objectMapper.writeValueAsString(any())).thenReturn("request-body");
when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class)))
.thenReturn(ResponseEntity.ok("response-body"));
// When
boolean result = pagerDutyService.resolveIncident(incidentKey, resolutionMessage);
// Then
assertTrue(result);
}
private PagerDutyIncidentRequest createExpectedRequest(IncidentTriggerRequest request) {
return PagerDutyIncidentRequest.builder()
.routingKey(config.getIntegrationKey())
.eventAction(IncidentAction.TRIGGER.getValue())
.payload(PagerDutyIncidentRequest.Payload.builder()
.summary(request.getSummary())
.source(request.getSource())
.severity(request.getSeverity().getValue())
.build())
.build();
}
}
2. Integration Test with WireMock
// PagerDutyIntegrationTest.java
@SpringBootTest
@TestPropertySource(properties = {
"pagerduty.api-key=test-api-key",
"pagerduty.integration-key=test-integration-key",
"[email protected]",
"pagerduty.api-url=http://localhost:${wiremock.server.port}"
})
@ExtendWith(SpringExtension.class)
class PagerDutyIntegrationTest {
@Autowired
private PagerDutyService pagerDutyService;
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void testTriggerIncident_Integration() throws Exception {
// Given
String expectedResponse = """
{
"status": "success",
"message": "Event processed",
"dedup_key": "test-incident-123"
}
""";
wireMock.stubFor(post(urlEqualTo("/v2/enqueue"))
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
.withBody(expectedResponse)));
IncidentTriggerRequest request = IncidentTriggerRequest.builder()
.summary("Integration Test Incident")
.source("integration-test")
.severity(IncidentSeverity.ERROR)
.build();
// When
IncidentTriggerResult result = pagerDutyService.triggerIncident(request);
// Then
assertTrue(result.isSuccess());
assertEquals("test-incident-123", result.getIncidentKey());
// Verify request was made
wireMock.verify(postRequestedFor(urlEqualTo("/v2/enqueue"))
.withHeader("Authorization", containing("test-api-key"))
.withHeader("Content-Type", containing("application/json")));
}
}
Configuration
1. Application Properties
# application.yml
pagerduty:
api-key: ${PAGERDUTY_API_KEY:your-api-key-here}
integration-key: ${PAGERDUTY_INTEGRATION_KEY:your-integration-key-here}
api-url: https://api.pagerduty.com
timeout-seconds: 30
max-retries: 3
from-email: [email protected]
service-id: ${PAGERDUTY_SERVICE_ID:your-service-id}
# RestTemplate configuration
rest-template:
connect-timeout: 5000
read-timeout: 10000
# Logging
logging:
level:
com.yourcompany.pagerduty: DEBUG
2. Spring Configuration
// AppConfig.java
@Configuration
public class AppConfig {
@Bean
@ConfigurationProperties(prefix = "pagerduty")
public PagerDutyConfig pagerDutyConfig() {
return new PagerDutyConfig();
}
@Bean
public RestTemplate restTemplate(PagerDutyConfig config) {
RestTemplate restTemplate = new RestTemplate();
// Configure timeouts
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(config.getTimeoutSeconds()));
factory.setReadTimeout(Duration.ofSeconds(config.getTimeoutSeconds()));
restTemplate.setRequestFactory(factory);
// Add error handler
restTemplate.setErrorHandler(new CustomErrorHandler());
return restTemplate;
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
// CustomErrorHandler.java
@Slf4j
public class CustomErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().isError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
log.error("HTTP error occurred: {} {}", response.getStatusCode(), response.getStatusText());
throw new HttpClientErrorException(response.getStatusCode(),
"PagerDuty API error: " + response.getStatusText());
}
}
This comprehensive PagerDuty integration provides:
- Incident triggering with proper error handling and retries
- Incident management (acknowledge, resolve)
- Event-driven architecture for extensibility
- Comprehensive testing with unit and integration tests
- Production-ready configuration with proper timeouts and retries
- REST API for easy integration with other systems
The implementation follows best practices for reliability, observability, and maintainability in production environments.