diff --git a/.github/workflows/log-deployments-and-changes.yml b/.github/workflows/log-deployments-and-changes.yml new file mode 100644 index 000000000000..1b95a7a0fe22 --- /dev/null +++ b/.github/workflows/log-deployments-and-changes.yml @@ -0,0 +1,38 @@ +name: Log deployments and changes example + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + demonstrate: + runs-on: ubuntu-22.04 + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build example + run: ./mvnw -B -pl microservices-log-deployments-and-changes -am package + + - name: Run simulated deployment and change pipeline + shell: bash + run: | + set -o pipefail + java -jar microservices-log-deployments-and-changes/target/microservices-log-deployments-and-changes-1.26.0-SNAPSHOT.jar 2>&1 | tee deployment-events.log + + - name: Preserve timeline and alerts + if: always() + uses: actions/upload-artifact@v4 + with: + name: deployment-events + path: deployment-events.log + if-no-files-found: warn diff --git a/microservices-log-deployments-and-changes/README.md b/microservices-log-deployments-and-changes/README.md new file mode 100644 index 000000000000..7b272ee160b4 --- /dev/null +++ b/microservices-log-deployments-and-changes/README.md @@ -0,0 +1,172 @@ +--- +title: "Microservices Log Deployments and Changes Pattern in Java" +shortTitle: Microservices Log Deployments and Changes +description: "Record operational changes in a shared timeline to correlate service failures with deployments and configuration updates." +category: Integration +language: en +tag: + - Microservices + - Enterprise patterns + - Fault tolerance +--- + +## Intent of Microservices Log Deployments and Changes Design Pattern + +Record deployments and operational changes with their service, version, environment, initiator, +timestamp, and outcome. Use this history to correlate changes with unexpected application behavior. + +## Detailed Explanation of Microservices Log Deployments and Changes Pattern with Real-World Examples + +Real-world example + +> An online shop releases a new orders service and changes the payments timeout. Payments then +> start failing. A shared timeline helps operators identify which changes preceded the failures, +> even though different teams performed the operations. + +In plain words + +> Keep a shared record of what changed, when, who changed it, and whether it succeeded. + +[Microservices.io](https://microservices.io/patterns/observability/log-deployments-and-changes.html) +describes recording deployments and environment changes to correlate them with application issues. + +This example follows the producer-to-central-store structure of +[Microservices Log Aggregation](../microservices-log-aggregation/README.md). Its events describe +operational changes rather than general application messages. Each `DeploymentPipeline` wraps a +service operation and appends its outcome to the shared `CentralLogStore`. `LogMonitor` presents +the history and selects alerts using a supplied rule. + +## Class diagram + +```mermaid +classDiagram + DeploymentPipeline --> CentralLogStore : appends outcomes + CentralLogStore o-- ChangeEvent : retains history + ChangeEvent --> ChangeType + LogMonitor --> CentralLogStore : reads history + class ChangeEvent { + Instant timestamp + String service + String version + String environment + String actor + ChangeType type + String description + boolean successful + } + class ChangeType { + <> + DEPLOYMENT + CONFIGURATION + ENDPOINT + PROTOCOL + } +``` + +## Programmatic Example of Microservices Log Deployments and Changes Pattern in Java + +Create a shared store and inject a clock. Tests use a fixed clock for deterministic UTC timestamps. + +```java +var store = new CentralLogStore(); +var pipeline = new DeploymentPipeline(store, Clock.systemUTC()); +var monitor = new LogMonitor(store); +pipeline.execute( + "orders", "2.0", "production", "release-engineer", ChangeType.DEPLOYMENT, + "Deploy orders release", () -> LOGGER.info("Deploying orders 2.0")); +``` + +The operation represents deployment tooling or a configuration update. `execute` invokes it once +and records an immutable `ChangeEvent` in a `finally` block. A successful return records success; +an exception records failure and propagates to the caller. The timestamp is the completion time. +Configuration, endpoint, and communication protocol updates use the corresponding `ChangeType`. +Callers do not need a separate logging step. + +Display the timeline and alert on failed production operations: + +```java +LOGGER.info("Deployment and change timeline:\n{}", monitor.timeline()); +monitor.alerts(event -> !event.successful() && "production".equals(event.environment())) + .forEach(event -> LOGGER.warn("ALERT: {} {} failed", event.service(), event.type())); +``` + +`App` simulates changes for orders and payments. Its protocol update deliberately fails; the demo +catches that failure to display the complete history. Output includes these lines (times and actor vary): + +```text +2026-01-01T12:00:00Z | orders | 2.0 | production | release-engineer | DEPLOYMENT | SUCCESS | Deploy orders release +2026-01-01T12:00:00Z | payments | 1.1 | production | release-engineer | PROTOCOL | FAILURE | Switch inter-service protocol to gRPC +ALERT: payments PROTOCOL failed +``` + +### Running and verifying the example + +From the repository root, with Java 21: + +```shell +./mvnw -pl microservices-log-deployments-and-changes -am test +./mvnw -pl microservices-log-deployments-and-changes spotless:check +./mvnw -pl microservices-log-deployments-and-changes -am package -DskipTests +java -jar microservices-log-deployments-and-changes/target/microservices-log-deployments-and-changes-1.26.0-SNAPSHOT.jar +``` + +On Windows, use `./mvnw.cmd` instead of `./mvnw`. Tests cover all event types, complete metadata, +exception propagation, and immutable snapshots. `LogMonitorTest` also integrates two producers +with the shared store and verifies the rendered timeline and selective alerts. + +### CI/CD integration + +The repository's `.github/workflows/log-deployments-and-changes.yml` defines the manually triggered +**Log deployments and changes example** workflow. It follows the existing Java 21/Maven setup, +builds this module, and runs the demonstration. `App` reads `GITHUB_ACTOR` for the initiator. The +workflow saves the console timeline and alerts as a downloadable artifact. Once the workflow is +on the default branch, run it from the repository's Actions tab. + +This is a simulated pipeline; it does not deploy the repository's other examples. For a real service, +wrap its deployment or change command with `DeploymentPipeline.execute`, supplying the actual +version, environment, and initiator. The operation must throw on failure, including when an external +command returns a nonzero exit code. Share the event destination across services. Production callers +should let failures fail the pipeline; only the demo catches its deliberately simulated failure. + +The store is single-threaded and in-memory, and visualization is a console timeline. It does not +retain history between processes, detect out-of-band changes, or send external notifications. +Production use needs durable shared collection, delivery handling, and monitoring that overlays +change timestamps on service metrics. Abrupt process termination can prevent a completion event +from being written. Descriptions should not contain credentials or raw configuration secrets. + +## When to Use the Microservices Log Deployments and Changes Pattern in Java + +* Multiple services or teams deploy independently. +* Operators need to correlate incidents with releases and environment changes. +* Configuration, endpoint, or protocol changes affect behavior without a new release. + +## Real-World Applications of Microservices Log Deployments and Changes Pattern in Java + +* Deployment tools can publish release markers alongside application metrics, as described by + [Microservices.io](https://microservices.io/patterns/observability/log-deployments-and-changes.html). +* Service delivery pipelines can record releases and configuration updates in one operational history. + +## Benefits and Trade-offs of Microservices Log Deployments and Changes Pattern + +Benefits: + +* Makes recent changes visible across service and team boundaries. +* Automates recording at the operation boundary, including failed attempts. +* Supports filtering by environment, service, type, or outcome. + +Trade-offs: + +* Every deployment and change path must participate for complete history. +* Durable collection and reliable delivery add operational work. +* Temporal correlation aids investigation but does not prove causation. + +## Related Java Design Patterns + +* [Microservices Log Aggregation](../microservices-log-aggregation/README.md): centralizes application logs. +* [Event Sourcing](../event-sourcing/README.md): reconstructs state from events; this example records operational outcomes. +* [Observer](../observer/README.md): can notify subscribers when change events arrive. + +## References and Credits + +* [Pattern: Log deployments and changes](https://microservices.io/patterns/observability/log-deployments-and-changes.html) +* [Issue #2696](https://github.com/iluwatar/java-design-patterns/issues/2696) diff --git a/microservices-log-deployments-and-changes/pom.xml b/microservices-log-deployments-and-changes/pom.xml new file mode 100644 index 000000000000..2456cafad98a --- /dev/null +++ b/microservices-log-deployments-and-changes/pom.xml @@ -0,0 +1,74 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + microservices-log-deployments-and-changes + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.logdeploymentsandchanges.App + + + + + + + + + diff --git a/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/App.java b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/App.java new file mode 100644 index 000000000000..db3f76ce5923 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/App.java @@ -0,0 +1,86 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import java.time.Clock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Demonstrates automatic deployment and change logging for two services. */ +public class App { + private static final Logger LOGGER = LoggerFactory.getLogger(App.class); + + /** Run simulated pipeline steps, then display their timeline and failure alerts. */ + public static void main(String[] args) { + var store = new CentralLogStore(); + var pipeline = new DeploymentPipeline(store, Clock.systemUTC()); + var monitor = new LogMonitor(store); + var actor = System.getenv().getOrDefault("GITHUB_ACTOR", "release-engineer"); + + pipeline.execute( + "orders", + "2.0", + "production", + actor, + ChangeType.DEPLOYMENT, + "Deploy orders release", + () -> LOGGER.info("Deploying orders 2.0")); + pipeline.execute( + "payments", + "1.1", + "production", + actor, + ChangeType.CONFIGURATION, + "Increase request timeout", + () -> LOGGER.info("Updating payments timeout")); + pipeline.execute( + "orders", + "2.0", + "production", + actor, + ChangeType.ENDPOINT, + "Route payments to /v2/payments", + () -> LOGGER.info("Updating payments endpoint")); + try { + pipeline.execute( + "payments", + "1.1", + "production", + actor, + ChangeType.PROTOCOL, + "Switch inter-service protocol to gRPC", + () -> { + throw new IllegalStateException("Protocol health check failed"); + }); + } catch (IllegalStateException exception) { + LOGGER.warn("Simulated change failed: {}", exception.getMessage()); + } + + LOGGER.info("Deployment and change timeline:\n{}", monitor.timeline()); + monitor + .alerts(event -> !event.successful() && "production".equals(event.environment())) + .forEach(event -> LOGGER.warn("ALERT: {} {} failed", event.service(), event.type())); + } +} diff --git a/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/CentralLogStore.java b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/CentralLogStore.java new file mode 100644 index 000000000000..f7eecec62f2f --- /dev/null +++ b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/CentralLogStore.java @@ -0,0 +1,44 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** Shared in-memory history for the services in this single-threaded example. */ +public class CentralLogStore { + private final List events = new ArrayList<>(); + + /** Append an event without filtering out successful operations. */ + public void store(ChangeEvent event) { + events.add(Objects.requireNonNull(event)); + } + + /** Return an immutable snapshot in insertion order. */ + public List getEvents() { + return List.copyOf(events); + } +} diff --git a/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/ChangeEvent.java b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/ChangeEvent.java new file mode 100644 index 000000000000..3a4777b9b73c --- /dev/null +++ b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/ChangeEvent.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-2022 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.logdeploymentsandchanges; + +import java.time.Instant; + +/** Immutable metadata describing the outcome of a deployment or change. */ +public record ChangeEvent( + Instant timestamp, + String service, + String version, + String environment, + String actor, + ChangeType type, + String description, + boolean successful) {} diff --git a/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/ChangeType.java b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/ChangeType.java new file mode 100644 index 000000000000..283931799d82 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/ChangeType.java @@ -0,0 +1,33 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +/** Operational changes that can affect a service's behavior. */ +public enum ChangeType { + DEPLOYMENT, + CONFIGURATION, + ENDPOINT, + PROTOCOL +} diff --git a/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/DeploymentPipeline.java b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/DeploymentPipeline.java new file mode 100644 index 000000000000..cec1575fa320 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/DeploymentPipeline.java @@ -0,0 +1,71 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import java.time.Clock; +import java.util.Objects; + +/** Wraps deployment tooling so each completed operation automatically records its outcome. */ +public class DeploymentPipeline { + private final CentralLogStore store; + private final Clock clock; + + public DeploymentPipeline(CentralLogStore store, Clock clock) { + this.store = Objects.requireNonNull(store); + this.clock = Objects.requireNonNull(clock); + } + + /** + * Execute an operation and log its metadata, preserving any runtime failure for the caller. + * Descriptions should summarize the change without including credentials or configuration + * secrets. + */ + public void execute( + String service, + String version, + String environment, + String actor, + ChangeType type, + String description, + Runnable operation) { + Objects.requireNonNull(operation); + boolean successful = false; + try { + operation.run(); + successful = true; + } finally { + store.store( + new ChangeEvent( + clock.instant(), + service, + version, + environment, + actor, + type, + description, + successful)); + } + } +} diff --git a/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/LogMonitor.java b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/LogMonitor.java new file mode 100644 index 000000000000..f484bea2bf18 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/main/java/com/iluwatar/logdeploymentsandchanges/LogMonitor.java @@ -0,0 +1,61 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** Presents the shared history as a console timeline and selects events for alerting. */ +public class LogMonitor { + private final CentralLogStore store; + + public LogMonitor(CentralLogStore store) { + this.store = store; + } + + /** Render all event metadata, one operation per line. */ + public String timeline() { + return store.getEvents().stream() + .map( + event -> + String.format( + "%s | %s | %s | %s | %s | %s | %s | %s", + event.timestamp(), + event.service(), + event.version(), + event.environment(), + event.actor(), + event.type(), + event.successful() ? "SUCCESS" : "FAILURE", + event.description())) + .collect(Collectors.joining(System.lineSeparator())); + } + + /** Select alerts using a caller-supplied rule, such as failed production changes. */ + public List alerts(Predicate rule) { + return store.getEvents().stream().filter(rule).toList(); + } +} diff --git a/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/AppTest.java b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/AppTest.java new file mode 100644 index 000000000000..1107269478da --- /dev/null +++ b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/AppTest.java @@ -0,0 +1,100 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class AppTest { + + @Test + void logsCompleteTimelineAndAlertsOnlyOnFailedChange() { + var logger = (Logger) LoggerFactory.getLogger(App.class); + var previousLevel = logger.getLevel(); + var appender = new ListAppender(); + appender.start(); + logger.addAppender(appender); + logger.setLevel(Level.INFO); + try { + App.main(new String[0]); + + var timelines = + appender.list.stream() + .filter( + event -> + event.getFormattedMessage().startsWith("Deployment and change timeline:")) + .toList(); + assertEquals(1, timelines.size()); + assertEquals(Level.INFO, timelines.getFirst().getLevel()); + var lines = timelines.getFirst().getFormattedMessage().lines().skip(1).toList(); + var actor = System.getenv().getOrDefault("GITHUB_ACTOR", "release-engineer"); + var expected = + List.of( + "orders | 2.0 | production | " + + actor + + " | DEPLOYMENT | SUCCESS | Deploy orders release", + "payments | 1.1 | production | " + + actor + + " | CONFIGURATION | SUCCESS | Increase request timeout", + "orders | 2.0 | production | " + + actor + + " | ENDPOINT | SUCCESS | Route payments to /v2/payments", + "payments | 1.1 | production | " + + actor + + " | PROTOCOL | FAILURE | Switch inter-service protocol to gRPC"); + assertEquals(expected.size(), lines.size()); + for (int index = 0; index < lines.size(); index++) { + var timestampEnd = lines.get(index).indexOf(" | "); + assertTrue(timestampEnd > 0); + Instant.parse(lines.get(index).substring(0, timestampEnd)); + assertEquals(expected.get(index), lines.get(index).substring(timestampEnd + 3)); + } + + var warnings = + appender.list.stream() + .filter(event -> event.getLevel().equals(Level.WARN)) + .map(ILoggingEvent::getFormattedMessage) + .toList(); + assertEquals( + List.of( + "Simulated change failed: Protocol health check failed", + "ALERT: payments PROTOCOL failed"), + warnings); + } finally { + logger.detachAppender(appender); + logger.setLevel(previousLevel); + appender.stop(); + } + } +} diff --git a/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/CentralLogStoreTest.java b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/CentralLogStoreTest.java new file mode 100644 index 000000000000..3a7a2a275191 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/CentralLogStoreTest.java @@ -0,0 +1,51 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import org.junit.jupiter.api.Test; + +class CentralLogStoreTest { + @Test + void preservesHistoryAndReturnsImmutableSnapshots() { + var store = new CentralLogStore(); + var empty = store.getEvents(); + var event = + new ChangeEvent( + Instant.EPOCH, "orders", "1", "test", "alice", ChangeType.DEPLOYMENT, "Release", true); + store.store(event); + var snapshot = store.getEvents(); + store.store(event); + assertTrue(empty.isEmpty()); + assertEquals(1, snapshot.size()); + assertEquals(2, store.getEvents().size()); + assertThrows(UnsupportedOperationException.class, snapshot::clear); + assertThrows(NullPointerException.class, () -> store.store(null)); + } +} diff --git a/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/DeploymentPipelineTest.java b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/DeploymentPipelineTest.java new file mode 100644 index 000000000000..a0dd5cd0aae0 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/DeploymentPipelineTest.java @@ -0,0 +1,100 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class DeploymentPipelineTest { + private static final Instant NOW = Instant.parse("2026-01-01T12:00:00Z"); + private final CentralLogStore store = new CentralLogStore(); + private final DeploymentPipeline pipeline = + new DeploymentPipeline(store, Clock.fixed(NOW, ZoneOffset.UTC)); + + @Test + void logsEveryChangeTypeWithAllMetadataAfterExecution() { + var calls = new AtomicInteger(); + for (var type : ChangeType.values()) { + pipeline.execute( + "orders", + "2.0", + "staging", + "alice", + type, + "Example change", + () -> { + assertEquals(calls.get(), store.getEvents().size()); + calls.incrementAndGet(); + }); + } + assertEquals(ChangeType.values().length, calls.get()); + assertEquals(calls.get(), store.getEvents().size()); + for (int index = 0; index < calls.get(); index++) { + assertEquals( + new ChangeEvent( + NOW, + "orders", + "2.0", + "staging", + "alice", + ChangeType.values()[index], + "Example change", + true), + store.getEvents().get(index)); + } + } + + @Test + void recordsFailureAndRethrowsOriginalException() { + var failure = new IllegalStateException("Deployment failed"); + var thrown = + assertThrows( + IllegalStateException.class, + () -> + pipeline.execute( + "orders", + "2.0", + "production", + "bob", + ChangeType.DEPLOYMENT, + "Release", + () -> { + throw failure; + })); + assertSame(failure, thrown); + assertEquals(1, store.getEvents().size()); + assertEquals( + new ChangeEvent( + NOW, "orders", "2.0", "production", "bob", ChangeType.DEPLOYMENT, "Release", false), + store.getEvents().getFirst()); + } +} diff --git a/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/LogMonitorTest.java b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/LogMonitorTest.java new file mode 100644 index 000000000000..9e517f4dff08 --- /dev/null +++ b/microservices-log-deployments-and-changes/src/test/java/com/iluwatar/logdeploymentsandchanges/LogMonitorTest.java @@ -0,0 +1,81 @@ +/* + * 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-2022 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.logdeploymentsandchanges; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import org.junit.jupiter.api.Test; + +class LogMonitorTest { + @Test + void emptyHistoryHasNoTimelineOrAlerts() { + var monitor = new LogMonitor(new CentralLogStore()); + assertEquals("", monitor.timeline()); + assertTrue(monitor.alerts(event -> true).isEmpty()); + } + + @Test + void integratesMultipleServicesWithTimelineAndProductionFailureAlerts() { + var store = new CentralLogStore(); + var clock = Clock.fixed(Instant.EPOCH, ZoneOffset.UTC); + var orders = new DeploymentPipeline(store, clock); + var payments = new DeploymentPipeline(store, clock); + orders.execute( + "orders", "2", "production", "alice", ChangeType.DEPLOYMENT, "Release", () -> {}); + for (var environment : new String[] {"staging", "production"}) { + assertThrows( + IllegalStateException.class, + () -> + payments.execute( + "payments", + "1", + environment, + "bob", + ChangeType.CONFIGURATION, + "Timeout", + () -> { + throw new IllegalStateException(); + })); + } + var monitor = new LogMonitor(store); + assertEquals( + String.join( + System.lineSeparator(), + "1970-01-01T00:00:00Z | orders | 2 | production | alice | DEPLOYMENT | SUCCESS | Release", + "1970-01-01T00:00:00Z | payments | 1 | staging | bob | CONFIGURATION | FAILURE | Timeout", + "1970-01-01T00:00:00Z | payments | 1 | production | bob | CONFIGURATION | FAILURE | Timeout"), + monitor.timeline()); + var alerts = + monitor.alerts(event -> !event.successful() && "production".equals(event.environment())); + assertEquals(1, alerts.size()); + assertEquals(store.getEvents().get(2), alerts.getFirst()); + assertEquals(3, monitor.alerts(event -> true).size()); + } +} diff --git a/pom.xml b/pom.xml index a71630d289d3..5fe60a126fde 100644 --- a/pom.xml +++ b/pom.xml @@ -170,6 +170,7 @@ microservices-distributed-tracing microservices-idempotent-consumer microservices-log-aggregation + microservices-log-deployments-and-changes microservices-self-registration microservices-messaging model-view-controller