Skip to content

Commit 022bace

Browse files
authored
refactor: add Pipeiline report listeners (#913)
1 parent cb61bd7 commit 022bace

35 files changed

Lines changed: 1131 additions & 66 deletions

File tree

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.response.data;
17+
18+
import io.flamingock.internal.common.core.recovery.RecoveryIssue;
19+
20+
import java.time.Instant;
21+
import java.util.ArrayList;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.stream.Collectors;
25+
26+
/**
27+
* Canonical renderer for {@link ExecuteResponseData}. Two outputs:
28+
*
29+
* <ul>
30+
* <li>{@link #summary(ExecuteResponseData)} — single-line, log-aggregator-friendly.
31+
* Used by {@code ExecuteOperationException#getMessage()}.</li>
32+
* <li>{@link #report(ExecuteResponseData)} — multi-line, fixed-width block intended for SLF4J
33+
* streams and {@code ExecuteOperationException#toString()}.</li>
34+
* </ul>
35+
*
36+
* <p>Both methods are fully defensive: null result, empty stages, missing per-stage state, and
37+
* partial data are tolerated; the formatter never throws. The default event listener and the
38+
* exception {@code toString()} both depend on this contract.
39+
*/
40+
public final class ExecutionReportFormatter {
41+
42+
private static final String LINE = "========================================================================";
43+
private static final String NEWLINE = System.lineSeparator();
44+
45+
private ExecutionReportFormatter() {
46+
}
47+
48+
public static String summary(ExecuteResponseData result) {
49+
if (result == null) {
50+
return "Flamingock execution: no execution data available";
51+
}
52+
53+
List<StageResult> stages = nonNullStages(result);
54+
List<String> failedStageNames = failedStageNames(stages);
55+
List<String> miChangeIds = manualInterventionChangeIds(stages);
56+
57+
String headline;
58+
if (isFailedStatus(result.getStatus()) || !failedStageNames.isEmpty()) {
59+
headline = String.format(
60+
"Flamingock execution failed: %d of %d stage(s) failed [%s]",
61+
result.getFailedStages(),
62+
result.getTotalStages(),
63+
String.join(", ", failedStageNames));
64+
} else {
65+
headline = String.format(
66+
"Flamingock execution completed: %d stage(s)",
67+
result.getTotalStages());
68+
}
69+
70+
String counts = String.format(
71+
"; changes applied=%d, failed=%d, skipped=%d; duration=%dms",
72+
result.getAppliedChanges(),
73+
result.getFailedChanges(),
74+
result.getSkippedChanges(),
75+
result.getTotalDurationMs());
76+
77+
StringBuilder sb = new StringBuilder(headline).append(counts);
78+
if (!miChangeIds.isEmpty()) {
79+
sb.append("; manual intervention required for change(s): ")
80+
.append(String.join(", ", miChangeIds));
81+
}
82+
return sb.toString();
83+
}
84+
85+
public static String report(ExecuteResponseData result) {
86+
if (result == null) {
87+
return banner("Flamingock execution report — NO DATA");
88+
}
89+
90+
StringBuilder sb = new StringBuilder();
91+
String statusLabel = statusLabel(result);
92+
sb.append(LINE).append(NEWLINE)
93+
.append(" Flamingock execution report — ").append(statusLabel).append(NEWLINE)
94+
.append(LINE).append(NEWLINE);
95+
96+
appendTimeBlock(sb, result);
97+
sb.append(NEWLINE);
98+
99+
sb.append(" Stages: ")
100+
.append(result.getTotalStages()).append(" total — ")
101+
.append(result.getCompletedStages()).append(" completed, ")
102+
.append(result.getFailedStages()).append(" failed")
103+
.append(NEWLINE);
104+
sb.append(" Changes: ")
105+
.append(result.getTotalChanges()).append(" total — ")
106+
.append(result.getAppliedChanges()).append(" applied, ")
107+
.append(result.getSkippedChanges()).append(" skipped, ")
108+
.append(result.getFailedChanges()).append(" failed")
109+
.append(NEWLINE);
110+
111+
List<StageResult> stages = nonNullStages(result);
112+
if (!stages.isEmpty()) {
113+
sb.append(NEWLINE).append(" Per-stage breakdown:").append(NEWLINE).append(NEWLINE);
114+
for (StageResult stage : stages) {
115+
appendStageBlock(sb, stage);
116+
sb.append(NEWLINE);
117+
}
118+
}
119+
120+
sb.append(LINE);
121+
return sb.toString();
122+
}
123+
124+
private static void appendTimeBlock(StringBuilder sb, ExecuteResponseData result) {
125+
Instant start = result.getStartTime();
126+
Instant end = result.getEndTime();
127+
sb.append(" Started: ").append(start != null ? start.toString() : "n/a").append(NEWLINE);
128+
sb.append(" Finished: ").append(end != null ? end.toString() : "n/a").append(NEWLINE);
129+
sb.append(" Duration: ").append(result.getTotalDurationMs()).append(" ms").append(NEWLINE);
130+
}
131+
132+
private static void appendStageBlock(StringBuilder sb, StageResult stage) {
133+
String label = stageLabel(stage);
134+
String name = stage.getStageName() != null ? stage.getStageName() : "(unnamed)";
135+
sb.append(" ").append(label).append(' ').append(name)
136+
.append(" (").append(stage.getDurationMs()).append(" ms)").append(NEWLINE);
137+
138+
List<ChangeResult> changes = stage.getChanges() != null ? stage.getChanges() : Collections.emptyList();
139+
int applied = (int) changes.stream().filter(c -> c != null && c.isApplied()).count();
140+
int skipped = (int) changes.stream().filter(c -> c != null && c.isAlreadyApplied()).count();
141+
int failed = (int) changes.stream().filter(c -> c != null && c.isFailed()).count();
142+
sb.append(" changes: ")
143+
.append(applied).append(" applied, ")
144+
.append(skipped).append(" skipped, ")
145+
.append(failed).append(" failed")
146+
.append(NEWLINE);
147+
148+
StageState state = stage.getState();
149+
if (state == null) {
150+
return;
151+
}
152+
if (state.isBlockedForManualIntervention()) {
153+
List<String> ids = changeIdsFromRecoveryIssues(state.getRecoveryIssues());
154+
if (!ids.isEmpty()) {
155+
sb.append(" change(s) requiring intervention: ")
156+
.append(String.join(", ", ids)).append(NEWLINE);
157+
}
158+
} else if (state.isFailed()) {
159+
state.getErrorInfo().ifPresent(err -> appendErrorBlock(sb, err));
160+
List<String> failedChangeIds = failedChangeIds(stage);
161+
if (!failedChangeIds.isEmpty()) {
162+
sb.append(" failed change(s): ")
163+
.append(String.join(", ", failedChangeIds)).append(NEWLINE);
164+
}
165+
}
166+
}
167+
168+
private static void appendErrorBlock(StringBuilder sb, ErrorInfo err) {
169+
String errorType = err.getErrorType() != null ? err.getErrorType() : "Error";
170+
sb.append(" error: ").append(errorType).append(NEWLINE);
171+
String message = err.getMessage();
172+
if (message != null && !message.isEmpty()) {
173+
// Indent continuation lines so multi-line messages stay aligned with the "error:" column.
174+
String[] lines = message.split("\\R");
175+
for (String line : lines) {
176+
sb.append(" ").append(line).append(NEWLINE);
177+
}
178+
}
179+
}
180+
181+
private static String stageLabel(StageResult stage) {
182+
StageState state = stage.getState();
183+
if (state == null) {
184+
return "[UNKNOWN] ";
185+
}
186+
if (state.isBlockedForManualIntervention()) {
187+
return "[BLOCKED — manual intervention required]";
188+
}
189+
if (state.isFailed()) {
190+
return "[FAILED] ";
191+
}
192+
if (state.isCompleted()) {
193+
return "[COMPLETED]";
194+
}
195+
if (state.isStarted()) {
196+
return "[STARTED] ";
197+
}
198+
if (state.isNotStarted()) {
199+
return "[PENDING] ";
200+
}
201+
return "[UNKNOWN] ";
202+
}
203+
204+
private static String statusLabel(ExecuteResponseData result) {
205+
ExecutionStatus status = result.getStatus();
206+
if (status == null) {
207+
return "UNKNOWN";
208+
}
209+
return status.name();
210+
}
211+
212+
private static boolean isFailedStatus(ExecutionStatus status) {
213+
return status == ExecutionStatus.FAILED;
214+
}
215+
216+
private static List<StageResult> nonNullStages(ExecuteResponseData result) {
217+
List<StageResult> raw = result.getStages();
218+
if (raw == null) {
219+
return Collections.emptyList();
220+
}
221+
List<StageResult> filtered = new ArrayList<>(raw.size());
222+
for (StageResult s : raw) {
223+
if (s != null) {
224+
filtered.add(s);
225+
}
226+
}
227+
return filtered;
228+
}
229+
230+
private static List<String> failedStageNames(List<StageResult> stages) {
231+
return stages.stream()
232+
.filter(s -> s.getState() != null && s.getState().isFailed())
233+
.map(s -> s.getStageName() != null ? s.getStageName() : "(unnamed)")
234+
.collect(Collectors.toList());
235+
}
236+
237+
private static List<String> manualInterventionChangeIds(List<StageResult> stages) {
238+
List<String> ids = new ArrayList<>();
239+
for (StageResult s : stages) {
240+
StageState state = s.getState();
241+
if (state != null && state.isBlockedForManualIntervention()) {
242+
ids.addAll(changeIdsFromRecoveryIssues(state.getRecoveryIssues()));
243+
}
244+
}
245+
return ids;
246+
}
247+
248+
private static List<String> changeIdsFromRecoveryIssues(List<RecoveryIssue> issues) {
249+
if (issues == null) {
250+
return Collections.emptyList();
251+
}
252+
return issues.stream()
253+
.filter(i -> i != null && i.getChangeId() != null)
254+
.map(RecoveryIssue::getChangeId)
255+
.collect(Collectors.toList());
256+
}
257+
258+
private static List<String> failedChangeIds(StageResult stage) {
259+
List<ChangeResult> changes = stage.getChanges();
260+
if (changes == null) {
261+
return Collections.emptyList();
262+
}
263+
return changes.stream()
264+
.filter(c -> c != null && c.isFailed() && c.getChangeId() != null)
265+
.map(ChangeResult::getChangeId)
266+
.collect(Collectors.toList());
267+
}
268+
269+
private static String banner(String headline) {
270+
return LINE + NEWLINE + " " + headline + NEWLINE + LINE;
271+
}
272+
}

0 commit comments

Comments
 (0)