From ca8117d5128433efad292025c911814bd776361f Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Mon, 16 Oct 2023 13:47:41 +0200 Subject: Include email content when persisting notification --- .../controller/notification/Notification.java | 37 +++++++++++++++++++--- .../controller/notification/NotificationsDb.java | 1 - .../persistence/NotificationsSerializer.java | 37 +++++++++++++++++++++- .../persistence/NotificationsSerializerTest.java | 11 +++++-- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java index 40c24c6f339..5116ecaf053 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java @@ -2,11 +2,15 @@ package com.yahoo.vespa.hosted.controller.notification; import java.time.Instant; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; /** * Represents an event that we want to notify the tenant about. The message(s) should be short @@ -78,34 +82,57 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve public static class MailContent { private final String template; - private final Map values; + private final SortedMap values; private final String subject; private MailContent(Builder b) { template = Objects.requireNonNull(b.template); - values = Map.copyOf(b.values); + values = new TreeMap<>(b.values); subject = b.subject; } public String template() { return template; } - public Map values() { return Map.copyOf(values); } + public SortedMap values() { return Collections.unmodifiableSortedMap(values); } public Optional subject() { return Optional.ofNullable(subject); } public static Builder fromTemplate(String template) { return new Builder(template); } public static class Builder { private final String template; - private final HashMap values = new HashMap<>(); + private final Map values = new HashMap<>(); private String subject; private Builder(String template) { this.template = template; } - public Builder with(String name, Object value) { values.put(name, value); return this; } + public Builder with(String name, String value) { values.put(name, value); return this; } + public Builder with(String name, Collection items) { values.put(name, List.copyOf(items)); return this; } public Builder subject(String s) { this.subject = s; return this; } public MailContent build() { return new MailContent(this); } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MailContent that = (MailContent) o; + return Objects.equals(template, that.template) && Objects.equals(values, that.values) && Objects.equals(subject, that.subject); + } + + @Override + public int hashCode() { + return Objects.hash(template, values, subject); + } + + @Override + public String toString() { + return "MailContent{" + + "template='" + template + '\'' + + ", values=" + values + + ", subject='" + subject + '\'' + + '}'; + } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java index e752e13eddd..a5d26feafaa 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java @@ -71,7 +71,6 @@ public class NotificationsDb { /** * Add a notification with given source and type. If a notification with same source and type * already exists, it'll be replaced by this one instead. - * Email content is not persisted here. The email dispatcher is responsible for reliable delivery. */ public void setNotification(NotificationSource source, Type type, Level level, List messages, Optional mailContent) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java index 7915a833be6..62b35d4cfd4 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; +import com.yahoo.slime.ObjectTraverser; import com.yahoo.slime.Slime; import com.yahoo.slime.SlimeUtils; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; @@ -15,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.notification.Notification; import com.yahoo.vespa.hosted.controller.notification.NotificationSource; import java.util.List; +import java.util.Optional; /** * (de)serializes notifications for a tenant @@ -60,6 +62,22 @@ public class NotificationsSerializer { notification.source().clusterId().ifPresent(clusterId -> notificationObject.setString(clusterIdField, clusterId.value())); notification.source().jobType().ifPresent(jobType -> notificationObject.setString(jobTypeField, jobType.serialized())); notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber)); + + notification.mailContent().ifPresent(mc -> { + notificationObject.setString("mail-template", mc.template()); + mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s)); + var mailParamsCursor = notificationObject.setObject("mail-params"); + mc.values().forEach((key, value) -> { + if (value instanceof String str) { + mailParamsCursor.setString(key, str); + } else if (value instanceof List l) { + var array = mailParamsCursor.setArray(key); + l.forEach(elem -> array.addString((String) elem)); + } else { + throw new ClassCastException("Unsupported param type: " + value.getClass()); + } + }); + }); } return slime; @@ -92,7 +110,24 @@ public class NotificationsSerializer { SlimeUtils.optionalString(inspector.field(clusterIdField)).map(ClusterSpec.Id::from), SlimeUtils.optionalString(inspector.field(jobTypeField)).map(jobName -> JobType.ofSerialized(jobName)), SlimeUtils.optionalLong(inspector.field(runNumberField))), - SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList()); + SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList(), + mailContentFrom(inspector)); + } + + private Optional mailContentFrom(final Inspector inspector) { + return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> { + var builder = Notification.MailContent.fromTemplate(template); + SlimeUtils.optionalString(inspector.field("mail-subject")).ifPresent(builder::subject); + var paramsCursor = inspector.field("mail-params"); + inspector.field("mail-params").traverse((ObjectTraverser) (name, insp) -> { + switch (insp.type()) { + case STRING -> builder.with(name, insp.asString()); + case ARRAY -> builder.with(name, SlimeUtils.entriesStream(insp).map(Inspector::asString).toList()); + default -> throw new IllegalArgumentException("Unsupported param type: " + insp.type()); + } + }); + return builder.build(); + }); } private static String asString(Notification.Type type) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java index 63aa45a5a34..26eb30b6525 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,6 +28,8 @@ public class NotificationsSerializerTest { void serialization_test() throws IOException { NotificationsSerializer serializer = new NotificationsSerializer(); TenantName tenantName = TenantName.from("tenant1"); + var mail = Notification.MailContent.fromTemplate("my-template").subject("My mail subject") + .with("string-param", "string-value").with("list-param", List.of("elem1", "elem2")).build(); List notifications = List.of( new Notification(Instant.ofEpochSecond(1234), Notification.Type.applicationPackage, @@ -37,7 +40,8 @@ public class NotificationsSerializerTest { Notification.Type.deployment, Notification.Level.error, NotificationSource.from(new RunId(ApplicationId.from(tenantName.value(), "app1", "instance1"), DeploymentContext.systemTest, 12)), - List.of("Failed to deploy: Node allocation failure"))); + List.of("Failed to deploy: Node allocation failure"), + Optional.of(mail))); Slime serialized = serializer.toSlime(notifications); assertEquals("{\"notifications\":[" + @@ -55,7 +59,10 @@ public class NotificationsSerializerTest { "\"application\":\"app1\"," + "\"instance\":\"instance1\"," + "\"jobId\":\"test.us-east-1\"," + - "\"runNumber\":12" + + "\"runNumber\":12," + + "\"mail-template\":\"my-template\"," + + "\"mail-subject\":\"My mail subject\"," + + "\"mail-params\":{\"list-param\":[\"elem1\",\"elem2\"],\"string-param\":\"string-value\"}" + "}]}", new String(SlimeUtils.toJsonBytes(serialized))); List deserialized = serializer.fromSlime(tenantName, serialized); -- cgit v1.2.3 From f567be9d917c92659aa596d804245314c71befe3 Mon Sep 17 00:00:00 2001 From: Bjørn Christian Seime Date: Mon, 16 Oct 2023 14:30:42 +0200 Subject: Introduce metrics for mail sending --- metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java | 7 ++++++- .../main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java b/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java index f03c54aa822..34cf2d98ef8 100644 --- a/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java +++ b/metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java @@ -61,7 +61,12 @@ public enum ControllerMetrics implements VespaMetrics { METERING_MEMORY_GB("metering.memoryGB", Unit.GIGABYTE, "Controller: Metering memory GB"), METERING_VCPU("metering.vcpu", Unit.VCPU, "Controller: Metering VCPU"), METERING_LAST_REPORTED("metering_last_reported", Unit.SECONDS_SINCE_EPOCH, "Controller: Metering last reported"), - METERING_TOTAL_REPORTED("metering_total_reported", Unit.ITEM, "Controller: Metering total reported (sum of resources)"); + METERING_TOTAL_REPORTED("metering_total_reported", Unit.ITEM, "Controller: Metering total reported (sum of resources)"), + + MAIL_SENT("mail.sent", Unit.OPERATION, "Mail sent"), + MAIL_FAILED("mail.failed", Unit.OPERATION, "Mail delivery failed"), + MAIL_THROTTLED("mail.throttled", Unit.OPERATION, "Mail delivery throttled"); + private final String name; private final Unit unit; diff --git a/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java b/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java index 9443a08e28b..36750adb749 100644 --- a/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java +++ b/metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java @@ -181,6 +181,10 @@ public class InfrastructureMetricSet { addMetric(metrics, ControllerMetrics.METERING_AGE_SECONDS.min()); addMetric(metrics, ControllerMetrics.METERING_LAST_REPORTED.max()); + addMetric(metrics, ControllerMetrics.MAIL_SENT.count()); + addMetric(metrics, ControllerMetrics.MAIL_FAILED.count()); + addMetric(metrics, ControllerMetrics.MAIL_THROTTLED.count()); + return metrics; } -- cgit v1.2.3