Skip to content

Commit

Permalink
Open-sourced generic, dynamic POC, RESTful alerting API
Browse files Browse the repository at this point in the history
  • Loading branch information
Dark-Alex-17 committed Feb 17, 2024
1 parent b2ed41c commit 0d1d154
Show file tree
Hide file tree
Showing 70 changed files with 3,272 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions .jpb/jpb-settings.xml
Original file line number Diff line number Diff line change
@@ -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 added alerting-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions amqp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions amqp/src/main/java/com/poc/alerting/amqp/AmqpMessage.java
Original file line number Diff line number Diff line change
@@ -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();
}
16 changes: 16 additions & 0 deletions amqp/src/main/java/com/poc/alerting/amqp/AmqpResponse.java
Original file line number Diff line number Diff line change
@@ -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;
}
21 changes: 21 additions & 0 deletions amqp/src/main/java/com/poc/alerting/amqp/ExceptionType.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
79 changes: 79 additions & 0 deletions amqp/src/main/java/com/poc/alerting/amqp/GsonMessageConverter.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
50 changes: 50 additions & 0 deletions amqp/src/main/java/com/poc/alerting/amqp/RabbitSender.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 20 additions & 0 deletions amqp/src/main/kotlin/com/poc/alerting/amqp/AlertingAmqpMessage.kt
Original file line number Diff line number Diff line change
@@ -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)
}
79 changes: 79 additions & 0 deletions amqp/src/main/kotlin/com/poc/alerting/amqp/AmqpConfiguration.kt
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
Loading

0 comments on commit 0d1d154

Please sign in to comment.