Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public SystemNotificationRenderResource(SystemNotificationRenderService systemNo
this.systemNotificationRenderService = systemNotificationRenderService;
}

@Deprecated
@POST
@NoAuditEvent("Doesn't change any data, only renders a notification message")
@Path("/html/{type}")
Expand All @@ -68,6 +69,7 @@ public TemplateRenderResponse renderHtml(@Parameter(name = "type", required = tr
return render(type, null, SystemNotificationRenderService.Format.HTML, request);
}

@Deprecated
@POST
@NoAuditEvent("Doesn't change any data, only renders a notification message")
@Path("/html/{type}/{key}")
Expand All @@ -80,6 +82,7 @@ public TemplateRenderResponse renderHtmlWithKey(@Parameter(name = "type", requir
return render(type, key, SystemNotificationRenderService.Format.HTML, request);
}

@Deprecated
@POST
@NoAuditEvent("Doesn't change any data, only renders a notification message")
@Path("/plaintext/{type}")
Expand All @@ -92,6 +95,7 @@ public TemplateRenderResponse renderPlainText(@Parameter(name = "type", required
return render(type, null, SystemNotificationRenderService.Format.PLAINTEXT, request);
}

@Deprecated
@POST
@NoAuditEvent("Doesn't change any data, only renders a notification message")
@Path("/plaintext/{type}/{key}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public RenderResponse render(Notification notification, Format format, Map<Strin
}
}

public class RenderResponse {
public static class RenderResponse {
public String title;
public String description;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public class AuditEventTypes implements PluginAuditEventTypes {

public static final String SYSTEM_JOB_ACKNOWLEDGE = PREFIX + "system_job:acknowledge";
public static final String SYSTEM_NOTIFICATION_CREATE = PREFIX + "system_notification:create";
public static final String SYSTEM_NOTIFICATION_DELETE = PREFIX + "system_notification:delete";
public static final String SYSTEM_NOTIFICATION_UPDATE = PREFIX + "system_notification:update";
public static final String URL_ALLOWLIST_UPDATE = PREFIX + "url_allowlist:update";
public static final String USER_ACCESS_TOKEN_CREATE = PREFIX + "user_access_token:create";
public static final String USER_ACCESS_TOKEN_DELETE = PREFIX + "user_access_token:delete";
Expand Down Expand Up @@ -354,7 +354,7 @@ public class AuditEventTypes implements PluginAuditEventTypes {
.add(SYSTEM_JOB_STOP)
.add(SYSTEM_JOB_ACKNOWLEDGE)
.add(SYSTEM_NOTIFICATION_CREATE)
.add(SYSTEM_NOTIFICATION_DELETE)
.add(SYSTEM_NOTIFICATION_UPDATE)
.add(URL_ALLOWLIST_UPDATE)
.add(USER_ACCESS_TOKEN_CREATE)
.add(USER_ACCESS_TOKEN_DELETE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.graylog2.indexer.IndexFailureImpl;
import org.graylog2.indexer.indexset.IndexSetConfig;
import org.graylog2.inputs.InputImpl;
import org.graylog2.notifications.NotificationImpl;
import org.graylog2.plugin.PluginModule;
import org.graylog2.security.AccessTokenImpl;
import org.graylog2.streams.OutputImpl;
Expand All @@ -49,7 +48,6 @@ protected void configure() {
IndexFailureImpl.class,
IndexSetConfig.class,
InputImpl.class,
NotificationImpl.class,
OutputImpl.class,
ServerNodeEntity.class,
StreamDestinationFilterRuleDTO.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.graylog2.periodical.LeaderPresenceCheckPeriodical;
import org.graylog2.periodical.NodeMetricPeriodical;
import org.graylog2.periodical.NodePingThread;
import org.graylog2.notifications.SystemNotificationCleanupPeriodical;
import org.graylog2.periodical.StaleInputRuntimeStateCleanup;
import org.graylog2.periodical.OrphanedTokenCleaner;
import org.graylog2.periodical.SearchVersionCheckPeriodical;
Expand Down Expand Up @@ -72,5 +73,6 @@ protected void configure() {
periodicalBinder.addBinding().to(OrphanedTokenCleaner.class);
periodicalBinder.addBinding().to(NodeMetricPeriodical.class);
periodicalBinder.addBinding().to(StaleInputRuntimeStateCleanup.class);
periodicalBinder.addBinding().to(SystemNotificationCleanupPeriodical.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
import org.graylog2.inputs.persistence.MongoInputStatusService;
import org.graylog2.notifications.DeletedStreamNotificationListener;
import org.graylog2.notifications.NotificationService;
import org.graylog2.notifications.NotificationServiceImpl;
import org.graylog2.notifications.NotificationServiceAdapter;
import org.graylog2.rest.resources.entities.preferences.service.EntityListPreferencesService;
import org.graylog2.rest.resources.entities.preferences.service.EntityListPreferencesServiceImpl;
import org.graylog2.rest.resources.system.contentpacks.titles.EntityTitleService;
Expand All @@ -75,7 +75,7 @@ public class PersistenceServicesBindings extends AbstractModule {
@Override
protected void configure() {
bind(SystemMessageService.class).to(SystemMessageServiceImpl.class).asEagerSingleton();
bind(NotificationService.class).to(NotificationServiceImpl.class).asEagerSingleton();
bind(NotificationService.class).to(NotificationServiceAdapter.class).asEagerSingleton();
bind(DeletedStreamNotificationListener.class).asEagerSingleton();
bind(IndexFailureService.class).to(IndexFailureServiceImpl.class).asEagerSingleton();
bind(org.graylog2.cluster.NodeService.class).to(NodeServiceImpl.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,8 @@
import org.graylog2.cluster.Node;
import org.graylog2.notifications.Notification;
import org.graylog2.notifications.NotificationService;
import org.graylog2.plugin.database.Persisted;
import org.graylog2.plugin.database.ValidationException;
import org.graylog2.plugin.database.validators.ValidationResult;
import org.graylog2.plugin.database.validators.Validator;

import java.util.List;
import java.util.Map;
import java.util.Optional;

public class NullNotificationService implements NotificationService {
Expand Down Expand Up @@ -89,40 +84,4 @@ public int destroyAllByType(Notification.Type type) {
public int destroyAllByTypeAndKey(Notification.Type type, @Nullable String key) {
return 0;
}

@Override
public <T extends Persisted> int destroy(T model) {
return 0;
}

@Override
public <T extends Persisted> int destroyAll(Class<T> modelClass) {
return 0;
}

@Override
public <T extends Persisted> String save(T model) throws ValidationException {
return null;
}

@Nullable
@Override
public <T extends Persisted> String saveWithoutValidation(T model) {
return null;
}

@Override
public <T extends Persisted> Map<String, List<ValidationResult>> validate(T model, Map<String, Object> fields) {
return null;
}

@Override
public <T extends Persisted> Map<String, List<ValidationResult>> validate(T model) {
return null;
}

@Override
public Map<String, List<ValidationResult>> validate(Map<String, Validator> validators, Map<String, Object> fields) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,6 @@ protected void configure() {
addMigration(V20251103123300_MigrateEntityListPreferences.class);
addMigration(V20260416120000_RemoveStrayFieldsFromInputs.class);
addMigration(V20260428120000_AddLayoutVariantToEntityListPreferencesId.class);
addMigration(V20260430000000_MigrateNotificationsToSystemNotifications.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.migrations;

import org.bson.Document;
import org.graylog.events.processor.systemnotification.SystemNotificationRenderService;
import org.graylog2.database.MongoCollection;
import org.graylog2.database.MongoCollections;
import org.graylog2.notifications.Notification;
import org.graylog2.notifications.NotificationBuilder;
import org.graylog2.notifications.SystemNotificationDto;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.inject.Inject;

import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

public class V20260430000000_MigrateNotificationsToSystemNotifications extends Migration {

private static final Logger LOG = LoggerFactory.getLogger(V20260430000000_MigrateNotificationsToSystemNotifications.class);
private static final String OLD_COLLECTION = "notifications";

private final ClusterConfigService clusterConfigService;
private final MongoCollections mongoCollections;
private final SystemNotificationRenderService renderService;

@Inject
public V20260430000000_MigrateNotificationsToSystemNotifications(
ClusterConfigService clusterConfigService,
MongoCollections mongoCollections,
SystemNotificationRenderService renderService) {
this.clusterConfigService = clusterConfigService;
this.mongoCollections = mongoCollections;
this.renderService = renderService;
}

@Override
public ZonedDateTime createdAt() {
return ZonedDateTime.parse("2026-04-30T00:00:00Z");
}

@Override
public void upgrade() {
if (Objects.nonNull(clusterConfigService.get(MigrationCompleted.class))) {
LOG.debug("Migration already completed, skipping.");
return;
}

final com.mongodb.client.MongoCollection<Document> oldCollection = mongoCollections.nonEntityCollection(OLD_COLLECTION, Document.class);
final MongoCollection<SystemNotificationDto> newCollection =
mongoCollections.collection("system_notifications", SystemNotificationDto.class);

int migrated = 0;
int errors = 0;

for (final Document doc : oldCollection.find()) {
try {
final String typeStr = doc.getString("type");
final String key = doc.getString("key");
final String severity = doc.getString("severity");
final String nodeId = doc.getString("node_id");

@SuppressWarnings("unchecked")
final Map<String, Object> details = doc.get("details", Map.class);

// Convert timestamp
final Instant triggeredAt;
final Object tsObj = doc.get("timestamp");
if (tsObj instanceof Date date) {
triggeredAt = date.toInstant();
} else if (tsObj instanceof String tsStr) {
triggeredAt = Instant.parse(tsStr);
} else {
triggeredAt = Instant.now();
}

// Render title and description
String title = null;
String description = null;
try {
final Notification.Type notifType = Notification.Type.valueOf(typeStr.toUpperCase(Locale.ENGLISH));
final Notification tempNotification = new NotificationBuilder()
.addType(notifType)
.addNode(nodeId != null ? nodeId : "");
if (details != null) {
details.forEach(tempNotification::addDetail);
}
final var rendered = renderService.render(
tempNotification,
SystemNotificationRenderService.Format.PLAINTEXT,
null
);
title = rendered.title != null ? rendered.title.strip() : null;
description = rendered.description != null ? rendered.description.strip() : null;
} catch (Exception e) {
LOG.warn("Could not render notification of type {}: {}", typeStr, e.getMessage());
}

final SystemNotificationDto dto = SystemNotificationDto.builder()
.type(typeStr)
.key(key)
.severity(severity != null ? severity : "normal")
.nodeId(nodeId != null ? nodeId : "")
.title(title)
.description(description)
.details(details != null ? details : Map.of())
.isRead(false)
.actor(null)
.lastChanged(null)
.triggeredAt(triggeredAt)
.build();

newCollection.insertOne(dto);
migrated++;
} catch (Exception e) {
LOG.error("Failed to migrate notification document {}: {}", doc.get("_id"), e.getMessage(), e);
errors++;
}
}

LOG.info("Migrated {} notifications to system_notifications ({} errors)", migrated, errors);

// Drop the old collection
oldCollection.drop();
LOG.info("Dropped old '{}' collection", OLD_COLLECTION);

clusterConfigService.write(new MigrationCompleted());
}

public record MigrationCompleted() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void handleStreamDeleted(StreamDeletedEvent streamDeletedEvent) {
Object rawValue = notification.getDetail("stream_id");
if (rawValue != null && rawValue.toString().equals(streamId)) {
LOG.debug("Removing notification that references stream: {}", notification);
notificationService.destroy(notification);
notificationService.fixed(notification);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import com.fasterxml.jackson.annotation.JsonValue;
import org.graylog2.cluster.Node;
import org.graylog2.plugin.database.Persisted;
import org.joda.time.DateTime;

import javax.annotation.Nullable;
Expand All @@ -27,7 +26,7 @@
import java.util.Map;
import java.util.Set;

public interface Notification extends Persisted {
public interface Notification {

/**
* Notification types that are not actionable by cloud users and should be
Expand Down
Loading
Loading