From c0fe6ad8a28a0017ae0fb3c85a28e09d0881126a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Mon, 13 Mar 2023 22:46:48 +0100 Subject: [PATCH 01/19] Extract inline classes --- .../com/studiomediatech/QueryPublisher.java | 20 +---- .../main/java/com/studiomediatech/Stat.java | 6 -- .../main/java/com/studiomediatech/Stats.java | 6 ++ .../queryresponse/ui/ConfigureApp.java | 71 +++++++++++++++ .../queryresponse/ui/ConfigureWebSocket.java | 27 ++++++ .../queryresponse/ui/QueryResponseUIApp.java | 90 ------------------- 6 files changed, 106 insertions(+), 114 deletions(-) create mode 100644 ui/src/main/java/com/studiomediatech/Stats.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index a03410b7..0e4a84c1 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -7,7 +7,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; @@ -24,8 +23,6 @@ import org.springframework.context.event.EventListener; import org.springframework.util.StringUtils; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.QueryBuilder; @@ -142,7 +139,7 @@ void on(QueryRecordedEvent event) { void onQueryResponseStats(Message message) { try { - handle(MAPPER.readValue(message.getBody(), Stats.class).elements); + handle(MAPPER.readValue(message.getBody(), Stats.class).elements()); } catch (RuntimeException | IOException ex) { logger().error("Failed to consumed stats", ex); } @@ -169,6 +166,7 @@ private void handleNodes(Collection stats) { .collect(Collectors.groupingBy(stat -> stat.uuid())); for (Entry> node : nodes.entrySet()) { + String uuid = node.getKey(); this.nodes.put(uuid, Instant.now()); @@ -332,18 +330,4 @@ private double calculateThroughput(String key, Collection source, List elements; - - @Override - public String toString() { - - return Optional.ofNullable(elements).orElse(Collections.emptyList()).stream().map(Object::toString) - .collect(Collectors.joining(", ")); - } - } - } diff --git a/ui/src/main/java/com/studiomediatech/Stat.java b/ui/src/main/java/com/studiomediatech/Stat.java index d015e099..a3072d5c 100644 --- a/ui/src/main/java/com/studiomediatech/Stat.java +++ b/ui/src/main/java/com/studiomediatech/Stat.java @@ -12,10 +12,4 @@ public record Stat(String key, Object value, Long timestamp, String uuid) { public static final String THROUGHPUT_RESPONSES = "throughput_responses"; public static final String AVG_THROUGHPUT = "avg_throughput"; - // @Override - // public String toString() { - // - // return key + "=" + value + (timestamp != null ? " " + timestamp : "") - // + (uuid != null ? " uuid=" + uuid : ""); - // } } \ No newline at end of file diff --git a/ui/src/main/java/com/studiomediatech/Stats.java b/ui/src/main/java/com/studiomediatech/Stats.java new file mode 100644 index 00000000..97b8d55b --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/Stats.java @@ -0,0 +1,6 @@ +package com.studiomediatech; + +import java.util.Collection; + +public record Stats(Collection elements) { +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java new file mode 100644 index 00000000..d3972e4d --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java @@ -0,0 +1,71 @@ +package com.studiomediatech.queryresponse.ui; + +import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import com.studiomediatech.QueryPublisher; +import com.studiomediatech.events.AsyncEventEmitter; +import com.studiomediatech.events.EventEmitter; +import com.studiomediatech.queryresponse.QueryBuilder; +import com.studiomediatech.queryresponse.QueryResponseTopicExchange; +import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; + +@Order(10) +@Configuration +class ConfigureApp { + + @Bean + ConnectionNameStrategy connectionNameStrategy(Environment env) { + + return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); + } + + @Bean + @Primary + TaskScheduler taskScheduler() { + + return new ThreadPoolTaskScheduler(); + } + + @Bean + EventEmitter eventEmitter(TaskScheduler scheduler, ApplicationEventPublisher publisher) { + + return new AsyncEventEmitter(scheduler, publisher); + } + + @Bean + WebSocketApiHandler handler(EventEmitter emitter) { + + return new WebSocketApiHandler(emitter); + } + + @Bean + QueryPublisher querier(WebSocketApiHandler handler, QueryBuilder queryBuilder) { + + return new QueryPublisher(handler, queryBuilder); + } + + @Bean(QueryResponseUIApp.QUERY_RESPONSE_STATS_QUEUE_BEAN) + Queue queryResponseStatsQueue() { + + return new AnonymousQueue(); + } + + @Bean + Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { + + return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) + .with("query-response/internal/stats"); + } +} \ No newline at end of file diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java new file mode 100644 index 00000000..69812bf6 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java @@ -0,0 +1,27 @@ +package com.studiomediatech.queryresponse.ui; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; + +@Order(100) +@Configuration +class ConfigureWebSocket implements WebSocketConfigurer { + + private final WebSocketApiHandler webSocketHandler; + + public ConfigureWebSocket(WebSocketApiHandler webSocketHandler) { + + this.webSocketHandler = webSocketHandler; + } + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + + // TODO: DO NOT ALLOW ORIGINS * !!! + registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*"); + } +} \ No newline at end of file diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java index 71bd3631..6a1dafc8 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java @@ -1,32 +1,11 @@ package com.studiomediatech.queryresponse.ui; -import org.springframework.amqp.core.AnonymousQueue; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.core.annotation.Order; -import org.springframework.core.env.Environment; -import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import com.studiomediatech.QueryPublisher; -import com.studiomediatech.events.AsyncEventEmitter; -import com.studiomediatech.events.EventEmitter; import com.studiomediatech.queryresponse.EnableQueryResponse; -import com.studiomediatech.queryresponse.QueryBuilder; -import com.studiomediatech.queryresponse.QueryResponseTopicExchange; -import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; @SpringBootApplication @EnableQueryResponse @@ -37,75 +16,6 @@ public class QueryResponseUIApp { public static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; public static void main(String[] args) { - SpringApplication.run(QueryResponseUIApp.class); } - - @Order(10) - @Configuration - static class AppConfig { - - @Bean - ConnectionNameStrategy connectionNameStrategy(Environment env) { - - return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); - } - - @Bean - @Primary - TaskScheduler taskScheduler() { - - return new ThreadPoolTaskScheduler(); - } - - @Bean - EventEmitter eventEmitter(TaskScheduler scheduler, ApplicationEventPublisher publisher) { - - return new AsyncEventEmitter(scheduler, publisher); - } - - @Bean - WebSocketApiHandler handler(EventEmitter emitter) { - - return new WebSocketApiHandler(emitter); - } - - @Bean - QueryPublisher querier(WebSocketApiHandler handler, QueryBuilder queryBuilder) { - - return new QueryPublisher(handler, queryBuilder); - } - - @Bean(QUERY_RESPONSE_STATS_QUEUE_BEAN) - Queue queryResponseStatsQueue() { - - return new AnonymousQueue(); - } - - @Bean - Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { - - return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) - .with("query-response/internal/stats"); - } - } - - @Order(100) - @Configuration - static class WebSocketConfig implements WebSocketConfigurer { - - private final WebSocketApiHandler webSocketHandler; - - public WebSocketConfig(WebSocketApiHandler webSocketHandler) { - - this.webSocketHandler = webSocketHandler; - } - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - - // TODO: DO NOT ALLOW ORIGINS * !!! - registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*"); - } - } } From 59cd48346de9a04850b1988cd5d4c9591c9d9106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Mon, 13 Mar 2023 23:09:56 +0100 Subject: [PATCH 02/19] Move message consumer --- .../com/studiomediatech/QueryPublisher.java | 10 +---- .../queryresponse/ui/ConfigureApp.java | 18 --------- .../queryresponse/ui/ConfigureMessaging.java | 36 +++++++++++++++++ .../queryresponse/ui/QueryResponseUIApp.java | 3 -- .../ui/messaging/MessageConsumer.java | 39 +++++++++++++++++++ .../ui/messaging/MessageConsumerAdatper.java | 20 ++++++++++ 6 files changed, 97 insertions(+), 29 deletions(-) create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index 0e4a84c1..fb376e1d 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -1,6 +1,5 @@ package com.studiomediatech; -import java.io.IOException; import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -18,22 +17,17 @@ import java.util.function.ToLongFunction; import java.util.stream.Collectors; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.context.event.EventListener; import org.springframework.util.StringUtils; -import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.QueryBuilder; -import com.studiomediatech.queryresponse.ui.QueryResponseUIApp; import com.studiomediatech.queryresponse.ui.api.RestApiAdapter; import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; +import com.studiomediatech.queryresponse.ui.messaging.MessageConsumerAdatper; import com.studiomediatech.queryresponse.util.Loggable; -public class QueryPublisher implements Loggable, RestApiAdapter { - - private static final ObjectMapper MAPPER = new ObjectMapper(); +public class QueryPublisher implements Loggable, RestApiAdapter, MessageConsumerAdatper { // This is a Fib! private static final int MAX_SIZE = 2584; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java index d3972e4d..81c023bd 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java @@ -25,12 +25,6 @@ @Configuration class ConfigureApp { - @Bean - ConnectionNameStrategy connectionNameStrategy(Environment env) { - - return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); - } - @Bean @Primary TaskScheduler taskScheduler() { @@ -56,16 +50,4 @@ QueryPublisher querier(WebSocketApiHandler handler, QueryBuilder queryBuilder) { return new QueryPublisher(handler, queryBuilder); } - @Bean(QueryResponseUIApp.QUERY_RESPONSE_STATS_QUEUE_BEAN) - Queue queryResponseStatsQueue() { - - return new AnonymousQueue(); - } - - @Bean - Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { - - return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) - .with("query-response/internal/stats"); - } } \ No newline at end of file diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java new file mode 100644 index 00000000..9d3181cd --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java @@ -0,0 +1,36 @@ +package com.studiomediatech.queryresponse.ui; + +import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import com.studiomediatech.queryresponse.QueryResponseTopicExchange; + +@Configuration +public class ConfigureMessaging { + + public static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; + public static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; + public static final String QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY = "query-response/internal/stats"; + + @Bean + ConnectionNameStrategy connectionNameStrategy(Environment env) { + return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); + } + + @Bean(QUERY_RESPONSE_STATS_QUEUE_BEAN) + Queue queryResponseStatsQueue() { + return new AnonymousQueue(); + } + + @Bean + Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { + return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) + .with(QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY); + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java index 6a1dafc8..888acf6d 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java @@ -12,9 +12,6 @@ @EnableScheduling @EnableWebSocket public class QueryResponseUIApp { - - public static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; - public static void main(String[] args) { SpringApplication.run(QueryResponseUIApp.class); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java new file mode 100644 index 00000000..65bff8f2 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java @@ -0,0 +1,39 @@ +package com.studiomediatech.queryresponse.ui.messaging; + +import java.io.IOException; +import java.util.Optional; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.support.AmqpHeaders; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.studiomediatech.Stats; +import com.studiomediatech.queryresponse.ui.ConfigureMessaging; +import com.studiomediatech.queryresponse.util.Logging; + +@Component +public class MessageConsumer implements Logging { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final MessageConsumerAdatper adapter; + + public MessageConsumer(Optional maybe) { + this.adapter = maybe.orElse(MessageConsumerAdatper.empty()); + } + + @RabbitListener(queues = "#{@" + ConfigureMessaging.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}") + void onQueryResponseStats(Message message, @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String key) { + + if (ConfigureMessaging.QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY.equals(key)) { + try { + adapter.handle(MAPPER.readValue(message.getBody(), Stats.class).elements()); + } catch (RuntimeException | IOException ex) { + log().error("Failed to consumed stats", ex); + } + } + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java new file mode 100644 index 00000000..a47c8c12 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java @@ -0,0 +1,20 @@ +package com.studiomediatech.queryresponse.ui.messaging; + +import java.util.Collection; + +import com.studiomediatech.Stat; +import com.studiomediatech.queryresponse.util.Logging; + +public interface MessageConsumerAdatper extends Logging { + + static MessageConsumerAdatper empty() { + return new MessageConsumerAdatper() { + // OK + }; + } + + default void handle(Collection elements) { + log().warn("NOT YET HANDLING {}", elements); + } + +} From 3cf6fdd5ca9d89a9b0305c5109bf435aa3fc3049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Wed, 15 Mar 2023 00:15:03 +0100 Subject: [PATCH 03/19] Move things around --- .../com/studiomediatech/QueryPublisher.java | 4 +- .../{ => queryresponse/stats}/Stat.java | 2 +- .../{ => queryresponse/stats}/Stats.java | 2 +- .../ui/api/WebSocketApiHandler.java | 2 +- .../ui/messaging/MessageConsumer.java | 42 ++++++++++++------- .../ui/messaging/MessageConsumerAdatper.java | 20 --------- .../ui/service/QueryResponseStatsService.java | 20 +++++++++ .../ui/service/StatsHandlerAdapter.java | 20 +++++++++ ui/src/main/resources/application.yaml | 2 +- 9 files changed, 72 insertions(+), 42 deletions(-) rename ui/src/main/java/com/studiomediatech/{ => queryresponse/stats}/Stat.java (93%) rename ui/src/main/java/com/studiomediatech/{ => queryresponse/stats}/Stats.java (62%) delete mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index fb376e1d..5dcd7991 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -22,12 +22,12 @@ import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.QueryBuilder; +import com.studiomediatech.queryresponse.stats.Stat; import com.studiomediatech.queryresponse.ui.api.RestApiAdapter; import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; -import com.studiomediatech.queryresponse.ui.messaging.MessageConsumerAdatper; import com.studiomediatech.queryresponse.util.Loggable; -public class QueryPublisher implements Loggable, RestApiAdapter, MessageConsumerAdatper { +public class QueryPublisher implements Loggable, RestApiAdapter { // This is a Fib! private static final int MAX_SIZE = 2584; diff --git a/ui/src/main/java/com/studiomediatech/Stat.java b/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stat.java similarity index 93% rename from ui/src/main/java/com/studiomediatech/Stat.java rename to ui/src/main/java/com/studiomediatech/queryresponse/stats/Stat.java index a3072d5c..7d9825c6 100644 --- a/ui/src/main/java/com/studiomediatech/Stat.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stat.java @@ -1,4 +1,4 @@ -package com.studiomediatech; +package com.studiomediatech.queryresponse.stats; public record Stat(String key, Object value, Long timestamp, String uuid) { diff --git a/ui/src/main/java/com/studiomediatech/Stats.java b/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stats.java similarity index 62% rename from ui/src/main/java/com/studiomediatech/Stats.java rename to ui/src/main/java/com/studiomediatech/queryresponse/stats/Stats.java index 97b8d55b..3505a120 100644 --- a/ui/src/main/java/com/studiomediatech/Stats.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stats.java @@ -1,4 +1,4 @@ -package com.studiomediatech; +package com.studiomediatech.queryresponse.stats; import java.util.Collection; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java index 9dbf49bd..1d11a212 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java @@ -17,9 +17,9 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.studiomediatech.Stat; import com.studiomediatech.events.EventEmitter; import com.studiomediatech.events.QueryRecordedEvent; +import com.studiomediatech.queryresponse.stats.Stat; import com.studiomediatech.queryresponse.util.Loggable; public class WebSocketApiHandler extends TextWebSocketHandler implements Loggable { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java index 65bff8f2..de5c44bb 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java @@ -3,37 +3,47 @@ import java.io.IOException; import java.util.Optional; +import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; -import com.studiomediatech.Stats; +import com.studiomediatech.queryresponse.stats.Stats; import com.studiomediatech.queryresponse.ui.ConfigureMessaging; +import com.studiomediatech.queryresponse.ui.service.StatsHandlerAdapter; import com.studiomediatech.queryresponse.util.Logging; +/** + * Consumes Query/Response messages from the internal topics, and directly delegates for handling via the abstract + * adapter. + */ @Component -public class MessageConsumer implements Logging { +class MessageConsumer implements Logging { private static final ObjectMapper MAPPER = new ObjectMapper(); - private final MessageConsumerAdatper adapter; + /** + * {@link AcknowledgeMode#NONE} + */ + private static final String ACK_MODE = "NONE"; + private static final String CONSUMERS_MIN = "3"; + private static final String CONSUMERS_MAX = "11"; - public MessageConsumer(Optional maybe) { - this.adapter = maybe.orElse(MessageConsumerAdatper.empty()); - } + private final StatsHandlerAdapter adapter; - @RabbitListener(queues = "#{@" + ConfigureMessaging.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}") - void onQueryResponseStats(Message message, @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String key) { + public MessageConsumer(Optional maybe) { + this.adapter = maybe.orElse(StatsHandlerAdapter.empty()); + } - if (ConfigureMessaging.QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY.equals(key)) { - try { - adapter.handle(MAPPER.readValue(message.getBody(), Stats.class).elements()); - } catch (RuntimeException | IOException ex) { - log().error("Failed to consumed stats", ex); - } + @RabbitListener(// + queues = "#{@" + ConfigureMessaging.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}", // + ackMode = ACK_MODE, concurrency = CONSUMERS_MIN + "-" + CONSUMERS_MAX) + void onQueryResponseStats(Message message) { + try { + adapter.handleConsumed(MAPPER.readValue(message.getBody(), Stats.class)); + } catch (RuntimeException | IOException ex) { + log().error("Failed to consumed stats", ex); } } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java deleted file mode 100644 index a47c8c12..00000000 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerAdatper.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.studiomediatech.queryresponse.ui.messaging; - -import java.util.Collection; - -import com.studiomediatech.Stat; -import com.studiomediatech.queryresponse.util.Logging; - -public interface MessageConsumerAdatper extends Logging { - - static MessageConsumerAdatper empty() { - return new MessageConsumerAdatper() { - // OK - }; - } - - default void handle(Collection elements) { - log().warn("NOT YET HANDLING {}", elements); - } - -} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java new file mode 100644 index 00000000..7b392654 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java @@ -0,0 +1,20 @@ +package com.studiomediatech.queryresponse.ui.service; + +import org.springframework.stereotype.Service; + +import com.studiomediatech.queryresponse.stats.Stat; +import com.studiomediatech.queryresponse.stats.Stats; +import com.studiomediatech.queryresponse.util.Logging; + +@Service +public class QueryResponseStatsService implements Logging, StatsHandlerAdapter { + + @Override + public void handleConsumed(Stats stats) { + stats.elements().stream().filter(s -> s.timestamp() != null).map(Stat::toString) + .forEach(str -> log().debug("STAT VALUE: {}", str)); + + stats.elements().stream().filter(s -> s.timestamp() == null).map(Stat::toString) + .forEach(str -> log().debug("STAT INFO: {}", str)); + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java new file mode 100644 index 00000000..161a76f2 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java @@ -0,0 +1,20 @@ +package com.studiomediatech.queryresponse.ui.service; + +import com.studiomediatech.queryresponse.stats.Stats; +import com.studiomediatech.queryresponse.util.Logging; + +/** + * Declares the capabilities of the incoming side for statistics to aggregate. + */ +public interface StatsHandlerAdapter extends Logging { + + static StatsHandlerAdapter empty() { + return new StatsHandlerAdapter() { + // OK + }; + } + + default void handleConsumed(Stats stats) { + log().warn("NOT YET HANDLING {}", stats); + } +} diff --git a/ui/src/main/resources/application.yaml b/ui/src/main/resources/application.yaml index ea22b08b..867e7a77 100644 --- a/ui/src/main/resources/application.yaml +++ b/ui/src/main/resources/application.yaml @@ -1,2 +1,2 @@ spring.application.name: query-response-ui -logging.level.com.studiomediatech.QueryPublisher: DEBUG \ No newline at end of file +logging.level.com.studiomediatech.queryresponse.ui: DEBUG From a14c3af9d214b518e8861587a80f902c197b9f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sat, 25 Mar 2023 08:36:34 +0100 Subject: [PATCH 04/19] Add links to API for better exploration --- .../ui/api/RestApiController.java | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java index 2864f654..9e9eb245 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java @@ -1,6 +1,8 @@ package com.studiomediatech.queryresponse.ui.api; -import java.util.Collections; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; @@ -19,12 +21,12 @@ public RestApiController(Optional maybe) { @GetMapping("/api") public Map getApiRoot() { - return Map.of("version", "v1"); + return Response.from(Map.of("now", Instant.now().toString())).withLinks("v1", "/api/v1"); } @GetMapping("/api/v1") public Map none() { - return Collections.emptyMap(); + return Response.from(Map.of("version", "v1")).withLinks("query-response", "/api/v1?q=query"); } @GetMapping(path = "/api/v1", params = "q") @@ -33,11 +35,50 @@ public Map query(String q, // NOSONAR @RequestParam(name = "t", required = false, defaultValue = "0") int t, @RequestParam(name = "limit", required = false, defaultValue = "0") int limit, @RequestParam(name = "l", required = false, defaultValue = "0") int l) { - return adapter.query(q, Math.max(0, Math.max(timeout, t)), Math.max(0, Math.max(limit, l))); + + int normalizedTimeout = Math.max(0, Math.max(timeout, t)); + int normalizedLimit = Math.max(0, Math.max(limit, l)); + + return adapter.query(q, normalizedTimeout, normalizedLimit); } @GetMapping("/api/v1/nodes") public Map nodes() { return adapter.nodes(); } + + protected interface Response { + public static ResponseBuilder from(Map map) { + return new ResponseBuilder(map); + } + } + + static class ResponseBuilder { + + private final Map map; + + private ResponseBuilder(Map map) { + this.map = map; + } + + public Map withLinks(String... args) { + + var links = new ArrayList(); + + for (int i = 0; i < args.length; i = i + 2) { + var rel = args[i]; + var val = args[i + 1]; + links.add(new Link(rel, val)); + } + + var results = new LinkedHashMap<>(map); + results.put("_links", links); + return results; + } + } + + protected record Link(String rel, String href) { + // OK + } + } From 19386b707d097c5dc40084d08cfd09458a8a3dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 26 Mar 2023 09:25:24 +0200 Subject: [PATCH 05/19] Use more decoupled app structure with ports/adapters --- .../ui/api/RestApiController.java | 26 ++++++++++++------- .../ui/{ => app}/QueryResponseUIApp.java | 17 +++++++++--- .../ui/messaging/MessageConsumer.java | 6 ++--- .../Messaging.java} | 12 +++++---- .../queryresponse/ui/service/Node.java | 21 +++++++++++++++ .../ui/service/NodesRepository.java | 7 +++++ .../ui/service/QueryResponseStatsService.java | 12 +++++++++ 7 files changed, 79 insertions(+), 22 deletions(-) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/{ => app}/QueryResponseUIApp.java (63%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/{ConfigureMessaging.java => messaging/Messaging.java} (70%) create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java index 9e9eb245..30e83c3e 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java @@ -20,13 +20,24 @@ public RestApiController(Optional maybe) { } @GetMapping("/api") - public Map getApiRoot() { - return Response.from(Map.of("now", Instant.now().toString())).withLinks("v1", "/api/v1"); + public Map showApi() { + return Response.from(Map.of("now", Instant.now())).withLinks("v0", "/api/v0", "v1", "/api/v1"); + } + + @GetMapping("/api/v0") + public Map v0() { + return Response.from(Map.of("version", "v0", "now", Instant.now())).withLinks("nodes", "/api/v0/nodes"); + } + + @GetMapping("/api/v0/nodes") + public Map nodes() { + return adapter.nodes(); } @GetMapping("/api/v1") - public Map none() { - return Response.from(Map.of("version", "v1")).withLinks("query-response", "/api/v1?q=query"); + public Map v1() { + return Response.from(Map.of("version", "v1", "now", Instant.now())).withLinks("query-response", + "/api/v1?q=query"); } @GetMapping(path = "/api/v1", params = "q") @@ -42,11 +53,6 @@ public Map query(String q, // NOSONAR return adapter.query(q, normalizedTimeout, normalizedLimit); } - @GetMapping("/api/v1/nodes") - public Map nodes() { - return adapter.nodes(); - } - protected interface Response { public static ResponseBuilder from(Map map) { return new ResponseBuilder(map); @@ -78,7 +84,7 @@ public Map withLinks(String... args) { } protected record Link(String rel, String href) { - // OK + // OK } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java similarity index 63% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java index 888acf6d..98fbe095 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/QueryResponseUIApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java @@ -1,18 +1,27 @@ -package com.studiomediatech.queryresponse.ui; +package com.studiomediatech.queryresponse.ui.app; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.socket.config.annotation.EnableWebSocket; import com.studiomediatech.queryresponse.EnableQueryResponse; +import com.studiomediatech.queryresponse.ui.messaging.Messaging; -@SpringBootApplication -@EnableQueryResponse -@EnableScheduling @EnableWebSocket +@EnableScheduling +@EnableQueryResponse +@SpringBootApplication public class QueryResponseUIApp { + public static void main(String[] args) { SpringApplication.run(QueryResponseUIApp.class); } + + @Configuration + @Import({ Messaging.class }) + static class Setup { + } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java index de5c44bb..7440202d 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.queryresponse.stats.Stats; -import com.studiomediatech.queryresponse.ui.ConfigureMessaging; import com.studiomediatech.queryresponse.ui.service.StatsHandlerAdapter; import com.studiomediatech.queryresponse.util.Logging; @@ -37,11 +36,12 @@ public MessageConsumer(Optional maybe) { } @RabbitListener(// - queues = "#{@" + ConfigureMessaging.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}", // + queues = "#{@" + Messaging.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}", // ackMode = ACK_MODE, concurrency = CONSUMERS_MIN + "-" + CONSUMERS_MAX) void onQueryResponseStats(Message message) { try { - adapter.handleConsumed(MAPPER.readValue(message.getBody(), Stats.class)); + Stats stats = MAPPER.readValue(message.getBody(), Stats.class); + adapter.handleConsumed(stats); } catch (RuntimeException | IOException ex) { log().error("Failed to consumed stats", ex); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Messaging.java similarity index 70% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Messaging.java index 9d3181cd..1fb33f8d 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureMessaging.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Messaging.java @@ -1,4 +1,4 @@ -package com.studiomediatech.queryresponse.ui; +package com.studiomediatech.queryresponse.ui.messaging; import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Binding; @@ -6,17 +6,19 @@ import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import com.studiomediatech.queryresponse.QueryResponseTopicExchange; @Configuration -public class ConfigureMessaging { +@ComponentScan(basePackageClasses = Messaging.class) +public class Messaging { - public static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; - public static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; - public static final String QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY = "query-response/internal/stats"; + static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; + static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; + static final String QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY = "query-response/internal/stats"; @Bean ConnectionNameStrategy connectionNameStrategy(Environment env) { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java new file mode 100644 index 00000000..e199cda7 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java @@ -0,0 +1,21 @@ +package com.studiomediatech.queryresponse.ui.service; + +import java.util.UUID; + +public final class Node { + + private final UUID uuid; + + private Node(UUID uuid) { + this.uuid = uuid; + } + + @Override + public String toString() { + return "Node [uuid=" + uuid + "]"; + } + + public static Node from(UUID uuid) { + return new Node(uuid); + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java new file mode 100644 index 00000000..7618558a --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java @@ -0,0 +1,7 @@ +package com.studiomediatech.queryresponse.ui.service; + +public interface NodesRepository { + + void save(Node node); + +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java index 7b392654..98669cc4 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java @@ -9,12 +9,24 @@ @Service public class QueryResponseStatsService implements Logging, StatsHandlerAdapter { + // private final NodesRepository nodesRepository; + // + // public QueryResponseStatsService(NodesRepository nodesRepository) { + // this.nodesRepository = nodesRepository; + // } + @Override public void handleConsumed(Stats stats) { + + log().info("Consumed stats with {} elements", stats.elements().size()); + + // stats.elements().stream().map(Stat::uuid).map(UUID::fromString).map(Node::from).forEach(nodesRepository::save); + stats.elements().stream().filter(s -> s.timestamp() != null).map(Stat::toString) .forEach(str -> log().debug("STAT VALUE: {}", str)); stats.elements().stream().filter(s -> s.timestamp() == null).map(Stat::toString) .forEach(str -> log().debug("STAT INFO: {}", str)); + } } From ac0443baafc201c46972c7db1ddac8660531eaae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 26 Mar 2023 10:59:23 +0200 Subject: [PATCH 06/19] Decouple ports for api and messaging --- ui/pom.xml | 16 ++++++++++++++++ .../java/com/studiomediatech/QueryPublisher.java | 4 ++-- .../queryresponse/ui/ConfigureApp.java | 8 ++++---- .../queryresponse/ui/ConfigureWebSocket.java | 6 +++--- .../queryresponse/ui/api/ApiConfig.java | 16 ++++++++++++++++ ...ontroller.java => RestApiControllerPort.java} | 6 ++++-- ...Handler.java => WebSocketApiHandlerPort.java} | 4 ++-- .../queryresponse/ui/app/QueryResponseUIApp.java | 10 ++++------ .../ui/{api => app}/RestApiAdapter.java | 2 +- .../ui/{service => app}/StatsHandlerAdapter.java | 2 +- ...ageConsumer.java => MessageConsumerPort.java} | 8 ++++---- .../{Messaging.java => MessagingConfig.java} | 9 +++++++-- .../ui/service/QueryResponseStatsService.java | 1 + .../ui/api/RestApiControllerTest.java | 4 ++-- 14 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/api/ApiConfig.java rename ui/src/main/java/com/studiomediatech/queryresponse/ui/api/{RestApiController.java => RestApiControllerPort.java} (94%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/api/{WebSocketApiHandler.java => WebSocketApiHandlerPort.java} (97%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/{api => app}/RestApiAdapter.java (89%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/{service => app}/StatsHandlerAdapter.java (89%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/{MessageConsumer.java => MessageConsumerPort.java} (84%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/{Messaging.java => MessagingConfig.java} (84%) diff --git a/ui/pom.xml b/ui/pom.xml index 9a67f457..007e53b4 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -69,6 +69,22 @@ ${project.artifactId} + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0 + + + + net.revelc.code.formatter diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index 5dcd7991..7a3f3cd0 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -44,11 +44,11 @@ public class QueryPublisher implements Loggable, RestApiAdapter { private List tps = new LinkedList<>(); private final QueryBuilder queryBuilder; - private final WebSocketApiHandler handler; + private final WebSocketApiHandlerPort handler; private final Map nodes = new ConcurrentHashMap<>(); - public QueryPublisher(WebSocketApiHandler handler, QueryBuilder queryBuilder) { + public QueryPublisher(WebSocketApiHandlerPort handler, QueryBuilder queryBuilder) { this.handler = handler; this.queryBuilder = queryBuilder; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java index 81c023bd..a53ceed6 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java @@ -19,7 +19,7 @@ import com.studiomediatech.events.EventEmitter; import com.studiomediatech.queryresponse.QueryBuilder; import com.studiomediatech.queryresponse.QueryResponseTopicExchange; -import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; +import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; @Order(10) @Configuration @@ -39,13 +39,13 @@ EventEmitter eventEmitter(TaskScheduler scheduler, ApplicationEventPublisher pub } @Bean - WebSocketApiHandler handler(EventEmitter emitter) { + WebSocketApiHandlerPort handler(EventEmitter emitter) { - return new WebSocketApiHandler(emitter); + return new WebSocketApiHandlerPort(emitter); } @Bean - QueryPublisher querier(WebSocketApiHandler handler, QueryBuilder queryBuilder) { + QueryPublisher querier(WebSocketApiHandlerPort handler, QueryBuilder queryBuilder) { return new QueryPublisher(handler, queryBuilder); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java index 69812bf6..c7ce4155 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java @@ -5,15 +5,15 @@ import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; +import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; @Order(100) @Configuration class ConfigureWebSocket implements WebSocketConfigurer { - private final WebSocketApiHandler webSocketHandler; + private final WebSocketApiHandlerPort webSocketHandler; - public ConfigureWebSocket(WebSocketApiHandler webSocketHandler) { + public ConfigureWebSocket(WebSocketApiHandlerPort webSocketHandler) { this.webSocketHandler = webSocketHandler; } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/ApiConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/ApiConfig.java new file mode 100644 index 00000000..9a2e9ec2 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/ApiConfig.java @@ -0,0 +1,16 @@ +package com.studiomediatech.queryresponse.ui.api; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; + +/** + * Configuration for port components, providing access or information over published APIs, specifically over HTTP and + * WebSockets. + */ +@Configuration +@EnableWebSocket +@ComponentScan(basePackageClasses = ApiConfig.class) +public class ApiConfig { + // OK +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java similarity index 94% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java index 30e83c3e..e08c76bf 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiController.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java @@ -10,12 +10,14 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.studiomediatech.queryresponse.ui.app.RestApiAdapter; + @RestController -public class RestApiController { +public class RestApiControllerPort { private final RestApiAdapter adapter; - public RestApiController(Optional maybe) { + public RestApiControllerPort(Optional maybe) { this.adapter = maybe.orElse(RestApiAdapter.empty()); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java similarity index 97% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java index 1d11a212..ad7b450b 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandler.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java @@ -22,7 +22,7 @@ import com.studiomediatech.queryresponse.stats.Stat; import com.studiomediatech.queryresponse.util.Loggable; -public class WebSocketApiHandler extends TextWebSocketHandler implements Loggable { +public class WebSocketApiHandlerPort extends TextWebSocketHandler implements Logging { private static final int SEND_TIME_LIMIT = 6 * 1000; private static final int SEND_BUFFER_SIZE_LIMIT = 512 * 1024; @@ -32,7 +32,7 @@ public class WebSocketApiHandler extends TextWebSocketHandler implements Loggabl private final EventEmitter emitter; private final ObjectMapper objectMapper; - public WebSocketApiHandler(EventEmitter emitter) { + public WebSocketApiHandlerPort(EventEmitter emitter) { this.emitter = emitter; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java index 98fbe095..75552591 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java @@ -5,14 +5,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import com.studiomediatech.queryresponse.EnableQueryResponse; -import com.studiomediatech.queryresponse.ui.messaging.Messaging; +import com.studiomediatech.queryresponse.ui.api.ApiConfig; +import com.studiomediatech.queryresponse.ui.messaging.MessagingConfig; -@EnableWebSocket @EnableScheduling -@EnableQueryResponse @SpringBootApplication public class QueryResponseUIApp { @@ -21,7 +18,8 @@ public static void main(String[] args) { } @Configuration - @Import({ Messaging.class }) + @Import({ MessagingConfig.class, ApiConfig.class }) static class Setup { + // OK } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/RestApiAdapter.java similarity index 89% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiAdapter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/RestApiAdapter.java index c0bfb5a4..ca270199 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/RestApiAdapter.java @@ -1,4 +1,4 @@ -package com.studiomediatech.queryresponse.ui.api; +package com.studiomediatech.queryresponse.ui.app; import java.util.Collections; import java.util.Map; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/StatsHandlerAdapter.java similarity index 89% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/StatsHandlerAdapter.java index 161a76f2..be125889 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/StatsHandlerAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/StatsHandlerAdapter.java @@ -1,4 +1,4 @@ -package com.studiomediatech.queryresponse.ui.service; +package com.studiomediatech.queryresponse.ui.app; import com.studiomediatech.queryresponse.stats.Stats; import com.studiomediatech.queryresponse.util.Logging; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java similarity index 84% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java index 7440202d..ec2779ec 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumer.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java @@ -10,7 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.queryresponse.stats.Stats; -import com.studiomediatech.queryresponse.ui.service.StatsHandlerAdapter; +import com.studiomediatech.queryresponse.ui.app.StatsHandlerAdapter; import com.studiomediatech.queryresponse.util.Logging; /** @@ -18,7 +18,7 @@ * adapter. */ @Component -class MessageConsumer implements Logging { +class MessageConsumerPort implements Logging { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -31,12 +31,12 @@ class MessageConsumer implements Logging { private final StatsHandlerAdapter adapter; - public MessageConsumer(Optional maybe) { + public MessageConsumerPort(Optional maybe) { this.adapter = maybe.orElse(StatsHandlerAdapter.empty()); } @RabbitListener(// - queues = "#{@" + Messaging.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}", // + queues = "#{@" + MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}", // ackMode = ACK_MODE, concurrency = CONSUMERS_MIN + "-" + CONSUMERS_MAX) void onQueryResponseStats(Message message) { try { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Messaging.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java similarity index 84% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Messaging.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java index 1fb33f8d..2c758523 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Messaging.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java @@ -10,11 +10,16 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import com.studiomediatech.queryresponse.EnableQueryResponse; import com.studiomediatech.queryresponse.QueryResponseTopicExchange; +/** + * Configuration for ports that enable messaging, specifically AMQP including Query/Response. + */ @Configuration -@ComponentScan(basePackageClasses = Messaging.class) -public class Messaging { +@EnableQueryResponse +@ComponentScan(basePackageClasses = MessagingConfig.class) +public class MessagingConfig { static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java index 98669cc4..7bcbc760 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java @@ -4,6 +4,7 @@ import com.studiomediatech.queryresponse.stats.Stat; import com.studiomediatech.queryresponse.stats.Stats; +import com.studiomediatech.queryresponse.ui.app.StatsHandlerAdapter; import com.studiomediatech.queryresponse.util.Logging; @Service diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java index 60f3cb7a..e193636a 100644 --- a/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java @@ -19,7 +19,7 @@ class RestApiControllerTest { @BeforeEach void setup() { - mockMvc = MockMvcBuilders.standaloneSetup(new RestApiController(Optional.empty())).build(); + mockMvc = MockMvcBuilders.standaloneSetup(new RestApiControllerPort(Optional.empty())).build(); } @Test @@ -29,7 +29,7 @@ void ensureHandlesQueryRequest() throws Exception { @Test void ensureHandlesNodesRequest() throws Exception { - mockMvc.perform(get("/api/v1/nodes")).andExpect(status().isOk()); + mockMvc.perform(get("/api/v0/nodes")).andExpect(status().isOk()); } } From cc367acda61be8a70fde05a45445ccfb2afab476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 26 Mar 2023 11:48:39 +0200 Subject: [PATCH 07/19] Add messaging config test --- ui/pom.xml | 13 +++++++++- ...st.java => RestApiControllerPortTest.java} | 2 +- .../ui/messaging/MessagingConfigIT.java | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) rename ui/src/test/java/com/studiomediatech/queryresponse/ui/api/{RestApiControllerTest.java => RestApiControllerPortTest.java} (96%) create mode 100644 ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java diff --git a/ui/pom.xml b/ui/pom.xml index 007e53b4..48b3c8d2 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -97,7 +97,18 @@ - + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + **/*IT.java + + + + org.springframework.boot spring-boot-maven-plugin diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java similarity index 96% rename from ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java rename to ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java index e193636a..539412b9 100644 --- a/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerTest.java +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java @@ -13,7 +13,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; @ExtendWith(MockitoExtension.class) -class RestApiControllerTest { +class RestApiControllerPortTest { MockMvc mockMvc; diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java new file mode 100644 index 00000000..0ffaad8d --- /dev/null +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java @@ -0,0 +1,24 @@ +package com.studiomediatech.queryresponse.ui.messaging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Queue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +@SpringBootTest(classes = MessagingConfig.class) +class MessagingConfigIT { + + @Autowired + ApplicationContext ctx; + + @Test + void test() { + String[] names = ctx.getBeanNamesForType(Queue.class); + assertThat(names).contains(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN); + } + +} From ca25cad0c6c2e2927e5b0c7edb8c22ef98f452a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 26 Mar 2023 13:34:26 +0200 Subject: [PATCH 08/19] Add infrastructure config --- .../com/studiomediatech/QueryPublisher.java | 8 ++--- .../ui/api/RestApiControllerPort.java | 2 +- .../ui/api/WebSocketApiHandlerPort.java | 2 ++ .../ui/app/QueryResponseUIApp.java | 3 +- .../MessageHandlerAdapter.java} | 10 +++--- .../ui/app/{ => adapter}/RestApiAdapter.java | 2 +- .../ui/app/telemetry/TelemetryService.java | 30 +++++++++++++++++ .../queryresponse/ui/infra/InfraConfig.java | 21 ++++++++++++ .../ui/messaging/MessageConsumerPort.java | 9 +++-- .../ui/messaging/MessagingConfig.java | 3 +- .../{stats => ui/messaging}/Stat.java | 2 +- .../{stats => ui/messaging}/Stats.java | 2 +- .../queryresponse/ui/service/Node.java | 21 ------------ .../ui/service/NodesRepository.java | 7 ---- .../ui/service/QueryResponseStatsService.java | 33 ------------------- .../ui/messaging/MessagingConfigIT.java | 16 ++++++--- 16 files changed, 85 insertions(+), 86 deletions(-) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/app/{StatsHandlerAdapter.java => adapter/MessageHandlerAdapter.java} (52%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/app/{ => adapter}/RestApiAdapter.java (87%) create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java rename ui/src/main/java/com/studiomediatech/queryresponse/{stats => ui/messaging}/Stat.java (92%) rename ui/src/main/java/com/studiomediatech/queryresponse/{stats => ui/messaging}/Stats.java (59%) delete mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java delete mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java delete mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index 7a3f3cd0..eb3c6342 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -22,10 +22,10 @@ import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.QueryBuilder; -import com.studiomediatech.queryresponse.stats.Stat; -import com.studiomediatech.queryresponse.ui.api.RestApiAdapter; -import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandler; -import com.studiomediatech.queryresponse.util.Loggable; +import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; +import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; +import com.studiomediatech.queryresponse.ui.messaging.Stat; +import com.studiomediatech.queryresponse.util.Logging; public class QueryPublisher implements Loggable, RestApiAdapter { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java index e08c76bf..f57be17f 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.studiomediatech.queryresponse.ui.app.RestApiAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; @RestController public class RestApiControllerPort { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java index ad7b450b..7801fdcd 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java @@ -21,6 +21,8 @@ import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.stats.Stat; import com.studiomediatech.queryresponse.util.Loggable; +import com.studiomediatech.queryresponse.ui.messaging.Stat; +import com.studiomediatech.queryresponse.util.Logging; public class WebSocketApiHandlerPort extends TextWebSocketHandler implements Logging { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java index 75552591..45626fe8 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java @@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; import com.studiomediatech.queryresponse.ui.api.ApiConfig; +import com.studiomediatech.queryresponse.ui.infra.InfraConfig; import com.studiomediatech.queryresponse.ui.messaging.MessagingConfig; @EnableScheduling @@ -18,7 +19,7 @@ public static void main(String[] args) { } @Configuration - @Import({ MessagingConfig.class, ApiConfig.class }) + @Import({ MessagingConfig.class, ApiConfig.class, InfraConfig.class }) static class Setup { // OK } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/StatsHandlerAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/MessageHandlerAdapter.java similarity index 52% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/app/StatsHandlerAdapter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/MessageHandlerAdapter.java index be125889..2ebcd0c9 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/StatsHandlerAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/MessageHandlerAdapter.java @@ -1,15 +1,15 @@ -package com.studiomediatech.queryresponse.ui.app; +package com.studiomediatech.queryresponse.ui.app.adapter; -import com.studiomediatech.queryresponse.stats.Stats; +import com.studiomediatech.queryresponse.ui.messaging.Stats; import com.studiomediatech.queryresponse.util.Logging; /** * Declares the capabilities of the incoming side for statistics to aggregate. */ -public interface StatsHandlerAdapter extends Logging { +public interface MessageHandlerAdapter extends Logging { - static StatsHandlerAdapter empty() { - return new StatsHandlerAdapter() { + static MessageHandlerAdapter empty() { + return new MessageHandlerAdapter() { // OK }; } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/RestApiAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/RestApiAdapter.java similarity index 87% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/app/RestApiAdapter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/RestApiAdapter.java index ca270199..8fc35e14 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/RestApiAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/RestApiAdapter.java @@ -1,4 +1,4 @@ -package com.studiomediatech.queryresponse.ui.app; +package com.studiomediatech.queryresponse.ui.app.adapter; import java.util.Collections; import java.util.Map; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java new file mode 100644 index 00000000..ec551865 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java @@ -0,0 +1,30 @@ +package com.studiomediatech.queryresponse.ui.app.telemetry; + +import java.util.Map; + +import com.studiomediatech.queryresponse.ui.app.adapter.MessageHandlerAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; +import com.studiomediatech.queryresponse.ui.messaging.Stat; +import com.studiomediatech.queryresponse.ui.messaging.Stats; +import com.studiomediatech.queryresponse.util.Logging; + +public class TelemetryService implements Logging, MessageHandlerAdapter, RestApiAdapter { + + @Override + public Map nodes() { + return Map.of("nodes", "none"); + } + + @Override + public void handleConsumed(Stats stats) { + + log().info("Consumed stats with {} elements", stats.elements().size()); + + stats.elements().stream().filter(s -> s.timestamp() != null).map(Stat::toString) + .forEach(str -> log().debug("STAT VALUE: {}", str)); + + stats.elements().stream().filter(s -> s.timestamp() == null).map(Stat::toString) + .forEach(str -> log().debug("STAT INFO: {}", str)); + + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java new file mode 100644 index 00000000..333f3617 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java @@ -0,0 +1,21 @@ +package com.studiomediatech.queryresponse.ui.infra; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import com.studiomediatech.queryresponse.ui.app.telemetry.TelemetryService; + +/** + * Configuration for components of the application, with adapters for the platform. + * + */ +@Configuration +@ComponentScan(basePackageClasses = InfraConfig.class) +public class InfraConfig { + + @Bean + public TelemetryService queryResponseTelemetryService() { + return new TelemetryService(); + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java index ec2779ec..855837b4 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java @@ -9,8 +9,7 @@ import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; -import com.studiomediatech.queryresponse.stats.Stats; -import com.studiomediatech.queryresponse.ui.app.StatsHandlerAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.MessageHandlerAdapter; import com.studiomediatech.queryresponse.util.Logging; /** @@ -29,10 +28,10 @@ class MessageConsumerPort implements Logging { private static final String CONSUMERS_MIN = "3"; private static final String CONSUMERS_MAX = "11"; - private final StatsHandlerAdapter adapter; + private final MessageHandlerAdapter adapter; - public MessageConsumerPort(Optional maybe) { - this.adapter = maybe.orElse(StatsHandlerAdapter.empty()); + public MessageConsumerPort(Optional maybe) { + this.adapter = maybe.orElse(MessageHandlerAdapter.empty()); } @RabbitListener(// diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java index 2c758523..e8477114 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java @@ -23,6 +23,7 @@ public class MessagingConfig { static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; + static final String QUERY_RESPONSE_STATS_QUEUE_BINDING_BEAN = "queryResponseQueriesQueueBinding"; static final String QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY = "query-response/internal/stats"; @Bean @@ -35,7 +36,7 @@ Queue queryResponseStatsQueue() { return new AnonymousQueue(); } - @Bean + @Bean(QUERY_RESPONSE_STATS_QUEUE_BINDING_BEAN) Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) .with(QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY); diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stat.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java similarity index 92% rename from ui/src/main/java/com/studiomediatech/queryresponse/stats/Stat.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java index 7d9825c6..d5c3d7d0 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stat.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java @@ -1,4 +1,4 @@ -package com.studiomediatech.queryresponse.stats; +package com.studiomediatech.queryresponse.ui.messaging; public record Stat(String key, Object value, Long timestamp, String uuid) { diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stats.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stats.java similarity index 59% rename from ui/src/main/java/com/studiomediatech/queryresponse/stats/Stats.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stats.java index 3505a120..c80c46b4 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/stats/Stats.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stats.java @@ -1,4 +1,4 @@ -package com.studiomediatech.queryresponse.stats; +package com.studiomediatech.queryresponse.ui.messaging; import java.util.Collection; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java deleted file mode 100644 index e199cda7..00000000 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/Node.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.studiomediatech.queryresponse.ui.service; - -import java.util.UUID; - -public final class Node { - - private final UUID uuid; - - private Node(UUID uuid) { - this.uuid = uuid; - } - - @Override - public String toString() { - return "Node [uuid=" + uuid + "]"; - } - - public static Node from(UUID uuid) { - return new Node(uuid); - } -} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java deleted file mode 100644 index 7618558a..00000000 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/NodesRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.studiomediatech.queryresponse.ui.service; - -public interface NodesRepository { - - void save(Node node); - -} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java deleted file mode 100644 index 7bcbc760..00000000 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/service/QueryResponseStatsService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.studiomediatech.queryresponse.ui.service; - -import org.springframework.stereotype.Service; - -import com.studiomediatech.queryresponse.stats.Stat; -import com.studiomediatech.queryresponse.stats.Stats; -import com.studiomediatech.queryresponse.ui.app.StatsHandlerAdapter; -import com.studiomediatech.queryresponse.util.Logging; - -@Service -public class QueryResponseStatsService implements Logging, StatsHandlerAdapter { - - // private final NodesRepository nodesRepository; - // - // public QueryResponseStatsService(NodesRepository nodesRepository) { - // this.nodesRepository = nodesRepository; - // } - - @Override - public void handleConsumed(Stats stats) { - - log().info("Consumed stats with {} elements", stats.elements().size()); - - // stats.elements().stream().map(Stat::uuid).map(UUID::fromString).map(Node::from).forEach(nodesRepository::save); - - stats.elements().stream().filter(s -> s.timestamp() != null).map(Stat::toString) - .forEach(str -> log().debug("STAT VALUE: {}", str)); - - stats.elements().stream().filter(s -> s.timestamp() == null).map(Stat::toString) - .forEach(str -> log().debug("STAT INFO: {}", str)); - - } -} diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java index 0ffaad8d..29c726f3 100644 --- a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java @@ -1,9 +1,9 @@ package com.studiomediatech.queryresponse.ui.messaging; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -16,9 +16,15 @@ class MessagingConfigIT { ApplicationContext ctx; @Test - void test() { - String[] names = ctx.getBeanNamesForType(Queue.class); - assertThat(names).contains(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN); - } + void ensure_has_telemetry_queue_with_binding() { + + Queue queue = (Queue) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN); + assertThat(queue).isNotNull(); + Binding binding = (Binding) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BINDING_BEAN); + assertThat(binding).isNotNull(); + assertThat(binding.getDestination()).isEqualTo(queue.getName()); + assertThat(binding.getRoutingKey()).isEqualTo("query-response/internal/stats"); + assertThat(binding.getExchange()).isEqualTo("query-response"); + } } From 5b355bb0a96a14e570212feadd6808bdb5e92716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 26 Mar 2023 14:15:37 +0200 Subject: [PATCH 09/19] Add WebSocket adapter --- .../queryresponse/ui/ConfigureApp.java | 26 +++---------------- .../ui/api/WebSocketApiHandlerPort.java | 11 +++++--- .../ui/app/adapter/EventEmitterAdapter.java} | 5 ++-- .../ui/app/adapter/WebSocketApiAdapter.java | 10 +++++++ .../ui/app/telemetry/TelemetryService.java | 7 +++++ .../ui/infra}/AsyncEventEmitter.java | 6 +++-- .../queryresponse/ui/infra/InfraConfig.java | 24 ++++++++++++++--- 7 files changed, 54 insertions(+), 35 deletions(-) rename ui/src/main/java/com/studiomediatech/{events/EventEmitter.java => queryresponse/ui/app/adapter/EventEmitterAdapter.java} (59%) create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java rename ui/src/main/java/com/studiomediatech/{events => queryresponse/ui/infra}/AsyncEventEmitter.java (80%) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java index a53ceed6..55efe800 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java @@ -1,45 +1,25 @@ package com.studiomediatech.queryresponse.ui; -import org.springframework.amqp.core.AnonymousQueue; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; -import org.springframework.core.env.Environment; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import com.studiomediatech.QueryPublisher; -import com.studiomediatech.events.AsyncEventEmitter; -import com.studiomediatech.events.EventEmitter; import com.studiomediatech.queryresponse.QueryBuilder; -import com.studiomediatech.queryresponse.QueryResponseTopicExchange; import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; +import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; +import com.studiomediatech.queryresponse.ui.infra.AsyncEventEmitter; @Order(10) @Configuration class ConfigureApp { @Bean - @Primary - TaskScheduler taskScheduler() { - - return new ThreadPoolTaskScheduler(); - } - - @Bean - EventEmitter eventEmitter(TaskScheduler scheduler, ApplicationEventPublisher publisher) { - - return new AsyncEventEmitter(scheduler, publisher); - } - - @Bean - WebSocketApiHandlerPort handler(EventEmitter emitter) { + WebSocketApiHandlerPort handler(EventEmitterAdapter emitter) { return new WebSocketApiHandlerPort(emitter); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java index 7801fdcd..afd12ff7 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; @@ -17,24 +18,26 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.studiomediatech.events.EventEmitter; import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.stats.Stat; import com.studiomediatech.queryresponse.util.Loggable; +import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; import com.studiomediatech.queryresponse.ui.messaging.Stat; import com.studiomediatech.queryresponse.util.Logging; -public class WebSocketApiHandlerPort extends TextWebSocketHandler implements Logging { +@Component +public class WebSocketApiHandlerPort extends TextWebSocketHandler implements WebSocketApiAdapter, Logging { private static final int SEND_TIME_LIMIT = 6 * 1000; private static final int SEND_BUFFER_SIZE_LIMIT = 512 * 1024; private final Map sessionsById = new ConcurrentHashMap<>(); - private final EventEmitter emitter; + private final EventEmitterAdapter emitter; private final ObjectMapper objectMapper; - public WebSocketApiHandlerPort(EventEmitter emitter) { + public WebSocketApiHandlerPort(EventEmitterAdapter emitter) { this.emitter = emitter; diff --git a/ui/src/main/java/com/studiomediatech/events/EventEmitter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/EventEmitterAdapter.java similarity index 59% rename from ui/src/main/java/com/studiomediatech/events/EventEmitter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/EventEmitterAdapter.java index 2ff0299e..4e70d411 100644 --- a/ui/src/main/java/com/studiomediatech/events/EventEmitter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/EventEmitterAdapter.java @@ -1,10 +1,9 @@ -package com.studiomediatech.events; +package com.studiomediatech.queryresponse.ui.app.adapter; /** * Declares the contract provided to clients, on how an event can be emitted. */ @FunctionalInterface -public interface EventEmitter { - +public interface EventEmitterAdapter { void emitEvent(Object event); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java new file mode 100644 index 00000000..70dcb027 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java @@ -0,0 +1,10 @@ +package com.studiomediatech.queryresponse.ui.app.adapter; + +public interface WebSocketApiAdapter { + + static WebSocketApiAdapter empty() { + return new WebSocketApiAdapter() { + // OK + }; + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java index ec551865..e374209c 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java @@ -4,12 +4,19 @@ import com.studiomediatech.queryresponse.ui.app.adapter.MessageHandlerAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; import com.studiomediatech.queryresponse.ui.messaging.Stat; import com.studiomediatech.queryresponse.ui.messaging.Stats; import com.studiomediatech.queryresponse.util.Logging; public class TelemetryService implements Logging, MessageHandlerAdapter, RestApiAdapter { + private final WebSocketApiAdapter webSocketApiAdapter; + + public TelemetryService(WebSocketApiAdapter webSocketApiAdapter) { + this.webSocketApiAdapter = webSocketApiAdapter; + } + @Override public Map nodes() { return Map.of("nodes", "none"); diff --git a/ui/src/main/java/com/studiomediatech/events/AsyncEventEmitter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/AsyncEventEmitter.java similarity index 80% rename from ui/src/main/java/com/studiomediatech/events/AsyncEventEmitter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/AsyncEventEmitter.java index 35f9e300..91143fbd 100644 --- a/ui/src/main/java/com/studiomediatech/events/AsyncEventEmitter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/AsyncEventEmitter.java @@ -1,15 +1,17 @@ -package com.studiomediatech.events; +package com.studiomediatech.queryresponse.ui.infra; import org.springframework.context.ApplicationEventPublisher; import org.springframework.scheduling.TaskScheduler; +import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; + import java.time.Instant; /** * An implementation that emits as a new scheduled task, ensuring that the calling thread is no longer blocked. */ -public class AsyncEventEmitter implements EventEmitter { +public class AsyncEventEmitter implements EventEmitterAdapter { // NOTE: Scheduled ASAP for any instant in the past. private static final Instant ASAP = Instant.EPOCH; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java index 333f3617..707ec34b 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java @@ -1,21 +1,39 @@ package com.studiomediatech.queryresponse.ui.infra; +import java.util.Optional; + +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; import com.studiomediatech.queryresponse.ui.app.telemetry.TelemetryService; /** * Configuration for components of the application, with adapters for the platform. - * */ @Configuration @ComponentScan(basePackageClasses = InfraConfig.class) public class InfraConfig { @Bean - public TelemetryService queryResponseTelemetryService() { - return new TelemetryService(); + @Primary + TaskScheduler taskScheduler() { + return new ThreadPoolTaskScheduler(); + } + + @Bean + EventEmitterAdapter eventEmitter(TaskScheduler scheduler, ApplicationEventPublisher publisher) { + return new AsyncEventEmitter(scheduler, publisher); + } + + @Bean + public TelemetryService telemetryService(Optional optionalWebSocketApiAdapter) { + return new TelemetryService(optionalWebSocketApiAdapter.orElse(WebSocketApiAdapter.empty())); } } From 4adccf8c2d4e6d77d39c0443757552ce77a8de43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 26 Mar 2023 15:36:57 +0200 Subject: [PATCH 10/19] Connect api via adapter --- .../com/studiomediatech/QueryPublisher.java | 58 ------------- .../ui/app/adapter/QueryPublisherAdapter.java | 17 ++++ .../ui/app/adapter/WebSocketApiAdapter.java | 11 ++- .../queryresponse/ui/app/telemetry/Node.java | 86 +++++++++++++++++++ .../ui/app/telemetry/NodeRepository.java | 14 +++ .../ui/app/telemetry/TelemetryService.java | 67 +++++++++++++-- .../queryresponse/ui/infra/InfraConfig.java | 29 ++++++- .../adapter/TelemetryServiceAdapter.java | 24 ++++++ .../ui/infra/repo/InMemoryNodeRepository.java | 31 +++++++ .../ui/messaging/QueryPublisherPort.java | 78 +++++++++++++++++ .../queryresponse/ui/messaging/Stat.java | 8 ++ 11 files changed, 353 insertions(+), 70 deletions(-) create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/QueryPublisherAdapter.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/NodeRepository.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/repo/InMemoryNodeRepository.java create mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index eb3c6342..0a70a366 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -1,6 +1,5 @@ package com.studiomediatech; -import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -32,7 +31,6 @@ public class QueryPublisher implements Loggable, RestApiAdapter { // This is a Fib! private static final int MAX_SIZE = 2584; private static final int SLIDING_WINDOW = 40; - private static final int DEFAULT_QUERY_TIMEOUT = 1500; static ToLongFunction statToLong = s -> ((Number) s.value()).longValue(); @@ -54,57 +52,6 @@ public QueryPublisher(WebSocketApiHandlerPort handler, QueryBuilder queryBuilder this.queryBuilder = queryBuilder; } - @Override - public Map query(String q, int timeout, int limit) { - - List defaults = List.of("No responses"); - - final Collection responses; - - long start = System.nanoTime(); - - if (q.contains(" ")) { - responses = queryParsed(q, defaults); - } else { - responses = queryStrict(q, timeout, limit, defaults); - } - - return Map.of("response", responses, "duration", Duration.ofNanos(System.nanoTime() - start)); - } - - private Collection queryParsed(String q, List defaults) { - - var query = QueryRecordedEvent.valueOf(q, "none"); - - query.getQuery(); - query.getTimeout(); - - Optional maybe = query.getLimit(); - - if (maybe.isPresent()) { - - return queryBuilder.queryFor(query.getQuery(), Object.class).waitingFor(query.getTimeout()) - .takingAtMost(maybe.get()).orDefaults(defaults); - - } - - return queryBuilder.queryFor(query.getQuery(), Object.class).waitingFor(query.getTimeout()) - .orDefaults(defaults); - - } - - private Collection queryStrict(String q, int timeout, int limit, List defaults) { - long queryTimeout = timeout > 0 ? timeout : DEFAULT_QUERY_TIMEOUT; - - if (limit > 0) { - return queryBuilder.queryFor(q, Object.class).waitingFor(queryTimeout).takingAtMost(limit) - .orDefaults(defaults); - } - - return queryBuilder.queryFor(q, Object.class).waitingFor(queryTimeout).orDefaults(defaults); - - } - @EventListener void on(QueryRecordedEvent event) { @@ -149,11 +96,6 @@ protected void handle(Collection stats) { handleNodes(stats); } - @Override - public Map nodes() { - return Map.of("nodes", this.nodes, "timestamp", Instant.now(Clock.systemUTC())); - } - private void handleNodes(Collection stats) { Map> nodes = stats.stream().filter(stat -> StringUtils.hasText(stat.uuid())) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/QueryPublisherAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/QueryPublisherAdapter.java new file mode 100644 index 00000000..f6fe0d5b --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/QueryPublisherAdapter.java @@ -0,0 +1,17 @@ +package com.studiomediatech.queryresponse.ui.app.adapter; + +import java.util.Collections; +import java.util.Map; + +public interface QueryPublisherAdapter { + + static QueryPublisherAdapter empty() { + return new QueryPublisherAdapter() { + // OK + }; + } + + default Map query(String q, int timeout, int limit) { + return Collections.emptyMap(); + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java index 70dcb027..f5f0c96d 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java @@ -1,10 +1,19 @@ package com.studiomediatech.queryresponse.ui.app.adapter; -public interface WebSocketApiAdapter { +import java.util.Collection; + +import com.studiomediatech.queryresponse.ui.app.telemetry.Node; +import com.studiomediatech.queryresponse.util.Logging; + +public interface WebSocketApiAdapter extends Logging { static WebSocketApiAdapter empty() { return new WebSocketApiAdapter() { // OK }; } + + default void publishNodes(Collection nodes) { + log().warn("NOT PUBLISHING NODES! {}", nodes); + } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java new file mode 100644 index 00000000..d6477c6a --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java @@ -0,0 +1,86 @@ +package com.studiomediatech.queryresponse.ui.app.telemetry; + +import java.util.Comparator; +import java.util.UUID; + +import org.springframework.util.Assert; + +public class Node { + + public static final Comparator SORT = Comparator.comparing(Node::getName).thenComparing(Node::getUUID); + + protected final UUID uuid; + protected String name; + protected String pid; + protected String host; + protected String uptime; + + public Node(UUID uuid) { + this.uuid = uuid; + } + + public Node(Node other) { + this(other.uuid); + this.name = other.name; + this.pid = other.pid; + this.host = other.host; + this.uptime = other.uptime; + } + + public static Node from(UUID uuid) { + return new Node(uuid); + } + + @Override + public String toString() { + return "Node [uuid=%s]".formatted(uuid); + } + + public UUID getUUID() { + return uuid; + } + + public Node update(Node other) { + + Assert.isTrue(other.uuid.equals(this.uuid), "Must be same node."); + + this.host = other.host; + this.name = other.name; + this.uptime = other.uptime; + this.pid = other.pid; + + return this; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPid() { + return pid; + } + + public void setPid(String pid) { + this.pid = pid; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getUptime() { + return uptime; + } + + public void setUptime(String uptime) { + this.uptime = uptime; + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/NodeRepository.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/NodeRepository.java new file mode 100644 index 00000000..daff6285 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/NodeRepository.java @@ -0,0 +1,14 @@ +package com.studiomediatech.queryresponse.ui.app.telemetry; + +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +public interface NodeRepository { + + Optional findOneByUUID(UUID uuid); + + void save(Node node); + + Collection findAll(); +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java index e374209c..5f6433ca 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java @@ -1,8 +1,13 @@ package com.studiomediatech.queryresponse.ui.app.telemetry; +import java.util.Collection; +import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.UUID; import com.studiomediatech.queryresponse.ui.app.adapter.MessageHandlerAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.QueryPublisherAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; import com.studiomediatech.queryresponse.ui.messaging.Stat; @@ -12,26 +17,74 @@ public class TelemetryService implements Logging, MessageHandlerAdapter, RestApiAdapter { private final WebSocketApiAdapter webSocketApiAdapter; + private final NodeRepository nodeRepository; + private final QueryPublisherAdapter queryPublisherAdapter; - public TelemetryService(WebSocketApiAdapter webSocketApiAdapter) { + public TelemetryService(WebSocketApiAdapter webSocketApiAdapter, NodeRepository nodeRepository, + QueryPublisherAdapter queryPublisherAdapter) { this.webSocketApiAdapter = webSocketApiAdapter; + this.nodeRepository = nodeRepository; + this.queryPublisherAdapter = queryPublisherAdapter; + } + + @Override + public Map query(String q, int timeout, int limit) { + return queryPublisherAdapter.query(q, timeout, limit); } @Override public Map nodes() { - return Map.of("nodes", "none"); + return Map.of("elements", nodeRepository.findAll()); } @Override public void handleConsumed(Stats stats) { - log().info("Consumed stats with {} elements", stats.elements().size()); - stats.elements().stream().filter(s -> s.timestamp() != null).map(Stat::toString) - .forEach(str -> log().debug("STAT VALUE: {}", str)); + Collection nodes = parseToNodesCollection(stats); + updateNodes(nodes); - stats.elements().stream().filter(s -> s.timestamp() == null).map(Stat::toString) - .forEach(str -> log().debug("STAT INFO: {}", str)); + // stats.elements().stream().filter(s -> s.timestamp() != + // null).map(Stat::toString) + // .forEach(str -> log().debug("STAT VALUE: {}", str)); + // + // stats.elements().stream().filter(s -> s.timestamp() == + // null).map(Stat::toString) + // .forEach(str -> log().debug("STAT INFO: {}", str)); + } + private void updateNodes(Collection nodes) { + for (Node node : nodes) { + Optional maybe = nodeRepository.findOneByUUID(node.getUUID()); + if (maybe.isPresent()) { + Node updated = maybe.get().update(node); + nodeRepository.save(updated); + } else { + nodeRepository.save(node); + } + } } + + protected Collection parseToNodesCollection(Stats stats) { + + Map nodes = new HashMap<>(); + + for (Stat stat : stats.elements()) { + + UUID uuid = UUID.fromString(stat.uuid()); + Node node = nodes.computeIfAbsent(uuid, key -> Node.from(key)); + + stat.whenKey("name", node::setName); + stat.whenKey("pid", node::setPid); + stat.whenKey("host", node::setHost); + stat.whenKey("uptime", node::setUptime); + } + + return nodes.values(); + } + + public void publishNodes() { + webSocketApiAdapter.publishNodes(nodeRepository.findAll()); + } + } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java index 707ec34b..35563b14 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/InfraConfig.java @@ -1,7 +1,6 @@ package com.studiomediatech.queryresponse.ui.infra; -import java.util.Optional; - +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -11,8 +10,11 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.QueryPublisherAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; +import com.studiomediatech.queryresponse.ui.app.telemetry.NodeRepository; import com.studiomediatech.queryresponse.ui.app.telemetry.TelemetryService; +import com.studiomediatech.queryresponse.ui.infra.repo.InMemoryNodeRepository; /** * Configuration for components of the application, with adapters for the platform. @@ -33,7 +35,26 @@ EventEmitterAdapter eventEmitter(TaskScheduler scheduler, ApplicationEventPublis } @Bean - public TelemetryService telemetryService(Optional optionalWebSocketApiAdapter) { - return new TelemetryService(optionalWebSocketApiAdapter.orElse(WebSocketApiAdapter.empty())); + @ConditionalOnMissingBean + WebSocketApiAdapter emptyWebSocketApiAdapter() { + return WebSocketApiAdapter.empty(); + } + + @Bean + @ConditionalOnMissingBean + NodeRepository inMemoryNodeRepository() { + return new InMemoryNodeRepository(); + } + + @Bean + @ConditionalOnMissingBean + QueryPublisherAdapter emptyQueryPublisherAdapter() { + return QueryPublisherAdapter.empty(); + } + + @Bean + public TelemetryService telemetryService(WebSocketApiAdapter webSocketApiAdapter, NodeRepository nodeRepository, + QueryPublisherAdapter queryPublisherAdapter) { + return new TelemetryService(webSocketApiAdapter, nodeRepository, queryPublisherAdapter); } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java new file mode 100644 index 00000000..ef681ced --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java @@ -0,0 +1,24 @@ +package com.studiomediatech.queryresponse.ui.infra.adapter; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import com.studiomediatech.queryresponse.ui.app.telemetry.TelemetryService; +import com.studiomediatech.queryresponse.util.Logging; + +@Component +public class TelemetryServiceAdapter implements Logging { + + private final TelemetryService service; + + public TelemetryServiceAdapter(TelemetryService service) { + this.service = service; + } + + @Scheduled(fixedDelayString = "PT3S") + public void aFewSecondsHasPassed() { + log().info("TIC TOC!"); + service.publishNodes(); + } + +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/repo/InMemoryNodeRepository.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/repo/InMemoryNodeRepository.java new file mode 100644 index 00000000..41ce606a --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/repo/InMemoryNodeRepository.java @@ -0,0 +1,31 @@ +package com.studiomediatech.queryresponse.ui.infra.repo; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import com.studiomediatech.queryresponse.ui.app.telemetry.Node; +import com.studiomediatech.queryresponse.ui.app.telemetry.NodeRepository; + +public class InMemoryNodeRepository implements NodeRepository { + + private final Map nodes = new ConcurrentHashMap<>(); + + @Override + public Optional findOneByUUID(UUID uuid) { + return Optional.ofNullable(nodes.get(uuid)); + } + + @Override + public void save(Node node) { + Node copy = new Node(node); + nodes.put(copy.getUUID(), copy); + } + + @Override + public Collection findAll() { + return nodes.values().stream().sorted(Node.SORT).toList(); + } +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java new file mode 100644 index 00000000..f3534f90 --- /dev/null +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java @@ -0,0 +1,78 @@ +package com.studiomediatech.queryresponse.ui.messaging; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import com.studiomediatech.events.QueryRecordedEvent; +import com.studiomediatech.queryresponse.QueryBuilder; +import com.studiomediatech.queryresponse.ui.app.adapter.QueryPublisherAdapter; +import com.studiomediatech.queryresponse.util.Logging; + +@Component +public class QueryPublisherPort implements Logging, QueryPublisherAdapter { + + private static final int DEFAULT_QUERY_TIMEOUT = 1500; + + private final QueryBuilder queryBuilder; + + public QueryPublisherPort(QueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + } + + @Override + public Map query(String q, int timeout, int limit) { + + List defaults = List.of("No responses"); + + final Collection responses; + + long start = System.nanoTime(); + + if (q.contains(" ")) { + responses = queryParsed(q, defaults); + } else { + responses = queryStrict(q, timeout, limit, defaults); + } + + return Map.of("response", responses, "duration", Duration.ofNanos(System.nanoTime() - start)); + } + + private Collection queryParsed(String q, List defaults) { + + var query = QueryRecordedEvent.valueOf(q, "none"); + + query.getQuery(); + query.getTimeout(); + + Optional maybe = query.getLimit(); + + if (maybe.isPresent()) { + + return queryBuilder.queryFor(query.getQuery(), Object.class).waitingFor(query.getTimeout()) + .takingAtMost(maybe.get()).orDefaults(defaults); + + } + + return queryBuilder.queryFor(query.getQuery(), Object.class).waitingFor(query.getTimeout()) + .orDefaults(defaults); + + } + + private Collection queryStrict(String q, int timeout, int limit, List defaults) { + long queryTimeout = timeout > 0 ? timeout : DEFAULT_QUERY_TIMEOUT; + + if (limit > 0) { + return queryBuilder.queryFor(q, Object.class).waitingFor(queryTimeout).takingAtMost(limit) + .orDefaults(defaults); + } + + return queryBuilder.queryFor(q, Object.class).waitingFor(queryTimeout).orDefaults(defaults); + + } + +} diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java index d5c3d7d0..ae2505e5 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/Stat.java @@ -1,5 +1,7 @@ package com.studiomediatech.queryresponse.ui.messaging; +import java.util.function.Consumer; + public record Stat(String key, Object value, Long timestamp, String uuid) { public static final String MIN_LATENCY = "min_latency"; @@ -12,4 +14,10 @@ public record Stat(String key, Object value, Long timestamp, String uuid) { public static final String THROUGHPUT_RESPONSES = "throughput_responses"; public static final String AVG_THROUGHPUT = "avg_throughput"; + @SuppressWarnings("unchecked") + public void whenKey(String key, Consumer target) { + if (this.key.equals(key)) { + target.accept((T) value); + } + } } \ No newline at end of file From d028c7d375b79bddeea12b28083b7d696397dac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sat, 15 Apr 2023 11:54:53 +0200 Subject: [PATCH 11/19] Better names --- ...pter.java => TelemetryHandlerAdapter.java} | 6 ++-- .../queryresponse/ui/app/telemetry/Node.java | 33 +++++++------------ .../ui/app/telemetry/TelemetryService.java | 5 +-- ...erPort.java => TelemetryConsumerPort.java} | 24 +++++++------- 4 files changed, 29 insertions(+), 39 deletions(-) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/{MessageHandlerAdapter.java => TelemetryHandlerAdapter.java} (72%) rename ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/{MessageConsumerPort.java => TelemetryConsumerPort.java} (55%) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/MessageHandlerAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java similarity index 72% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/MessageHandlerAdapter.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java index 2ebcd0c9..b4d81f99 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/MessageHandlerAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java @@ -6,10 +6,10 @@ /** * Declares the capabilities of the incoming side for statistics to aggregate. */ -public interface MessageHandlerAdapter extends Logging { +public interface TelemetryHandlerAdapter extends Logging { - static MessageHandlerAdapter empty() { - return new MessageHandlerAdapter() { + static TelemetryHandlerAdapter empty() { + return new TelemetryHandlerAdapter() { // OK }; } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java index d6477c6a..94c89189 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java @@ -31,15 +31,6 @@ public static Node from(UUID uuid) { return new Node(uuid); } - @Override - public String toString() { - return "Node [uuid=%s]".formatted(uuid); - } - - public UUID getUUID() { - return uuid; - } - public Node update(Node other) { Assert.isTrue(other.uuid.equals(this.uuid), "Must be same node."); @@ -52,7 +43,16 @@ public Node update(Node other) { return this; } - public String getName() { + @Override + public String toString() { + return "Node [uuid=%s]".formatted(uuid); + } + + public UUID getUUID() { + return uuid; + } + + private String getName() { return name; } @@ -60,27 +60,16 @@ public void setName(String name) { this.name = name; } - public String getPid() { - return pid; - } - public void setPid(String pid) { this.pid = pid; } - public String getHost() { - return host; - } - public void setHost(String host) { this.host = host; } - public String getUptime() { - return uptime; - } - public void setUptime(String uptime) { this.uptime = uptime; } + } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java index 5f6433ca..ffa508f5 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java @@ -6,7 +6,7 @@ import java.util.Optional; import java.util.UUID; -import com.studiomediatech.queryresponse.ui.app.adapter.MessageHandlerAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.TelemetryHandlerAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.QueryPublisherAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; @@ -14,7 +14,7 @@ import com.studiomediatech.queryresponse.ui.messaging.Stats; import com.studiomediatech.queryresponse.util.Logging; -public class TelemetryService implements Logging, MessageHandlerAdapter, RestApiAdapter { +public class TelemetryService implements Logging, TelemetryHandlerAdapter, RestApiAdapter { private final WebSocketApiAdapter webSocketApiAdapter; private final NodeRepository nodeRepository; @@ -39,6 +39,7 @@ public Map nodes() { @Override public void handleConsumed(Stats stats) { + log().info("Consumed stats with {} elements", stats.elements().size()); Collection nodes = parseToNodesCollection(stats); diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java similarity index 55% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java index 855837b4..670ab436 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessageConsumerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java @@ -9,40 +9,40 @@ import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; -import com.studiomediatech.queryresponse.ui.app.adapter.MessageHandlerAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.TelemetryHandlerAdapter; import com.studiomediatech.queryresponse.util.Logging; /** - * Consumes Query/Response messages from the internal topics, and directly delegates for handling via the abstract - * adapter. + * Consumes Query/Response telemetry messages from the internal topics, and directly delegates for handling via the + * abstract adapter. */ @Component -class MessageConsumerPort implements Logging { +class TelemetryConsumerPort implements Logging { private static final ObjectMapper MAPPER = new ObjectMapper(); /** * {@link AcknowledgeMode#NONE} */ - private static final String ACK_MODE = "NONE"; - private static final String CONSUMERS_MIN = "3"; - private static final String CONSUMERS_MAX = "11"; + private static final String NONE = "NONE"; + private static final String CONSUMERS_MIN = "1"; + private static final String CONSUMERS_MAX = "7"; - private final MessageHandlerAdapter adapter; + private final TelemetryHandlerAdapter adapter; - public MessageConsumerPort(Optional maybe) { - this.adapter = maybe.orElse(MessageHandlerAdapter.empty()); + public TelemetryConsumerPort(Optional maybe) { + this.adapter = maybe.orElse(TelemetryHandlerAdapter.empty()); } @RabbitListener(// queues = "#{@" + MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN + "}", // - ackMode = ACK_MODE, concurrency = CONSUMERS_MIN + "-" + CONSUMERS_MAX) + ackMode = NONE, concurrency = CONSUMERS_MIN + "-" + CONSUMERS_MAX) void onQueryResponseStats(Message message) { try { Stats stats = MAPPER.readValue(message.getBody(), Stats.class); adapter.handleConsumed(stats); } catch (RuntimeException | IOException ex) { - log().error("Failed to consumed stats", ex); + log().error("Failed to consumed telemetry message", ex); } } } From f0591bc6a6a85495d15b8f75de9245963d1aba9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sat, 15 Apr 2023 12:09:20 +0200 Subject: [PATCH 12/19] Enable web sockets --- .../queryresponse/ui/ConfigureApp.java | 33 ------------------- .../WebSocketConfig.java} | 10 ++---- 2 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java rename ui/src/main/java/com/studiomediatech/queryresponse/ui/{ConfigureWebSocket.java => api/WebSocketConfig.java} (71%) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java deleted file mode 100644 index 55efe800..00000000 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureApp.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.studiomediatech.queryresponse.ui; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.core.annotation.Order; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; - -import com.studiomediatech.QueryPublisher; -import com.studiomediatech.queryresponse.QueryBuilder; -import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; -import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; -import com.studiomediatech.queryresponse.ui.infra.AsyncEventEmitter; - -@Order(10) -@Configuration -class ConfigureApp { - - @Bean - WebSocketApiHandlerPort handler(EventEmitterAdapter emitter) { - - return new WebSocketApiHandlerPort(emitter); - } - - @Bean - QueryPublisher querier(WebSocketApiHandlerPort handler, QueryBuilder queryBuilder) { - - return new QueryPublisher(handler, queryBuilder); - } - -} \ No newline at end of file diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketConfig.java similarity index 71% rename from ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java rename to ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketConfig.java index c7ce4155..b9a459a7 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/ConfigureWebSocket.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketConfig.java @@ -1,26 +1,22 @@ -package com.studiomediatech.queryresponse.ui; +package com.studiomediatech.queryresponse.ui.api; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; - @Order(100) @Configuration -class ConfigureWebSocket implements WebSocketConfigurer { +class WebSocketConfig implements WebSocketConfigurer { private final WebSocketApiHandlerPort webSocketHandler; - public ConfigureWebSocket(WebSocketApiHandlerPort webSocketHandler) { - + public WebSocketConfig(WebSocketApiHandlerPort webSocketHandler) { this.webSocketHandler = webSocketHandler; } @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - // TODO: DO NOT ALLOW ORIGINS * !!! registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*"); } From 31049c704e346df9f8664d89d262bc917ad68da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Tue, 18 Apr 2023 06:22:51 +0200 Subject: [PATCH 13/19] Rephrasing towards telemetry --- docs/events.adoc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/events.adoc b/docs/events.adoc index 231dafc4..d624ab31 100644 --- a/docs/events.adoc +++ b/docs/events.adoc @@ -1,13 +1,14 @@ = Query/Response Events -== Metrics or "Stats" +== Telemetry -Participating nodes in a Query/Response topology will broadcast events of -structured statistics for the UI and monitoring applications to consume. The +Participating nodes in a Query/Response network topology will broadcast events +with structured telemetry for the UI and monitoring applications to consume. The common data-shape encapsulates a way to communicate measurements, events and -node-identifying and describing meta-data. +node-identifying and describing meta data. -The entry structure and format is encoded as a JSON UTF-8 encoded string. +The entry structure and format is encoded as a JSON UTF-8 encoded string +payload. ```json { @@ -33,7 +34,7 @@ means that in practice there are operational metrics which may _come and go_ as well as logical statistics which may pertain to the topology or cluster of nodes. -=== Table of published statistics +=== Table of published telemetry [cols="2,2,1,1,3"] |=== From 3c671636349eede3dbcfd81209626e3f5565b0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Tue, 18 Apr 2023 06:23:03 +0200 Subject: [PATCH 14/19] Enable logging --- ui-frontend/src/ws.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-frontend/src/ws.js b/ui-frontend/src/ws.js index 45946cfe..16ff6ae1 100644 --- a/ui-frontend/src/ws.js +++ b/ui-frontend/src/ws.js @@ -40,8 +40,8 @@ const connectSocket = () => { }; sock.onmessage = (msg) => { - // console.log("RECEIVED EVENT", msg); - // console.log("RECEIVED EVENT PAYLOAD", JSON.stringify(JSON.parse(msg.data))); + //console.log("RECEIVED EVENT", msg); + console.log("RECEIVED EVENT PAYLOAD", JSON.stringify(JSON.parse(msg.data))); listeners.forEach((listener) => listener(msg)); }; } catch (err) { From 460767bd3566eae09c0c68f37fe5237189477f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Tue, 18 Apr 2023 06:23:28 +0200 Subject: [PATCH 15/19] Interrim --- .../queryresponse/ui/app/telemetry/Node.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java index 94c89189..91a72615 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java @@ -5,7 +5,9 @@ import org.springframework.util.Assert; -public class Node { +import com.studiomediatech.queryresponse.util.Logging; + +public class Node implements Logging { public static final Comparator SORT = Comparator.comparing(Node::getName).thenComparing(Node::getUUID); @@ -35,6 +37,8 @@ public Node update(Node other) { Assert.isTrue(other.uuid.equals(this.uuid), "Must be same node."); + log().info("Updating {} with {}", this, other); + this.host = other.host; this.name = other.name; this.uptime = other.uptime; @@ -48,11 +52,7 @@ public String toString() { return "Node [uuid=%s]".formatted(uuid); } - public UUID getUUID() { - return uuid; - } - - private String getName() { + public String getName() { return name; } @@ -60,16 +60,32 @@ public void setName(String name) { this.name = name; } + public String getPid() { + return pid; + } + public void setPid(String pid) { this.pid = pid; } + public String getHost() { + return host; + } + public void setHost(String host) { this.host = host; } + public String getUptime() { + return uptime; + } + public void setUptime(String uptime) { this.uptime = uptime; } + public UUID getUUID() { + return uuid; + } + } From 46eb233904045af07e2c7772e67cc7373b3a2b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 30 Apr 2023 10:42:19 +0200 Subject: [PATCH 16/19] Add binding for modern routing key --- .../ui/messaging/MessagingConfig.java | 50 +++++++++++-------- .../ui/messaging/MessagingConfigIT.java | 15 +++++- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java index e8477114..6b77c80b 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java @@ -14,31 +14,39 @@ import com.studiomediatech.queryresponse.QueryResponseTopicExchange; /** - * Configuration for ports that enable messaging, specifically AMQP including Query/Response. + * Configuration for ports that enable messaging, specifically AMQP including + * Query/Response. */ @Configuration @EnableQueryResponse @ComponentScan(basePackageClasses = MessagingConfig.class) public class MessagingConfig { - static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; - static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; - static final String QUERY_RESPONSE_STATS_QUEUE_BINDING_BEAN = "queryResponseQueriesQueueBinding"; - static final String QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY = "query-response/internal/stats"; - - @Bean - ConnectionNameStrategy connectionNameStrategy(Environment env) { - return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); - } - - @Bean(QUERY_RESPONSE_STATS_QUEUE_BEAN) - Queue queryResponseStatsQueue() { - return new AnonymousQueue(); - } - - @Bean(QUERY_RESPONSE_STATS_QUEUE_BINDING_BEAN) - Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { - return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) - .with(QUERY_RESPONSE_INTERNAL_STATS_ROUTING_KEY); - } + static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; + static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; + + protected static final String QUERY_RESPONSE_STATS_BINDING = "queryResponseStatsBinding"; + protected static final String QUERY_RESPONSE_TELEMETRY_BINDING = "queryResponseTelemetryBinding"; + + @Bean + ConnectionNameStrategy connectionNameStrategy(Environment env) { + return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); + } + + @Bean(QUERY_RESPONSE_STATS_QUEUE_BEAN) + Queue queryResponseStatsQueue() { + return new AnonymousQueue(); + } + + @Bean(QUERY_RESPONSE_STATS_BINDING) + Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { + return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) + .with("query-response/internal/stats"); + } + + @Bean(QUERY_RESPONSE_TELEMETRY_BINDING) + Binding queryResponseTelemetryQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { + return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) + .with("query-response/internal/telemetry"); + } } diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java index 29c726f3..2ca72a2f 100644 --- a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java @@ -21,10 +21,23 @@ void ensure_has_telemetry_queue_with_binding() { Queue queue = (Queue) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN); assertThat(queue).isNotNull(); - Binding binding = (Binding) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BINDING_BEAN); + Binding binding = (Binding) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_BINDING); assertThat(binding).isNotNull(); assertThat(binding.getDestination()).isEqualTo(queue.getName()); assertThat(binding.getRoutingKey()).isEqualTo("query-response/internal/stats"); assertThat(binding.getExchange()).isEqualTo("query-response"); } + + @Test + void ensure_has_telemetry_queue_with_modernised_binding() throws Exception { + + Queue queue = (Queue) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN); + assertThat(queue).isNotNull(); + + Binding binding = (Binding) ctx.getBean(MessagingConfig.QUERY_RESPONSE_TELEMETRY_BINDING); + assertThat(binding).isNotNull(); + assertThat(binding.getDestination()).isEqualTo(queue.getName()); + assertThat(binding.getRoutingKey()).isEqualTo("query-response/internal/telemetry"); + assertThat(binding.getExchange()).isEqualTo("query-response"); + } } From 71c94fa0e1dc267aee047d14579532d47df3b97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 7 Jan 2024 13:16:18 +0100 Subject: [PATCH 17/19] Interrim --- .../ui/messaging/MessagingConfig.java | 57 +++++++++---------- .../ui/messaging/MessagingConfigIT.java | 8 +-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java index 6b77c80b..e3c33832 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfig.java @@ -14,39 +14,38 @@ import com.studiomediatech.queryresponse.QueryResponseTopicExchange; /** - * Configuration for ports that enable messaging, specifically AMQP including - * Query/Response. + * Configuration for ports that enable messaging, specifically AMQP including Query/Response. */ @Configuration @EnableQueryResponse @ComponentScan(basePackageClasses = MessagingConfig.class) public class MessagingConfig { - static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; - static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; - - protected static final String QUERY_RESPONSE_STATS_BINDING = "queryResponseStatsBinding"; - protected static final String QUERY_RESPONSE_TELEMETRY_BINDING = "queryResponseTelemetryBinding"; - - @Bean - ConnectionNameStrategy connectionNameStrategy(Environment env) { - return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); - } - - @Bean(QUERY_RESPONSE_STATS_QUEUE_BEAN) - Queue queryResponseStatsQueue() { - return new AnonymousQueue(); - } - - @Bean(QUERY_RESPONSE_STATS_BINDING) - Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { - return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) - .with("query-response/internal/stats"); - } - - @Bean(QUERY_RESPONSE_TELEMETRY_BINDING) - Binding queryResponseTelemetryQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { - return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) - .with("query-response/internal/telemetry"); - } + static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsQueue"; + static final String QUERY_RESPONSE_QUERIES_QUEUE_BEAN = "queryResponseQueriesQueue"; + + protected static final String QUERY_RESPONSE_STATS_BINDING = "queryResponseStatsBinding"; + protected static final String QUERY_RESPONSE_TELEMETRY_BINDING = "queryResponseTelemetryBinding"; + + @Bean + ConnectionNameStrategy connectionNameStrategy(Environment env) { + return connectionFactory -> env.getProperty("spring.application.name", "query-response-ui"); + } + + @Bean(QUERY_RESPONSE_STATS_QUEUE_BEAN) + Queue queryResponseStatsQueue() { + return new AnonymousQueue(); + } + + @Bean(QUERY_RESPONSE_STATS_BINDING) + Binding queryResponseStatsQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { + return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) + .with("query-response/internal/stats"); + } + + @Bean(QUERY_RESPONSE_TELEMETRY_BINDING) + Binding queryResponseTelemetryQueueBinding(QueryResponseTopicExchange queryResponseTopicExchange) { + return BindingBuilder.bind(queryResponseStatsQueue()).to(queryResponseTopicExchange) + .with("query-response/internal/telemetry"); + } } diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java index 2ca72a2f..4cb35d42 100644 --- a/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/messaging/MessagingConfigIT.java @@ -27,10 +27,10 @@ void ensure_has_telemetry_queue_with_binding() { assertThat(binding.getRoutingKey()).isEqualTo("query-response/internal/stats"); assertThat(binding.getExchange()).isEqualTo("query-response"); } - + @Test - void ensure_has_telemetry_queue_with_modernised_binding() throws Exception { - + void ensure_has_telemetry_queue_with_modernised_binding() throws Exception { + Queue queue = (Queue) ctx.getBean(MessagingConfig.QUERY_RESPONSE_STATS_QUEUE_BEAN); assertThat(queue).isNotNull(); @@ -39,5 +39,5 @@ void ensure_has_telemetry_queue_with_modernised_binding() throws Exception { assertThat(binding.getDestination()).isEqualTo(queue.getName()); assertThat(binding.getRoutingKey()).isEqualTo("query-response/internal/telemetry"); assertThat(binding.getExchange()).isEqualTo("query-response"); - } + } } From 6719e1e9f24c8d40ac8f384d9d51b19ba26a84f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 25 May 2025 10:16:16 +0200 Subject: [PATCH 18/19] Rebased on main --- .../java/com/studiomediatech/QueryPublisher.java | 10 +++++++++- .../queryresponse/ui/api/RestApiControllerPort.java | 12 ++++++------ .../ui/api/WebSocketApiHandlerPort.java | 6 ++---- .../queryresponse/ui/app/QueryResponseUIApp.java | 2 ++ .../ui/app/adapter/TelemetryHandlerAdapter.java | 6 +++--- .../ui/app/adapter/WebSocketApiAdapter.java | 6 +++--- .../queryresponse/ui/app/telemetry/Node.java | 6 +++--- .../ui/app/telemetry/TelemetryService.java | 8 ++++---- .../ui/infra/adapter/TelemetryServiceAdapter.java | 6 +++--- .../ui/messaging/QueryPublisherPort.java | 4 ++-- .../ui/messaging/TelemetryConsumerPort.java | 6 +++--- .../ui/api/RestApiControllerPortTest.java | 2 +- 12 files changed, 41 insertions(+), 33 deletions(-) diff --git a/ui/src/main/java/com/studiomediatech/QueryPublisher.java b/ui/src/main/java/com/studiomediatech/QueryPublisher.java index 0a70a366..43720d36 100644 --- a/ui/src/main/java/com/studiomediatech/QueryPublisher.java +++ b/ui/src/main/java/com/studiomediatech/QueryPublisher.java @@ -1,5 +1,6 @@ package com.studiomediatech; +import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -16,15 +17,20 @@ import java.util.function.ToLongFunction; import java.util.stream.Collectors; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.context.event.EventListener; import org.springframework.util.StringUtils; +import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.QueryBuilder; import com.studiomediatech.queryresponse.ui.api.WebSocketApiHandlerPort; +import com.studiomediatech.queryresponse.ui.app.QueryResponseUIApp; import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; import com.studiomediatech.queryresponse.ui.messaging.Stat; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.ui.messaging.Stats; +import com.studiomediatech.queryresponse.util.Loggable; public class QueryPublisher implements Loggable, RestApiAdapter { @@ -32,6 +38,8 @@ public class QueryPublisher implements Loggable, RestApiAdapter { private static final int MAX_SIZE = 2584; private static final int SLIDING_WINDOW = 40; + private static final ObjectMapper MAPPER = new ObjectMapper(); + static ToLongFunction statToLong = s -> ((Number) s.value()).longValue(); private List queries = new LinkedList<>(); diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java index f57be17f..47a63b0b 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPort.java @@ -36,12 +36,6 @@ public Map nodes() { return adapter.nodes(); } - @GetMapping("/api/v1") - public Map v1() { - return Response.from(Map.of("version", "v1", "now", Instant.now())).withLinks("query-response", - "/api/v1?q=query"); - } - @GetMapping(path = "/api/v1", params = "q") public Map query(String q, // NOSONAR @RequestParam(name = "timeout", required = false, defaultValue = "0") int timeout, @@ -55,6 +49,12 @@ public Map query(String q, // NOSONAR return adapter.query(q, normalizedTimeout, normalizedLimit); } + @GetMapping(path = "/api/v1", params = "!q") + public Map v1() { + return Response.from(Map.of("version", "v1", "now", Instant.now())).withLinks("query-response", + "/api/v1?q=query"); + } + protected interface Response { public static ResponseBuilder from(Map map) { return new ResponseBuilder(map); diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java index afd12ff7..ff419c84 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/api/WebSocketApiHandlerPort.java @@ -19,15 +19,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.events.QueryRecordedEvent; -import com.studiomediatech.queryresponse.stats.Stat; -import com.studiomediatech.queryresponse.util.Loggable; import com.studiomediatech.queryresponse.ui.app.adapter.EventEmitterAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; import com.studiomediatech.queryresponse.ui.messaging.Stat; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; @Component -public class WebSocketApiHandlerPort extends TextWebSocketHandler implements WebSocketApiAdapter, Logging { +public class WebSocketApiHandlerPort extends TextWebSocketHandler implements WebSocketApiAdapter, Loggable { private static final int SEND_TIME_LIMIT = 6 * 1000; private static final int SEND_BUFFER_SIZE_LIMIT = 512 * 1024; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java index 45626fe8..d1067c3e 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/QueryResponseUIApp.java @@ -14,6 +14,8 @@ @SpringBootApplication public class QueryResponseUIApp { + public static final String QUERY_RESPONSE_STATS_QUEUE_BEAN = "queryResponseStatsBean"; + public static void main(String[] args) { SpringApplication.run(QueryResponseUIApp.class); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java index b4d81f99..4ee13d6d 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/TelemetryHandlerAdapter.java @@ -1,12 +1,12 @@ package com.studiomediatech.queryresponse.ui.app.adapter; import com.studiomediatech.queryresponse.ui.messaging.Stats; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; /** * Declares the capabilities of the incoming side for statistics to aggregate. */ -public interface TelemetryHandlerAdapter extends Logging { +public interface TelemetryHandlerAdapter extends Loggable { static TelemetryHandlerAdapter empty() { return new TelemetryHandlerAdapter() { @@ -15,6 +15,6 @@ static TelemetryHandlerAdapter empty() { } default void handleConsumed(Stats stats) { - log().warn("NOT YET HANDLING {}", stats); + logger().warn("NOT YET HANDLING {}", stats); } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java index f5f0c96d..63c72091 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/adapter/WebSocketApiAdapter.java @@ -3,9 +3,9 @@ import java.util.Collection; import com.studiomediatech.queryresponse.ui.app.telemetry.Node; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; -public interface WebSocketApiAdapter extends Logging { +public interface WebSocketApiAdapter extends Loggable { static WebSocketApiAdapter empty() { return new WebSocketApiAdapter() { @@ -14,6 +14,6 @@ static WebSocketApiAdapter empty() { } default void publishNodes(Collection nodes) { - log().warn("NOT PUBLISHING NODES! {}", nodes); + logger().warn("NOT PUBLISHING NODES! {}", nodes); } } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java index 91a72615..36abcd3b 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/Node.java @@ -5,9 +5,9 @@ import org.springframework.util.Assert; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; -public class Node implements Logging { +public class Node implements Loggable { public static final Comparator SORT = Comparator.comparing(Node::getName).thenComparing(Node::getUUID); @@ -37,7 +37,7 @@ public Node update(Node other) { Assert.isTrue(other.uuid.equals(this.uuid), "Must be same node."); - log().info("Updating {} with {}", this, other); + logger().info("Updating {} with {}", this, other); this.host = other.host; this.name = other.name; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java index ffa508f5..e2fd1155 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/app/telemetry/TelemetryService.java @@ -6,15 +6,15 @@ import java.util.Optional; import java.util.UUID; -import com.studiomediatech.queryresponse.ui.app.adapter.TelemetryHandlerAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.QueryPublisherAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.RestApiAdapter; +import com.studiomediatech.queryresponse.ui.app.adapter.TelemetryHandlerAdapter; import com.studiomediatech.queryresponse.ui.app.adapter.WebSocketApiAdapter; import com.studiomediatech.queryresponse.ui.messaging.Stat; import com.studiomediatech.queryresponse.ui.messaging.Stats; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; -public class TelemetryService implements Logging, TelemetryHandlerAdapter, RestApiAdapter { +public class TelemetryService implements Loggable, TelemetryHandlerAdapter, RestApiAdapter { private final WebSocketApiAdapter webSocketApiAdapter; private final NodeRepository nodeRepository; @@ -40,7 +40,7 @@ public Map nodes() { @Override public void handleConsumed(Stats stats) { - log().info("Consumed stats with {} elements", stats.elements().size()); + logger().info("Consumed stats with {} elements", stats.elements().size()); Collection nodes = parseToNodesCollection(stats); updateNodes(nodes); diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java index ef681ced..50958189 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/infra/adapter/TelemetryServiceAdapter.java @@ -4,10 +4,10 @@ import org.springframework.stereotype.Component; import com.studiomediatech.queryresponse.ui.app.telemetry.TelemetryService; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; @Component -public class TelemetryServiceAdapter implements Logging { +public class TelemetryServiceAdapter implements Loggable { private final TelemetryService service; @@ -17,7 +17,7 @@ public TelemetryServiceAdapter(TelemetryService service) { @Scheduled(fixedDelayString = "PT3S") public void aFewSecondsHasPassed() { - log().info("TIC TOC!"); + logger().info("TIC TOC!"); service.publishNodes(); } diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java index f3534f90..f4a8d9a0 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/QueryPublisherPort.java @@ -11,10 +11,10 @@ import com.studiomediatech.events.QueryRecordedEvent; import com.studiomediatech.queryresponse.QueryBuilder; import com.studiomediatech.queryresponse.ui.app.adapter.QueryPublisherAdapter; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; @Component -public class QueryPublisherPort implements Logging, QueryPublisherAdapter { +public class QueryPublisherPort implements Loggable, QueryPublisherAdapter { private static final int DEFAULT_QUERY_TIMEOUT = 1500; diff --git a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java index 670ab436..85bbe92c 100644 --- a/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java +++ b/ui/src/main/java/com/studiomediatech/queryresponse/ui/messaging/TelemetryConsumerPort.java @@ -10,14 +10,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.studiomediatech.queryresponse.ui.app.adapter.TelemetryHandlerAdapter; -import com.studiomediatech.queryresponse.util.Logging; +import com.studiomediatech.queryresponse.util.Loggable; /** * Consumes Query/Response telemetry messages from the internal topics, and directly delegates for handling via the * abstract adapter. */ @Component -class TelemetryConsumerPort implements Logging { +class TelemetryConsumerPort implements Loggable { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -42,7 +42,7 @@ void onQueryResponseStats(Message message) { Stats stats = MAPPER.readValue(message.getBody(), Stats.class); adapter.handleConsumed(stats); } catch (RuntimeException | IOException ex) { - log().error("Failed to consumed telemetry message", ex); + logger().error("Failed to consumed telemetry message", ex); } } } diff --git a/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java b/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java index 539412b9..9013e2fc 100644 --- a/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java +++ b/ui/src/test/java/com/studiomediatech/queryresponse/ui/api/RestApiControllerPortTest.java @@ -24,7 +24,7 @@ void setup() { @Test void ensureHandlesQueryRequest() throws Exception { - mockMvc.perform(get("/api/v1/").param("q", "hello")).andExpect(status().isOk()); + mockMvc.perform(get("/api/v1").param("q", "hello")).andExpect(status().isOk()); } @Test From b7eedd02d125a0ca22355bc5753394755d52051f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20T=C3=B6rnstr=C3=B6m?= Date: Sun, 25 May 2025 10:35:07 +0200 Subject: [PATCH 19/19] Add ui demo target --- Makefile | 15 ++++++++++++--- ui/Makefile | 8 +++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index bc3c5d78..8729edfc 100644 --- a/Makefile +++ b/Makefile @@ -43,11 +43,20 @@ down: repo-clean: rm -rf ~/.m2/repository/com/studiomediatech/query-response-spring-amqp -.PHONY: demo run-demo +## +## Demo targets +## +## These targets are used to run the demo application and its components. +## They depend on the `install` target to ensure that the application is built +## and ready to run. The up target starts the necessary Docker containers, +## and the run-demo target orchestrates the execution of the demo components. +## Please note that the parallel execution of the demo components is what makes +## it possible to start the individual components in the background. +## +.PHONY: demo run-demo run-demo run-query run-response run-ui demo: install up ${MAKE} -j3 run-demo -.PHONY: run-demo run-query run-response run-ui run-demo: run-ui run-query run-response run-query: @@ -59,4 +68,4 @@ run-response: ${MAKE} -C examples/responding/ run-ui: - ${MAKE} -C ui/ + ${MAKE} demo -C ui/ diff --git a/ui/Makefile b/ui/Makefile index cc0f16a2..69781a90 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -1,7 +1,7 @@ JAVA_HOME=$(shell unset JAVA_HOME; /usr/libexec/java_home -v 21) MVN := ../mvnw -.PHONY: run build test verify v clean tidy +.PHONY: run demo build test verify v clean tidy run: ${MVN} spring-boot:run @@ -10,14 +10,16 @@ test verify v: ${MVN} clean verify build: - ${MVN} clean prepare-package -DskipTests + ${MVN} clean prepare-package mkdir -p target/classes/static @$(MAKE) -C ../ui-frontend build cp -r ../ui-frontend/dist/* target/classes/static/ - ${MVN} package -DskipTests + ${MVN} package clean: rm -rf target/ tidy: ${MVN} formatter:format + +demo: build run \ No newline at end of file