Open-sourced generic, dynamic POC, RESTful alerting API

This commit is contained in:
2024-02-16 17:37:37 -07:00
parent b2ed41c549
commit 0d1d154c7a
70 changed files with 3272 additions and 0 deletions
+37
View File
@@ -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/
+6
View File
@@ -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

+19
View File
@@ -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)
}
}
}
+27
View File
@@ -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))
}
}
+22
View File
@@ -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"
+29
View File
@@ -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)
}
}
}
+19
View File
@@ -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
+75
View File
@@ -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")
}
+938
View File
@@ -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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

+75
View File
@@ -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>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+60
View File
@@ -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>
Binary file not shown.
+5
View File
@@ -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
Vendored Executable
+185
View File
@@ -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
View File
@@ -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
+6
View File
@@ -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
+27
View File
@@ -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
}
@@ -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
}
@@ -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>
}
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
rootProject.name = "alerting-poc"
include(":persistence", ":amqp", ":api", ":batch")