/**
* Copyright 2015 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.streamsets.datacollector.execution.alerts;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import com.streamsets.datacollector.alerts.AlertsUtil;
import com.streamsets.datacollector.config.AuthenticationType;
import com.streamsets.datacollector.config.DataRuleDefinition;
import com.streamsets.datacollector.config.RuleDefinition;
import com.streamsets.datacollector.config.RuleDefinitionsWebhookConfig;
import com.streamsets.datacollector.config.WebhookCommonConfig;
import com.streamsets.datacollector.creation.RuleDefinitionsConfigBean;
import com.streamsets.datacollector.email.EmailException;
import com.streamsets.datacollector.email.EmailSender;
import com.streamsets.datacollector.execution.EventListenerManager;
import com.streamsets.datacollector.main.RuntimeInfo;
import com.streamsets.datacollector.metrics.MetricsConfigurator;
import com.streamsets.datacollector.util.PipelineException;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class AlertManager {
private static Logger LOG = LoggerFactory.getLogger(AlertManager.class);
private final String pipelineId;
private final String pipelineTitle;
private final String revision;
private final EmailSender emailSender;
private final MetricRegistry metrics;
private final RuntimeInfo runtimeInfo;
private final EventListenerManager eventListenerManager;
public AlertManager(
String pipelineId,
String pipelineTitle,
String revision,
EmailSender emailSender,
MetricRegistry metrics,
RuntimeInfo runtimeInfo,
EventListenerManager eventListenerManager
) {
this.pipelineId = pipelineId;
this.pipelineTitle = pipelineTitle;
this.revision = revision;
this.emailSender = emailSender;
this.metrics = metrics;
this.runtimeInfo = runtimeInfo;
this.eventListenerManager = eventListenerManager;
}
public void alert(List<String> emailIds, Throwable throwable) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
throwable.printStackTrace(printWriter);
String description = stringWriter.toString();
String subject = "ERROR: " + throwable;
long timestamp = System.currentTimeMillis();
String errorCode = "UNKNOWN";
if (throwable instanceof PipelineException) {
PipelineException pipelineException = (PipelineException)throwable;
timestamp = pipelineException.getErrorMessage().getTimestamp();
subject = "ERROR: " + pipelineException.getLocalizedMessage();
errorCode = pipelineException.getErrorCode().getCode();
}
try {
URL url = Resources.getResource(EmailConstants.ALERT_ERROR_EMAIL_TEMPLATE);
String emailBody = Resources.toString(url, Charsets.UTF_8);
java.text.DateFormat dateTimeFormat = new SimpleDateFormat(EmailConstants.DATE_MASK, Locale.ENGLISH);
emailBody = emailBody.replace(EmailConstants.ERROR_CODE, errorCode)
.replace(EmailConstants.TIME_KEY, dateTimeFormat.format(new Date(timestamp)))
.replace(EmailConstants.PIPELINE_NAME_KEY, pipelineTitle)
.replace(EmailConstants.DESCRIPTION_KEY, description)
.replace(EmailConstants.URL_KEY, runtimeInfo.getBaseHttpUrl() + EmailConstants.PIPELINE_URL + pipelineId);
subject = EmailConstants.STREAMSETS_DATA_COLLECTOR_ALERT + subject;
if (LOG.isDebugEnabled()) {
LOG.debug("Email Alert: subject = " + subject + ", body = " + emailBody);
}
if(emailSender == null) {
LOG.error("Email Sender is not configured. Alert with message '{}' will not be sent via email:",
emailBody, throwable);
} else {
emailSender.send(emailIds, subject, emailBody);
}
} catch (EmailException | IOException e) {
LOG.error("Error sending alert email, reason: {}", e.toString(), e);
//Log error and move on. This should not stop the pipeline.
}
}
@SuppressWarnings("unchecked")
public void alert(
Object value,
List<String> emailIds,
RuleDefinitionsConfigBean ruleDefinitionsConfigBean,
RuleDefinition ruleDefinition
) {
Gauge<Object> gauge = MetricsConfigurator.getGauge(metrics, AlertsUtil.getAlertGaugeName(ruleDefinition.getId()));
if (gauge == null) {
Gauge<Object> alertResponseGauge = AlertManagerHelper.createAlertResponseGauge(
pipelineId,
revision,
metrics,
value,
ruleDefinition
);
eventListenerManager.broadcastAlerts(new AlertInfo(pipelineId, ruleDefinition, alertResponseGauge));
//send email the first time alert is triggered
if(ruleDefinition.isSendEmail()) {
try {
URL url = Resources.getResource(EmailConstants.METRIC_EMAIL_TEMPLATE);
String emailBody = Resources.toString(url, Charsets.UTF_8);
java.text.DateFormat dateTimeFormat = new SimpleDateFormat(EmailConstants.DATE_MASK, Locale.ENGLISH);
emailBody = emailBody.replace(EmailConstants.ALERT_VALUE_KEY, String.valueOf(value))
.replace(EmailConstants.TIME_KEY, dateTimeFormat.format(new Date((Long) System.currentTimeMillis())))
.replace(EmailConstants.PIPELINE_NAME_KEY, pipelineTitle)
.replace(EmailConstants.CONDITION_KEY, ruleDefinition.getCondition())
.replace(EmailConstants.URL_KEY, runtimeInfo.getBaseHttpUrl() + EmailConstants.PIPELINE_URL + pipelineId);
if(ruleDefinition instanceof DataRuleDefinition) {
emailBody = emailBody.replace(EmailConstants.ALERT_NAME_KEY, ((DataRuleDefinition)ruleDefinition).getLabel());
} else {
emailBody = emailBody.replace(EmailConstants.ALERT_NAME_KEY, ruleDefinition.getAlertText());
}
if(emailSender == null) {
LOG.error("Email Sender is not configured. Alert '{}' with message '{}' will not be sent via email.",
ruleDefinition.getId(), emailBody);
} else {
emailSender.send(emailIds, EmailConstants.STREAMSETS_DATA_COLLECTOR_ALERT + ruleDefinition.getAlertText(), emailBody);
}
} catch (EmailException | IOException e) {
LOG.error("Error sending alert email, reason: {}", e.toString(), e);
//Log error and move on. This should not stop the pipeline.
}
}
if (ruleDefinitionsConfigBean != null && ruleDefinitionsConfigBean.webhookConfigs != null &&
!ruleDefinitionsConfigBean.webhookConfigs.isEmpty()) {
invokeWebhook(value, ruleDefinitionsConfigBean, ruleDefinition);
}
} else {
AlertManagerHelper.updateAlertGauge(gauge, value, ruleDefinition);
}
}
public void alertException(Object value, RuleDefinition ruleDefinition) {
AlertManagerHelper.alertException(pipelineId, revision, metrics, value, ruleDefinition);
}
private void invokeWebhook(
Object value,
RuleDefinitionsConfigBean ruleDefinitionsConfigBean,
RuleDefinition ruleDefinition
) {
DateFormat dateTimeFormat = new SimpleDateFormat(EmailConstants.DATE_MASK, Locale.ENGLISH);
String alertName;
if(ruleDefinition instanceof DataRuleDefinition) {
alertName = ((DataRuleDefinition)ruleDefinition).getLabel();
} else {
alertName = ruleDefinition.getAlertText();
}
for (RuleDefinitionsWebhookConfig webhookConfig : ruleDefinitionsConfigBean.webhookConfigs) {
if (!StringUtils.isEmpty(webhookConfig.webhookUrl)) {
Response response = null;
try {
String payload = webhookConfig.payload
.replace(WebhookConstants.ALERT_TEXT_KEY, ruleDefinition.getAlertText())
.replace(WebhookConstants.ALERT_NAME_KEY, alertName)
.replace(WebhookConstants.ALERT_VALUE_KEY, String.valueOf(value))
.replace(WebhookConstants.ALERT_CONDITION_KEY, ruleDefinition.getCondition())
.replace(WebhookConstants.TIME_KEY, dateTimeFormat.format(new Date((Long) System.currentTimeMillis())))
.replace(WebhookConstants.PIPELINE_TITLE_KEY, pipelineTitle)
.replace(WebhookConstants.PIPELINE_URL_KEY, runtimeInfo.getBaseHttpUrl() +
EmailConstants.PIPELINE_URL + pipelineId.replaceAll(" ", "%20"));
WebTarget webTarget = ClientBuilder.newClient().target(webhookConfig.webhookUrl);
configurePasswordAuth(webhookConfig, webTarget);
Invocation.Builder builder = webTarget.request();
for (String headerKey: webhookConfig.headers.keySet()) {
builder.header(headerKey, webhookConfig.headers.get(headerKey));
}
response = builder.post(Entity.entity(payload, webhookConfig.contentType));
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
LOG.error(
"Error calling Webhook URL, status code '{}': {}",
response.getStatus(),
response.readEntity(String.class)
);
}
} catch (Exception e) {
LOG.error("Error calling Webhook URL : {}", e.toString(), e);
} finally {
if (response != null) {
response.close();
}
}
}
}
}
private void configurePasswordAuth(WebhookCommonConfig webhookConfig, WebTarget webTarget) {
if (webhookConfig.authType == AuthenticationType.BASIC) {
webTarget.register(
HttpAuthenticationFeature.basic(webhookConfig.username, webhookConfig.password)
);
}
if (webhookConfig.authType == AuthenticationType.DIGEST) {
webTarget.register(
HttpAuthenticationFeature.digest(webhookConfig.username, webhookConfig.password)
);
}
if (webhookConfig.authType == AuthenticationType.UNIVERSAL) {
webTarget.register(
HttpAuthenticationFeature.universal(webhookConfig.username, webhookConfig.password)
);
}
}
}