All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.graylog.events.legacy.LegacyAlertConditionMigrator Maven / Gradle / Ivy

There is a newer version: 6.1.4
Show newest version
/*
 * 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
 * .
 */
package org.graylog.events.legacy;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import org.bson.Document;
import org.graylog.events.conditions.Expr;
import org.graylog.events.conditions.Expression;
import org.graylog.events.notifications.DBNotificationService;
import org.graylog.events.notifications.EventNotificationHandler;
import org.graylog.events.notifications.EventNotificationSettings;
import org.graylog.events.notifications.NotificationDto;
import org.graylog.events.notifications.NotificationResourceHandler;
import org.graylog.events.processor.EventDefinitionDto;
import org.graylog.events.processor.EventDefinitionHandler;
import org.graylog.events.processor.EventProcessorConfig;
import org.graylog.events.processor.aggregation.AggregationConditions;
import org.graylog.events.processor.aggregation.AggregationEventProcessorConfig;
import org.graylog.plugins.views.search.searchtypes.pivot.SeriesSpec;
import org.graylog.plugins.views.search.searchtypes.pivot.series.Average;
import org.graylog.plugins.views.search.searchtypes.pivot.series.Count;
import org.graylog.plugins.views.search.searchtypes.pivot.series.Max;
import org.graylog.plugins.views.search.searchtypes.pivot.series.Min;
import org.graylog.plugins.views.search.searchtypes.pivot.series.StdDev;
import org.graylog.plugins.views.search.searchtypes.pivot.series.Sum;
import org.graylog2.database.MongoConnection;
import org.graylog2.shared.users.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.isNullOrEmpty;

/**
 * Takes care of migrating legacy alert condition and alarm callback configurations to new {@link org.graylog.events.processor.EventDefinition event definitions}
 * and {@link org.graylog.events.notifications.EventNotificationConfig notification configurations}.
 *
 * This class is deliberately avoiding the usage of the legacy Java classes by using the raw MongoDB client
 * so we can safely delete the legacy classes at one point and still run the migrations.
 */
public class LegacyAlertConditionMigrator {
    private static final Logger LOG = LoggerFactory.getLogger(LegacyAlertConditionMigrator.class);

    private final MongoCollection streamsCollection;
    private final MongoCollection alarmCallbacksCollection;
    private final EventDefinitionHandler eventDefinitionHandler;
    private final NotificationResourceHandler notificationResourceHandler;
    private final DBNotificationService dbNotificationService;
    private final UserService userService;
    private final long executeEveryMs;

    @Inject
    public LegacyAlertConditionMigrator(MongoConnection mongoConnection,
                                        EventDefinitionHandler eventDefinitionHandler,
                                        NotificationResourceHandler notificationResourceHandler,
                                        DBNotificationService dbNotificationService,
                                        UserService userService,
                                        @Named("alert_check_interval") int alertCheckInterval) {
        this.streamsCollection = mongoConnection.getMongoDatabase().getCollection("streams");
        this.alarmCallbacksCollection = mongoConnection.getMongoDatabase().getCollection("alarmcallbackconfigurations");
        this.eventDefinitionHandler = eventDefinitionHandler;
        this.notificationResourceHandler = notificationResourceHandler;
        this.dbNotificationService = dbNotificationService;
        this.userService = userService;

        // The old alert conditions have been executed every "alert_check_interval" in seconds
        this.executeEveryMs = alertCheckInterval * 1000L;
    }

    public MigrationResult run(Set completedAlertConditions, Set completedAlarmCallbacks) {
        final MigrationResult.Builder result = MigrationResult.builder();

        streamsCollection.find().forEach(stream -> {
            final String streamId = stream.getObjectId("_id").toHexString();
            final String streamTitle = stream.getString("title");

            final FindIterable iterable = alarmCallbacksCollection.find(Filters.eq("stream_id", streamId));
            final Set notifications = Streams.stream(iterable)
                    .map(alarmCallback -> {
                        final String callbackId = alarmCallback.getObjectId("_id").toHexString();

                        if (completedAlarmCallbacks.contains(callbackId)) {
                            result.addCompletedAlarmCallback(callbackId);
                            return dbNotificationService.get(callbackId).orElse(null);
                        }

                        try {
                            final NotificationDto notificationDto = migrateAlarmCallback(alarmCallback);
                            result.addCompletedAlarmCallback(callbackId);
                            return notificationDto;
                        } catch (Exception e) {
                            LOG.error("Couldn't migrate legacy alarm callback on stream <{}/{}>: {}", streamTitle, streamId, alarmCallback, e);
                            return null;
                        }
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());

            if (!stream.containsKey("alert_conditions")) {
                return;
            }

            @SuppressWarnings("unchecked")
            final List list = (List) stream.get("alert_conditions");

            list.forEach(alertCondition -> {
                final String conditionId = alertCondition.getString("id");
                final String conditionType = alertCondition.getString("type");

                if (completedAlertConditions.contains(conditionId)) {
                    result.addCompletedAlertCondition(conditionId);
                    return;
                }
                try {
                    switch (conditionType) {
                        case "message_count":
                            migrateMessageCount(new Helper(stream, alertCondition, notifications));
                            result.addCompletedAlertCondition(conditionId);
                            break;
                        case "field_value":
                            migrateFieldValue(new Helper(stream, alertCondition, notifications));
                            result.addCompletedAlertCondition(conditionId);
                            break;
                        case "field_content_value":
                            migrateFieldContentValue(new Helper(stream, alertCondition, notifications));
                            result.addCompletedAlertCondition(conditionId);
                            break;
                        default:
                            LOG.warn("Couldn't migrate unknown legacy alert condition type: {}", conditionType);
                    }
                } catch (Exception e) {
                    LOG.error("Couldn't migrate legacy alert condition on stream <{}/{}>: {}", streamTitle, streamId, alertCondition, e);
                }
            });
        });

        return result.build();
    }

    /**
     * Example alarm callback data structure:
     * 
{@code
     *     {
     *       "_id": "54e3deadbeefdeadbeef0001",
     *       "stream_id" : "54e3deadbeefdeadbeef0001",
     *       "type" : "org.graylog2.alarmcallbacks.HTTPAlarmCallback",
     *       "title" : "HTTP Callback Test",
     *       "configuration" : {
     *         "url" : "http://localhost:11000/"
     *       },
     *       "created_at": "2019-01-01T00:00:00.000Z",
     *       "creator_user_id" : "admin"
     *     }
     * }
*/ private NotificationDto migrateAlarmCallback(Document alarmCallback) { final String title = alarmCallback.getString("title"); final String type = alarmCallback.getString("type"); final Document configDoc = (Document) alarmCallback.get("configuration"); final Map configuration = configDoc.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); final LegacyAlarmCallbackEventNotificationConfig config = LegacyAlarmCallbackEventNotificationConfig.builder() .callbackType(type) .configuration(configuration) .build(); final NotificationDto dto = NotificationDto.builder() .title(firstNonNull(title, "Untitled")) .description("Migrated legacy alarm callback") .config(config) .build(); LOG.info("Migrate legacy alarm callback <{}>", dto.title()); return notificationResourceHandler.create(dto, userService.getRootUser()); } /** * Example message count alert condition data structure on streams: *
{@code
     *         {
     *           "id" : "00000000-0000-0000-0000-000000000001",
     *           "type" : "message_count",
     *           "title" : "Message Count - MORE",
     *           "parameters" : {
     *             "backlog" : 10,
     *             "repeat_notifications" : false,
     *             "query" : "hello:world",
     *             "grace" : 2,
     *             "threshold_type" : "MORE",
     *             "threshold" : 1,
     *             "time" : 10
     *           },
     *           "creator_user_id" : "admin",
     *           "created_at": "2019-01-01T00:00:00.000Z"
     *         }
     * }
*/ private void migrateMessageCount(Helper helper) { final String seriesId = helper.newSeriesId(); var messageCountSeries = Count.builder().id(seriesId).build(); final Expression expression = helper.createExpression(seriesId, "MORE"); final EventProcessorConfig config = helper.createAggregationProcessorConfig(messageCountSeries, expression, executeEveryMs); final EventDefinitionDto definitionDto = helper.createEventDefinition(config); LOG.info("Migrate legacy message count alert condition <{}>", definitionDto.title()); eventDefinitionHandler.create(definitionDto, userService.getRootUser()); } /** * Example field value alert condition data structure on streams: *
{@code
     *         {
     *           "id" : "00000000-0000-0000-0000-000000000001",
     *           "type" : "field_value",
     *           "title" : "Field Value - HIGHER - MEAN",
     *           "parameters" : {
     *             "backlog" : 15,
     *             "repeat_notifications" : false,
     *             "field" : "test_field_1",
     *             "query" : "*",
     *             "grace" : 1,
     *             "threshold_type" : "HIGHER",
     *             "threshold" : 23,
     *             "time" : 5,
     *             "type" : "MEAN"
     *           },
     *           "creator_user_id" : "admin",
     *           "created_at": "2019-01-01T00:00:00.000Z"
     *         }
     * }
*/ private void migrateFieldValue(Helper helper) { final String type = helper.parameters().getString("type"); final String field = helper.parameters().getString("field"); final String seriesId = helper.newSeriesId(); var aggregationSeries = switch (type.toUpperCase(Locale.US)) { case "MEAN": yield Average.builder().id(seriesId).field(field).build(); case "MIN": yield Min.builder().id(seriesId).field(field).build(); case "MAX": yield Max.builder().id(seriesId).field(field).build(); case "SUM": yield Sum.builder().id(seriesId).field(field).build(); case "STDDEV": yield StdDev.builder().id(seriesId).field(field).build(); default: throw new IllegalStateException("Couldn't migrate field value alert condition with unknown type: " + type); }; final Expression expression = helper.createExpression(seriesId, "HIGHER"); final EventProcessorConfig config = helper.createAggregationProcessorConfig(aggregationSeries, expression, executeEveryMs); final EventDefinitionDto definitionDto = helper.createEventDefinition(config); LOG.info("Migrate legacy field value alert condition <{}>", definitionDto.title()); eventDefinitionHandler.create(definitionDto, userService.getRootUser()); } /** * Example field content value alert condition data structure on streams: *
{@code
     *         {
     *           "id" : "00000000-0000-0000-0000-000000000001",
     *           "type" : "field_content_value",
     *           "title" : "Field Content - WITHOUT QUERY",
     *           "parameters" : {
     *             "backlog" : 100,
     *             "repeat_notifications" : false,
     *             "field" : "test_field_2",
     *             "query" : "",
     *             "grace" : 2,
     *             "value" : "hello"
     *           },
     *           "creator_user_id" : "admin",
     *           "created_at": "2019-01-01T00:00:00.000Z"
     *         }
     * }
*/ private void migrateFieldContentValue(Helper helper) { final String field = helper.parameters().getString("field"); final String value = helper.parameters().getString("value"); // The configured condition query can be empty String query = field + ":\"" + value + "\""; if (!isNullOrEmpty(helper.query) && !"*".equals(helper.query.trim())) { query = query + " AND " + helper.query; } final String seriesId = helper.newSeriesId(); var messageCountSeries = Count.builder().id(seriesId).build(); final Expr.NumberReference left = Expr.NumberReference.create(seriesId); final Expr.NumberValue right = Expr.NumberValue.create(0); final Expression expression = Expr.Greater.create(left, right); final EventProcessorConfig config = AggregationEventProcessorConfig.builder() .streams(ImmutableSet.of(helper.streamId)) .query(query) .series(ImmutableList.of(messageCountSeries)) .groupBy(ImmutableList.of()) .conditions(AggregationConditions.builder() .expression(expression) .build()) .searchWithinMs(executeEveryMs) // The FieldContentValueAlertCondition was just using the alert scanner interval .executeEveryMs(executeEveryMs) .build(); final EventDefinitionDto definitionDto = helper.createEventDefinition(config); LOG.info("Migrate legacy field content value alert condition <{}>", definitionDto.title()); eventDefinitionHandler.create(definitionDto, userService.getRootUser()); } private static class Helper { private final String streamId; private final String title; private final Document parameters; private final Set notifications; private final long gracePeriod; private final long backlogSize; private final String query; private final long time; private final long threshold; private final String thresholdType; Helper(Document stream, Document alertCondition, Set notifications) { streamId = stream.getObjectId("_id").toHexString(); title = alertCondition.getString("title"); parameters = (Document) alertCondition.get("parameters"); this.notifications = notifications; gracePeriod = firstNonNull((Number) parameters.get("grace"), 0).longValue(); backlogSize = firstNonNull((Number) parameters.get("backlog"), 0).longValue(); query = parameters.get("query", ""); time = firstNonNull((Number) parameters.get("time"), 0).longValue(); threshold = firstNonNull((Number) parameters.get("threshold"), 0).longValue(); thresholdType = parameters.get("threshold_type", "MORE"); } Document parameters() { return parameters; } EventProcessorConfig createAggregationProcessorConfig(SeriesSpec aggregationSeries, Expression expression, long executeEveryMs) { return AggregationEventProcessorConfig.builder() .streams(ImmutableSet.of(streamId)) .query(query) .series(ImmutableList.of(aggregationSeries)) .groupBy(ImmutableList.of()) .conditions(AggregationConditions.builder() .expression(expression) .build()) .searchWithinMs(time * 60 * 1000) .executeEveryMs(executeEveryMs) .build(); } Expression createExpression(String seriesId, String greaterValue) { final Expr.NumberReference left = Expr.NumberReference.create(seriesId); final Expr.NumberValue right = Expr.NumberValue.create(threshold); return greaterValue.equalsIgnoreCase(thresholdType) ? Expr.Greater.create(left, right) : Expr.Lesser.create(left, right); } EventDefinitionDto createEventDefinition(EventProcessorConfig config) { final ImmutableList notificationList = notifications.stream() .map(notification -> EventNotificationHandler.Config.builder() .notificationId(notification.id()) .build()) .collect(ImmutableList.toImmutableList()); return EventDefinitionDto.builder() .title(firstNonNull(title, "Untitled")) .description("Migrated message count alert condition") .config(config) .alert(true) .priority(2) .keySpec(ImmutableList.of()) .notificationSettings(EventNotificationSettings.builder() .gracePeriodMs(gracePeriod * 60 * 1000) .backlogSize(backlogSize) .build()) .notifications(notificationList) .build(); } String newSeriesId() { return UUID.randomUUID().toString(); } } @AutoValue public static abstract class MigrationResult { public abstract ImmutableSet completedAlertConditions(); public abstract ImmutableSet completedAlarmCallbacks(); public static Builder builder() { return new AutoValue_LegacyAlertConditionMigrator_MigrationResult.Builder(); } @AutoValue.Builder public abstract static class Builder { abstract ImmutableSet.Builder completedAlertConditionsBuilder(); abstract ImmutableSet.Builder completedAlarmCallbacksBuilder(); public Builder addCompletedAlertCondition(String id) { completedAlertConditionsBuilder().add(id); return this; } public Builder addCompletedAlarmCallback(String id) { completedAlarmCallbacksBuilder().add(id); return this; } public abstract MigrationResult build(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy