diff --git a/README.md b/README.md index ef17102..68e840c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ This plugin can be used for connecting [Graylog](https://www.graylog.org/) alerts to the [Prometheus](https://prometheus.io/) [AlertManager](https://prometheus.io/docs/alerting/alertmanager/). -**Required Graylog version:** 4.0 and later +Similar to [Graylog AlertManager Notification Plugin](https://github.com/GDATASoftwareAG/Graylog-Plugin-AlertManager-Callback), but uses new Graylog API for notifications. + +**Required Graylog version:** 4.x (tested only on 4.2.5) Installation ------------ @@ -16,6 +18,61 @@ and can be configured in your `graylog.conf` file. Restart `graylog-server` and you are done. +Screenshots +----------- +![image](https://user-images.githubusercontent.com/2664578/151272305-5699394c-89de-40c3-a240-32201a99bd5b.png) + +![image](https://user-images.githubusercontent.com/2664578/151272408-8592e929-0ef0-4f84-b42a-f4c2a41b818a.png) + + +Custom variables +---------------- + +Options allow use JMTE Templates in labels & annotations. + +Allowed ones: +``` +# config - plugin configuration (AlertManagerNotifyConfig) +config.api_url +config.alert_name +config.labels +config.annotations +config.grace + +# context - info about event definition (EventNotificationModelData) +context.event_definition_id +context.event_definition_type +context.event_definition_title +context.event_definition_description +context.job_definition_id +context.job_trigger_id + +# event - info about current event (EventDto) +event.id +event.event_definition_type +event.event_definition_id +event.origin_context +event.timestamp +event.timestamp_processing +event.timerange_start +event.timerange_end +event.streams +event.source_streams +event.message +event.source +event.key_tuple +event.key +event.priority +event.alert +event.fields +event.group_by_fields + +node_id +backlog - matched messages +backlog_size - amount of messages in backlog +message - event.message without context.event_definition_title prefix +``` + Development ----------- diff --git a/src/main/java/com/strayge/AlertManagerNotify.java b/src/main/java/com/strayge/AlertManagerNotify.java index 24448bb..a81c653 100644 --- a/src/main/java/com/strayge/AlertManagerNotify.java +++ b/src/main/java/com/strayge/AlertManagerNotify.java @@ -1,15 +1,39 @@ package com.strayge; +import static java.util.Objects.requireNonNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.inject.Inject; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.floreysoft.jmte.Engine; +import com.google.common.collect.ImmutableList; + import org.graylog.events.notifications.EventNotification; import org.graylog.events.notifications.EventNotificationContext; -// import org.graylog.events.notifications.EventNotificationModelData; -// import org.graylog.events.notifications.EventNotificationService; +import org.graylog.events.notifications.EventNotificationModelData; +import org.graylog.events.notifications.EventNotificationService; import org.graylog.events.notifications.PermanentEventNotificationException; import org.graylog.events.notifications.TemporaryEventNotificationException; -// import org.graylog2.plugin.MessageSummary; +import org.graylog2.jackson.TypeReferences; +import org.graylog2.notifications.NotificationService; +import org.graylog2.plugin.MessageSummary; +import org.graylog2.plugin.system.NodeId; +import org.graylog2.streams.StreamService; +import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// import com.google.common.collect.ImmutableList; /** * This is the plugin. Your class should implement one of the existing plugin @@ -22,52 +46,149 @@ public interface Factory extends EventNotification.Factory { } private static final Logger LOG = LoggerFactory.getLogger(AlertManagerNotify.class); - // private final EventNotificationService notificationCallbackService; + private final EventNotificationService notificationCallbackService; + private final NodeId nodeId; + private final ObjectMapper objectMapper; + + @Inject + public AlertManagerNotify( + EventNotificationService notificationCallbackService, + StreamService streamService, + NotificationService notificationService, + NodeId nodeId, + ObjectMapper objectMapper + ) { + this.notificationCallbackService = notificationCallbackService; + this.nodeId = requireNonNull(nodeId, "nodeId"); + this.objectMapper = requireNonNull(objectMapper, "objectMapper"); + } @Override public void execute(EventNotificationContext ctx) throws TemporaryEventNotificationException, PermanentEventNotificationException { - LOG.info("AlertManagerNotify.execute called"); - - final AlertManagerNotifyConfig config = (AlertManagerNotifyConfig) ctx.notificationConfig(); - // config.url() - - // ImmutableList backlog = notificationCallbackService.getBacklogForEvent(ctx); - // final EventNotificationModelData model = EventNotificationModelData.of(ctx, backlog); - - // final EventNotificationModelData model = getModel(ctx, backlog); - // model.eventDefinitionTitle() - // ctx.notificationId() - - // final Request request = new Request.Builder() - // .url(httpUrl) - // .post(RequestBody.create(CONTENT_TYPE, body)) - // .build(); - - // try (final Response r = httpClient.newCall(request).execute()) { - // if (!r.isSuccessful()) { - // throw new PermanentEventNotificationException( - // "Expected successful HTTP response [2xx] but got [" + r.code() + "]. " + config.url()); - // } - // } catch (IOException e) { - // throw new PermanentEventNotificationException(e.getMessage()); - // } + AlertManagerNotifyConfig config = (AlertManagerNotifyConfig) ctx.notificationConfig(); + ImmutableList backlog = notificationCallbackService.getBacklogForEvent(ctx); + EventNotificationModelData model = EventNotificationModelData.of(ctx, backlog); + + Engine templateEngine = Engine.createEngine(); + Map templateModel = createTemplateModel(config, backlog, model); + + Map annotations = extractKeyValuePairsFromField(config.annotations()); + Map resolvedAnnotations = transformTemplateValues(templateEngine, templateModel, annotations); + + Map labels = extractKeyValuePairsFromField(config.labels()); + labels.put("alertname", config.alertName()); + Map resolvedLabels = transformTemplateValues(templateEngine, templateModel, labels); + + int grace_period = 1; + if (!"".equals(config.grace())) { + try { + grace_period = Integer.parseInt(config.grace()); + } catch (NumberFormatException e) { + LOG.error("AlertManagerNotify: invalid grace period"); + } + } + DateTime startAt = new DateTime(); + DateTime endsAt = startAt.plusMinutes(grace_period).plusSeconds(20); + + AlertManagerPayload payloadObject = new AlertManagerPayload(); + payloadObject.annotations = resolvedAnnotations; + payloadObject.labels = resolvedLabels; + payloadObject.generatorURL = model.eventDefinitionId(); + payloadObject.startsAt = startAt.toString(); + payloadObject.endsAt = endsAt.toString(); + + try { + String payload = this.objectMapper.writeValueAsString(payloadObject); + sendToAlertManager(config.apiUrl(), payload); + } catch (JsonProcessingException e) { + throw new PermanentEventNotificationException(e.getMessage()); + } } - // private EventNotificationModelData getModel(EventNotificationContext ctx, ImmutableList backlog) { - // final Optional definitionDto = ctx.eventDefinition(); - // final Optional jobTriggerDto = ctx.jobTrigger(); - // return EventNotificationModelData.builder() - // // .eventDefinition(definitionDto) - // .eventDefinitionId(definitionDto.map(EventDefinitionDto::id).orElse(UNKNOWN)) - // .eventDefinitionType(definitionDto.map(d -> d.config().type()).orElse(UNKNOWN)) - // .eventDefinitionTitle(definitionDto.map(EventDefinitionDto::title).orElse(UNKNOWN)) - // .eventDefinitionDescription(definitionDto.map(EventDefinitionDto::description).orElse(UNKNOWN)) - // .jobDefinitionId(jobTriggerDto.map(JobTriggerDto::jobDefinitionId).orElse(UNKNOWN)) - // .jobTriggerId(jobTriggerDto.map(JobTriggerDto::id).orElse(UNKNOWN)) - // .event(ctx.event()) - // .backlog(backlog) - // // .backlogSize(backlog.size()) - // .build(); - // } + private Map createTemplateModel( + AlertManagerNotifyConfig config, + ImmutableList backlog, + EventNotificationModelData model + ) { + + Map templateModel = new HashMap<>(); + templateModel.put("node_id", this.nodeId); + templateModel.put("config", objectToJson(config)); + templateModel.put("backlog", backlog); + templateModel.put("backlog_size", backlog.size()); + templateModel.put("context", objectToJson(model)); + templateModel.put("event", objectToJson(model.event())); + + String message = model.event().message(); + String messagePrefix = model.eventDefinitionTitle() + ": "; + if (message.startsWith(messagePrefix)) { + message = message.substring(messagePrefix.length()); + } + templateModel.put("message", message); + return templateModel; + } + + private Map transformTemplateValues( + Engine templateEngine, + Map templateModel, + Map customValueMap + ) { + final Map transformedCustomValueMap = new HashMap<>(); + customValueMap.forEach((key, value) -> { + if (value instanceof String) { + transformedCustomValueMap.put(key, templateEngine.transform((String) value, templateModel)); + } else { + transformedCustomValueMap.put(key, value); + } + }); + return transformedCustomValueMap; + } + + private Map extractKeyValuePairsFromField(String textFieldValue) { + Map extractedPairs = new HashMap<>(); + + if (textFieldValue != null && !"".equals(textFieldValue)) { + final String preparedTextFieldValue = textFieldValue.replaceAll(";", "\n"); + Properties properties = new Properties(); + InputStream stringInputStream = new ByteArrayInputStream(preparedTextFieldValue.getBytes(StandardCharsets.UTF_8)); + try { + properties.load(stringInputStream); + properties.forEach((key, value) -> extractedPairs.put((String) key, (String) value)); + } catch (IOException e) { + LOG.error("AlertManagerNotify: parse property failed " + e.getMessage()); + } + } + return extractedPairs; + } + + private boolean sendToAlertManager(String url, String payload) { + try { + payload = "[" + payload + "]"; + URL alertManagerUrl = new URL(url); + HttpURLConnection connection = (HttpURLConnection) alertManagerUrl.openConnection(); + connection.setDoInput(true); + connection.setDoOutput(true); + + connection.setRequestProperty("Content-Type", "application/json;"); + connection.setRequestProperty("Accept", "application/json,text/plain"); + connection.setRequestProperty("Method", "POST"); + try (OutputStream os = connection.getOutputStream()) { + os.write(payload.getBytes(StandardCharsets.UTF_8)); + } + int HttpResult = connection.getResponseCode(); + connection.disconnect(); + if (HttpResult != HttpURLConnection.HTTP_OK) { + LOG.error("AlertManagerNotify: AlertManager returned bad code: " + HttpResult); + } + return HttpResult == HttpURLConnection.HTTP_OK; + } catch (IOException e) { + LOG.error("AlertManagerNotify: request failed " + e.getMessage()); + return false; + } + } + + private Object objectToJson(Object object) { + return this.objectMapper.convertValue(object, TypeReferences.MAP_STRING_OBJECT); + } } diff --git a/src/main/java/com/strayge/AlertManagerNotifyConfig.java b/src/main/java/com/strayge/AlertManagerNotifyConfig.java index 87e7c01..875974a 100644 --- a/src/main/java/com/strayge/AlertManagerNotifyConfig.java +++ b/src/main/java/com/strayge/AlertManagerNotifyConfig.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; + import org.graylog.events.event.EventDto; import org.graylog.events.notifications.EventNotificationConfig; import org.graylog.events.notifications.EventNotificationExecutionJob; diff --git a/src/main/java/com/strayge/AlertManagerNotifyConfigEntity.java b/src/main/java/com/strayge/AlertManagerNotifyConfigEntity.java index 3a37076..254e04c 100644 --- a/src/main/java/com/strayge/AlertManagerNotifyConfigEntity.java +++ b/src/main/java/com/strayge/AlertManagerNotifyConfigEntity.java @@ -1,14 +1,16 @@ package com.strayge; +import java.util.Map; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; + import org.graylog.events.contentpack.entities.EventNotificationConfigEntity; import org.graylog.events.notifications.EventNotificationConfig; -import org.graylog2.contentpacks.model.entities.references.ValueReference; import org.graylog2.contentpacks.model.entities.EntityDescriptor; -import java.util.Map; +import org.graylog2.contentpacks.model.entities.references.ValueReference; @AutoValue @JsonTypeName(AlertManagerNotifyConfigEntity.TYPE_NAME) diff --git a/src/main/java/com/strayge/AlertManagerNotifyMetaData.java b/src/main/java/com/strayge/AlertManagerNotifyMetaData.java index 0c9a924..a8e034f 100644 --- a/src/main/java/com/strayge/AlertManagerNotifyMetaData.java +++ b/src/main/java/com/strayge/AlertManagerNotifyMetaData.java @@ -1,13 +1,13 @@ package com.strayge; -import org.graylog2.plugin.PluginMetaData; -import org.graylog2.plugin.ServerStatus; -import org.graylog2.plugin.Version; - import java.net.URI; import java.util.Collections; import java.util.Set; +import org.graylog2.plugin.PluginMetaData; +import org.graylog2.plugin.ServerStatus; +import org.graylog2.plugin.Version; + /** * Implement the PluginMetaData interface here. */ diff --git a/src/main/java/com/strayge/AlertManagerNotifyModule.java b/src/main/java/com/strayge/AlertManagerNotifyModule.java index 2415d91..13c577a 100644 --- a/src/main/java/com/strayge/AlertManagerNotifyModule.java +++ b/src/main/java/com/strayge/AlertManagerNotifyModule.java @@ -1,11 +1,11 @@ package com.strayge; -import org.graylog2.plugin.PluginConfigBean; -import org.graylog2.plugin.PluginModule; - import java.util.Collections; import java.util.Set; +import org.graylog2.plugin.PluginConfigBean; +import org.graylog2.plugin.PluginModule; + /** * Extend the PluginModule abstract class here to add you plugin to the system. */ diff --git a/src/main/java/com/strayge/AlertManagerNotifyPayload.java b/src/main/java/com/strayge/AlertManagerNotifyPayload.java new file mode 100644 index 0000000..eaaca6d --- /dev/null +++ b/src/main/java/com/strayge/AlertManagerNotifyPayload.java @@ -0,0 +1,22 @@ +package com.strayge; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +class AlertManagerPayload { + @JsonProperty("labels") + public Map labels; + + @JsonProperty("annotations") + public Map annotations; + + @JsonProperty("generatorURL") + public String generatorURL; + + @JsonProperty("startsAt") + public String startsAt; + + @JsonProperty("endsAt") + public String endsAt; +} diff --git a/src/main/java/com/strayge/AlertManagerNotifyPlugin.java b/src/main/java/com/strayge/AlertManagerNotifyPlugin.java index 9a5ffdf..f0d0820 100644 --- a/src/main/java/com/strayge/AlertManagerNotifyPlugin.java +++ b/src/main/java/com/strayge/AlertManagerNotifyPlugin.java @@ -1,12 +1,12 @@ package com.strayge; +import java.util.Collection; +import java.util.Collections; + import org.graylog2.plugin.Plugin; import org.graylog2.plugin.PluginMetaData; import org.graylog2.plugin.PluginModule; -import java.util.Collection; -import java.util.Collections; - /** * Implement the Plugin interface here. */ diff --git a/src/web/AlertManagerNotifyForm.jsx b/src/web/AlertManagerNotifyForm.jsx index 708e562..77c95cc 100644 --- a/src/web/AlertManagerNotifyForm.jsx +++ b/src/web/AlertManagerNotifyForm.jsx @@ -53,7 +53,7 @@ class AlertManagerNotifyForm extends React.Component { label="Alert Name" type="text" bsStyle={validation.errors.alert_name ? 'error' : null} - help={lodash.get(validation, 'errors.alert_name[0]', 'Alert Name')} + help={lodash.get(validation, 'errors.alert_name[0]', 'Will be transmitted as "alertname" label')} value={config.alert_name || ''} onChange={this.handleChange} required /> @@ -62,7 +62,10 @@ class AlertManagerNotifyForm extends React.Component { label="Labels" type="text" bsStyle={validation.errors.labels ? 'error' : null} - help={lodash.get(validation, 'errors.labels[0]', 'Custom labels.')} + help={lodash.get( + validation, 'errors.labels[0]', + 'The custom AlertManager label key-value-pairs separated by ";". Ex.: name1=value1;name2=value2' + )} value={config.labels || ''} onChange={this.handleChange} />