From cd2dbb72d7aca165d88cff5000f4175e2532e98b Mon Sep 17 00:00:00 2001 From: YongHwan Kwon <67625677+hwan33@users.noreply.github.com> Date: Wed, 27 Dec 2023 15:06:55 +0900 Subject: [PATCH] feat: Add Microservice Pattern, Log aggregation (#2690) (#2719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add Microservice Pattern, Log aggregation. Related: #2690 * docs: Add javaDoc for public methods. Related: #2690 --------- Co-authored-by: Ilkka Seppälä --- log-aggregation/README.md | 59 +++++++++ log-aggregation/etc/log-aggregation.png | Bin 0 -> 69263 bytes log-aggregation/etc/log-aggregation.puml | 51 ++++++++ log-aggregation/pom.xml | 32 +++++ .../java/com/iluwatar/logaggregation/App.java | 53 ++++++++ .../logaggregation/CentralLogStore.java | 63 +++++++++ .../logaggregation/LogAggregator.java | 120 ++++++++++++++++++ .../com/iluwatar/logaggregation/LogEntry.java | 42 ++++++ .../com/iluwatar/logaggregation/LogLevel.java | 38 ++++++ .../iluwatar/logaggregation/LogProducer.java | 54 ++++++++ .../logaggregation/LogAggregatorTest.java | 80 ++++++++++++ pom.xml | 1 + 12 files changed, 593 insertions(+) create mode 100644 log-aggregation/README.md create mode 100644 log-aggregation/etc/log-aggregation.png create mode 100644 log-aggregation/etc/log-aggregation.puml create mode 100644 log-aggregation/pom.xml create mode 100644 log-aggregation/src/main/java/com/iluwatar/logaggregation/App.java create mode 100644 log-aggregation/src/main/java/com/iluwatar/logaggregation/CentralLogStore.java create mode 100644 log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java create mode 100644 log-aggregation/src/main/java/com/iluwatar/logaggregation/LogEntry.java create mode 100644 log-aggregation/src/main/java/com/iluwatar/logaggregation/LogLevel.java create mode 100644 log-aggregation/src/main/java/com/iluwatar/logaggregation/LogProducer.java create mode 100644 log-aggregation/src/test/java/com/iluwatar/logaggregation/LogAggregatorTest.java diff --git a/log-aggregation/README.md b/log-aggregation/README.md new file mode 100644 index 000000000000..f6d979517b9a --- /dev/null +++ b/log-aggregation/README.md @@ -0,0 +1,59 @@ +--- +title: Log aggregation +category: Architectural +language: en +tag: + - Microservices + - Extensibility +--- + +## Intent + +Centralize, streamline, and optimize the process of log management so that insights can be quickly +derived, problems can be swiftly identified and resolved, and the system's overall health can be +monitored efficiently. + +## Explanation + +Real-world example + +> AWS CloudWatch aggregates logs from various AWS services for monitoring and alerting. + + +In plain words + +> The primary goal is to consolidate logs from different sources, making them more accessible and +> actionable. Various tools and platforms, such as Elasticsearch-Logstash-Kibana (ELK) stack, +> Splunk, +> Graylog, and others, are employed in these real-world scenarios to facilitate log aggregation. + +Wikipedia says + +> You have applied the Microservice architecture pattern. The application consists of multiple +> services and service instances that are running on multiple machines. Requests often span multiple +> service instances. Each service instance generates writes information about what it is doing to a +> log file in a standardized format. The log file contains errors, warnings, information and debug +> messages. + + +## Class diagram + +![class diagram](./etc/log-aggregation.png) + +## Applicability + +1. Distributed Systems and Microservices + - In modern architectures where systems are split into smaller, independent microservices running across multiple servers or even data centers, aggregating logs from all these services is crucial for a holistic view of system health and activity. + +2. Troubleshooting and Debugging + - When system failures or unexpected behaviors occur, engineers need consolidated logs to trace and diagnose issues. Log aggregation makes this process efficient by collecting all relevant logs in one place. + +3. Security and Compliance Monitoring + - Many industries have regulatory requirements for log retention and analysis. Log aggregation helps in collecting, retaining, and analyzing logs for unauthorized access, potential breaches, and other security threats. + +4. Performance Monitoring + - Aggregated logs can be used to identify performance bottlenecks, slow database queries, or service endpoints experiencing high latencies. + +## Credits + +* [Pattern: Log aggregation](https://microservices.io/patterns/observability/application-logging.html) diff --git a/log-aggregation/etc/log-aggregation.png b/log-aggregation/etc/log-aggregation.png new file mode 100644 index 0000000000000000000000000000000000000000..4ce56be7064f52a216ce3d94152fa003f26bd3b3 GIT binary patch literal 69263 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 QF!9S$5^_fp#LnFKKjjnm)&Kwi literal 0 HcmV?d00001 diff --git a/log-aggregation/etc/log-aggregation.puml b/log-aggregation/etc/log-aggregation.puml new file mode 100644 index 000000000000..b35873e17ad7 --- /dev/null +++ b/log-aggregation/etc/log-aggregation.puml @@ -0,0 +1,51 @@ +@startuml + +package com.iluwatar.logaggregation { + + class App { + + main(args: String[]) {static} + } + + class CentralLogStore { + - logs: ConcurrentLinkedQueue + + storeLog(logEntry: LogEntry) + + displayLogs() + } + + class LogAggregator { + - BUFFER_THRESHOLD: int {static} + - centralLogStore: CentralLogStore + - buffer: ConcurrentLinkedQueue + - minLogLevel: LogLevel + - executorService: ExecutorService + - logCount: AtomicInteger + + collectLog(logEntry: LogEntry) + + stop() + } + + class LogEntry { + - serviceName: String + - level: LogLevel + - message: String + - timestamp: LocalDateTime + } + + enum LogLevel { + DEBUG + INFO + ERROR + } + + class LogProducer { + - serviceName: String + - aggregator: LogAggregator + + generateLog(level: LogLevel, message: String) + } +} + +LogProducer --> "-aggregator" LogAggregator +LogAggregator --> "-centralLogStore" CentralLogStore +LogAggregator --> "-buffer" LogEntry +CentralLogStore --> "-logs" LogEntry + +@enduml diff --git a/log-aggregation/pom.xml b/log-aggregation/pom.xml new file mode 100644 index 000000000000..886d712ff403 --- /dev/null +++ b/log-aggregation/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + log-aggregation + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-junit-jupiter + test + + + + 17 + 17 + UTF-8 + + + diff --git a/log-aggregation/src/main/java/com/iluwatar/logaggregation/App.java b/log-aggregation/src/main/java/com/iluwatar/logaggregation/App.java new file mode 100644 index 000000000000..ef9ca0a7ba87 --- /dev/null +++ b/log-aggregation/src/main/java/com/iluwatar/logaggregation/App.java @@ -0,0 +1,53 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +/** + * The main application class responsible for demonstrating the log aggregation mechanism. Creates + * services, generates logs, aggregates, and finally displays the logs. + */ +public class App { + + /** + * The entry point of the application. + * + * @param args Command line arguments. + * @throws InterruptedException If any thread has interrupted the current thread. + */ + public static void main(String[] args) throws InterruptedException { + final CentralLogStore centralLogStore = new CentralLogStore(); + final LogAggregator aggregator = new LogAggregator(centralLogStore, LogLevel.INFO); + + final LogProducer serviceA = new LogProducer("ServiceA", aggregator); + final LogProducer serviceB = new LogProducer("ServiceB", aggregator); + + serviceA.generateLog(LogLevel.INFO, "This is an INFO log from ServiceA"); + serviceB.generateLog(LogLevel.ERROR, "This is an ERROR log from ServiceB"); + serviceA.generateLog(LogLevel.DEBUG, "This is a DEBUG log from ServiceA"); + + aggregator.stop(); + centralLogStore.displayLogs(); + } +} diff --git a/log-aggregation/src/main/java/com/iluwatar/logaggregation/CentralLogStore.java b/log-aggregation/src/main/java/com/iluwatar/logaggregation/CentralLogStore.java new file mode 100644 index 000000000000..f7226638c98d --- /dev/null +++ b/log-aggregation/src/main/java/com/iluwatar/logaggregation/CentralLogStore.java @@ -0,0 +1,63 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +import java.util.concurrent.ConcurrentLinkedQueue; +import lombok.extern.slf4j.Slf4j; + +/** + * A centralized store for logs. It collects logs from various services and stores them. + * This class is thread-safe, ensuring that logs from different services are safely stored + * concurrently without data races. + */ +@Slf4j +public class CentralLogStore { + + private final ConcurrentLinkedQueue logs = new ConcurrentLinkedQueue<>(); + + /** + * Stores the given log entry into the central log store. + * + * @param logEntry The log entry to store. + */ + public void storeLog(LogEntry logEntry) { + if (logEntry == null) { + LOGGER.error("Received null log entry. Skipping."); + return; + } + logs.offer(logEntry); + } + + /** + * Displays all logs currently stored in the central log store. + */ + public void displayLogs() { + LOGGER.info("----- Centralized Logs -----"); + for (LogEntry logEntry : logs) { + LOGGER.info( + logEntry.getTimestamp() + " [" + logEntry.getLevel() + "] " + logEntry.getMessage()); + } + } +} diff --git a/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java new file mode 100644 index 000000000000..406ce144a03d --- /dev/null +++ b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java @@ -0,0 +1,120 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; + +/** + * Responsible for collecting and buffering logs from different services. + * Once the logs reach a certain threshold or after a certain time interval, + * they are flushed to the central log store. This class ensures logs are collected + * and processed asynchronously and efficiently, providing both an immediate collection + * and periodic flushing. + */ +@Slf4j +public class LogAggregator { + + private static final int BUFFER_THRESHOLD = 3; + private final CentralLogStore centralLogStore; + private final ConcurrentLinkedQueue buffer = new ConcurrentLinkedQueue<>(); + private final LogLevel minLogLevel; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final AtomicInteger logCount = new AtomicInteger(0); + + /** + * constructor of LogAggregator. + * + * @param centralLogStore central log store implement + * @param minLogLevel min log level to store log + */ + public LogAggregator(CentralLogStore centralLogStore, LogLevel minLogLevel) { + this.centralLogStore = centralLogStore; + this.minLogLevel = minLogLevel; + startBufferFlusher(); + } + + /** + * Collects a given log entry, and filters it by the defined log level. + * + * @param logEntry The log entry to collect. + */ + public void collectLog(LogEntry logEntry) { + if (logEntry.getLevel() == null || minLogLevel == null) { + LOGGER.warn("Log level or threshold level is null. Skipping."); + return; + } + + if (logEntry.getLevel().compareTo(minLogLevel) < 0) { + LOGGER.debug("Log level below threshold. Skipping."); + return; + } + + buffer.offer(logEntry); + + if (logCount.incrementAndGet() >= BUFFER_THRESHOLD) { + flushBuffer(); + } + } + + /** + * Stops the log aggregator service and flushes any remaining logs to + * the central log store. + * + * @throws InterruptedException If any thread has interrupted the current thread. + */ + public void stop() throws InterruptedException { + executorService.shutdownNow(); + if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) { + LOGGER.error("Log aggregator did not terminate."); + } + flushBuffer(); + } + + private void flushBuffer() { + LogEntry logEntry; + while ((logEntry = buffer.poll()) != null) { + centralLogStore.storeLog(logEntry); + logCount.decrementAndGet(); + } + } + + private void startBufferFlusher() { + executorService.execute(() -> { + while (!Thread.currentThread().isInterrupted()) { + try { + Thread.sleep(5000); // Flush every 5 seconds. + flushBuffer(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } +} diff --git a/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogEntry.java b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogEntry.java new file mode 100644 index 000000000000..bece65c64d21 --- /dev/null +++ b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogEntry.java @@ -0,0 +1,42 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents a single log entry, capturing essential details like the service name, + * log level, message, and the timestamp when the log was generated. + */ +@Data +@AllArgsConstructor +public class LogEntry { + private String serviceName; + private LogLevel level; + private String message; + private LocalDateTime timestamp; +} diff --git a/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogLevel.java b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogLevel.java new file mode 100644 index 000000000000..ed9258649189 --- /dev/null +++ b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogLevel.java @@ -0,0 +1,38 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +/** + * Enum representing different log levels. + * Defines the severity of a log message, helping in filtering and prioritization. + *
    + *
  • DEBUG: Detailed information, typically of interest only when diagnosing problems.
  • + *
  • INFO: Confirmation that things are working as expected.
  • + *
  • ERROR: Indicates a problem that needs attention.
  • + *
+ */ +public enum LogLevel { + DEBUG, INFO, ERROR +} diff --git a/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogProducer.java b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogProducer.java new file mode 100644 index 000000000000..f6ca7f0ded12 --- /dev/null +++ b/log-aggregation/src/main/java/com/iluwatar/logaggregation/LogProducer.java @@ -0,0 +1,54 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a service that produces logs. + * The logs are generated based on certain activities or events within the service. + * Once a log is generated, it's passed on to the aggregator for further processing. + */ +@AllArgsConstructor +@Slf4j +public class LogProducer { + + private String serviceName; + private LogAggregator aggregator; + + /** + * Generates a log entry with the given log level and message. + * + * @param level The level of the log. + * @param message The message of the log. + */ + public void generateLog(LogLevel level, String message) { + final LogEntry logEntry = new LogEntry(serviceName, level, message, LocalDateTime.now()); + LOGGER.info("Producing log: " + logEntry.getMessage()); + aggregator.collectLog(logEntry); + } +} diff --git a/log-aggregation/src/test/java/com/iluwatar/logaggregation/LogAggregatorTest.java b/log-aggregation/src/test/java/com/iluwatar/logaggregation/LogAggregatorTest.java new file mode 100644 index 000000000000..182e7252769f --- /dev/null +++ b/log-aggregation/src/test/java/com/iluwatar/logaggregation/LogAggregatorTest.java @@ -0,0 +1,80 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2023 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.logaggregation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LogAggregatorTest { + + @Mock + private CentralLogStore centralLogStore; + private LogAggregator logAggregator; + + @BeforeEach + void setUp() { + logAggregator = new LogAggregator(centralLogStore, LogLevel.INFO); + } + + @Test + void whenThreeInfoLogsAreCollected_thenCentralLogStoreShouldStoreAllOfThem() { + logAggregator.collectLog(createLogEntry(LogLevel.INFO, "Sample log message 1")); + logAggregator.collectLog(createLogEntry(LogLevel.INFO, "Sample log message 2")); + + verifyNoInteractionsWithCentralLogStore(); + + logAggregator.collectLog(createLogEntry(LogLevel.INFO, "Sample log message 3")); + + verifyCentralLogStoreInvokedTimes(3); + } + + @Test + void whenDebugLogIsCollected_thenNoLogsShouldBeStored() { + logAggregator.collectLog(createLogEntry(LogLevel.DEBUG, "Sample debug log message")); + + verifyNoInteractionsWithCentralLogStore(); + } + + private static LogEntry createLogEntry(LogLevel logLevel, String message) { + return new LogEntry("ServiceA", logLevel, message, LocalDateTime.now()); + } + + private void verifyNoInteractionsWithCentralLogStore() { + verify(centralLogStore, times(0)).storeLog(any()); + } + + private void verifyCentralLogStoreInvokedTimes(int times) { + verify(centralLogStore, times(times)).storeLog(any()); + } +} diff --git a/pom.xml b/pom.xml index 133e8565dcf3..48e725a4f9bc 100644 --- a/pom.xml +++ b/pom.xml @@ -208,6 +208,7 @@ thread-local-storage optimistic-offline-lock crtp + log-aggregation health-check