diff --git a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/client/PagerDutyHttpEventClient.java b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/client/PagerDutyHttpEventClient.java index 9c5d2e6..5f0944a 100644 --- a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/client/PagerDutyHttpEventClient.java +++ b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/client/PagerDutyHttpEventClient.java @@ -42,20 +42,22 @@ public String getDefaultRoutingKey() { @Override public CompletableFuture acknowledgeEvent(final String routingKey, final String dedupKey) { - return eventAction(routingKey, dedupKey, "\",\"event_action\":\"acknowledge\"}"); + return eventAction(routingKey, dedupKey, "acknowledge"); } @Override public CompletableFuture resolveEvent(final String routingKey, final String dedupKey) { - return eventAction(routingKey, dedupKey, "\",\"event_action\":\"resolve\"}"); + return eventAction(routingKey, dedupKey, "resolve"); } private CompletableFuture eventAction(final String routingKey, final String dedupKey, - final String actionBody) { + final String eventAction) { Objects.requireNonNull(routingKey, "Routing key is a required field."); Objects.requireNonNull(dedupKey, "De-duplication key is a required field."); - final var json = "{\"routing_key\":\"" + routingKey + "\",\"dedup_key\":\"" + dedupKey + actionBody; + final var json = String.format(""" + {"routing_key":"%s","dedup_key":"%s","event_action":"%s"}""", + routingKey, dedupKey, eventAction); return createAndSendRequest(routingKey, json); } @@ -74,14 +76,11 @@ public CompletableFuture triggerEvent(final String clien ? "" : payload.getLinks().stream().map(PagerDutyLinkRef::toJson) .collect(Collectors.joining(",", ",\"links\":[", "]")); - final var json = "{\"event_action\":\"trigger\",\"payload\":" + payloadJson - + ",\"routing_key\":\"" + routingKey + '"' - + ",\"dedup_key\":\"" + payload.getDedupKey() + '"' - + ",\"client\":\"" + clientName + '"' - + (clientUrl == null ? "" : ",\"client_url\":\"" + clientUrl + '"') - + imagesJson - + linksJson - + '}'; + final var json = String.format(""" + {"event_action":"trigger","routing_key":"%s","dedup_key":"%s","payload":%s,"client":"%s"%s%s%s}""", + routingKey, payload.getDedupKey(), payloadJson, clientName, + (clientUrl == null ? "" : ",\"client_url\":\"" + clientUrl + '"'), + imagesJson, linksJson); return createAndSendRequest(routingKey, json); } diff --git a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadBuilder.java b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadBuilder.java index 78c551f..0e0a534 100644 --- a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadBuilder.java +++ b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadBuilder.java @@ -2,11 +2,16 @@ import java.time.ZonedDateTime; import java.util.*; +import java.util.stream.Collectors; import static java.time.ZoneOffset.UTC; final class PagerDutyEventPayloadBuilder implements PagerDutyEventPayload.Builder { + private static final Map NO_CUSTOM_DETAILS = Map.of(); + private static final List NO_LINKS = List.of(); + private static final List NO_IMAGES = List.of(); + private String dedupKey; private String summary; private String source; @@ -20,54 +25,77 @@ final class PagerDutyEventPayloadBuilder implements PagerDutyEventPayload.Builde private List images; PagerDutyEventPayloadBuilder() { - this.customDetails = Map.of(); - this.links = List.of(); - this.images = List.of(); + this.customDetails = NO_CUSTOM_DETAILS; + this.links = NO_LINKS; + this.images = NO_IMAGES; } PagerDutyEventPayloadBuilder(final PagerDutyEventPayload prototype) { - this.dedupKey = prototype.getDedupKey(); - this.summary = prototype.getSummary(); - this.source = prototype.getSource(); - this.severity = prototype.getSeverity(); - this.timestamp = prototype.getTimestamp(); - this.component = prototype.getComponent(); - this.group = prototype.getGroup(); - this.type = prototype.getType(); - this.customDetails = prototype.getCustomDetails().size() > 1 - ? new LinkedHashMap<>(prototype.getCustomDetails()) - : Map.copyOf(prototype.getCustomDetails()); - this.links = prototype.getLinks().size() > 1 - ? new ArrayList<>(prototype.getLinks()) - : List.copyOf(prototype.getLinks()); - this.images = prototype.getImages().size() > 1 - ? new ArrayList<>(prototype.getImages()) - : List.copyOf(prototype.getImages()); + this.dedupKey(prototype.getDedupKey()); + this.summary(prototype.getSummary()); + this.source(prototype.getSource()); + this.severity(prototype.getSeverity()); + this.timestamp(prototype.getTimestamp()); + this.component(prototype.getComponent()); + this.group(prototype.getGroup()); + this.type(prototype.getType()); + final var customDetails = prototype.getCustomDetails(); + this.customDetails = customDetails == null || customDetails.isEmpty() + ? NO_CUSTOM_DETAILS + : customDetails.size() > 1 + ? new LinkedHashMap<>(customDetails) + : Map.copyOf(customDetails); + final var links = prototype.getLinks(); + this.links = links == null || links.isEmpty() + ? NO_LINKS + : links.size() > 1 + ? new ArrayList<>(links) + : List.copyOf(links); + final var images = prototype.getImages(); + this.images = images == null || images.isEmpty() + ? NO_IMAGES + : images.size() > 1 + ? new ArrayList<>(images) + : List.copyOf(images); } @Override public PagerDutyEventPayload create() { - return new PagerDutyEventPayloadVal( - dedupKey == null || dedupKey.isBlank() ? UUID.randomUUID().toString() : dedupKey, - Objects.requireNonNull(summary, "'Summary' is a required payload field."), - Objects.requireNonNull(source, "'Source' is a required payload field."), - Objects.requireNonNull(severity, "'Severity' is a required payload field."), - timestamp == null ? ZonedDateTime.now(UTC) : timestamp, + Objects.requireNonNull(summary, "'Summary' is a required payload field."); + Objects.requireNonNull(source, "'Source' is a required payload field."); + Objects.requireNonNull(severity, "'Severity' is a required payload field."); + if (dedupKey == null || dedupKey.isBlank()) { + dedupKey = UUID.randomUUID().toString(); + } + if (timestamp == null) { + timestamp = ZonedDateTime.now(UTC); + } + final var json = getPayloadJson(); + return new PagerDutyEventPayloadRecord( + dedupKey, + summary, + source, + severity, + timestamp, component, group, type, - getCustomDetails(), - links, - images); + customDetails.size() > 1 ? Collections.unmodifiableMap(customDetails) : customDetails, + links.size() > 1 ? Collections.unmodifiableList(links) : links, + images.size() > 1 ? Collections.unmodifiableList(images) : images, + json); } @Override public Builder dedupKey(final String dedupKey) { + if (dedupKey != null && dedupKey.length() > 255) { + throw new IllegalArgumentException("Max length for 'dedup_key' is 255"); + } this.dedupKey = dedupKey; return this; } @Override public Builder summary(final String summary) { - this.summary = summary; + this.summary = summary.length() > 1_024 ? summary.substring(0, 1_024) : summary; return this; } @@ -205,7 +233,7 @@ public String getType() { @Override public Map getCustomDetails() { - return customDetails == null ? Map.of() : customDetails; + return customDetails; } @Override @@ -218,17 +246,75 @@ public List getImages() { return images; } + private static void appendString(final StringBuilder jsonBuilder, final String field, final String str) { + if (str != null && !str.isBlank()) { + jsonBuilder.append(",\""); + jsonBuilder.append(field); + jsonBuilder.append("\":\""); + jsonBuilder.append(str); + jsonBuilder.append('"'); + } + } + + private static String escapeQuotes(final String str) { + final char[] chars = str.toCharArray(); + final char[] escaped = new char[chars.length << 1]; + char c; + for (int escapes = 0, from = 0, dest = 0, to = 0; ; to++) { + if (to == chars.length) { + if (from == 0) { + return str; + } else { + final int len = to - from; + System.arraycopy(chars, from, escaped, dest, len); + dest += len; + return new String(escaped, 0, dest); + } + } else { + c = chars[to]; + if (c == '\\') { + escapes++; + } else if (c == '"' && (escapes & 1) == 0) { + final int len = to - from; + System.arraycopy(chars, from, escaped, dest, len); + dest += len; + escaped[dest++] = '\\'; + from = to; + escapes = 0; + } else { + escapes = 0; + } + } + } + } + + private static String toJson(final Map object) { + return object.entrySet().stream().map(entry -> { + final var val = entry.getValue(); + if (val instanceof Number || val instanceof Boolean) { + return '"' + entry.getKey() + "\":" + val; + } else { + final var str = val.toString(); + return '"' + entry.getKey() + "\":\"" + (str.indexOf('"') < 0 ? str : escapeQuotes(str)) + '"'; + } + }).collect(Collectors.joining(",", "{", "}")); + } + @Override public String getPayloadJson() { - return "{\"summary\":\"" + summary - + "\",\"source\":\"" + source - + "\",\"severity\":\"" + severity - + "\",\"timestamp\":\"" + timestamp - + (component == null ? '"' : "\",\"component\":\"" + component + '"') - + (group == null ? "" : ",\"group\":\"" + group + '"') - + (type == null ? "" : ",\"type\":\"" + type + '"') - + (customDetails == null ? "" : ",\"custom_details\":" + PagerDutyEventPayloadVal.toJson(customDetails)) - + '}'; + final var jsonBuilder = new StringBuilder(2_048); + jsonBuilder.append(String.format(""" + {"summary":"%s","source":"%s","severity":"%s","timestamp":"%s\"""", + summary, source, severity, timestamp)); + appendString(jsonBuilder, "component", component); + appendString(jsonBuilder, "group", group); + appendString(jsonBuilder, "class", type); + if (!customDetails.isEmpty()) { + jsonBuilder.append(""" + ,"custom_details":"""); + jsonBuilder.append(toJson(customDetails)); + } + return jsonBuilder.append('}').toString(); } @Override diff --git a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadRecord.java b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadRecord.java new file mode 100644 index 0000000..1f9f1ac --- /dev/null +++ b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadRecord.java @@ -0,0 +1,84 @@ +package systems.comodal.pagerduty.event.data; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +record PagerDutyEventPayloadRecord(String dedupKey, + String summary, + String source, + PagerDutySeverity severity, + ZonedDateTime timestamp, + String component, + String group, + String type, + Map customDetails, + List links, + List images, + String json) implements PagerDutyEventPayload { + + @Override + public String getDedupKey() { + return dedupKey; + } + + @Override + public String getSummary() { + return summary; + } + + @Override + public String getSource() { + return source; + } + + @Override + public PagerDutySeverity getSeverity() { + return severity; + } + + @Override + public ZonedDateTime getTimestamp() { + return timestamp; + } + + @Override + public String getComponent() { + return component; + } + + @Override + public String getGroup() { + return group; + } + + @Override + public String getType() { + return type; + } + + @Override + public Map getCustomDetails() { + return customDetails; + } + + @Override + public List getLinks() { + return links; + } + + @Override + public List getImages() { + return images; + } + + @Override + public String getPayloadJson() { + return json; + } + + @Override + public String toString() { + return json; + } +} diff --git a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadVal.java b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadVal.java deleted file mode 100644 index dd85a1a..0000000 --- a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyEventPayloadVal.java +++ /dev/null @@ -1,186 +0,0 @@ -package systems.comodal.pagerduty.event.data; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -final class PagerDutyEventPayloadVal implements PagerDutyEventPayload { - - private final String dedupKey; - private final String summary; - private final String source; - private final PagerDutySeverity severity; - private final ZonedDateTime timestamp; - private final String component; - private final String group; - private final String type; - private final Map customDetails; - private final List links; - private final List images; - private final String json; - - PagerDutyEventPayloadVal(final String dedupKey, - final String summary, - final String source, - final PagerDutySeverity severity, - final ZonedDateTime timestamp, - final String component, - final String group, - final String type, - final Map customDetails, - final List links, - final List images) { - this.dedupKey = dedupKey; - this.summary = summary; - this.source = source; - this.severity = severity; - this.timestamp = timestamp; - this.component = component; - this.group = group; - this.type = type; - this.customDetails = customDetails; - this.links = links; - this.images = images; - this.json = "{\"summary\":\"" + summary - + "\",\"source\":\"" + source - + "\",\"severity\":\"" + severity - + "\",\"timestamp\":\"" + timestamp - + (component == null ? '"' : "\",\"component\":\"" + component + '"') - + (group == null ? "" : ",\"group\":\"" + group + '"') - + (type == null ? "" : ",\"class\":\"" + type + '"') - + (customDetails.isEmpty() ? "" : ",\"custom_details\":" + toJson(customDetails)) - + '}'; - } - - private static String escapeQuotes(final String str) { - final char[] chars = str.toCharArray(); - final char[] escaped = new char[chars.length << 1]; - char c; - for (int escapes = 0, from = 0, dest = 0, to = 0; ; to++) { - if (to == chars.length) { - if (from == 0) { - return str; - } else { - final int len = to - from; - System.arraycopy(chars, from, escaped, dest, len); - dest += len; - return new String(escaped, 0, dest); - } - } else { - c = chars[to]; - if (c == '\\') { - escapes++; - } else if (c == '"' && (escapes & 1) == 0) { - final int len = to - from; - System.arraycopy(chars, from, escaped, dest, len); - dest += len; - escaped[dest++] = '\\'; - from = to; - escapes = 0; - } else { - escapes = 0; - } - } - } - } - - static String toJson(final Map object) { - return object.entrySet().stream().map(entry -> { - final var val = entry.getValue(); - if (val instanceof Number || val instanceof Boolean) { - return '"' + entry.getKey() + "\":" + val; - } else { - final var str = val.toString(); - return '"' + entry.getKey() + "\":\"" + (str.indexOf('"') < 0 ? str : escapeQuotes(str)) + '"'; - } - }).collect(Collectors.joining(",", "{", "}")); - } - - @Override - public String getDedupKey() { - return dedupKey; - } - - @Override - public String getSummary() { - return summary; - } - - @Override - public String getSource() { - return source; - } - - @Override - public PagerDutySeverity getSeverity() { - return severity; - } - - @Override - public ZonedDateTime getTimestamp() { - return timestamp; - } - - @Override - public String getComponent() { - return component; - } - - @Override - public String getGroup() { - return group; - } - - @Override - public String getType() { - return type; - } - - @Override - public Map getCustomDetails() { - return customDetails; - } - - @Override - public List getLinks() { - return links; - } - - @Override - public List getImages() { - return images; - } - - @Override - public String getPayloadJson() { - return json; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PagerDutyEventPayloadVal that = (PagerDutyEventPayloadVal) o; - return Objects.equals(dedupKey, that.dedupKey) && - Objects.equals(summary, that.summary) && - Objects.equals(source, that.source) && - severity == that.severity && - Objects.equals(timestamp, that.timestamp) && - Objects.equals(component, that.component) && - Objects.equals(group, that.group) && - Objects.equals(type, that.type) && - Objects.equals(customDetails, that.customDetails); - } - - @Override - public int hashCode() { - return Objects.hash(dedupKey, summary, source, severity, timestamp, component, group, type, customDetails); - } - - @Override - public String toString() { - return getPayloadJson(); - } -} diff --git a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyImageRef.java b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyImageRef.java index d9f3f68..ed6ecbd 100644 --- a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyImageRef.java +++ b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyImageRef.java @@ -13,9 +13,8 @@ static PagerDutyImageRef.Builder build() { String getAlt(); default String toJson() { - return "{\"src\":\"" + getSrc() - + "\",\"href\":\"" + getHref() - + "\",\"alt\":\"" + getAlt() + "\"}"; + return String.format(""" + {"src":"%s","href":"%s","alt":"%s"}""", getSrc(), getHref(), getAlt()); } interface Builder extends PagerDutyImageRef { diff --git a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyLinkRef.java b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyLinkRef.java index 51ba87d..90b0c55 100644 --- a/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyLinkRef.java +++ b/systems.comodal.pagerduty_event_client/src/main/java/systems/comodal/pagerduty/event/data/PagerDutyLinkRef.java @@ -11,7 +11,8 @@ static PagerDutyLinkRef.Builder build() { String getText(); default String toJson() { - return "{\"href\":\"" + getHref() + "\",\"text\":\"" + getText() + "\"}"; + return String.format(""" + {"href":"%s","text":"%s"}""", getHref(), getText()); } interface Builder extends PagerDutyLinkRef { diff --git a/systems.comodal.pagerduty_event_client_test/src/main/java/systems/comodal/test/pagerduty/EventClientTests.java b/systems.comodal.pagerduty_event_client_test/src/main/java/systems/comodal/test/pagerduty/EventClientTests.java index 2a55a35..b28ebf6 100644 --- a/systems.comodal.pagerduty_event_client_test/src/main/java/systems/comodal/test/pagerduty/EventClientTests.java +++ b/systems.comodal.pagerduty_event_client_test/src/main/java/systems/comodal/test/pagerduty/EventClientTests.java @@ -15,6 +15,8 @@ public final class EventClientTests implements EventClientTest { private final String dedupKey = UUID.randomUUID().toString(); + private final String response = String.format(""" + {"status":"success","message":"Event processed","dedup_key":"%s"}""", dedupKey); @Override public void createContext(final HttpServer httpServer, @@ -36,22 +38,31 @@ public void createContext(final HttpServer httpServer, assertEquals(routingKey, headers.getFirst("X-Routing-Key")); final var body = new String(httpExchange.getRequestBody().readAllBytes()); + if (body.startsWith("{\"event_action\":\"trigger")) { - assertEquals("{\"event_action\":\"trigger\",\"payload\":" + - "{\"summary\":\"test-summary\",\"source\":\"test-source\",\"severity\":\"critical\",\"timestamp\":\"2018-08-01T02:03:04Z\",\"component\":\"test-component\",\"group\":\"test-group\",\"class\":\"test-class\"," + - "\"custom_details\":{\"test-num-metric\":1,\"test-string-metric\":\"val\"}" + - "},\"routing_key\":\"" + routingKey + - "\",\"dedup_key\":\"" + dedupKey + - "\",\"client\":\"" + clientName + - "\",\"images\":[{\"src\":\"https://www.pagerduty.com/wp-content/uploads/2016/05/pagerduty-logo-green.png\",\"href\":\"https://www.pagerduty.com/\",\"alt\":\"pagerduty\"}]" + - ",\"links\":[{\"href\":\"https://github.com/comodal/pagerduty-client\",\"text\":\"Github pagerduty-client\"}]}", body); - writeResponse(httpExchange, "{\"status\":\"success\",\"message\":\"Event processed\",\"dedup_key\":\"" + dedupKey + "\"}"); + final var expected = String.format(""" + { + "event_action":"trigger", + "routing_key":"%s", + "dedup_key":"%s", + "payload":{"summary":"test-summary","source":"test-source","severity":"critical","timestamp":"2018-08-01T02:03:04Z","component":"test-component","group":"test-group","class":"test-class","custom_details":{"test-num-metric":1,"test-string-metric":"val"}}, + "client":"%s", + "images":[{"src":"https://www.pagerduty.com/wp-content/uploads/2016/05/pagerduty-logo-green.png","href":"https://www.pagerduty.com/","alt":"pagerduty"}], + "links":[{"href":"https://github.com/comodal/pagerduty-client","text":"Github pagerduty-client"}] + }""".replaceAll("\\n", ""), + routingKey, dedupKey, clientName); + assertEquals(expected, body); + writeResponse(httpExchange, response); } else if (body.endsWith("acknowledge\"}")) { - assertEquals("{\"routing_key\":\"" + routingKey + "\",\"dedup_key\":\"" + dedupKey + "\",\"event_action\":\"acknowledge\"}", body); - writeResponse(httpExchange, "{\"status\":\"success\",\"message\":\"Event processed\",\"dedup_key\":\"" + dedupKey + "\"}"); + final var expected = String.format(""" + {"routing_key":"%s","dedup_key":"%s","event_action":"acknowledge"}""", routingKey, dedupKey); + assertEquals(expected, body); + writeResponse(httpExchange, response); } else if (body.endsWith("resolve\"}")) { - assertEquals("{\"routing_key\":\"" + routingKey + "\",\"dedup_key\":\"" + dedupKey + "\",\"event_action\":\"resolve\"}", body); - writeResponse(httpExchange, "{\"status\":\"success\",\"message\":\"Event processed\",\"dedup_key\":\"" + dedupKey + "\"}"); + final var expected = String.format(""" + {"routing_key":"%s","dedup_key":"%s","event_action":"resolve"}""", routingKey, dedupKey); + assertEquals(expected, body); + writeResponse(httpExchange, response); } else { fail("Invalid request body: " + body); }