Skip to content

Commit

Permalink
Add migration to make email alarm callbacks explicit (Graylog2#2961)
Browse files Browse the repository at this point in the history
Since 8b15f35, we do not call the email
alarm callback if a stream's alert condition is triggered, but no
callback exists. Therefore we need to explicitly create email alarm
callbacks for streams like this, so existing users still get the
behavior they expect.
  • Loading branch information
dennisoelkers authored and joschi committed Oct 18, 2016
1 parent 1ac1d26 commit 6d486f1
Show file tree
Hide file tree
Showing 9 changed files with 532 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public AlarmCallbackConfiguration load(String alarmCallbackId) {

@Override
public AlarmCallbackConfiguration create(String streamId, CreateAlarmCallbackRequest request, String userId) {
return AlarmCallbackConfigurationAVImpl.create(new ObjectId().toHexString(), streamId, request.type, request.configuration, new Date(), userId);
return AlarmCallbackConfigurationAVImpl.create(new ObjectId().toHexString(), streamId, request.type(), request.configuration(), new Date(), userId);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* This file is part of Graylog.
*
* Graylog is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog2.alarmcallbacks.events;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;

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

@AutoValue
@JsonAutoDetect
public abstract class EmailAlarmCallbackMigrated {
private static final String FIELD_CALLBACK_IDS = "callback_ids";

@JsonProperty(FIELD_CALLBACK_IDS)
public abstract Map<String, Optional<String>> callbackIds();

@JsonCreator
public static EmailAlarmCallbackMigrated create(@JsonProperty(FIELD_CALLBACK_IDS) Map<String, Optional<String>> callbackIds) {
return new AutoValue_EmailAlarmCallbackMigrated(callbackIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.graylog2.periodical.ConfigurationManagementPeriodical;
import org.graylog2.periodical.ContentPackLoaderPeriodical;
import org.graylog2.periodical.DefaultStreamMigrationPeriodical;
import org.graylog2.periodical.EmailAlarmCallbackMigrationPeriodical;
import org.graylog2.periodical.GarbageCollectionWarningThread;
import org.graylog2.periodical.IndexFailuresPeriodical;
import org.graylog2.periodical.IndexRangesCleanupPeriodical;
Expand Down Expand Up @@ -68,5 +69,6 @@ protected void configure() {
periodicalBinder.addBinding().to(LdapGroupMappingMigration.class);
periodicalBinder.addBinding().to(IndexFailuresPeriodical.class);
periodicalBinder.addBinding().to(DefaultStreamMigrationPeriodical.class);
periodicalBinder.addBinding().to(EmailAlarmCallbackMigrationPeriodical.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* This file is part of Graylog.
*
* Graylog is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Graylog. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog2.periodical;

import com.google.common.annotations.VisibleForTesting;
import org.graylog2.alarmcallbacks.AlarmCallbackConfiguration;
import org.graylog2.alarmcallbacks.AlarmCallbackConfigurationService;
import org.graylog2.alarmcallbacks.EmailAlarmCallback;
import org.graylog2.alarmcallbacks.events.EmailAlarmCallbackMigrated;
import org.graylog2.plugin.cluster.ClusterConfigService;
import org.graylog2.plugin.configuration.ConfigurationRequest;
import org.graylog2.plugin.database.Persisted;
import org.graylog2.plugin.database.ValidationException;
import org.graylog2.plugin.database.users.User;
import org.graylog2.plugin.periodical.Periodical;
import org.graylog2.rest.models.alarmcallbacks.requests.CreateAlarmCallbackRequest;
import org.graylog2.shared.users.UserService;
import org.graylog2.streams.StreamService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class EmailAlarmCallbackMigrationPeriodical extends Periodical {
private static final Logger LOG = LoggerFactory.getLogger(EmailAlarmCallbackMigrationPeriodical.class);
private final ClusterConfigService clusterConfigService;
private final StreamService streamService;
private final AlarmCallbackConfigurationService alarmCallbackService;
private final EmailAlarmCallback emailAlarmCallback;
private final User localAdminUser;

@Inject
public EmailAlarmCallbackMigrationPeriodical(ClusterConfigService clusterConfigService,
StreamService streamService,
AlarmCallbackConfigurationService alarmCallbackService,
EmailAlarmCallback emailAlarmCallback,
UserService userService) {
this.clusterConfigService = clusterConfigService;
this.streamService = streamService;
this.alarmCallbackService = alarmCallbackService;
this.emailAlarmCallback = emailAlarmCallback;
this.localAdminUser = userService.getAdminUser();
}

@Override
public boolean runsForever() {
return true;
}

@Override
public boolean stopOnGracefulShutdown() {
return false;
}

@Override
public boolean masterOnly() {
return true;
}

@Override
public boolean startOnThisNode() {
return this.clusterConfigService.get(EmailAlarmCallbackMigrated.class) == null;
}

@Override
public boolean isDaemon() {
return false;
}

@Override
public int getInitialDelaySeconds() {
return 0;
}

@Override
public int getPeriodSeconds() {
return 0;
}

@Override
protected Logger getLogger() {
return LOG;
}

@Override
public void doRun() {
final Map<String, Optional<String>> streamMigrations = this.streamService.loadAll()
.stream()
.filter(stream -> !stream.getAlertReceivers().isEmpty()
&& !streamService.getAlertConditions(stream).isEmpty()
&& alarmCallbackService.getForStream(stream).isEmpty())
.collect(Collectors.toMap(Persisted::getId, this::migrateStream));
final boolean allSucceeded = streamMigrations.values()
.stream()
.allMatch(Optional::isPresent);

final long count = streamMigrations.size();
if (allSucceeded) {
if (count > 0) {
LOG.info("Successfully migrated " + count + " streams to include explicit email alarm callback.");
} else {
LOG.info("No streams needed to be migrated.");
}
this.clusterConfigService.write(EmailAlarmCallbackMigrated.create(streamMigrations));
} else {
final long errors = streamMigrations.values()
.stream()
.filter(callbackId -> !callbackId.isPresent())
.count();
LOG.error("Failed migrating " + errors + "/" + count + " streams to include explicit email alarm callback.");
}
}

private Optional<String> migrateStream(org.graylog2.plugin.streams.Stream stream) {
final Map<String, Object> defaultConfig = this.getDefaultEmailAlarmCallbackConfig();
LOG.debug("Creating email alarm callback for stream <" + stream.getId() + ">");
final AlarmCallbackConfiguration alarmCallbackConfiguration = alarmCallbackService.create(stream.getId(),
CreateAlarmCallbackRequest.create(
EmailAlarmCallback.class.getCanonicalName(),
defaultConfig
),
localAdminUser.getId()
);
try {
final String callbackId = this.alarmCallbackService.save(alarmCallbackConfiguration);
LOG.debug("Successfully created email alarm callback <" + callbackId + "> for stream <" + stream.getId() + ">.");
return Optional.of(callbackId);
} catch (ValidationException e) {
LOG.error("Unable to create email alarm callback for stream <" + stream.getId() + ">: ", e);
}
return Optional.empty();
}

@VisibleForTesting
Map<String, Object> getDefaultEmailAlarmCallbackConfig() {
final ConfigurationRequest configurationRequest = this.emailAlarmCallback.getRequestedConfiguration();

return configurationRequest.getFields().entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getDefaultValue()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,32 @@
package org.graylog2.rest.models.alarmcallbacks.requests;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
import org.graylog2.alarmcallbacks.AlarmCallbackConfiguration;

import java.util.Map;

@JsonAutoDetect
public class CreateAlarmCallbackRequest {
public String type;
public Map<String, Object> configuration;
@AutoValue
public abstract class CreateAlarmCallbackRequest {
private static final String FIELD_TYPE = "type";
private static final String FIELD_CONFIGURATION = "configuration";

@JsonProperty(FIELD_TYPE)
public abstract String type();

@JsonProperty(FIELD_CONFIGURATION)
public abstract Map<String, Object> configuration();

@JsonCreator
public static CreateAlarmCallbackRequest create(@JsonProperty(FIELD_TYPE) String type,
@JsonProperty(FIELD_CONFIGURATION) Map<String, Object> configuration) {
return new AutoValue_CreateAlarmCallbackRequest(type, configuration);
}

public static CreateAlarmCallbackRequest create(AlarmCallbackConfiguration alarmCallbackConfiguration) {
return create(alarmCallbackConfiguration.getType(), alarmCallbackConfiguration.getConfiguration());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ public AlarmCallbackSummary get(@ApiParam(name = "streamid", value = "The id of
@AuditEvent(type = AuditEventTypes.ALARM_CALLBACK_CREATE)
public Response create(@ApiParam(name = "streamid", value = "The stream id this new alarm callback belongs to.", required = true)
@PathParam("streamid") String streamid,
@ApiParam(name = "JSON body", required = true) CreateAlarmCallbackRequest cr) throws NotFoundException {
@ApiParam(name = "JSON body", required = true) CreateAlarmCallbackRequest originalCr) throws NotFoundException {
checkPermission(RestPermissions.STREAMS_EDIT, streamid);

// make sure the values are correctly converted to the declared configuration types
cr.configuration = convertConfigurationValues(cr);
final CreateAlarmCallbackRequest cr = CreateAlarmCallbackRequest.create(originalCr.type(), convertConfigurationValues(originalCr));

final AlarmCallbackConfiguration alarmCallbackConfiguration = alarmCallbackConfigurationService.create(streamid, cr, getCurrentUser().getName());

Expand Down Expand Up @@ -256,16 +256,16 @@ public void update(@ApiParam(name = "streamid", value = "The stream id this alar
private Map<String, Object> convertConfigurationValues(final CreateAlarmCallbackRequest alarmCallbackRequest) {
final ConfigurationRequest requestedConfiguration;
try {
final AlarmCallback alarmCallback = alarmCallbackFactory.create(alarmCallbackRequest.type);
final AlarmCallback alarmCallback = alarmCallbackFactory.create(alarmCallbackRequest.type());
requestedConfiguration = alarmCallback.getRequestedConfiguration();
} catch (ClassNotFoundException e) {
throw new BadRequestException("Unable to load alarm callback of type " + alarmCallbackRequest.type, e);
throw new BadRequestException("Unable to load alarm callback of type " + alarmCallbackRequest.type(), e);
}

// coerce the configuration to their correct types according to the alarmcallback's requested config
final Map<String, Object> configuration;
try {
configuration = ConfigurationMapConverter.convertValues(alarmCallbackRequest.configuration,
configuration = ConfigurationMapConverter.convertValues(alarmCallbackRequest.configuration(),
requestedConfiguration);
} catch (ValidationException e) {
throw new BadRequestException("Invalid configuration map", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,7 @@ public Response cloneStream(@ApiParam(name = "streamId", required = true) @PathP
}

for (AlarmCallbackConfiguration alarmCallbackConfiguration : alarmCallbackConfigurationService.getForStream(sourceStream)) {
final CreateAlarmCallbackRequest request = new CreateAlarmCallbackRequest();
request.type = alarmCallbackConfiguration.getType();
request.configuration = alarmCallbackConfiguration.getConfiguration();
final CreateAlarmCallbackRequest request = CreateAlarmCallbackRequest.create(alarmCallbackConfiguration);
final AlarmCallbackConfiguration alarmCallback = alarmCallbackConfigurationService.create(stream.getId(), request, getCurrentUser().getName());
alarmCallbackConfigurationService.save(alarmCallback);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ public void testLoadInvalidObjectId() throws Exception {
@Test
@UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL)
public void testCreate() throws Exception {
final CreateAlarmCallbackRequest request = new CreateAlarmCallbackRequest();
request.type = "";
request.configuration = Collections.emptyMap();
final CreateAlarmCallbackRequest request = CreateAlarmCallbackRequest.create("", Collections.emptyMap());

final String streamId = "54e3deadbeefdeadbeefaffe";
final String userId = "someuser";
Expand All @@ -171,4 +169,4 @@ public void testDeleteAlarmCallback() throws Exception {

assertEquals("After deletion, there should be only one document left in the collection", 1, alarmCallbackConfigurationService.count());
}
}
}
Loading

0 comments on commit 6d486f1

Please sign in to comment.