Skip to content

Commit 4c90a61

Browse files
authored
Watcher: migrate PagerDuty v1 events API to v2 API (#32285)
The PagerDuty v1 API is EOL and will stop accepting new accounts shortly. This commit swaps out the watcher use of the v1 API with the new v2 API. It does not change anything about the existing watcher API. Closes #32243
1 parent cd0de16 commit 4c90a61

File tree

7 files changed

+296
-46
lines changed

7 files changed

+296
-46
lines changed

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/actions/pagerduty/ExecutablePagerDutyAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public Action.Result execute(final String actionId, WatchExecutionContext ctx, P
4747
return new PagerDutyAction.Result.Simulated(event);
4848
}
4949

50-
SentEvent sentEvent = account.send(event, payload);
50+
SentEvent sentEvent = account.send(event, payload, ctx.id().watchId());
5151
return new PagerDutyAction.Result.Executed(account.getName(), sentEvent);
5252
}
5353

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/IncidentEvent.java

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,22 @@
2424
import java.io.IOException;
2525
import java.util.ArrayList;
2626
import java.util.Arrays;
27+
import java.util.Collections;
2728
import java.util.List;
2829
import java.util.Map;
2930
import java.util.Objects;
31+
import java.util.stream.Collectors;
3032

3133
/**
3234
* Official documentation for this can be found at
3335
*
34-
* https://developer.pagerduty.com/documentation/howto/manually-trigger-an-incident/
35-
* https://developer.pagerduty.com/documentation/integration/events/trigger
36-
* https://developer.pagerduty.com/documentation/integration/events/acknowledge
37-
* https://developer.pagerduty.com/documentation/integration/events/resolve
36+
* https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2
3837
*/
3938
public class IncidentEvent implements ToXContentObject {
4039

4140
static final String HOST = "events.pagerduty.com";
42-
static final String PATH = "/generic/2010-04-15/create_event.json";
41+
static final String PATH = "/v2/enqueue";
42+
static final String ACCEPT_HEADER = "application/vnd.pagerduty+json;version=2";
4343

4444
final String description;
4545
@Nullable final HttpProxy proxy;
@@ -93,46 +93,81 @@ public int hashCode() {
9393
return result;
9494
}
9595

96-
public HttpRequest createRequest(final String serviceKey, final Payload payload) throws IOException {
96+
HttpRequest createRequest(final String serviceKey, final Payload payload, final String watchId) throws IOException {
9797
return HttpRequest.builder(HOST, -1)
9898
.method(HttpMethod.POST)
9999
.scheme(Scheme.HTTPS)
100100
.path(PATH)
101101
.proxy(proxy)
102-
.jsonBody(new ToXContent() {
103-
@Override
104-
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
105-
builder.field(Fields.SERVICE_KEY.getPreferredName(), serviceKey);
106-
builder.field(Fields.EVENT_TYPE.getPreferredName(), eventType);
107-
builder.field(Fields.DESCRIPTION.getPreferredName(), description);
108-
if (incidentKey != null) {
109-
builder.field(Fields.INCIDENT_KEY.getPreferredName(), incidentKey);
110-
}
111-
if (client != null) {
112-
builder.field(Fields.CLIENT.getPreferredName(), client);
113-
}
114-
if (clientUrl != null) {
115-
builder.field(Fields.CLIENT_URL.getPreferredName(), clientUrl);
116-
}
117-
if (attachPayload) {
118-
builder.startObject(Fields.DETAILS.getPreferredName());
119-
builder.field(Fields.PAYLOAD.getPreferredName());
120-
payload.toXContent(builder, params);
121-
builder.endObject();
122-
}
123-
if (contexts != null && contexts.length > 0) {
124-
builder.startArray(Fields.CONTEXTS.getPreferredName());
125-
for (IncidentEventContext context : contexts) {
126-
context.toXContent(builder, params);
127-
}
128-
builder.endArray();
129-
}
130-
return builder;
131-
}
132-
})
102+
.setHeader("Accept", ACCEPT_HEADER)
103+
.jsonBody((b, p) -> buildAPIXContent(b, p, serviceKey, payload, watchId))
133104
.build();
134105
}
135106

107+
XContentBuilder buildAPIXContent(XContentBuilder builder, Params params, String serviceKey,
108+
Payload payload, String watchId) throws IOException {
109+
builder.field(Fields.ROUTING_KEY.getPreferredName(), serviceKey);
110+
builder.field(Fields.EVENT_ACTION.getPreferredName(), eventType);
111+
if (incidentKey != null) {
112+
builder.field(Fields.DEDUP_KEY.getPreferredName(), incidentKey);
113+
}
114+
115+
builder.startObject(Fields.PAYLOAD.getPreferredName());
116+
{
117+
builder.field(Fields.SUMMARY.getPreferredName(), description);
118+
119+
if (attachPayload && payload != null) {
120+
builder.startObject(Fields.CUSTOM_DETAILS.getPreferredName());
121+
{
122+
builder.field(Fields.PAYLOAD.getPreferredName(), payload, params);
123+
}
124+
builder.endObject();
125+
}
126+
127+
if (watchId != null) {
128+
builder.field(Fields.SOURCE.getPreferredName(), watchId);
129+
} else {
130+
builder.field(Fields.SOURCE.getPreferredName(), "watcher");
131+
}
132+
// TODO externalize this into something user editable
133+
builder.field(Fields.SEVERITY.getPreferredName(), "critical");
134+
}
135+
builder.endObject();
136+
137+
if (client != null) {
138+
builder.field(Fields.CLIENT.getPreferredName(), client);
139+
}
140+
if (clientUrl != null) {
141+
builder.field(Fields.CLIENT_URL.getPreferredName(), clientUrl);
142+
}
143+
144+
if (contexts != null && contexts.length > 0) {
145+
toXContentV2Contexts(builder, params, contexts);
146+
}
147+
148+
return builder;
149+
}
150+
151+
/**
152+
* Turns the V1 API contexts into 2 distinct lists, images and links. The V2 API has separated these out into 2 top level fields.
153+
*/
154+
private void toXContentV2Contexts(XContentBuilder builder, ToXContent.Params params,
155+
IncidentEventContext[] contexts) throws IOException {
156+
// contexts can be either links or images, and the v2 api needs them separate
157+
Map<IncidentEventContext.Type, List<IncidentEventContext>> groups = Arrays.stream(contexts)
158+
.collect(Collectors.groupingBy(iec -> iec.type));
159+
160+
List<IncidentEventContext> links = groups.getOrDefault(IncidentEventContext.Type.LINK, Collections.emptyList());
161+
if (links.isEmpty() == false) {
162+
builder.array(Fields.LINKS.getPreferredName(), links.toArray());
163+
}
164+
165+
List<IncidentEventContext> images = groups.getOrDefault(IncidentEventContext.Type.IMAGE, Collections.emptyList());
166+
if (images.isEmpty() == false) {
167+
builder.array(Fields.IMAGES.getPreferredName(), images.toArray());
168+
}
169+
}
170+
136171
@Override
137172
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
138173
builder.startObject();
@@ -445,8 +480,15 @@ interface Fields {
445480
// we need to keep this for BWC
446481
ParseField CONTEXT_DEPRECATED = new ParseField("context");
447482

448-
ParseField SERVICE_KEY = new ParseField("service_key");
449483
ParseField PAYLOAD = new ParseField("payload");
450-
ParseField DETAILS = new ParseField("details");
484+
ParseField ROUTING_KEY = new ParseField("routing_key");
485+
ParseField EVENT_ACTION = new ParseField("event_action");
486+
ParseField DEDUP_KEY = new ParseField("dedup_key");
487+
ParseField SUMMARY = new ParseField("summary");
488+
ParseField SOURCE = new ParseField("source");
489+
ParseField SEVERITY = new ParseField("severity");
490+
ParseField LINKS = new ParseField("links");
491+
ParseField IMAGES = new ParseField("images");
492+
ParseField CUSTOM_DETAILS = new ParseField("custom_details");
451493
}
452494
}

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/IncidentEventContext.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,85 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
9292
return builder.endObject();
9393
}
9494

95+
public static IncidentEventContext parse(XContentParser parser) throws IOException {
96+
Type type = null;
97+
String href = null;
98+
String text = null;
99+
String src = null;
100+
String alt = null;
101+
102+
String currentFieldName = null;
103+
XContentParser.Token token;
104+
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
105+
if (token == XContentParser.Token.FIELD_NAME) {
106+
currentFieldName = parser.currentName();
107+
} else if (Strings.hasLength(currentFieldName)) {
108+
if (XField.TYPE.match(currentFieldName, parser.getDeprecationHandler())) {
109+
try {
110+
type = Type.valueOf(parser.text().toUpperCase(Locale.ROOT));
111+
} catch (IllegalArgumentException e) {
112+
String msg = "could not parse trigger incident event context. unknown context type [{}]";
113+
throw new ElasticsearchParseException(msg, parser.text());
114+
}
115+
} else {
116+
if (XField.HREF.match(currentFieldName, parser.getDeprecationHandler())) {
117+
href = parser.text();
118+
} else if (XField.TEXT.match(currentFieldName, parser.getDeprecationHandler())) {
119+
text = parser.text();
120+
} else if (XField.SRC.match(currentFieldName, parser.getDeprecationHandler())) {
121+
src = parser.text();
122+
} else if (XField.ALT.match(currentFieldName, parser.getDeprecationHandler())) {
123+
alt = parser.text();
124+
} else {
125+
String msg = "could not parse trigger incident event context. unknown field [{}]";
126+
throw new ElasticsearchParseException(msg, currentFieldName);
127+
}
128+
}
129+
}
130+
}
131+
132+
return createAndValidateTemplate(type, href, src, alt, text);
133+
}
134+
135+
private static IncidentEventContext createAndValidateTemplate(Type type, String href, String src, String alt,
136+
String text) {
137+
if (type == null) {
138+
throw new ElasticsearchParseException("could not parse trigger incident event context. missing required field [{}]",
139+
XField.TYPE.getPreferredName());
140+
}
141+
142+
switch (type) {
143+
case LINK:
144+
if (href == null) {
145+
throw new ElasticsearchParseException("could not parse trigger incident event context. missing required field " +
146+
"[{}] for [{}] context", XField.HREF.getPreferredName(), Type.LINK.name().toLowerCase(Locale.ROOT));
147+
}
148+
if (src != null) {
149+
throw new ElasticsearchParseException("could not parse trigger incident event context. unexpected field [{}] for " +
150+
"[{}] context", XField.SRC.getPreferredName(), Type.LINK.name().toLowerCase(Locale.ROOT));
151+
}
152+
if (alt != null) {
153+
throw new ElasticsearchParseException("could not parse trigger incident event context. unexpected field [{}] for " +
154+
"[{}] context", XField.ALT.getPreferredName(), Type.LINK.name().toLowerCase(Locale.ROOT));
155+
}
156+
return link(href, text);
157+
case IMAGE:
158+
if (src == null) {
159+
throw new ElasticsearchParseException("could not parse trigger incident event context. missing required field " +
160+
"[{}] for [{}] context", XField.SRC.getPreferredName(), Type.IMAGE.name().toLowerCase(Locale.ROOT));
161+
}
162+
if (text != null) {
163+
throw new ElasticsearchParseException("could not parse trigger incident event context. unexpected field [{}] for " +
164+
"[{}] context", XField.TEXT.getPreferredName(), Type.IMAGE.name().toLowerCase(Locale.ROOT));
165+
}
166+
return image(src, href, alt);
167+
default:
168+
throw new ElasticsearchParseException("could not parse trigger incident event context. unknown context type [{}]",
169+
type);
170+
}
171+
}
172+
173+
95174
public static class Template implements ToXContentObject {
96175

97176
final Type type;

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/PagerDutyAccount.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ public IncidentEventDefaults getDefaults() {
4848
return eventDefaults;
4949
}
5050

51-
public SentEvent send(IncidentEvent event, Payload payload) throws IOException {
52-
HttpRequest request = event.createRequest(serviceKey, payload);
51+
public SentEvent send(IncidentEvent event, Payload payload, String watchId) throws IOException {
52+
HttpRequest request = event.createRequest(serviceKey, payload, watchId);
5353
HttpResponse response = httpClient.execute(request);
5454
return SentEvent.responded(event, request, response);
5555
}

x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/actions/pagerduty/PagerDutyActionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public void testExecute() throws Exception {
111111
when(response.status()).thenReturn(200);
112112
HttpRequest request = mock(HttpRequest.class);
113113
SentEvent sentEvent = SentEvent.responded(event, request, response);
114-
when(account.send(event, payload)).thenReturn(sentEvent);
114+
when(account.send(event, payload, wid.watchId())).thenReturn(sentEvent);
115115
when(service.getAccount(accountName)).thenReturn(account);
116116

117117
Action.Result result = executable.execute("_id", ctx, payload);

0 commit comments

Comments
 (0)