Commit 230896f0 by fernandosure Committed by Johannes Edmeier

Add notifier for opsgenie

parent 69f85687
...@@ -165,6 +165,60 @@ To enable pagerduty notifications you just have to add a generic service to your ...@@ -165,6 +165,60 @@ To enable pagerduty notifications you just have to add a generic service to your
| |
|=== |===
[[opsgenie-notifications]]
==== OpsGenie notifications ====
To enable OpsGenie notifications you just have to add a new JSON Rest API integration to your OpsGenie account and set `spring.boot.admin.notify.opsgenie.api-key` to the apiKey you received.
.OpsGenie notifications configuration options
|===
| Property name |Description |Default value
| spring.boot.admin.notify.opsgenie.enabled
| Enable OpsGenie notifications
| `true`
| spring.boot.admin.notify.opsgenie.ignore-changes
| Comma-delimited list of status changes to be ignored. Format: "<from-status>:<to-status>". Wildcards allowed.
| `"UNKNOWN:UP"`
| spring.boot.admin.notify.opsgenie.api-key
| apiKey you received when creating the integration
|
| spring.boot.admin.notify.opsgenie.url
| OpsGenie Alert API url
| `+++"https://api.opsgenie.com/v1/json/alert"+++`
| spring.boot.admin.notify.opsgenie.description
| Description to use in the event. SpEL-expressions are supported
| `+++"#{application.name}/#{application.id} is #{to.status}"+++`
| spring.boot.admin.notify.opsgenie.recipients
| User, group, schedule or escalation names to calculate which users will receive the notifications of the alert.
|
| spring.boot.admin.notify.opsgenie.actions
| Comma separated list of actions that can be executed.
|
| spring.boot.admin.notify.opsgenie.source
| Field to specify source of alert. By default, it will be assigned to IP address of incoming request.
|
| spring.boot.admin.notify.opsgenie.tags
| Comma separated list of labels attached to the alert.
|
| spring.boot.admin.notify.opsgenie.entity
| The entity the alert is related to.
|
| spring.boot.admin.notify.opsgenie.user
| Default owner of the execution. If user is not specified, the system becomes owner of the execution.
|
|===
[hipchat-notifications] [hipchat-notifications]
==== Hipchat notifications ==== ==== Hipchat notifications ====
To enable Hipchat notifications you need to create an API token from you Hipchat account and set the appropriate configuration properties. To enable Hipchat notifications you need to create an API token from you Hipchat account and set the appropriate configuration properties.
......
...@@ -17,6 +17,15 @@ package de.codecentric.boot.admin.config; ...@@ -17,6 +17,15 @@ package de.codecentric.boot.admin.config;
import java.util.List; import java.util.List;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
import de.codecentric.boot.admin.notify.LetsChatNotifier;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
...@@ -33,14 +42,6 @@ import org.springframework.context.annotation.Configuration; ...@@ -33,14 +42,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.mail.MailSender; import org.springframework.mail.MailSender;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
import de.codecentric.boot.admin.notify.LetsChatNotifier;
import de.codecentric.boot.admin.notify.filter.FilteringNotifier; import de.codecentric.boot.admin.notify.filter.FilteringNotifier;
import de.codecentric.boot.admin.notify.filter.web.NotificationFilterController; import de.codecentric.boot.admin.notify.filter.web.NotificationFilterController;
import de.codecentric.boot.admin.web.PrefixHandlerMapping; import de.codecentric.boot.admin.web.PrefixHandlerMapping;
...@@ -136,6 +137,22 @@ public class NotifierConfiguration { ...@@ -136,6 +137,22 @@ public class NotifierConfiguration {
} }
} }
@Configuration
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.opsgenie", name = "api-key")
@AutoConfigureBefore({ NotifierListenerConfiguration.class,
CompositeNotifierConfiguration.class })
public static class OpsGenieNotifierConfiguration {
@Bean
@ConditionalOnMissingBean
@ConfigurationProperties("spring.boot.admin.notify.opsgenie")
public OpsGenieNotifier opsgenieNotifier() {
return new OpsGenieNotifier();
}
}
@Configuration @Configuration
@ConditionalOnProperty(prefix = "spring.boot.admin.notify.hipchat", name = "url") @ConditionalOnProperty(prefix = "spring.boot.admin.notify.hipchat", name = "url")
@AutoConfigureBefore({ NotifierListenerConfiguration.class, @AutoConfigureBefore({ NotifierListenerConfiguration.class,
......
/*
* Copyright 2014-2017 the original author or authors.
*
* Licensed 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 de.codecentric.boot.admin.notify;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
/**
* Notifier submitting events to opsgenie.com.
*
* @author Fernando Sure
*/
public class OpsGenieNotifier extends AbstractStatusChangeNotifier {
private static final URI DEFAULT_URI = URI.create("https://api.opsgenie.com/v1/json/alert");
private static final String DEFAULT_MESSAGE = "#{application.name}/#{application.id} is #{to.status}";
private final SpelExpressionParser parser = new SpelExpressionParser();
private RestTemplate restTemplate = new RestTemplate();
/**
* BASE URL for OpsGenie API
*/
private URI url = DEFAULT_URI;
/**
* Integration ApiKey
*/
private String apiKey;
/**
* Comma separated list of users, groups, schedules or escalation names
* to calculate which users will receive the notifications of the alert.
*/
private String recipients;
/**
* Comma separated list of actions that can be executed.
*/
private String actions;
/**
* Field to specify source of alert. By default, it will be assigned to IP address of incoming request
*/
private String source;
/**
* Comma separated list of labels attached to the alert
*/
private String tags;
/**
* The entity the alert is related to.
*/
private String entity;
/**
* Default owner of the execution. If user is not specified, the system becomes owner of the execution.
*/
private String user;
/**
* Trigger description. SpEL template using event as root;
*/
private Expression description;
public OpsGenieNotifier() {
this.description = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION);
}
@Override
protected void doNotify(ClientApplicationEvent event) throws Exception {
restTemplate.exchange(buildUrl(event), HttpMethod.POST, createRequest(event), Void.class);
}
protected String buildUrl(ClientApplicationEvent event) {
if ((event instanceof ClientApplicationStatusChangedEvent) &&
("UP".equals(((ClientApplicationStatusChangedEvent) event).getTo().getStatus()))) {
return String.format("%s/close", url.toString());
}
return url.toString();
}
protected HttpEntity createRequest(ClientApplicationEvent event) {
Map<String, Object> body = new HashMap<>();
body.put("apiKey", apiKey);
body.put("message", getMessage(event));
body.put("alias", event.getApplication().getName() + "/" + event.getApplication().getId());
body.put("description", getDescription(event));
if (event instanceof ClientApplicationStatusChangedEvent &&
!"UP".equals(((ClientApplicationStatusChangedEvent) event).getTo().getStatus())) {
if (recipients != null) {
body.put("recipients", recipients);
}
if (actions != null) {
body.put("actions", actions);
}
if (source != null) {
body.put("source", source);
}
if (tags != null) {
body.put("tags", tags);
}
if (entity != null) {
body.put("entity", entity);
}
if (user != null) {
body.put("user", user);
}
Map<String, Object> details = new HashMap<>();
details.put("type", "link");
details.put("href", event.getApplication().getHealthUrl());
details.put("text", "Application health-endpoint");
body.put("details", details);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(body, headers);
}
protected String getMessage(ClientApplicationEvent event) {
return description.getValue(event, String.class);
}
protected String getDescription(ClientApplicationEvent event) {
return String.format("Application %s (%s) went from %s to %s", event.getApplication().getName(),
event.getApplication().getId(), ((ClientApplicationStatusChangedEvent) event).getFrom().getStatus(),
((ClientApplicationStatusChangedEvent) event).getTo().getStatus());
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getApiKey() {
return apiKey;
}
public void setDescription(String description) {
this.description = parser.parseExpression(description, ParserContext.TEMPLATE_EXPRESSION);
}
public String getMessage() {
return description.getExpressionString();
}
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String getRecipients() {
return recipients;
}
public void setRecipients(String recipients) {
this.recipients = recipients;
}
public String getActions() {
return actions;
}
public void setActions(String actions) {
this.actions = actions;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getTags() {
return tags;
}
public void setTags(String tags) {
this.tags = tags;
}
public String getEntity() {
return entity;
}
public void setEntity(String entity) {
this.entity = entity;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
}
...@@ -25,6 +25,14 @@ import java.util.ArrayList; ...@@ -25,6 +25,14 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.OpsGenieNotifier;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
...@@ -37,13 +45,6 @@ import de.codecentric.boot.admin.event.ClientApplicationEvent; ...@@ -37,13 +45,6 @@ import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent; import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo; import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.notify.CompositeNotifier;
import de.codecentric.boot.admin.notify.HipchatNotifier;
import de.codecentric.boot.admin.notify.MailNotifier;
import de.codecentric.boot.admin.notify.Notifier;
import de.codecentric.boot.admin.notify.NotifierListener;
import de.codecentric.boot.admin.notify.PagerdutyNotifier;
import de.codecentric.boot.admin.notify.SlackNotifier;
public class NotifierConfigurationTest { public class NotifierConfigurationTest {
private static final ClientApplicationEvent APP_DOWN = new ClientApplicationStatusChangedEvent( private static final ClientApplicationEvent APP_DOWN = new ClientApplicationStatusChangedEvent(
...@@ -87,6 +88,13 @@ public class NotifierConfigurationTest { ...@@ -87,6 +88,13 @@ public class NotifierConfigurationTest {
} }
@Test @Test
public void test_opsgenie() {
load(null, "spring.boot.admin.notify.opsgenie.api-key:foo");
assertThat(context.getBean(OpsGenieNotifier.class),
is(instanceOf(OpsGenieNotifier.class)));
}
@Test
public void test_hipchat() { public void test_hipchat() {
load(null, "spring.boot.admin.notify.hipchat.url:http://example.com"); load(null, "spring.boot.admin.notify.hipchat.url:http://example.com");
assertThat(context.getBean(HipchatNotifier.class), is(instanceOf(HipchatNotifier.class))); assertThat(context.getBean(HipchatNotifier.class), is(instanceOf(HipchatNotifier.class)));
......
package de.codecentric.boot.admin.notify;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class OpsGenieNotifierTest {
private OpsGenieNotifier notifier;
private RestTemplate restTemplate;
@Before
public void setUp() {
restTemplate = mock(RestTemplate.class);
notifier = new OpsGenieNotifier();
notifier.setApiKey("--service--");
notifier.setRecipients("--recipients--");
notifier.setRestTemplate(restTemplate);
}
@Test
public void test_onApplicationEvent_resolve() {
StatusInfo infoDown = StatusInfo.ofDown();
StatusInfo infoUp = StatusInfo.ofUp();
ClientApplicationStatusChangedEvent event = getEvent(infoDown, infoUp);
notifier.notify(event);
verify(restTemplate).exchange(eq("https://api.opsgenie.com/v1/json/alert/close"), eq(HttpMethod.POST),
eq(expectedRequest(event)), eq(Void.class));
}
@Test
public void test_onApplicationEvent_trigger() {
StatusInfo infoDown = StatusInfo.ofDown();
StatusInfo infoUp = StatusInfo.ofUp();
ClientApplicationStatusChangedEvent event = getEvent(infoUp, infoDown);
notifier.notify(event);
verify(restTemplate).exchange(eq("https://api.opsgenie.com/v1/json/alert"), eq(HttpMethod.POST),
eq(expectedRequest(event)), eq(Void.class));
}
private ClientApplicationStatusChangedEvent getEvent(StatusInfo from, StatusInfo to) {
return new ClientApplicationStatusChangedEvent(
Application.create("App").withId("-id-").withHealthUrl("http://health").build(), from, to);
}
private String getMessage(ClientApplicationEvent event) {
return String.format("%s/%s is %s", event.getApplication().getName(), event.getApplication().getId(),
((ClientApplicationStatusChangedEvent) event).getTo().getStatus());
}
private String getDescription(ClientApplicationEvent event) {
return String.format("Application %s (%s) went from %s to %s", event.getApplication().getName(),
event.getApplication().getId(), ((ClientApplicationStatusChangedEvent) event).getFrom().getStatus(),
((ClientApplicationStatusChangedEvent) event).getTo().getStatus());
}
private HttpEntity expectedRequest(ClientApplicationEvent event) {
Map<String, Object> expected = new HashMap<>();
expected.put("apiKey", "--service--");
expected.put("message", getMessage(event));
expected.put("alias", event.getApplication().getName() + "/" + event.getApplication().getId());
expected.put("description", getDescription(event));
if (event instanceof ClientApplicationStatusChangedEvent &&
!"UP".equals(((ClientApplicationStatusChangedEvent) event).getTo().getStatus())) {
expected.put("recipients", "--recipients--");
Map<String, Object> details = new HashMap<>();
details.put("type", "link");
details.put("href", event.getApplication().getHealthUrl());
details.put("text", "Application health-endpoint");
expected.put("details", details);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<>(expected, headers);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment