Open-sourced generic, dynamic POC, RESTful alerting API
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JpaPluginProjectSettings">
|
||||
<option name="lastSelectedLanguage" value="Kotlin" />
|
||||
</component>
|
||||
</project>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -0,0 +1,19 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.amqp:spring-amqp")
|
||||
implementation("org.springframework.amqp:spring-rabbit")
|
||||
implementation("com.google.code.gson:gson")
|
||||
implementation("org.projectlombok:lombok")
|
||||
implementation("org.apache.commons:commons-lang3")
|
||||
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
}
|
||||
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
tasks.getByName<Jar>("jar") {
|
||||
enabled = true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.poc.alerting.amqp;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface AmqpMessage extends Serializable {
|
||||
String correlationId = "correlation-id";
|
||||
|
||||
String getRoutingKey();
|
||||
|
||||
String getExchange();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.poc.alerting.amqp;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public abstract class AmqpResponse<T> implements Serializable {
|
||||
private T value;
|
||||
private ExceptionType exceptionType;
|
||||
private String exceptionMessage;
|
||||
private Integer errorCode;
|
||||
private String errorDescription;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.poc.alerting.amqp;
|
||||
|
||||
public enum ExceptionType {
|
||||
INVALID_REQUEST_EXCEPTION(400),
|
||||
INVALID_ACCOUNT_EXCEPTION(403),
|
||||
NOT_FOUND_EXCEPTION(404),
|
||||
REQUEST_TIMEOUT_EXCEPTION(408),
|
||||
CONFLICT_EXCEPTION(409),
|
||||
UNPROCESSABLE_ENTITY_EXCEPTION(422),
|
||||
INTERNAL_SERVER_EXCEPTION(500);
|
||||
|
||||
private final int status;
|
||||
|
||||
ExceptionType(final int status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.poc.alerting.amqp;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.support.converter.MessageConversionException;
|
||||
import org.springframework.amqp.support.converter.MessageConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class GsonMessageConverter implements MessageConverter {
|
||||
private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").create();
|
||||
|
||||
@Override
|
||||
public Message toMessage(final Object object, final MessageProperties messageProperties) throws MessageConversionException {
|
||||
if (!(object instanceof Serializable)) {
|
||||
throw new MessageConversionException("Message object is not serializable");
|
||||
}
|
||||
|
||||
try {
|
||||
final String json = GSON.toJson(object);
|
||||
if (json == null) {
|
||||
throw new MessageConversionException("Unable to serialize the message to JSON");
|
||||
}
|
||||
|
||||
LOG.debug("json = {}", json);
|
||||
|
||||
final byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
|
||||
messageProperties.setContentEncoding("UTF-8");
|
||||
messageProperties.setContentLength(bytes.length);
|
||||
messageProperties.setTimestamp(new Date());
|
||||
messageProperties.setType(object.getClass().getName());
|
||||
if (messageProperties.getMessageId() == null) {
|
||||
messageProperties.setMessageId(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
return new Message(bytes, messageProperties);
|
||||
} catch (final Exception e) {
|
||||
throw new MessageConversionException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromMessage(final Message message) throws MessageConversionException {
|
||||
final byte[] messageBody = message.getBody();
|
||||
if (messageBody == null) {
|
||||
LOG.warn("No message body found for message: {}", message);
|
||||
return null;
|
||||
}
|
||||
|
||||
final MessageProperties messageProperties = message.getMessageProperties();
|
||||
final String className = StringUtils.trimAllWhitespace(messageProperties.getType());
|
||||
if (StringUtils.isEmpty(className)) {
|
||||
LOG.error("Could not determine class from message: {}", message);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final String json = new String(messageBody, StandardCharsets.UTF_8);
|
||||
LOG.debug("json = {}", json);
|
||||
return GSON.fromJson(json, Class.forName(className));
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Could not deserialize message: " + message, e);
|
||||
throw new MessageConversionException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.poc.alerting.amqp;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.amqp.core.Message;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class MessageDataTransferObject implements Serializable {
|
||||
private String routingKey;
|
||||
private String exchange;
|
||||
private Message message;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.poc.alerting.amqp;
|
||||
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RabbitSender {
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
|
||||
public void send(final AmqpMessage amqpMessage) {
|
||||
try {
|
||||
LOG.info("Sending message: {}", amqpMessage.toString());
|
||||
rabbitTemplate.convertAndSend(amqpMessage.getExchange(), amqpMessage.getRoutingKey(), amqpMessage);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Error sending message, serializing to disk", e);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T sendAndReceive(final AmqpMessage amqpMessage) {
|
||||
final AmqpResponse<T> amqpResponse = (AmqpResponse) rabbitTemplate.convertSendAndReceive(amqpMessage.getExchange(), amqpMessage.getRoutingKey(), amqpMessage);
|
||||
final String errorMessage = "Something went wrong";
|
||||
if (amqpResponse == null) {
|
||||
LOG.error(errorMessage);
|
||||
}
|
||||
final ExceptionType exceptionType = amqpResponse.getExceptionType();
|
||||
if (exceptionType != null) {
|
||||
final String exceptionMessage = amqpResponse.getExceptionMessage();
|
||||
final int statusCode = exceptionType.getStatus();
|
||||
if (amqpResponse.getErrorCode() != null) {
|
||||
final Integer errorCode = amqpResponse.getErrorCode();
|
||||
final String errorDescription = amqpResponse.getErrorDescription();
|
||||
LOG.error(errorMessage);
|
||||
} else {
|
||||
LOG.error(errorMessage);
|
||||
}
|
||||
}
|
||||
return amqpResponse.getValue();
|
||||
}
|
||||
|
||||
public void sendWithoutBackup(final AmqpMessage amqpMessage) {
|
||||
LOG.info("Sending message: {}", amqpMessage.toString());
|
||||
rabbitTemplate.convertAndSend(amqpMessage.getExchange(), amqpMessage.getRoutingKey(), amqpMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.poc.alerting.amqp
|
||||
|
||||
sealed class AlertingAmqpMessage(
|
||||
val alertId: String,
|
||||
val accountId: String
|
||||
): AmqpMessage {
|
||||
override fun getRoutingKey(): String {
|
||||
return "account_$accountId"
|
||||
}
|
||||
|
||||
override fun getExchange(): String {
|
||||
return "poc_alerting"
|
||||
}
|
||||
|
||||
class Add(val frequency: String, alertId: String, accountId: String): AlertingAmqpMessage(alertId, accountId)
|
||||
class Update(val frequency: String, alertId: String, accountId: String): AlertingAmqpMessage(alertId, accountId)
|
||||
class Delete(alertId: String, accountId: String): AlertingAmqpMessage(alertId, accountId)
|
||||
class Pause(alertId: String, accountId: String): AlertingAmqpMessage(alertId, accountId)
|
||||
class Resume(alertId: String, accountId: String): AlertingAmqpMessage(alertId, accountId)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.poc.alerting.amqp
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils.MILLIS_PER_MINUTE
|
||||
import org.springframework.amqp.core.Binding
|
||||
import org.springframework.amqp.core.BindingBuilder
|
||||
import org.springframework.amqp.core.DirectExchange
|
||||
import org.springframework.amqp.core.Exchange
|
||||
import org.springframework.amqp.core.ExchangeBuilder
|
||||
import org.springframework.amqp.core.FanoutExchange
|
||||
import org.springframework.amqp.core.Queue
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = ["com.poc.alerting.amqp"])
|
||||
open class AmqpConfiguration {
|
||||
companion object {
|
||||
const val AMQP_NAME = "poc_alerting"
|
||||
const val NEW_ACCOUNT = "new_account"
|
||||
const val FANOUT_NAME = "${AMQP_NAME}_fanout"
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun connectionFactory(): ConnectionFactory {
|
||||
return CachingConnectionFactory().apply {
|
||||
virtualHost = "poc"
|
||||
username = "poc_user"
|
||||
setPassword("s!mpleP@ssw0rd")
|
||||
setAddresses("localhost")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun newAccountQueue(): Queue {
|
||||
return Queue(NEW_ACCOUNT, true)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun newAccountExchange(): FanoutExchange {
|
||||
return FanoutExchange(NEW_ACCOUNT)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun newAccountBinding(newAccountQueue: Queue, newAccountExchange: FanoutExchange): Binding {
|
||||
return BindingBuilder.bind(newAccountQueue).to(newAccountExchange)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun pocAlertingExchange(): FanoutExchange {
|
||||
return FanoutExchange(FANOUT_NAME)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun directExchange(): DirectExchange {
|
||||
return ExchangeBuilder.directExchange(AMQP_NAME)
|
||||
.durable(false)
|
||||
.withArgument("alternate-exchange", NEW_ACCOUNT)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun exchangeBinding(directExchange: Exchange, pocAlertingExchange: FanoutExchange): Binding {
|
||||
return BindingBuilder.bind(directExchange).to(pocAlertingExchange)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun rabbitTemplate(connectionFactory: ConnectionFactory, gsonMessageConverter: GsonMessageConverter): RabbitTemplate {
|
||||
return RabbitTemplate(connectionFactory).apply {
|
||||
setExchange(FANOUT_NAME)
|
||||
messageConverter = gsonMessageConverter
|
||||
setReplyTimeout(MILLIS_PER_MINUTE)
|
||||
setMandatory(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
plugins {
|
||||
id("io.swagger.core.v3.swagger-gradle-plugin") version "2.1.9"
|
||||
kotlin("plugin.jpa") version "1.5.10"
|
||||
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "1.8"
|
||||
targetCompatibility = "1.8"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"implementation"(project(":persistence"))
|
||||
"implementation"(project(":amqp"))
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-aop")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-tomcat")
|
||||
implementation("javax.validation:validation-api")
|
||||
}
|
||||
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
enabled = true
|
||||
mainClass.set("com.poc.alerting.api.AlertingApi")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.poc.alerting.api;
|
||||
|
||||
import java.beans.FeatureDescriptor;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
|
||||
public final class PropertyCopier {
|
||||
private PropertyCopier() {
|
||||
}
|
||||
|
||||
public static void copyNonNullProperties(final Object source, final Object target, final List<String> ignoredProperties) {
|
||||
BeanUtils.copyProperties(source, target, getNullPropertyNames(source, ignoredProperties));
|
||||
}
|
||||
|
||||
private static String[] getNullPropertyNames(final Object source, final List<String> ignoredProperties) {
|
||||
final BeanWrapper wrappedSource = new BeanWrapperImpl(source);
|
||||
return Stream.of(wrappedSource.getPropertyDescriptors())
|
||||
.map(FeatureDescriptor::getName)
|
||||
.filter(propertyName -> wrappedSource.getPropertyValue(propertyName) == null || (ignoredProperties != null && ignoredProperties.contains(propertyName)))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.poc.alerting.api
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication(scanBasePackages = ["com.poc.alerting"])
|
||||
open class AlertingApi
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<AlertingApi>(*args)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.poc.alerting.api.controller
|
||||
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Add
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Delete
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Pause
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Resume
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Update
|
||||
import com.poc.alerting.amqp.RabbitSender
|
||||
import com.poc.alerting.api.PropertyCopier
|
||||
import com.poc.alerting.persistence.dto.Alert
|
||||
import com.poc.alerting.persistence.repositories.AccountRepository
|
||||
import com.poc.alerting.persistence.repositories.AlertRepository
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.validation.Valid
|
||||
|
||||
@RestController
|
||||
@Validated
|
||||
open class AlertController @Autowired constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val alertRepository: AlertRepository,
|
||||
private val rabbitSender: RabbitSender
|
||||
) {
|
||||
@PostMapping("/accounts/{account_id}/alerts")
|
||||
open fun addAlert(@PathVariable("account_id") accountId: String,
|
||||
@RequestBody body: @Valid Alert
|
||||
): ResponseEntity<Alert> {
|
||||
val account = accountRepository.findByExtId(accountId)
|
||||
body.account = account
|
||||
val alert = alertRepository.save(body).also {
|
||||
rabbitSender.send(Add(body.frequency, body.extId, accountId))
|
||||
}
|
||||
|
||||
return ResponseEntity(alert, HttpStatus.CREATED)
|
||||
}
|
||||
|
||||
@DeleteMapping("/accounts/{account_id}/alerts/{alert_id}")
|
||||
open fun deleteAlert(@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String
|
||||
): ResponseEntity<Void> {
|
||||
rabbitSender.send(Delete(alertId, accountId))
|
||||
return ResponseEntity(HttpStatus.OK)
|
||||
}
|
||||
|
||||
@GetMapping("/accounts/{account_id}/alerts/{alert_id}")
|
||||
open fun getAlertById(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String,
|
||||
@RequestParam(value = "include_recipients", required = false, defaultValue = "false") includeRecipients: @Valid Boolean?
|
||||
): ResponseEntity<Alert> {
|
||||
return ResponseEntity.ok(alertRepository.findByExtIdAndAccount_ExtId(alertId, accountId))
|
||||
}
|
||||
|
||||
@GetMapping("/accounts/{account_id}/alerts")
|
||||
open fun getListOfAlerts(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@RequestParam(value = "include_recipients", required = false, defaultValue = "false") includeRecipients: @Valid Boolean?
|
||||
): ResponseEntity<List<Alert>> {
|
||||
return ResponseEntity.ok(alertRepository.findAllByAccount_ExtId(accountId))
|
||||
}
|
||||
|
||||
@PatchMapping("/accounts/{account_id}/alerts/{alert_id}")
|
||||
open fun updateAlert(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String,
|
||||
@RequestBody body: @Valid Alert
|
||||
): ResponseEntity<Alert> {
|
||||
val existingAlert = alertRepository.findByExtIdAndAccount_ExtId(alertId, accountId)
|
||||
|
||||
rabbitSender.send(Update(body.frequency, alertId, accountId))
|
||||
|
||||
body.enabled.let {
|
||||
if (body.enabled.isNotBlank() && body.enabled.toBoolean() != existingAlert.enabled.toBoolean()) {
|
||||
if (body.enabled.toBoolean()) {
|
||||
rabbitSender.send(Resume(alertId, accountId))
|
||||
} else {
|
||||
rabbitSender.send(Pause(alertId, accountId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PropertyCopier.copyNonNullProperties(body, existingAlert, null)
|
||||
return ResponseEntity.ok(alertRepository.save(existingAlert))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.poc.alerting.api.controller
|
||||
|
||||
import com.poc.alerting.api.PropertyCopier
|
||||
import com.poc.alerting.persistence.dto.Recipient
|
||||
import com.poc.alerting.persistence.repositories.RecipientRepository
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.validation.annotation.Validated
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import javax.validation.Valid
|
||||
|
||||
@RestController
|
||||
@Validated
|
||||
open class RecipientController @Autowired constructor(
|
||||
private val recipientRepository: RecipientRepository
|
||||
) {
|
||||
@PostMapping("/accounts/{account_id}/alerts/{alert_id}/recipients")
|
||||
open fun addRecipient(@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String,
|
||||
@RequestBody body: @Valid MutableList<Recipient>
|
||||
): ResponseEntity<Iterable<Recipient>> {
|
||||
return ResponseEntity(recipientRepository.saveAll(body), HttpStatus.CREATED)
|
||||
}
|
||||
|
||||
@DeleteMapping("/accounts/{account_id}/alerts/{alert_id}/recipients/{recipient_id}")
|
||||
open fun deleteRecipient(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String,
|
||||
@PathVariable("recipient_id") recipientId: String
|
||||
): ResponseEntity<Void> {
|
||||
recipientRepository.delete(recipientRepository.findByExtIdAndAlert_ExtIdAndAccount_ExtId(recipientId, alertId, accountId))
|
||||
return ResponseEntity(HttpStatus.OK)
|
||||
}
|
||||
|
||||
@GetMapping("/accounts/{account_id}/recipients")
|
||||
open fun getAllRecipients(
|
||||
@PathVariable("account_id") accountId: String
|
||||
): ResponseEntity<List<Recipient>> {
|
||||
return ResponseEntity.ok(recipientRepository.findAllByAccount_ExtId(accountId))
|
||||
}
|
||||
|
||||
@GetMapping("/accounts/{account_id}/alerts/{alert_id}/recipients/{recipient_id}")
|
||||
open fun getRecipient(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String,
|
||||
@PathVariable("recipient_id") recipientId: String
|
||||
): ResponseEntity<Recipient> {
|
||||
return ResponseEntity.ok(recipientRepository.findByExtIdAndAlert_ExtIdAndAccount_ExtId(recipientId, alertId, accountId))
|
||||
}
|
||||
|
||||
@GetMapping("/accounts/{account_id}/alerts/{alert_id}/recipients")
|
||||
open fun getRecipientList(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String
|
||||
): ResponseEntity<List<Recipient>> {
|
||||
return ResponseEntity.ok(recipientRepository.findAllByAlert_ExtIdAndAccount_ExtId(alertId, accountId))
|
||||
}
|
||||
|
||||
@PatchMapping("/accounts/{account_id}/alerts/{alert_id}/recipients/{recipient_id}")
|
||||
open fun updateRecipient(
|
||||
@PathVariable("account_id") accountId: String,
|
||||
@PathVariable("alert_id") alertId: String,
|
||||
@PathVariable("recipient_id") recipientId: String,
|
||||
@RequestBody body: @Valid Recipient
|
||||
): ResponseEntity<Recipient> {
|
||||
val existingRecipient = recipientRepository.findByExtIdAndAlert_ExtIdAndAccount_ExtId(recipientId, alertId, accountId)
|
||||
PropertyCopier.copyNonNullProperties(body, existingRecipient, null)
|
||||
return ResponseEntity.ok(recipientRepository.save(existingRecipient))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
server:
|
||||
servlet:
|
||||
contextPath: "/poc/alerting/v1"
|
||||
port: 8080
|
||||
spring:
|
||||
jackson:
|
||||
time-zone: UTC
|
||||
default-property-inclusion: non_null
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
datasource:
|
||||
url: "jdbc:h2:tcp://localhost:9091/mem:alerting"
|
||||
username: "defaultUser"
|
||||
password: "secret"
|
||||
application:
|
||||
name: AlertingApi
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
profiles:
|
||||
active: "api"
|
||||
@@ -0,0 +1,29 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
dependencies {
|
||||
"implementation"(project(":persistence"))
|
||||
"implementation"(project(":amqp"))
|
||||
|
||||
implementation("org.slf4j:slf4j-api")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.amqp:spring-amqp")
|
||||
implementation("org.springframework.amqp:spring-rabbit")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-quartz")
|
||||
implementation("org.springframework:spring-jdbc")
|
||||
implementation("org.projectlombok:lombok")
|
||||
implementation("org.apache.commons:commons-lang3")
|
||||
implementation("org.apache.httpcomponents:fluent-hc")
|
||||
implementation("com.google.code.gson:gson")
|
||||
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
}
|
||||
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
enabled = true
|
||||
mainClass.set("com.poc.alerting.batch.BatchWorkerKt")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("com.poc.alerting.batch.BatchWorkerKt")
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.poc.alerting.batch;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AccountConsumerManager {
|
||||
private volatile boolean shuttingDown = false;
|
||||
@Getter private final Map<String, SimpleMessageListenerContainer> consumers = new ConcurrentHashMap<>();
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
void registerAndStart(final String queueName, final SimpleMessageListenerContainer newListenerContainer) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (shuttingDown) {
|
||||
LOG.warn("Shutdown process is underway. Not registering consumer for queue {}", queueName);
|
||||
return;
|
||||
}
|
||||
|
||||
final SimpleMessageListenerContainer oldListenerContainer = consumers.get(queueName);
|
||||
if (oldListenerContainer != null) {
|
||||
oldListenerContainer.stop();
|
||||
}
|
||||
newListenerContainer.start();
|
||||
consumers.put(queueName, newListenerContainer);
|
||||
LOG.info("Registered a new consumer on queue {}", queueName);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopConsumers() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
shuttingDown = true;
|
||||
LOG.info("Shutting down consumers on queues {}", consumers.keySet());
|
||||
consumers.entrySet().parallelStream().forEach(entry -> {
|
||||
LOG.info("Shutting down consumer on queue {}", entry.getKey());
|
||||
try {
|
||||
entry.getValue().stop();
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Encountered error while stopping consumer on queue " + entry.getKey(), e);
|
||||
}
|
||||
});
|
||||
|
||||
LOG.info("Finished shutting down all consumers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.poc.alerting.batch;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.http.client.fluent.Request;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
|
||||
import org.springframework.amqp.support.converter.MessageConverter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class ApplicationStartup implements ApplicationListener<ApplicationReadyEvent> {
|
||||
ConnectionFactory connectionFactory;
|
||||
MessageListenerAdapter accountWorkerListenerAdapter;
|
||||
MessageConverter gsonMessageConverter;
|
||||
ApplicationEventPublisher applicationEventPublisher;
|
||||
ConsumerCreator consumerCreator;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ApplicationStartup.class);
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(@NotNull final ApplicationReadyEvent event) {
|
||||
LOG.info("Creating consumers for existing queues");
|
||||
try {
|
||||
|
||||
final String rabbitMqUrl = String.format("http://%s:15672/api/exchanges/poc/alerting/bindings/source", "localhost");
|
||||
|
||||
//auth is kind of a kluge here. Apparently the HttpClient Fluent API doesn't support
|
||||
//it except by explicitly setting the auth header.
|
||||
final String json = Request.Get(rabbitMqUrl)
|
||||
.connectTimeout(1000)
|
||||
.socketTimeout(1000)
|
||||
.addHeader("Authorization", "Basic " + getAuthToken())
|
||||
.execute().returnContent().asString();
|
||||
|
||||
final JsonParser parser = new JsonParser();
|
||||
final JsonArray array = parser.parse(json).getAsJsonArray();
|
||||
|
||||
StreamSupport.stream(array.spliterator(), false)
|
||||
.map(jsonElement -> jsonElement.getAsJsonObject().get("destination").getAsString())
|
||||
.forEach(queueName -> consumerCreator.createConsumer(queueName));
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Error create consumers for existing queues", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getAuthToken() {
|
||||
final String basicPlaintext = "poc" + ":" + "s!mpleP@ssw0rd";
|
||||
|
||||
final Base64.Encoder encoder = Base64.getEncoder();
|
||||
return encoder.encodeToString(basicPlaintext.getBytes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.poc.alerting.batch;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.springframework.amqp.support.ConditionalExceptionLogger;
|
||||
|
||||
public class ExclusiveConsumerExceptionLogger implements ConditionalExceptionLogger {
|
||||
@Override
|
||||
public void log(final Log logger, final String message, final Throwable t) {
|
||||
//do not log exclusive consumer warnings
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.poc.alerting.batch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.DirectExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.core.QueueBuilder;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.poc.alerting.amqp.AmqpMessage;
|
||||
import com.poc.alerting.amqp.AmqpResponse;
|
||||
import com.poc.alerting.amqp.ExceptionType;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.poc.alerting.amqp.AmqpConfiguration.AMQP_NAME;
|
||||
import static com.poc.alerting.batch.WorkerConfiguration.NEW_CONSUMER;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class QueueCreator {
|
||||
private final RabbitAdmin rabbitAdmin;
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
private final DirectExchange directExchange;
|
||||
|
||||
public <T> AmqpResponse<T> createQueue(final AmqpMessage amqpMessage) throws IOException, ClassNotFoundException {
|
||||
final String routingKey = amqpMessage.getRoutingKey();
|
||||
LOG.info("Attempting to create new queue {}", routingKey);
|
||||
setupQueueForAccount(routingKey);
|
||||
final AmqpResponse response = (AmqpResponse) rabbitTemplate.convertSendAndReceive(AMQP_NAME, routingKey, amqpMessage);
|
||||
if (response != null && ExceptionType.INVALID_ACCOUNT_EXCEPTION.equals(response.getExceptionType())) {
|
||||
LOG.info("Invalid account, removing queue {}", routingKey);
|
||||
CompletableFuture.runAsync(() -> rabbitAdmin.deleteQueue(routingKey, false, true));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private void setupQueueForAccount(final String routingKey) {
|
||||
final Properties properties = rabbitAdmin.getQueueProperties(routingKey);
|
||||
if (properties == null) {
|
||||
final Queue queue = QueueBuilder.nonDurable(routingKey)
|
||||
.withArgument("x-expires", DateUtils.MILLIS_PER_DAY)
|
||||
.build();
|
||||
|
||||
rabbitAdmin.declareQueue(queue);
|
||||
rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(directExchange).with(routingKey));
|
||||
sendCreateConsumerMessage(routingKey);
|
||||
} else if ((Integer) properties.get(RabbitAdmin.QUEUE_CONSUMER_COUNT) < 1) {
|
||||
LOG.info("{} queue already exists. Adding consumer.", routingKey);
|
||||
sendCreateConsumerMessage(routingKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendCreateConsumerMessage(final String queueName) {
|
||||
rabbitTemplate.convertAndSend(NEW_CONSUMER, "", queueName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.poc.alerting.batch
|
||||
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Add
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Delete
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Pause
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Resume
|
||||
import com.poc.alerting.amqp.AlertingAmqpMessage.Update
|
||||
import com.poc.alerting.batch.jobs.AlertQueryJob
|
||||
import com.poc.alerting.batch.jobs.AlertQueryJob.Companion.ACCOUNT_ID
|
||||
import com.poc.alerting.batch.jobs.AlertQueryJob.Companion.ALERT_ID
|
||||
import com.poc.alerting.batch.jobs.AlertQueryJob.Companion.CRON
|
||||
import com.poc.alerting.persistence.dto.Alert
|
||||
import com.poc.alerting.persistence.repositories.AlertRepository
|
||||
import org.quartz.CronScheduleBuilder
|
||||
import org.quartz.JobBuilder
|
||||
import org.quartz.JobDataMap
|
||||
import org.quartz.JobDetail
|
||||
import org.quartz.JobKey
|
||||
import org.quartz.Scheduler
|
||||
import org.quartz.Trigger
|
||||
import org.quartz.TriggerBuilder
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.ZoneId
|
||||
import java.util.TimeZone
|
||||
|
||||
@Service
|
||||
open class AccountWorker @Autowired constructor(
|
||||
private val alertRepository: AlertRepository,
|
||||
private val scheduler: Scheduler
|
||||
){
|
||||
fun processMessage(message: AlertingAmqpMessage) {
|
||||
when (message) {
|
||||
is Add -> createJob(message.alertId, message.accountId, message.frequency)
|
||||
is Update -> updateJob(message.alertId, message.accountId, message.frequency)
|
||||
is Delete -> deleteJob(message.alertId, message.accountId)
|
||||
is Pause -> pauseJob(message.alertId, message.accountId)
|
||||
is Resume -> resumeJob(message.alertId, message.accountId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createJob(alertId: String, accountId: String, cron: String): Alert {
|
||||
val jobDetail = buildJob(alertId, accountId, cron)
|
||||
val trigger = createTrigger(alertId, jobDetail, cron)
|
||||
|
||||
with (scheduler) {
|
||||
scheduleJob(jobDetail, trigger)
|
||||
start()
|
||||
}
|
||||
|
||||
return alertRepository.findByExtIdAndAccount_Id(alertId, accountId)
|
||||
}
|
||||
|
||||
private fun updateJob(alertId: String, accountId: String, cron: String): Alert {
|
||||
scheduler.deleteJob(JobKey.jobKey(alertId, accountId))
|
||||
return createJob(alertId, accountId, cron)
|
||||
}
|
||||
|
||||
private fun deleteJob(alertId: String, accountId: String): Alert {
|
||||
val alert = alertRepository.findByExtIdAndAccount_Id(alertId, accountId)
|
||||
scheduler.deleteJob(JobKey.jobKey(alertId, accountId))
|
||||
alertRepository.delete(alert)
|
||||
return alert
|
||||
}
|
||||
|
||||
private fun pauseJob(alertId: String, accountId: String): Alert {
|
||||
scheduler.pauseJob(JobKey.jobKey(alertId, accountId))
|
||||
return alertRepository.findByExtIdAndAccount_Id(alertId, accountId)
|
||||
}
|
||||
|
||||
private fun resumeJob(alertId: String, accountId: String): Alert {
|
||||
scheduler.resumeJob(JobKey.jobKey(alertId, accountId))
|
||||
return alertRepository.findByExtIdAndAccount_Id(alertId, accountId)
|
||||
}
|
||||
|
||||
private fun buildJob(alertId: String, accountId: String, cron: String): JobDetail {
|
||||
val jobDataMap = JobDataMap()
|
||||
jobDataMap[ALERT_ID] = alertId
|
||||
jobDataMap[ACCOUNT_ID] = accountId
|
||||
jobDataMap[CRON] = cron
|
||||
return JobBuilder.newJob().ofType(AlertQueryJob::class.java)
|
||||
.storeDurably()
|
||||
.withIdentity(alertId, accountId)
|
||||
.usingJobData(jobDataMap)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createTrigger(alertId: String, jobDetail: JobDetail, cron: String): Trigger {
|
||||
return TriggerBuilder.newTrigger()
|
||||
.forJob(jobDetail)
|
||||
.withIdentity("${alertId}_trigger")
|
||||
.withSchedule(
|
||||
CronScheduleBuilder.cronSchedule(cron)
|
||||
.withMisfireHandlingInstructionFireAndProceed()
|
||||
.inTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
|
||||
)
|
||||
.usingJobData("cron", cron)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.poc.alerting.batch
|
||||
|
||||
import org.quartz.Job
|
||||
import org.quartz.SchedulerContext
|
||||
import org.quartz.spi.TriggerFiredBundle
|
||||
import org.springframework.beans.MutablePropertyValues
|
||||
import org.springframework.beans.PropertyAccessorFactory
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.ApplicationContextAware
|
||||
import org.springframework.scheduling.quartz.SpringBeanJobFactory
|
||||
|
||||
|
||||
class AutowiringSpringBeanJobFactory : SpringBeanJobFactory(), ApplicationContextAware {
|
||||
private var ctx: ApplicationContext? = null
|
||||
private var schedulerContext: SchedulerContext? = null
|
||||
override fun setApplicationContext(context: ApplicationContext) {
|
||||
ctx = context
|
||||
}
|
||||
|
||||
override fun createJobInstance(bundle: TriggerFiredBundle): Any {
|
||||
val job: Job = ctx!!.getBean(bundle.jobDetail.jobClass)
|
||||
val bw = PropertyAccessorFactory.forBeanPropertyAccess(job)
|
||||
val pvs = MutablePropertyValues()
|
||||
pvs.addPropertyValues(bundle.jobDetail.jobDataMap)
|
||||
pvs.addPropertyValues(bundle.trigger.jobDataMap)
|
||||
if (this.schedulerContext != null) {
|
||||
pvs.addPropertyValues(this.schedulerContext)
|
||||
}
|
||||
bw.setPropertyValues(pvs, true)
|
||||
return job
|
||||
}
|
||||
|
||||
override fun setSchedulerContext(schedulerContext: SchedulerContext) {
|
||||
this.schedulerContext = schedulerContext
|
||||
super.setSchedulerContext(schedulerContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.poc.alerting.batch
|
||||
|
||||
import org.springframework.boot.Banner
|
||||
import org.springframework.boot.WebApplicationType
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder
|
||||
|
||||
@SpringBootApplication(scanBasePackages = ["com.poc.alerting"])
|
||||
open class BatchWorker
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
SpringApplicationBuilder().sources(BatchWorker::class.java)
|
||||
.bannerMode(Banner.Mode.OFF)
|
||||
.web(WebApplicationType.NONE)
|
||||
.run(*args)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.poc.alerting.batch
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
|
||||
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
open class ConsumerCreator @Autowired constructor(
|
||||
private val connectionFactory: ConnectionFactory,
|
||||
private val accountWorkerListenerAdapter: MessageListenerAdapter,
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
private val accountConsumerManager: AccountConsumerManager
|
||||
){
|
||||
fun createConsumer(queueName: String) {
|
||||
val consumer = SimpleMessageListenerContainer(connectionFactory)
|
||||
consumer.setExclusive(true)
|
||||
consumer.setExclusiveConsumerExceptionLogger(ExclusiveConsumerExceptionLogger())
|
||||
consumer.setQueueNames(queueName)
|
||||
consumer.setMessageListener(accountWorkerListenerAdapter)
|
||||
consumer.setIdleEventInterval(DateUtils.MILLIS_PER_HOUR)
|
||||
consumer.setApplicationEventPublisher(applicationEventPublisher)
|
||||
consumer.setDefaultRequeueRejected(false)
|
||||
consumer.setAutoDeclare(false)
|
||||
consumer.setShutdownTimeout(DateUtils.MILLIS_PER_SECOND * 10)
|
||||
accountConsumerManager.registerAndStart(queueName, consumer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.poc.alerting.batch
|
||||
|
||||
import com.poc.alerting.amqp.AmqpConfiguration.Companion.NEW_ACCOUNT
|
||||
import com.poc.alerting.amqp.GsonMessageConverter
|
||||
import org.springframework.amqp.core.Binding
|
||||
import org.springframework.amqp.core.BindingBuilder
|
||||
import org.springframework.amqp.core.FanoutExchange
|
||||
import org.springframework.amqp.core.Queue
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin
|
||||
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
|
||||
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
open class WorkerConfiguration: AsyncConfigurer {
|
||||
companion object {
|
||||
const val NEW_CONSUMER = "new_consumer"
|
||||
}
|
||||
|
||||
@Bean("asyncExecutor")
|
||||
override fun getAsyncExecutor(): Executor {
|
||||
return ThreadPoolTaskExecutor()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun rabbitAdmin(connectionFactory: ConnectionFactory): RabbitAdmin {
|
||||
return RabbitAdmin(connectionFactory)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun queueCreatorListenerAdapter(queueCreator: QueueCreator, gsonMessageConverter: GsonMessageConverter): MessageListenerAdapter {
|
||||
return MessageListenerAdapter(queueCreator, "createQueue").apply {
|
||||
setMessageConverter(gsonMessageConverter)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun queueCreatorContainer(connectionFactory: ConnectionFactory, queueCreatorListenerAdapter: MessageListenerAdapter,
|
||||
gsonMessageConverter: GsonMessageConverter): SimpleMessageListenerContainer {
|
||||
return SimpleMessageListenerContainer(connectionFactory).apply {
|
||||
setExclusive(true)
|
||||
setExclusiveConsumerExceptionLogger(ExclusiveConsumerExceptionLogger())
|
||||
setQueueNames(NEW_ACCOUNT)
|
||||
setMessageListener(queueCreatorListenerAdapter)
|
||||
setDefaultRequeueRejected(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun newConsumerExchange(): FanoutExchange {
|
||||
return FanoutExchange(NEW_CONSUMER)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun newConsumerQueue(): Queue {
|
||||
return Queue("${NEW_CONSUMER}_${InetAddress.getLocalHost().hostName}", true)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun newConsumerBinding(newConsumerQueue: Queue, newConsumerExchange: FanoutExchange): Binding {
|
||||
return BindingBuilder.bind(newConsumerQueue).to(newConsumerExchange)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun consumerCreatorListenerAdapter(consumerCreator: ConsumerCreator, gsonMessageConverter: GsonMessageConverter): MessageListenerAdapter {
|
||||
return MessageListenerAdapter(consumerCreator, "createConsumer").apply {
|
||||
setMessageConverter(gsonMessageConverter)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun consumerCreatorContainer(connectionFactory: ConnectionFactory, consumerCreatorListenerAdapter: MessageListenerAdapter,
|
||||
newConsumerQueue: Queue): SimpleMessageListenerContainer {
|
||||
return SimpleMessageListenerContainer(connectionFactory).apply {
|
||||
setExclusive(true)
|
||||
setExclusiveConsumerExceptionLogger(ExclusiveConsumerExceptionLogger())
|
||||
setQueues(newConsumerQueue)
|
||||
setMessageListener(consumerCreatorListenerAdapter)
|
||||
setDefaultRequeueRejected(false)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun accountWorkerListenerAdapter(accountWorker: AccountWorker, gsonMessageConverter: GsonMessageConverter): MessageListenerAdapter {
|
||||
return MessageListenerAdapter(accountWorker, "processMessage").apply {
|
||||
setMessageConverter(gsonMessageConverter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.poc.alerting.batch.jobs
|
||||
|
||||
import com.poc.alerting.persistence.repositories.AlertRepository
|
||||
import org.quartz.Job
|
||||
import org.quartz.JobExecutionContext
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Duration
|
||||
import java.util.Date
|
||||
|
||||
@Component
|
||||
open class AlertQueryJob @Autowired constructor(
|
||||
private val alertRepository: AlertRepository
|
||||
): Job {
|
||||
companion object {
|
||||
const val ALERT_ID = "alertId"
|
||||
const val CRON = "cron"
|
||||
const val ACCOUNT_ID = "accountId"
|
||||
}
|
||||
|
||||
override fun execute(context: JobExecutionContext) {
|
||||
val data = context.jobDetail.jobDataMap
|
||||
val alertId = data.getString(ALERT_ID)
|
||||
val cron = data.getString(CRON)
|
||||
val accountId = data.getString(ACCOUNT_ID)
|
||||
|
||||
val alert = alertRepository.findByExtIdAndAccount_Id(alertId, accountId)
|
||||
|
||||
with(alert) {
|
||||
val queryResult = type.query()
|
||||
println("PERFORMING QUERY for $alertId-$accountId. Running with the following CRON expression: $cron")
|
||||
if (queryResult >= threshold.toInt()) {
|
||||
val currentTime = Date()
|
||||
if (!isTriggered) {
|
||||
isTriggered = true
|
||||
}
|
||||
|
||||
notificationSentTimestamp.let {
|
||||
if (Duration.between(notificationSentTimestamp.toInstant(), currentTime.toInstant()).toSeconds() >= 15) {
|
||||
println("Alert Triggered!!!!!!!!!!!!!")
|
||||
notificationSentTimestamp = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
lastTriggerTimestamp = currentTime
|
||||
alertRepository.save(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.poc.alerting.batch.jobs
|
||||
|
||||
import com.poc.alerting.batch.AutowiringSpringBeanJobFactory
|
||||
import org.quartz.spi.JobFactory
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.scheduling.quartz.SchedulerFactoryBean
|
||||
import javax.sql.DataSource
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableAutoConfiguration
|
||||
open class ScheduleAlertQueryConfiguration {
|
||||
@Bean
|
||||
open fun jobFactory(applicationContext: ApplicationContext): JobFactory {
|
||||
return AutowiringSpringBeanJobFactory().apply {
|
||||
setApplicationContext(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun schedulerFactory(applicationContext: ApplicationContext, dataSource: DataSource, jobFactory: JobFactory): SchedulerFactoryBean {
|
||||
return SchedulerFactoryBean().apply {
|
||||
setOverwriteExistingJobs(true)
|
||||
isAutoStartup = true
|
||||
setDataSource(dataSource)
|
||||
setJobFactory(jobFactory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
server:
|
||||
port: 0
|
||||
servlet:
|
||||
encoding:
|
||||
charset: UTF-8
|
||||
enabled: true
|
||||
spring:
|
||||
profiles:
|
||||
active: "batch"
|
||||
datasource:
|
||||
url: "jdbc:h2:tcp://localhost:9091/mem:alerting;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;"
|
||||
username: "defaultUser"
|
||||
password: "secret"
|
||||
application:
|
||||
name: BatchWorker
|
||||
jackson:
|
||||
time-zone: UTC
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
@@ -0,0 +1,75 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
id("org.springframework.boot") version "2.5.0"
|
||||
id("io.spring.dependency-management") version "1.0.11.RELEASE"
|
||||
kotlin("jvm") version "1.5.10"
|
||||
kotlin("plugin.spring") version "1.5.10"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "com.poc.alerting"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict", "-Xextended-compiler-checks")
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
apply(plugin = "org.springframework.boot")
|
||||
apply(plugin = "io.spring.dependency-management")
|
||||
apply(plugin = "java")
|
||||
apply(plugin = "java-library")
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
failOnVersionConflict()
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDir("src/main/kotlin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("com.poc.alerting.persistence.Persistence")
|
||||
}
|
||||
@@ -0,0 +1,938 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Alerting API
|
||||
description: 'This is a POC API for a dynamic, reactive alerting system.'
|
||||
version: 0.0.1
|
||||
servers:
|
||||
- url: http://localhost:8080/poc/alerting/v1
|
||||
tags:
|
||||
- name: Alerts
|
||||
- name: Recipients
|
||||
security:
|
||||
- alerting_auth:
|
||||
- read
|
||||
- write
|
||||
paths:
|
||||
/accounts/{account_id}/alerts:
|
||||
get:
|
||||
tags:
|
||||
- Alerts
|
||||
description: Get the list of alerts for an account
|
||||
summary: Get the list of alerts for an account
|
||||
operationId: getListOfAlerts
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account to get alerts from.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: include_recipients
|
||||
in: query
|
||||
description: >
|
||||
If set to true, then the list of all recipients belonging to each alert will be included in the response.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
example: true
|
||||
responses:
|
||||
200:
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Alert'
|
||||
example:
|
||||
- id: "percentageOfDeliveryFailures"
|
||||
name: "Percentage of Delivery Failures"
|
||||
threshold: "90"
|
||||
type: "PERCENTAGE_OF_DELIVERY_FAILURES"
|
||||
frequency: "15m"
|
||||
enabled: true
|
||||
last_triggered: "2021-04-22T16:33:21.959Z"
|
||||
notification_sent: "2021-04-22T16:33:21.959Z"
|
||||
is_triggered: true,
|
||||
reference_time_period: "1 Week"
|
||||
created: "2021-04-22T16:33:21.959Z"
|
||||
updated: "2021-04-22T16:33:21.959Z"
|
||||
updated_by: "someOtherUser"
|
||||
recipients:
|
||||
- id: "someUsersEmail"
|
||||
recipient: "someUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-02-22T16:09:28.139Z"
|
||||
updated: "2021-04-26T04:01:13.448Z"
|
||||
updated_by: "someUser"
|
||||
- id: "someOtherUsersEmail"
|
||||
recipient: "someOtherUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-03-04T07:17:33.244Z"
|
||||
updated: "2021-03-04T08:00:54.562Z"
|
||||
updated_by: "someOtherUser"
|
||||
- id: "failureThresholdAlert"
|
||||
name: "Too Many Errors"
|
||||
threshold: "1820"
|
||||
type: "FAILURE_THRESHOLD_ALERT"
|
||||
frequency: "5m"
|
||||
enabled: false
|
||||
last_triggered: "2021-04-22T16:33:21.959Z"
|
||||
notification_sent: "2021-04-22T16:33:21.959Z"
|
||||
is_triggered: true
|
||||
reference_time_period: "1 Month"
|
||||
created: "2021-04-22T16:33:21.959Z"
|
||||
updated: "2021-04-26T04:01:13.448Z"
|
||||
updated_by: "someOtherUser"
|
||||
recipients:
|
||||
- id: "someUserHttpRecipient"
|
||||
recipient: "https://some.test.callback.com/callback"
|
||||
type: "HTTP"
|
||||
username: "mikeRoweSoft"
|
||||
password: "iSuPpOrTwInDoWs"
|
||||
created: "2021-05-19T02:39:12.922Z"
|
||||
updated: "2021-05-19T02:39:12.922Z"
|
||||
updated_by: "someUser"
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alert
|
||||
- read:alert
|
||||
|
||||
post:
|
||||
tags:
|
||||
- Alerts
|
||||
description: Add an alert to the specified account.
|
||||
summary: Add an alert to the specified account
|
||||
operationId: addAlert
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of an account to add an alert to.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: >
|
||||
External ID used by users to refer to alert.
|
||||
If an ID is specified, the ID must be account-unique.
|
||||
If not provided, it will be automatically generated.
|
||||
name:
|
||||
type: string
|
||||
description: The name of this alert
|
||||
default: "Default value differs by the alert type"
|
||||
threshold:
|
||||
type: string
|
||||
description: A threshold must be given for a custom alert.
|
||||
default: "Default value differs by the alert type"
|
||||
type:
|
||||
type: string
|
||||
description: The type of alert being added to the account
|
||||
enum:
|
||||
- PERCENTAGE_OF_DELIVERY_FAILURES
|
||||
- FAILURE_THRESHOLD_ALERT
|
||||
- HARD_CODED_ALERT_1
|
||||
- HARD_CODED_ALERT_2
|
||||
frequency:
|
||||
type: string
|
||||
description: The frequency of how often the alerting condition is checked.
|
||||
default: "15m"
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Boolean value denoting whether the alert is enabled or not.
|
||||
default: true
|
||||
reference_time_period:
|
||||
type: string
|
||||
description: Time period to run the alerting condition against (lags in time series data).
|
||||
default: "1 Month"
|
||||
example:
|
||||
id: "testAlert1"
|
||||
name: "Too Many Messages Are Being Sent"
|
||||
threshold: "666"
|
||||
type: "FAILURE_THRESHOLD_ALERT"
|
||||
frequency: "5m"
|
||||
enabled: false
|
||||
reference_time_period: "1 Week"
|
||||
required:
|
||||
- type
|
||||
responses:
|
||||
201:
|
||||
description: Successfully created new alert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Alert'
|
||||
example:
|
||||
id: "testAlert1"
|
||||
name: "Too Many Messages Are Being Sent"
|
||||
threshold: "666"
|
||||
type: "FAILURE_THRESHOLD_ALERT"
|
||||
frequency: "15m"
|
||||
enabled: false
|
||||
reference_time_period: "1 Week"
|
||||
created: "2021-04-22T16:33:21.959Z"
|
||||
updated: "2021-04-22T16:33:21.959Z"
|
||||
updated_by: "someUser"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
409:
|
||||
description: An alert with the specified ID already exists on the given account
|
||||
422:
|
||||
description: >
|
||||
* Invalid alert request
|
||||
|
||||
* The type was unspecified, or does not exist
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alert
|
||||
- read:alert
|
||||
|
||||
/accounts/{account_id}/alerts/{alert_id}:
|
||||
get:
|
||||
tags:
|
||||
- Alerts
|
||||
description: Get an alert given an alert ID and account ID.
|
||||
summary: Get an alert given an alert ID and account ID
|
||||
operationId: getAlertById
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account to get alerts from.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: External ID used by users to refer to an alert.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someHardcodedAlert"
|
||||
- name: include_recipients
|
||||
in: query
|
||||
description: >
|
||||
If set to true, then the list of all recipients belonging to the alert will be included in the response.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
example: true
|
||||
responses:
|
||||
200:
|
||||
description: Found alert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Alert'
|
||||
example:
|
||||
id: "someHardcodedAlert"
|
||||
name: "Percentage of Delivery Failures"
|
||||
threshold: "90"
|
||||
type: "PERCENTAGE_OF_DELIVERY_FAILURES"
|
||||
frequency: "15m"
|
||||
enabled: true
|
||||
last_triggered: "2021-04-22T16:33:21.959Z"
|
||||
notification_sent: "2021-04-22T16:33:21.959Z"
|
||||
is_triggered: true
|
||||
reference_time_period: "1 Week"
|
||||
created: "2021-04-22T16:33:21.959Z"
|
||||
updated: "2021-04-22T16:33:21.959Z"
|
||||
updated_by: "someOtherUser"
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFoundAlert'
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
patch:
|
||||
tags:
|
||||
- Alerts
|
||||
description: >
|
||||
Update the attributes of an existing alert. Note that if a user attempts to update the type of an alert, the value will be ignored.
|
||||
summary: Update the attributes of an existing alert
|
||||
operationId: updateAlert
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: >
|
||||
ID of the account that the alert belongs to. This cannot be updated and will be ignored if placed in the payload
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: External ID used by users to refer to an alert. This cannot be updated and will be ignored if placed in the payload
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "testAlert1"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: A new name for the specified alert
|
||||
threshold:
|
||||
type: string
|
||||
description: A new threshold for the alert
|
||||
frequency:
|
||||
type: string
|
||||
description: The frequency of how often the alerting condition is checked
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Boolean value denoting whether the alert is enabled or not
|
||||
reference_time_period:
|
||||
type: string
|
||||
description: Time period to run the alerting condition against (lags in time series data)
|
||||
example:
|
||||
name: "Too Many Messages Are Being Sent"
|
||||
threshold: "955"
|
||||
frequency: "15m"
|
||||
enabled: true
|
||||
reference_time_period: "1 Month"
|
||||
responses:
|
||||
200:
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Alert'
|
||||
example:
|
||||
id: "testAlert1"
|
||||
name: "Too Many Messages Are Being Sent"
|
||||
threshold: "955"
|
||||
type: "FAILURE_THRESHOLD_ALERT"
|
||||
frequency: "15m"
|
||||
enabled: true
|
||||
last_triggered: "2021-04-22T16:33:21.959Z"
|
||||
notification_sent: "2021-04-22T16:33:21.959Z"
|
||||
is_triggered: false
|
||||
reference_time_period: "1 Month"
|
||||
created: "2021-04-22T16:33:21.959Z"
|
||||
updated: "2021-05-19T12:45:09.127Z"
|
||||
updated_by: "someUser"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFoundAlert'
|
||||
422:
|
||||
description: >
|
||||
* Invalid update request
|
||||
|
||||
* User was attempting to update alert type
|
||||
|
||||
* User attempted to update name value to an empty value
|
||||
|
||||
* User attempted to update threshold value to empty value
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
delete:
|
||||
tags:
|
||||
- Alerts
|
||||
description: >
|
||||
Remove the specified alert from the account. When an alert is deleted, all recipients belonging to the alert are also removed.
|
||||
summary: Remove the specified alert from the account
|
||||
operationId: deleteAlert
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account that the alert exists on.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: External ID used by users to refer to an alert.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "testAlert1"
|
||||
responses:
|
||||
204:
|
||||
description: Successfully deleted alert
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFoundAlert'
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alert
|
||||
- read:alert
|
||||
|
||||
/accounts/{account_id}/recipients:
|
||||
get:
|
||||
tags:
|
||||
- Recipients
|
||||
description: Get the list of all recipients on the specified account
|
||||
summary: Get the list of all recipients on the specified account
|
||||
operationId: getAllRecipients
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account to get recipients from
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
responses:
|
||||
200:
|
||||
description: Successfully returned the list of recipients
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Recipient'
|
||||
example:
|
||||
- id: "someUsersEmail"
|
||||
recipient: "someUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-02-22T16:09:28.139Z"
|
||||
updated: "2021-04-26T04:01:13.448Z"
|
||||
updated_by: "someUser"
|
||||
- id: "someOtherUsersEmail"
|
||||
recipient: "someOtherUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-03-04T07:17:33.244Z"
|
||||
updated: "2021-03-04T08:00:54.562Z"
|
||||
updated_by: "someOtherUser"
|
||||
- id: "someUserHttpRecipient"
|
||||
recipient: "https://some.test.callback.com/callback"
|
||||
type: "HTTP"
|
||||
username: "mikeRoweSoft"
|
||||
password: "iSuPpOrTwInDoWs"
|
||||
created: "2021-05-19T02:39:12.922Z"
|
||||
updated: "2021-05-19T02:39:12.922Z"
|
||||
updated_by: "someUser"
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
/accounts/{account_id}/alerts/{alert_id}/recipients:
|
||||
get:
|
||||
tags:
|
||||
- Recipients
|
||||
description: Get the list of recipients for an alert
|
||||
summary: Get the list of recipients for an alert
|
||||
operationId: getRecipientList
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account to get recipients from
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: The alert that the recipients belong to
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "percentageOfDeliveryFailures"
|
||||
responses:
|
||||
200:
|
||||
description: Successfully returned list of recipients
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Recipient'
|
||||
example:
|
||||
- id: "someUsersEmail"
|
||||
recipient: "someUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-02-22T16:09:28.139Z"
|
||||
updated: "2021-04-26T04:01:13.448Z"
|
||||
updated_by: "someUser"
|
||||
- id: "someOtherUsersEmail"
|
||||
recipient: "someOtherUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-03-04T07:17:33.244Z"
|
||||
updated: "2021-03-04T08:00:54.562Z"
|
||||
updated_by: "someOtherUser"
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFoundAlert'
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
post:
|
||||
tags:
|
||||
- Recipients
|
||||
description: Add one or more recipients to an alert. Note that no more than 25 recipients can be added to an alert.
|
||||
summary: Add one or more recipients to an alert
|
||||
operationId: addRecipient
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account that the recipient exists on.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: The alert that the recipient(s) will belong to
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "percentageOfDeliveryFailures"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: >
|
||||
External ID used by users to refer to a recipient.
|
||||
This must be unique among all recipient IDs given for the specified account. If this field is not provided, an ID is generated.
|
||||
recipient:
|
||||
type: string
|
||||
description: >
|
||||
Identifier that indicates who is receiving the notification.
|
||||
|
||||
* When the type is SMS, the recipient value must conform to the E.164 format recommendation
|
||||
|
||||
* When the type is EMAIL, the recipient value must conform to RFC-5322
|
||||
|
||||
* When the type is HTTP, as of RFC 3986, URIs should no longer support credentials.
|
||||
As a result, all URIs must conform to “http[s]://host[:port]/path?querystring” format.
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- EMAIL
|
||||
- SMS
|
||||
- HTTP
|
||||
description: The type of the recipient.
|
||||
username:
|
||||
type: string
|
||||
description: >
|
||||
Username for recipient HTTP webhook authentication. Only applicable to HTTP recipient types.
|
||||
password:
|
||||
type: string
|
||||
description: >
|
||||
Password for recipient HTTP webhook authentication. Only applicable to HTTP recipient types.
|
||||
required:
|
||||
- recipient
|
||||
- type
|
||||
example:
|
||||
- id: "someUserHttpRecipient"
|
||||
recipient: "https://some.test.callback.com/callback"
|
||||
type: "HTTP"
|
||||
username: "mikeRoweSoft"
|
||||
password: "iSuPpOrTwInDoWs"
|
||||
- id: "someUsersEmail"
|
||||
recipient: "someUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
responses:
|
||||
201:
|
||||
description: Successfully created new recipient(s)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Recipient'
|
||||
example:
|
||||
- id: "someUserHttpRecipient"
|
||||
recipient: "https://some.test.callback.com/callback"
|
||||
type: "HTTP"
|
||||
username: "mikeRoweSoft"
|
||||
password: "iSuPpOrTwInDoWs"
|
||||
created: "2021-05-19T02:39:12.922Z"
|
||||
updated: "2021-05-19T02:39:12.922Z"
|
||||
updated_by: "someUser"
|
||||
- id: "someUsersEmail"
|
||||
recipient: "someUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-02-22T16:09:28.139Z"
|
||||
updated: "2021-02-22T16:09:28.139Z"
|
||||
updated_by: "someUser"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFoundAlert'
|
||||
409:
|
||||
description: A recipient with the specified ID already exists on the given account
|
||||
422:
|
||||
description: >
|
||||
* Invalid recipient request
|
||||
|
||||
* User provided an unsupported type for one or more of the recipients
|
||||
|
||||
* User attempted to add more than 25 recipients to the alert
|
||||
|
||||
* Alert already has the maximum number of recipients
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
/accounts/{account_id}/alerts/{alert_id}/recipients/{recipient_id}:
|
||||
get:
|
||||
tags:
|
||||
- Recipients
|
||||
description: Get a recipient given the account ID, alert ID, and recipient ID.
|
||||
summary: Get a recipient given the account ID, alert ID, and recipient ID
|
||||
operationId: getRecipient
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account to get recipients from
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: The alert that the recipient belongs to
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someHardCodedAlert"
|
||||
- name: recipient_id
|
||||
in: path
|
||||
description: External ID used by users to refer to a recipient
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someUsersEmail"
|
||||
responses:
|
||||
200:
|
||||
description: Found Recipient
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Recipient'
|
||||
example:
|
||||
id: "someUsersEmail"
|
||||
recipient: "someUser@somedomain.com"
|
||||
type: "EMAIL"
|
||||
created: "2021-02-22T16:09:28.139Z"
|
||||
updated: "2021-04-26T04:01:13.448Z"
|
||||
updated_by: "someUser"
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
description: >
|
||||
* Recipient ID is unknown
|
||||
|
||||
* Alert ID is unknown
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
patch:
|
||||
tags:
|
||||
- Recipients
|
||||
description: Update the attributes of a recipient.
|
||||
summary: Update the attributes of a recipient
|
||||
operationId: updateRecipient
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account that the recipient exists on.
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: The alert that the recipient belongs to
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someHardCodedAlert"
|
||||
- name: recipient_id
|
||||
in: path
|
||||
description: External ID used by users to refer to a recipient
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someUserHttpRecipient"
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
recipient:
|
||||
type: string
|
||||
description: >
|
||||
Identifier that indicates who is receiving the notification.
|
||||
|
||||
* When the type is SMS, the recipient value must conform to the E.164 format recommendation
|
||||
|
||||
* When the type is EMAIL, the recipient value must conform to RFC-5322
|
||||
|
||||
* When the type is HTTP, as of RFC 3986, URIs should no longer support credentials.
|
||||
As a result, all URIs must conform to “http[s]://host[:port]/path?querystring” format.
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- EMAIL
|
||||
- SMS
|
||||
- HTTP
|
||||
description: The type of the recipient
|
||||
username:
|
||||
type: string
|
||||
description: >
|
||||
Username for recipient HTTP webhook authentication. Only applicable to HTTP recipient types.
|
||||
password:
|
||||
type: string
|
||||
description: >
|
||||
Password for recipient HTTP webhook authentication. Only applicable to HTTP recipient types.
|
||||
example:
|
||||
password: "AppleHQLooksLikeAHalo.Suspicious!"
|
||||
responses:
|
||||
200:
|
||||
description: Successfully updated recipient
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Recipient'
|
||||
example:
|
||||
id: "someUserHttpRecipient"
|
||||
recipient: "https://some.test.callback.com/callback"
|
||||
type: "HTTP"
|
||||
username: "mikeRoweSoft"
|
||||
password: "AppleHQLooksLikeAHalo.Suspicious!"
|
||||
created: "2021-05-19T02:39:12.922Z"
|
||||
updated: "2021-05-19T03:13:59.164Z"
|
||||
updated_by: "someUser"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
description: >
|
||||
* Recipient ID is unknown
|
||||
|
||||
* Alert ID is unknown
|
||||
422:
|
||||
description: >
|
||||
* Invalid update recipient request
|
||||
|
||||
* Invalid type provided
|
||||
|
||||
* Invalid or unsupported recipient URI given
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
delete:
|
||||
tags:
|
||||
- Recipients
|
||||
description: Delete a recipient.
|
||||
summary: Delete a recipient
|
||||
operationId: deleteRecipient
|
||||
parameters:
|
||||
- name: account_id
|
||||
in: path
|
||||
description: ID of the account that the recipient exists on
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1234567
|
||||
- name: alert_id
|
||||
in: path
|
||||
description: The alert that the recipient(s) belong to
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someHardCodedAlert"
|
||||
- name: recipient_id
|
||||
in: path
|
||||
description: >
|
||||
External ID used by users to refer to a recipient.
|
||||
Multiple recipients can be deleted by specify a comma-separated list of recipient ID's.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: "someUserHttpRecipient,someUsersEmail"
|
||||
responses:
|
||||
204:
|
||||
description: Successfully removed recipient(s)
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
description: >
|
||||
* One or more recipient IDs are unknown
|
||||
|
||||
* Alert ID is unknown
|
||||
security:
|
||||
- alerting_auth:
|
||||
- write:alerts
|
||||
- read:alerts
|
||||
|
||||
components:
|
||||
responses:
|
||||
Forbidden:
|
||||
description: >
|
||||
* Unknown Account
|
||||
|
||||
* User is not allowed to represent the given account
|
||||
|
||||
BadRequest:
|
||||
description: Request is invalid
|
||||
|
||||
NotFoundAlert:
|
||||
description: Alert ID is unknown
|
||||
|
||||
schemas:
|
||||
Alert:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Hardcoded or account-unique identifier for the alert
|
||||
name:
|
||||
type: string
|
||||
description: The name of this alert
|
||||
threshold:
|
||||
type: string
|
||||
description: The threshold value associated with this alert
|
||||
type:
|
||||
type: string
|
||||
description: The type of alert. The type will be an enum value matching one of the hard coded alerts.
|
||||
enum:
|
||||
- PERCENTAGE_OF_DELIVERY_FAILURES
|
||||
- FAILURE_THRESHOLD_ALERT
|
||||
- HARD_CODED_ALERT_1
|
||||
- HARD_CODED_ALERT_2
|
||||
frequency:
|
||||
type: string
|
||||
description: The frequency of how often the alerting condition is checked
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Boolean value denoting whether the alert is enabled or not
|
||||
last_triggered:
|
||||
type: string
|
||||
description: >
|
||||
Timestamp of when the alert was last triggered, conforming to RFC 3339 format. The time zone is always UTC
|
||||
notification_sent:
|
||||
type: string
|
||||
description: >
|
||||
Timestamp of when the last notification was sent, conforming to RFC 3339 format. The time zone is always UTC
|
||||
is_triggered:
|
||||
type: boolean
|
||||
description: Boolean value denoting whether the alert is still currently being triggered
|
||||
reference_time_period:
|
||||
type: string
|
||||
description: Time period to run the alerting condition against (lags in time series data)
|
||||
created:
|
||||
type: string
|
||||
description: >
|
||||
Timestamp of when the alert was created, conforming to RFC 3339 format.
|
||||
The time zone is always UTC. For the create operation, the updated time will be the same as the created time.
|
||||
updated:
|
||||
type: string
|
||||
description: >
|
||||
Timestamp of when the alert was last updated, conforming to RFC 3339 format. The time zone is always UTC
|
||||
updated_by:
|
||||
type: string
|
||||
description: The last user to update the alert
|
||||
recipients:
|
||||
type: array
|
||||
description: >
|
||||
The list of full recipient objects is only returned when the include_recipients parameter is set to true
|
||||
items:
|
||||
$ref: '#/components/schemas/Recipient'
|
||||
|
||||
Recipient:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Account-unique identifier for the recipient
|
||||
recipient:
|
||||
type: string
|
||||
description: Identifier that indicates who is receiving the notification
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- EMAIL
|
||||
- SMS
|
||||
- HTTP
|
||||
description: Type of recipient.
|
||||
username:
|
||||
type: string
|
||||
description: >
|
||||
Username for recipient HTTP webhook authentication. Only applicable to HTTP recipient types.
|
||||
password:
|
||||
type: string
|
||||
description: >
|
||||
Password for recipient HTTP webhook authentication. Only applicable to HTTP recipient types.
|
||||
created:
|
||||
type: string
|
||||
description: >
|
||||
Timestamp of when the recipient was created, conforming to RFC 3339 format. The time zone is always UTC.
|
||||
updated:
|
||||
type: string
|
||||
description: >
|
||||
Timestamp of when the recipient was last updated, conforming to RFC 3339 format. The time zone is always UTC.
|
||||
For the create operation, the updated time will be the same as the created time.
|
||||
updated_by:
|
||||
type: string
|
||||
description: The last user to update the recipient.
|
||||
|
||||
securitySchemes:
|
||||
alerting_auth:
|
||||
type: oauth2
|
||||
flows:
|
||||
authorizationCode:
|
||||
authorizationUrl: https://login.alexjclarke.com/oauth/authorize
|
||||
tokenUrl: https://login.alexjclarke.com/oauth/token
|
||||
scopes:
|
||||
read:alerts: read your alerts
|
||||
write:alerts: modify alerts in your account
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+3
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+3
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+3
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+4
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,60 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="dist/swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="dist/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="dist/favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="dist/swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="dist/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "AlertingApi.yaml",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
BIN
Binary file not shown.
+5
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 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
|
||||
#
|
||||
# https://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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,6 @@
|
||||
config.stopBubbling = true
|
||||
lombok.accessors.chain = true
|
||||
lombok.accessors.fluent = false
|
||||
lombok.addLombokGeneratedAnnotation = true
|
||||
lombok.data.flagUsage = error
|
||||
lombok.LOG.fieldName = LOG
|
||||
@@ -0,0 +1,27 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
plugins {
|
||||
id("org.flywaydb.flyway") version "7.9.1"
|
||||
kotlin("plugin.jpa") version "1.5.10"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"implementation"("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.flywaydb:flyway-core")
|
||||
implementation("org.apache.tomcat:tomcat-jdbc")
|
||||
implementation("com.h2database:h2:1.4.200")
|
||||
}
|
||||
|
||||
flyway {
|
||||
url = "jdbc:h2:mem:alerting"
|
||||
user = "defaultUser"
|
||||
password = "secret"
|
||||
locations = arrayOf("classpath:resources/db/migration")
|
||||
}
|
||||
|
||||
tasks.getByName<BootJar>("bootJar") {
|
||||
enabled = true
|
||||
mainClass.set("com.poc.alerting.persistence.Persistence")
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.poc.alerting.persistence
|
||||
|
||||
import org.h2.tools.Server
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
|
||||
|
||||
@Configuration
|
||||
@EnableJpaRepositories("com.poc.alerting.persistence.repositories")
|
||||
@EntityScan("com.poc.alerting.persistence.dto")
|
||||
@EnableAutoConfiguration
|
||||
open class H2Config {
|
||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||
@Profile("persistence")
|
||||
open fun inMemoryH2DatabaseServer(): Server {
|
||||
return Server.createTcpServer(
|
||||
"-tcp", "-tcpAllowOthers", "-tcpPort", "9091"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.poc.alerting.persistence
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
|
||||
@SpringBootApplication(scanBasePackages = ["com.poc.alerting.persistence"])
|
||||
open class Persistence
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<Persistence>(*args)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.poc.alerting.persistence.dto
|
||||
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.Id
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
@Entity
|
||||
data class Account(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
val id: Long,
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
val name: String,
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
val extId: String
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.poc.alerting.persistence.dto
|
||||
|
||||
import org.hibernate.annotations.CreationTimestamp
|
||||
import org.hibernate.annotations.UpdateTimestamp
|
||||
import java.util.Date
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Temporal
|
||||
import javax.persistence.TemporalType
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
@Entity
|
||||
data class Alert(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
val id: Long,
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
val extId: String,
|
||||
|
||||
@Size(max = 255)
|
||||
var threshold: String,
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
val type: AlertTypeEnum,
|
||||
|
||||
@Size(max = 255)
|
||||
var frequency: String = "",
|
||||
|
||||
var enabled: String = "",
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
var lastTriggerTimestamp: Date,
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
var notificationSentTimestamp: Date,
|
||||
|
||||
var isTriggered: Boolean,
|
||||
|
||||
@Size(max = 255)
|
||||
var referenceTimePeriod: String,
|
||||
|
||||
@CreationTimestamp
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
val created: Date,
|
||||
|
||||
@UpdateTimestamp
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
var updated: Date,
|
||||
|
||||
@Size(max = 255)
|
||||
var updatedBy: String,
|
||||
|
||||
@ManyToOne
|
||||
var account: Account
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.poc.alerting.persistence.dto
|
||||
|
||||
import kotlin.random.Random
|
||||
|
||||
enum class AlertTypeEnum(val query: () -> Int) {
|
||||
HARDCODED_ALERT_1({ Random.nextInt(0,1000) }),
|
||||
HARDCODED_ALERT_2({ Random.nextInt(0,1000) })
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.poc.alerting.persistence.dto
|
||||
|
||||
import org.hibernate.annotations.CreationTimestamp
|
||||
import org.hibernate.annotations.UpdateTimestamp
|
||||
import java.util.Date
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Temporal
|
||||
import javax.persistence.TemporalType
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
@Entity
|
||||
data class Recipient(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
val id: Long,
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
val extId: String,
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 255)
|
||||
val recipient: String,
|
||||
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
val type: RecipientTypeEnum,
|
||||
|
||||
@Size(max = 255)
|
||||
val username: String,
|
||||
|
||||
@Size(max = 255)
|
||||
val password: String,
|
||||
|
||||
@CreationTimestamp
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
val created: Date,
|
||||
|
||||
@UpdateTimestamp
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
val updated: Date,
|
||||
|
||||
@Size(max = 255)
|
||||
val updatedBy: String,
|
||||
|
||||
@ManyToOne
|
||||
val alert: Alert,
|
||||
|
||||
@ManyToOne
|
||||
val account: Account
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.poc.alerting.persistence.dto
|
||||
|
||||
enum class RecipientTypeEnum {
|
||||
EMAIL,
|
||||
SMS,
|
||||
HTTP
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.poc.alerting.persistence.repositories
|
||||
|
||||
import com.poc.alerting.persistence.dto.Account
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface AccountRepository : JpaRepository<Account, Long> {
|
||||
fun findByExtId(accountExtId: String): Account
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.poc.alerting.persistence.repositories
|
||||
|
||||
import com.poc.alerting.persistence.dto.Alert
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
import org.springframework.data.repository.PagingAndSortingRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface AlertRepository : CrudRepository<Alert, Long>, PagingAndSortingRepository<Alert, Long> {
|
||||
fun findByExtIdAndAccount_ExtId(alertId: String, accountExtId: String): Alert
|
||||
|
||||
fun findAllByAccount_ExtId(accountExtId: String): List<Alert>
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.poc.alerting.persistence.repositories
|
||||
|
||||
import com.poc.alerting.persistence.dto.Recipient
|
||||
import org.springframework.data.repository.CrudRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface RecipientRepository : CrudRepository<Recipient, Long> {
|
||||
fun findByExtIdAndAlert_ExtIdAndAccount_ExtId(recipientId: String, alertId: String, accountExtId: String): Recipient
|
||||
|
||||
fun findAllByAccount_ExtId(accountExtId: String): List<Recipient>
|
||||
|
||||
fun findAllByAlert_ExtIdAndAccount_ExtId(alertId: String, accountExtId: String): List<Recipient>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: "persistence"
|
||||
datasource:
|
||||
url: "jdbc:h2:mem:alerting;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;"
|
||||
driver-class-name: org.h2.Driver
|
||||
username: defaultUser
|
||||
password: secret
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
naming:
|
||||
implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
|
||||
physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
server:
|
||||
port: 8081
|
||||
@@ -0,0 +1,11 @@
|
||||
DROP TABLE qrtz_calendars IF EXISTS;
|
||||
DROP TABLE qrtz_cron_triggers IF EXISTS;
|
||||
DROP TABLE qrtz_fired_triggeres IF EXISTS;
|
||||
DROP TABLE qrtz_paused_trigger_grps IF EXISTS;
|
||||
DROP TABLE qrtz_scheduler_state IF EXISTS;
|
||||
DROP TABLE qrtz_locks IF EXISTS;
|
||||
DROP TABLE qrtz_job_details IF EXISTS;
|
||||
DROP TABLE qrtz_simple_triggers IF EXISTS;
|
||||
DROP TABLE qrtz_simprop_triggers IF EXISTS;
|
||||
DROP TABLE qrtz_blob_triggers IF EXISTS;
|
||||
DROP TABLE qrtz_triggers IF EXISTS;
|
||||
@@ -0,0 +1,238 @@
|
||||
CREATE TABLE QRTZ_CALENDARS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
CALENDAR_NAME VARCHAR (200) NOT NULL ,
|
||||
CALENDAR IMAGE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_CRON_TRIGGERS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR (200) NOT NULL ,
|
||||
TRIGGER_GROUP VARCHAR (200) NOT NULL ,
|
||||
CRON_EXPRESSION VARCHAR (120) NOT NULL ,
|
||||
TIME_ZONE_ID VARCHAR (80)
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_FIRED_TRIGGERS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
ENTRY_ID VARCHAR (95) NOT NULL ,
|
||||
TRIGGER_NAME VARCHAR (200) NOT NULL ,
|
||||
TRIGGER_GROUP VARCHAR (200) NOT NULL ,
|
||||
INSTANCE_NAME VARCHAR (200) NOT NULL ,
|
||||
FIRED_TIME BIGINT NOT NULL ,
|
||||
SCHED_TIME BIGINT NOT NULL ,
|
||||
PRIORITY INTEGER NOT NULL ,
|
||||
STATE VARCHAR (16) NOT NULL,
|
||||
JOB_NAME VARCHAR (200) NULL ,
|
||||
JOB_GROUP VARCHAR (200) NULL ,
|
||||
IS_NONCONCURRENT BOOLEAN NULL ,
|
||||
REQUESTS_RECOVERY BOOLEAN NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR (200) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SCHEDULER_STATE (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
INSTANCE_NAME VARCHAR (200) NOT NULL ,
|
||||
LAST_CHECKIN_TIME BIGINT NOT NULL ,
|
||||
CHECKIN_INTERVAL BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_LOCKS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
LOCK_NAME VARCHAR (40) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_JOB_DETAILS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
JOB_NAME VARCHAR (200) NOT NULL ,
|
||||
JOB_GROUP VARCHAR (200) NOT NULL ,
|
||||
DESCRIPTION VARCHAR (250) NULL ,
|
||||
JOB_CLASS_NAME VARCHAR (250) NOT NULL ,
|
||||
IS_DURABLE BOOLEAN NOT NULL ,
|
||||
IS_NONCONCURRENT BOOLEAN NOT NULL ,
|
||||
IS_UPDATE_DATA BOOLEAN NOT NULL ,
|
||||
REQUESTS_RECOVERY BOOLEAN NOT NULL ,
|
||||
JOB_DATA IMAGE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR (200) NOT NULL ,
|
||||
TRIGGER_GROUP VARCHAR (200) NOT NULL ,
|
||||
REPEAT_COUNT BIGINT NOT NULL ,
|
||||
REPEAT_INTERVAL BIGINT NOT NULL ,
|
||||
TIMES_TRIGGERED BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_SIMPROP_TRIGGERS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR(200) NOT NULL,
|
||||
TRIGGER_GROUP VARCHAR(200) NOT NULL,
|
||||
STR_PROP_1 VARCHAR(512) NULL,
|
||||
STR_PROP_2 VARCHAR(512) NULL,
|
||||
STR_PROP_3 VARCHAR(512) NULL,
|
||||
INT_PROP_1 INTEGER NULL,
|
||||
INT_PROP_2 INTEGER NULL,
|
||||
LONG_PROP_1 BIGINT NULL,
|
||||
LONG_PROP_2 BIGINT NULL,
|
||||
DEC_PROP_1 NUMERIC(13,4) NULL,
|
||||
DEC_PROP_2 NUMERIC(13,4) NULL,
|
||||
BOOL_PROP_1 BOOLEAN NULL,
|
||||
BOOL_PROP_2 BOOLEAN NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_BLOB_TRIGGERS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR (200) NOT NULL ,
|
||||
TRIGGER_GROUP VARCHAR (200) NOT NULL ,
|
||||
BLOB_DATA IMAGE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE QRTZ_TRIGGERS (
|
||||
SCHED_NAME VARCHAR(120) NOT NULL,
|
||||
TRIGGER_NAME VARCHAR (200) NOT NULL ,
|
||||
TRIGGER_GROUP VARCHAR (200) NOT NULL ,
|
||||
JOB_NAME VARCHAR (200) NOT NULL ,
|
||||
JOB_GROUP VARCHAR (200) NOT NULL ,
|
||||
DESCRIPTION VARCHAR (250) NULL ,
|
||||
NEXT_FIRE_TIME BIGINT NULL ,
|
||||
PREV_FIRE_TIME BIGINT NULL ,
|
||||
PRIORITY INTEGER NULL ,
|
||||
TRIGGER_STATE VARCHAR (16) NOT NULL ,
|
||||
TRIGGER_TYPE VARCHAR (8) NOT NULL ,
|
||||
START_TIME BIGINT NOT NULL ,
|
||||
END_TIME BIGINT NULL ,
|
||||
CALENDAR_NAME VARCHAR (200) NULL ,
|
||||
MISFIRE_INSTR SMALLINT NULL ,
|
||||
JOB_DATA IMAGE NULL
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_CALENDARS ADD
|
||||
CONSTRAINT PK_QRTZ_CALENDARS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
CALENDAR_NAME
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_CRON_TRIGGERS ADD
|
||||
CONSTRAINT PK_QRTZ_CRON_TRIGGERS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_FIRED_TRIGGERS ADD
|
||||
CONSTRAINT PK_QRTZ_FIRED_TRIGGERS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
ENTRY_ID
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_PAUSED_TRIGGER_GRPS ADD
|
||||
CONSTRAINT PK_QRTZ_PAUSED_TRIGGER_GRPS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_GROUP
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_SCHEDULER_STATE ADD
|
||||
CONSTRAINT PK_QRTZ_SCHEDULER_STATE PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
INSTANCE_NAME
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_LOCKS ADD
|
||||
CONSTRAINT PK_QRTZ_LOCKS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
LOCK_NAME
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_JOB_DETAILS ADD
|
||||
CONSTRAINT PK_QRTZ_JOB_DETAILS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
JOB_NAME,
|
||||
JOB_GROUP
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD
|
||||
CONSTRAINT PK_QRTZ_SIMPLE_TRIGGERS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD
|
||||
CONSTRAINT PK_QRTZ_SIMPROP_TRIGGERS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_TRIGGERS ADD
|
||||
CONSTRAINT PK_QRTZ_TRIGGERS PRIMARY KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
);
|
||||
|
||||
ALTER TABLE QRTZ_CRON_TRIGGERS ADD
|
||||
CONSTRAINT FK_QRTZ_CRON_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
) REFERENCES QRTZ_TRIGGERS (
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
) ON DELETE CASCADE;
|
||||
|
||||
|
||||
ALTER TABLE QRTZ_SIMPLE_TRIGGERS ADD
|
||||
CONSTRAINT FK_QRTZ_SIMPLE_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
) REFERENCES QRTZ_TRIGGERS (
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE QRTZ_SIMPROP_TRIGGERS ADD
|
||||
CONSTRAINT FK_QRTZ_SIMPROP_TRIGGERS_QRTZ_TRIGGERS FOREIGN KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
) REFERENCES QRTZ_TRIGGERS (
|
||||
SCHED_NAME,
|
||||
TRIGGER_NAME,
|
||||
TRIGGER_GROUP
|
||||
) ON DELETE CASCADE;
|
||||
|
||||
|
||||
ALTER TABLE QRTZ_TRIGGERS ADD
|
||||
CONSTRAINT FK_QRTZ_TRIGGERS_QRTZ_JOB_DETAILS FOREIGN KEY
|
||||
(
|
||||
SCHED_NAME,
|
||||
JOB_NAME,
|
||||
JOB_GROUP
|
||||
) REFERENCES QRTZ_JOB_DETAILS (
|
||||
SCHED_NAME,
|
||||
JOB_NAME,
|
||||
JOB_GROUP
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,63 @@
|
||||
CREATE TABLE `account` (
|
||||
`id` long PRIMARY KEY,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`ext_id` varchar(255) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO `account`
|
||||
VALUES (1, 'Test Account 1', '1111'),
|
||||
(2, 'Test Account 2', '2222');
|
||||
|
||||
CREATE TABLE `alert` (
|
||||
`id` long PRIMARY KEY AUTO_INCREMENT,
|
||||
`ext_id` varchar(255) UNIQUE NOT NULL,
|
||||
`name` varchar(255),
|
||||
`threshold` varchar(255),
|
||||
`type` ENUM ('HARDCODED_ALERT_1', 'HARDCODED_ALERT_2'),
|
||||
`frequency` varchar(255),
|
||||
`enabled` boolean default false,
|
||||
`last_trigger_timestamp` timestamp,
|
||||
`notification_sent_timestamp` timestamp,
|
||||
`is_triggered` boolean default false,
|
||||
`reference_time_period` varchar(255),
|
||||
`created` timestamp,
|
||||
`updated` timestamp,
|
||||
`updated_by` varchar(255),
|
||||
`account_id` long NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO `alert`
|
||||
VALUES (1, '1111-alert-1', 'Some Test Alert for Account 1111', '90', 'HARDCODED_ALERT_1', '15m', true, '2021-04-22 16:33:21.959', '2021-04-22 16:33:21.959', false, '1 Month', '2021-04-22 16:33:21.959', '2021-04-22 16:33:21.959', 'someUser1', 1),
|
||||
(2, '1111-alert-2', 'Some Other Test Alert for Account 1111', '666', 'HARDCODED_ALERT_2', '5m', true, null, null, false, '1 Week', '2021-04-22 16:33:21.959', '2021-04-26 04:01:13.448', 'someUser1', 1),
|
||||
(3, '2222-alert-1', 'Some Test Alert for Account 2222', '90', 'HARDCODED_ALERT_1', '15m', true, '2021-04-22 16:33:21.959', '2021-04-22 16:33:21.959', false, '1 Month', '2021-04-22 16:33:21.959', '2021-04-22 16:33:21.959', 'someOtherUser2', 2),
|
||||
(4, '2222-alert-2', 'Some Other Test Alert for Account 2222', '666', 'HARDCODED_ALERT_2', '5m', true, null, null, false, '1 Week', '2021-04-22 16:33:21.959', '2021-04-26 04:01:13.448', 'someOtherUser2', 2);
|
||||
|
||||
ALTER TABLE `alert` ADD FOREIGN KEY (`account_id`) REFERENCES `account` (`id`);
|
||||
|
||||
CREATE TABLE `recipient` (
|
||||
`id` long PRIMARY KEY AUTO_INCREMENT,
|
||||
`ext_id` varchar(255) UNIQUE NOT NULL,
|
||||
`recipient` varchar(255) NOT NULL,
|
||||
`type` ENUM ('EMAIL', 'SMS', 'HTTP'),
|
||||
`username` varchar(255),
|
||||
`password` varchar(255),
|
||||
`created` timestamp NOT NULL,
|
||||
`updated` timestamp NOT NULL,
|
||||
`updated_by` varchar(255) NOT NULL,
|
||||
`alert_id` long,
|
||||
`account_id` long
|
||||
);
|
||||
|
||||
INSERT INTO `recipient`
|
||||
VALUES (1, 'someUsersEmail-account-1111', 'someUser1@somedomain.com', 'EMAIL', null, null, '2021-02-22 16:09:28.139', '2021-02-22 16:09:28.139', 'someUser1', 1, 1),
|
||||
(2, 'someOtherUsersEmail-account-1111', 'someOtherUser1@somedomain.com', 'EMAIL', null, null, '2021-03-04 07:17:33.244', '2021-03-04 08:00:54.562', 'someOtherUser1', 1, 1),
|
||||
(3, 'someUserHttpRecipient-account-1111', 'https://some.test.callback.com/callback1', 'HTTP', 'mikeRoweSoft', 'iSuPpOrTwInDoWs', '2021-05-19 02:39:12.922', '2021-05-19 02:39:12.922', 'someUser1', 2, 1),
|
||||
(4, 'someUserHttpRecipient-account-2222', 'https://some.test.callback.com/callback2', 'HTTP', 'teriDactyl', 'L1f3F1nd$AWay', '2021-05-19 02:39:12.922', '2021-05-19 02:39:12.922', 'someUser2', 2, 2),
|
||||
(5, 'someUsersEmail-account-2222', 'someUser2@somedomain.com', 'EMAIL', null, null, '2021-02-22 16:09:28.139', '2021-02-22 16:09:28.139', 'someUser2', 1, 2),
|
||||
(6, 'someOtherUsersSms-account-2222', '13035552222', 'SMS', null, null, '2021-02-22 16:09:28.139', '2021-02-22 16:09:28.139', 'someOtherUser2', 1, 2);
|
||||
|
||||
ALTER TABLE `recipient` ADD FOREIGN KEY (`alert_id`) REFERENCES `alert` (`id`);
|
||||
|
||||
ALTER TABLE `recipient` ADD FOREIGN KEY (`account_id`) REFERENCES `account` (`id`);
|
||||
|
||||
CREATE SEQUENCE `hibernate_sequence` START WITH 5 INCREMENT BY 1;
|
||||
@@ -0,0 +1,2 @@
|
||||
rootProject.name = "alerting-poc"
|
||||
include(":persistence", ":amqp", ":api", ":batch")
|
||||
Reference in New Issue
Block a user