Skip to content

Commit 84ae721

Browse files
dieppadavidfrigoletbercianorosantana85
authored
1.4.0 (#144)
Co-authored-by: davidfrigolet <dfrigolet@gmail.com> Co-authored-by: Rubén Berciano <bercianor@gmail.com> Co-authored-by: Oliver Santana <oliver.sm@gmail.com>
1 parent 9cb272c commit 84ae721

14 files changed

Lines changed: 794 additions & 405 deletions

File tree

docs/cli/cli.md

Lines changed: 159 additions & 211 deletions
Large diffs are not rendered by default.

docs/flamingock-library-config/events.md

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ sidebar_position: 90
44

55
import Tabs from '@theme/Tabs';
66
import TabItem from '@theme/TabItem';
7+
import VersionBadge from '@site/src/components/VersionBadge';
78

89
# Events
910

@@ -47,20 +48,24 @@ If a stage fails, both StageFailedEvent and PipelineFailedEvent will be emitted.
4748

4849
Flamingock supports events at two levels:
4950

50-
- **Pipeline Events**: Provide information about the entire migration process.
51+
- **Pipeline Events**: Provide information about the entire execution.
5152
- **Stage Events**: Provide granular information about individual stage executions.
5253

5354
This allows you to monitor both high-level progress and detailed stage-by-stage execution status.
5455

55-
## Event Data
56+
## Event data
5657

5758
Events provide access to relevant information about the execution state:
5859

59-
- **Started Events** (`PipelineStartedEvent`, `StageStartedEvent`): These events are simple markers with no additional data.
60-
- **Completed Events**:
61-
- `StageCompletedEvent`: Provides access to the execution result via `getResult()`, which returns a `StageExecutor.Output` object containing the stage summary with details like the number of applied changes.
62-
- `PipelineCompletedEvent`: A simple marker event with no additional data.
63-
- **Failed Events** (`StageFailedEvent`, `PipelineFailedEvent`): Provide access to the exception that caused the failure via `getException()`, allowing you to inspect the error details.
60+
- **Started events** (`PipelineStartedEvent`, `StageStartedEvent`): simple marker events with no additional data.
61+
- **Completed events**:
62+
- `PipelineCompletedEvent`: `getResult()` returns an `ExecuteResponseData` with the overall status, durations, per-stage breakdown, and aggregate change counters.
63+
- `StageCompletedEvent`: `getResult()` returns a `StageResult` with the stage state, duration, and per-change details.
64+
- **Failed events**:
65+
- `PipelineFailedEvent`: `getResult()` returns the `ExecuteResponseData` accumulated up to the failure; `getException()` returns the underlying `Exception`.
66+
- `StageFailedEvent`: `getResult()` returns the `StageResult` for the failed stage; `getException()` returns the underlying `Exception`.
67+
68+
See [Event payload reference](#event-payload-reference) below for the fields you can read from `ExecuteResponseData` and `StageResult`. For the structured execution report that Flamingock writes by default after every run, see [Execution report logging](./execution-report.md).
6469

6570
## Standalone basic example
6671

@@ -105,17 +110,19 @@ In the Flamingock builder, you must configure the events you intend to use and i
105110
public class StageCompletedListener implements Consumer<IStageCompletedEvent> {
106111
@Override
107112
public void accept(IStageCompletedEvent event) {
108-
System.out.println("Stage execution completed with " + event.getResult().getSummary().getAppliedChangesCount() + " changes applied");
113+
StageResult result = event.getResult();
114+
System.out.println("Stage '" + result.getStageName() + "' completed with "
115+
+ result.getAppliedCount() + " change(s) applied");
109116
}
110117
}
111-
}
112118
```
113119
</TabItem>
114120
<TabItem value="kotlin" label="Kotlin">
115121
```kotlin
116122
class StageCompletedListener : (IStageCompletedEvent) -> Unit {
117123
override fun invoke(event: IStageCompletedEvent) {
118-
println("Stage execution completed with ${event.result.summary.appliedChangesCount} changes applied")
124+
val result = event.result
125+
println("Stage '${result.stageName}' completed with ${result.appliedCount} change(s) applied")
119126
}
120127
}
121128
```
@@ -124,6 +131,10 @@ In the Flamingock builder, you must configure the events you intend to use and i
124131

125132
## Spring-based basic example
126133

134+
:::note
135+
Earlier releases shipped the `SpringStage*Event` classes implementing the wrong interfaces (`IPipeline*Event` instead of `IStage*Event`). They are now correctly typed against `IStage*Event`. Any Spring listener that was previously typed against the wrong interface will need to be re-typed when upgrading.
136+
:::
137+
127138
### Beans
128139

129140
<Tabs groupId="languages">
@@ -226,3 +237,36 @@ class StageCompletedListener : ApplicationListener<SpringStageCompletedEvent> {
226237
</TabItem>
227238
</Tabs>
228239

240+
## Event payload reference <VersionBadge version="1.4.0" />
241+
242+
The pipeline-level events carry `ExecuteResponseData`; the stage-level events carry `StageResult`. The fields a typical listener reads are:
243+
244+
### `ExecuteResponseData`
245+
246+
| Method | Description |
247+
|--------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
248+
| `getStatus()` | Overall outcome: `SUCCESS`, `FAILED`, `PARTIAL`, or `NO_CHANGES` (`ExecutionStatus`). |
249+
| `getStartTime()` / `getEndTime()` | Run window as `Instant`s. |
250+
| `getTotalDurationMs()` | Total run duration in milliseconds. |
251+
| `getTotalStages()` / `getCompletedStages()` / `getFailedStages()` / `getUpToDateStages()` / `getNotReachedStages()` | Stage counters. `upToDate` = confirmed already applied without running; `notReached` = never reached this run. |
252+
| `getTotalChanges()` / `getAppliedChanges()` / `getAlreadyAppliedChanges()` / `getFailedChanges()` / `getNotReachedChanges()` | Aggregate change counters across the whole run. `applied` = newly applied this run; `alreadyApplied` = found already applied. |
253+
| `getStages()` | `List<StageResult>` — one entry per stage, in declaration order. |
254+
255+
The counters always reconcile against the totals: `completed + failed + upToDate + notReached == totalStages`, and `applied + alreadyApplied + failed + notReached == totalChanges`.
256+
257+
### `StageResult`
258+
259+
| Method | Description |
260+
|--------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
261+
| `getStageId()` / `getStageName()` | Stage identifiers. |
262+
| `getState()` | `StageState` — what the executor did this run. Sealed abstract type with five variants (`NotStarted`, `Started`, `Completed`, `Failed`, `BlockedForMI`), not a plain enum. Query the variant via `isNotStarted()` / `isStarted()` / `isCompleted()` / `isFailed()` / `isBlockedForManualIntervention()`. |
263+
| `getPlannerVerdict()` | `PlannerVerdict` — what the audit log says about the stage, independent of execution: `NOT_EVALUATED`, `NEEDS_WORK`, or `UP_TO_DATE`. Drives the `[UP TO DATE]` / `[NOT REACHED]` labels in the report when the stage was not executed. |
264+
| `getDurationMs()` | Stage duration in milliseconds. |
265+
| `getTotalChanges()` | Number of changes declared on the stage in the loaded pipeline (structural count, independent of how many ran). |
266+
| `getAppliedCount()` / `getAlreadyAppliedCount()` / `getFailedCount()` | Per-stage change counters. |
267+
| `getChanges()` | `List<ChangeResult>` — one entry per change in the stage. |
268+
269+
`getErrorInfo()` and `getRecoveryIssues()` are declared on the base `StageState` type so you can call them on any instance without down-casting; what they return depends on the variant. `getErrorInfo()` returns an `Optional<ErrorInfo>` — populated on `Failed` and `BlockedForMI`, empty otherwise. `getRecoveryIssues()` returns a `List<RecoveryIssue>` — empty unless the variant is `BlockedForMI`, in which case each `RecoveryIssue.getChangeId()` identifies a change you need to resolve.
270+
271+
See also: [Execution report logging](./execution-report.md) for the default SLF4J report Flamingock writes from these payloads after every run.
272+
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
sidebar_position: 95
3+
---
4+
5+
import Tabs from '@theme/Tabs';
6+
import TabItem from '@theme/TabItem';
7+
import VersionBadge from '@site/src/components/VersionBadge';
8+
9+
# Execution report logging
10+
<VersionBadge version="1.4.0" variant="subtitle" />
11+
12+
After every run, Flamingock emits a structured execution report through SLF4J under the logger name `FK-Report` — at `INFO` on success and `ERROR` on failure. The report shows the overall status, duration, per-stage breakdown, and — when something went wrong — the failing change IDs and error details.
13+
14+
This is enabled by default. **If you're happy seeing the report in your logs, you don't need to configure anything.**
15+
16+
Related: [Events](./events.md).
17+
18+
## What the report looks like
19+
20+
On a successful run:
21+
22+
```
23+
========================================================================
24+
Flamingock execution report — SUCCESS
25+
========================================================================
26+
Started: 2026-05-15T08:00:00Z
27+
Finished: 2026-05-15T08:00:00.180Z
28+
Duration: 180 ms
29+
30+
Stages: 2 total — 2 completed, 0 failed, 0 up to date, 0 not reached
31+
Changes: 3 total — 3 newly applied, 0 already applied, 0 failed, 0 not reached
32+
33+
Per-stage breakdown:
34+
35+
[COMPLETED] user-database-stage (75 ms)
36+
changes: 2 newly applied, 0 already applied, 0 failed, 0 not reached
37+
38+
[COMPLETED] kafka-topics-stage (105 ms)
39+
changes: 1 newly applied, 0 already applied, 0 failed, 0 not reached
40+
41+
========================================================================
42+
```
43+
44+
On a failed run:
45+
46+
```
47+
========================================================================
48+
Flamingock execution report — FAILED
49+
========================================================================
50+
Started: 2026-05-15T08:00:00Z
51+
Finished: 2026-05-15T08:00:00.312Z
52+
Duration: 312 ms
53+
54+
Stages: 2 total — 1 completed, 1 failed, 0 up to date, 0 not reached
55+
Changes: 3 total — 2 newly applied, 0 already applied, 1 failed, 0 not reached
56+
57+
Per-stage breakdown:
58+
59+
[COMPLETED] user-database-stage (75 ms)
60+
changes: 2 newly applied, 0 already applied, 0 failed, 0 not reached
61+
62+
[FAILED] kafka-topics-stage (237 ms)
63+
changes: 0 newly applied, 0 already applied, 1 failed, 0 not reached
64+
error: TopicCreationFailed
65+
Broker rejected creation: replication factor 3 > available brokers 1
66+
failed change(s): create-orders-topic
67+
68+
========================================================================
69+
```
70+
71+
## Reading the counters
72+
73+
Each run reports two count lines, and both always add up to the total:
74+
75+
- **Stages:** `completed + failed + up to date + not reached = total`.
76+
- **Changes:** `newly applied + already applied + failed + not reached = total`.
77+
78+
What each bucket means:
79+
80+
- **newly applied** — change executed and applied during this run.
81+
- **already applied** — change was found already applied (confirmed against the audit log), so nothing ran for it.
82+
- **failed** — change failed this run. A transactional change that failed and was automatically rolled back is also counted here.
83+
- **not reached** — change was never processed this run (for example, it sits after a failed change in the same stage, or in a stage the run never got to).
84+
85+
## Other run shapes
86+
87+
Beyond the success and failure examples above, the report adapts to what actually happened:
88+
89+
- **`NO CHANGES`** — the headline reads `Flamingock execution report — NO CHANGES` when nothing needed applying (every change is already applied, or there was nothing to do). This is the common steady-state report you'll see on most application restarts after the first.
90+
- **`[UP TO DATE]` stages** — a stage Flamingock confirmed was already applied without running anything appears with the `[UP TO DATE]` label and no duration, e.g. `[UP TO DATE] user-database-stage (3 changes already applied)`.
91+
- **`[NOT REACHED]` stages** — when a run stops early (for example after a failure), the stages it never got to are summarised in a separate `Not reached (N):` section that lists each stage name with its change count.
92+
- **`[BLOCKED — manual intervention required]` stages** — a stage that needs manual intervention shows this distinct label, followed by a `change(s) requiring intervention:` line listing the change IDs to resolve.
93+
94+
:::note
95+
If your code catches the exception raised when stages fail, its `getMessage()` returns a single-line summary of the same data — failed stage names plus the change counters and duration, for example:
96+
97+
```
98+
Flamingock execution failed: 1 of 2 stage(s) failed [kafka-topics-stage]; changes newly_applied=2, already_applied=0, failed=1, not_reached=0; duration=312ms
99+
```
100+
101+
When a stage is blocked for manual intervention, the line ends with `; manual intervention required for change(s): <ids>`. The multi-line report above is emitted by the SLF4J listener, never by the exception itself, so log aggregators don't see duplicated dumps on `printStackTrace`.
102+
:::
103+
104+
## Silencing the report via SLF4J (recommended for production)
105+
106+
Set the `FK-Report` logger level to `OFF`. No code change, no rebuild — purely a logging-configuration change.
107+
108+
<Tabs groupId="logging">
109+
<TabItem value="logback" label="Logback" default>
110+
```xml
111+
<configuration>
112+
<logger name="FK-Report" level="OFF"/>
113+
</configuration>
114+
```
115+
</TabItem>
116+
<TabItem value="log4j2" label="Log4j2">
117+
```xml
118+
<Configuration>
119+
<Loggers>
120+
<Logger name="FK-Report" level="OFF" additivity="false"/>
121+
</Loggers>
122+
</Configuration>
123+
```
124+
</TabItem>
125+
</Tabs>
126+
127+
Use `WARN` (any level between `INFO` and ERROR)" instead of `OFF` to keep failure reports while suppressing successful runs — the failure listener logs at `ERROR`, the success listener at `INFO`.
128+
129+
## Disabling the report via the builder flag
130+
131+
Set the flag to `false` on the runner builder. Default is `true`.
132+
133+
<Tabs groupId="languages">
134+
<TabItem value="java" label="Java" default>
135+
```java
136+
Flamingock.builder()
137+
.setEnableDefaultExecutionReport(false)
138+
.build()
139+
.run();
140+
```
141+
</TabItem>
142+
<TabItem value="kotlin" label="Kotlin">
143+
```kotlin
144+
Flamingock.builder()
145+
.setEnableDefaultExecutionReport(false)
146+
.build()
147+
.run()
148+
```
149+
</TabItem>
150+
</Tabs>
151+
152+
This is the right choice when you want no default report at all — for instance, when you're registering your own listener and want only that to fire.
153+
154+
## Bring your own listener
155+
156+
Register your own listener with the existing builder setters (`setPipelineCompletedListener`, `setPipelineFailedListener`, etc.). See [Events](./events.md) for the full listener-registration surface and the payload reference.
157+
158+
When both the default and a custom listener are registered, **both fire** (additive). To have only your listener fire, set `setEnableDefaultExecutionReport(false)` as shown above.
159+
160+
A minimal failure listener that reads the typed result and logs the IDs of any change blocked for manual intervention:
161+
162+
<Tabs groupId="languages">
163+
<TabItem value="java" label="Java" default>
164+
```java
165+
public class PipelineFailedListener implements Consumer<IPipelineFailedEvent> {
166+
167+
private static final Logger logger = LoggerFactory.getLogger(PipelineFailedListener.class);
168+
169+
@Override
170+
public void accept(IPipelineFailedEvent event) {
171+
ExecuteResponseData result = event.getResult();
172+
logger.error("Flamingock failed: {} failed stage(s), {} failed change(s), {} ms",
173+
result.getFailedStages(),
174+
result.getFailedChanges(),
175+
result.getTotalDurationMs());
176+
177+
result.getStages().stream()
178+
.filter(stage -> stage.getState().isBlockedForManualIntervention())
179+
.flatMap(stage -> stage.getState().getRecoveryIssues().stream())
180+
.map(RecoveryIssue::getChangeId)
181+
.forEach(id -> logger.error("Manual intervention required for change: {}", id));
182+
}
183+
}
184+
```
185+
</TabItem>
186+
<TabItem value="kotlin" label="Kotlin">
187+
```kotlin
188+
class PipelineFailedListener : (IPipelineFailedEvent) -> Unit {
189+
190+
private val logger = LoggerFactory.getLogger(PipelineFailedListener::class.java)
191+
192+
override fun invoke(event: IPipelineFailedEvent) {
193+
val result = event.result
194+
logger.error("Flamingock failed: {} failed stage(s), {} failed change(s), {} ms",
195+
result.failedStages, result.failedChanges, result.totalDurationMs)
196+
197+
result.stages
198+
.filter { it.state.isBlockedForManualIntervention }
199+
.flatMap { it.state.recoveryIssues }
200+
.map { it.changeId }
201+
.forEach { logger.error("Manual intervention required for change: {}", it) }
202+
}
203+
}
204+
```
205+
</TabItem>
206+
</Tabs>
207+
208+
### Reusing the canonical report formatter
209+
210+
If you want to emit the same multi-line block that the default listener writes — but to a different logger, sink, or external system (Slack, metrics, JSON) — call `ExecutionReportFormatter.report(...)`:
211+
212+
<Tabs groupId="languages">
213+
<TabItem value="java" label="Java" default>
214+
```java
215+
String report = ExecutionReportFormatter.report(event.getResult());
216+
mySink.publish(report);
217+
```
218+
</TabItem>
219+
<TabItem value="kotlin" label="Kotlin">
220+
```kotlin
221+
val report = ExecutionReportFormatter.report(event.result)
222+
mySink.publish(report)
223+
```
224+
</TabItem>
225+
</Tabs>
226+
227+
The same class also exposes `summary(...)`, which produces the one-line variant.
228+
229+
## Spring Boot
230+
231+
The default `FK-Report` listener is wired by the core builder and fires the same way in Spring Boot — silence it via the SLF4J configuration above or the builder flag.
232+
233+
Spring listeners receive the same typed payload through Flamingock's Spring `ApplicationEvent` wrappers. A minimal `@EventListener` handler for failures:
234+
235+
<Tabs groupId="languages">
236+
<TabItem value="java" label="Java" default>
237+
```java
238+
@Configuration
239+
public class FlamingockListeners {
240+
241+
private static final Logger logger = LoggerFactory.getLogger(FlamingockListeners.class);
242+
243+
@EventListener
244+
public void onFlamingockFailure(SpringPipelineFailedEvent event) {
245+
ExecuteResponseData result = event.getResult();
246+
logger.error("Flamingock failed: {} failed stage(s) in {} ms",
247+
result.getFailedStages(), result.getTotalDurationMs());
248+
}
249+
}
250+
```
251+
</TabItem>
252+
<TabItem value="kotlin" label="Kotlin">
253+
```kotlin
254+
@Configuration
255+
class FlamingockListeners {
256+
257+
private val logger = LoggerFactory.getLogger(FlamingockListeners::class.java)
258+
259+
@EventListener
260+
fun onFlamingockFailure(event: SpringPipelineFailedEvent) {
261+
val result = event.result
262+
logger.error("Flamingock failed: {} failed stage(s) in {} ms",
263+
result.failedStages, result.totalDurationMs)
264+
}
265+
}
266+
```
267+
</TabItem>
268+
</Tabs>

0 commit comments

Comments
 (0)