summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorn.christian@seime.no>2023-10-17 13:25:09 +0200
committerGitHub <noreply@github.com>2023-10-17 13:25:09 +0200
commit072231eba066a878276248338c3755284a0f0166 (patch)
treeed4175490caeddbde8937b3b3f82d5f5122c6d8a
parentd3efe7b2195e4c367af1b2c8230926331d9cf907 (diff)
parentf567be9d917c92659aa596d804245314c71befe3 (diff)
Merge pull request #28952 from vespa-engine/bjorncs/email-notification
Bjorncs/email notification
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java11
-rw-r--r--metrics/src/main/java/ai/vespa/metrics/ControllerMetrics.java7
-rw-r--r--metrics/src/main/java/ai/vespa/metrics/set/InfrastructureMetricSet.java4
6 files changed, 87 insertions, 10 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<String, Object> values;
+ private final SortedMap<String, Object> 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<String, Object> values() { return Map.copyOf(values); }
+ public SortedMap<String, Object> values() { return Collections.unmodifiableSortedMap(values); }
public Optional<String> 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<String, Object> values = new HashMap<>();
+ private final Map<String, Object> 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<String> 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<String> messages,
Optional<MailContent> 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<Notification.MailContent> 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<Notification> 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<Notification> deserialized = serializer.fromSlime(tenantName, serialized);
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;
}