Skip to content

Commit 53634dd

Browse files
Add human-readable boxcutter report summaries to logs
Introduces utility functions to format boxcutter reconciliation and teardown reports into concise, human-readable summaries. This makes debugging easier without enabling verbose V(1) logging and solves the scenarios reported over huge logs Generated-by: Cursor
1 parent 1355ff7 commit 53634dd

File tree

3 files changed

+676
-5
lines changed

3 files changed

+676
-5
lines changed

internal/operator-controller/controllers/clusterextensionrevision_controller.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
ocv1 "github.com/operator-framework/operator-controller/api/v1"
3636
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
37+
"github.com/operator-framework/operator-controller/internal/operator-controller/util"
3738
)
3839

3940
const (
@@ -158,12 +159,14 @@ func (c *ClusterExtensionRevisionReconciler) reconcile(ctx context.Context, rev
158159

159160
rres, err := c.RevisionEngine.Reconcile(ctx, *revision, opts...)
160161
if err != nil {
162+
// Generate human-readable summary for logging
161163
if rres != nil {
162-
l.Error(err, "revision reconcile failed")
163-
l.V(1).Info("reconcile failure report", "report", rres.String())
164+
summary := util.SummarizeRevisionResult(rres)
165+
l.Error(err, "revision reconcile failed", "summary", summary)
164166
} else {
165167
l.Error(err, "revision reconcile failed")
166168
}
169+
167170
meta.SetStatusCondition(&rev.Status.Conditions, metav1.Condition{
168171
Type: ocv1.ClusterExtensionRevisionTypeAvailable,
169172
Status: metav1.ConditionFalse,
@@ -320,9 +323,10 @@ func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *
320323

321324
tres, err := c.RevisionEngine.Teardown(ctx, *revision)
322325
if err != nil {
326+
// Generate human-readable summary for logging
323327
if tres != nil {
324-
l.Error(err, "revision teardown failed")
325-
l.V(1).Info("teardown failure report", "report", tres.String())
328+
summary := util.SummarizeRevisionTeardownResult(tres)
329+
l.Error(err, "revision teardown failed", "summary", summary)
326330
} else {
327331
l.Error(err, "revision teardown failed")
328332
}
@@ -337,7 +341,10 @@ func (c *ClusterExtensionRevisionReconciler) teardown(ctx context.Context, rev *
337341
}
338342

339343
// Log detailed teardown reports only in debug mode (V(1)) to reduce verbosity.
340-
l.V(1).Info("teardown report", "report", tres.String())
344+
summary := util.SummarizeRevisionTeardownResult(tres)
345+
if summary != "" {
346+
l.Info("teardown report", "report", summary)
347+
}
341348
if !tres.IsComplete() {
342349
// TODO: If it is not complete, it seems like it would be good to update
343350
// the status in some way to tell the user that the teardown is still
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
package util
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"pkg.package-operator.run/boxcutter/machinery"
8+
)
9+
10+
// SummarizeRevisionResult creates a concise, human-readable summary of a boxcutter
11+
// RevisionResult, extracting key information without the verbose details of String().
12+
// This is similar to how crdupgradesafety.conciseUnhandledMessage works for CRD diffs.
13+
func SummarizeRevisionResult(result machinery.RevisionResult) string {
14+
if result == nil {
15+
return ""
16+
}
17+
18+
var parts []string
19+
20+
// Check for validation errors first (using error interface)
21+
if verr := result.GetValidationError(); verr != nil {
22+
parts = append(parts, fmt.Sprintf("validation error: %s", verr.Error()))
23+
}
24+
25+
// Summarize phase information
26+
phases := result.GetPhases()
27+
if len(phases) > 0 {
28+
phaseSummary := summarizePhases(phases)
29+
if phaseSummary != "" {
30+
parts = append(parts, phaseSummary)
31+
}
32+
}
33+
34+
// Add completion status
35+
if !result.IsComplete() {
36+
if result.InTransistion() {
37+
parts = append(parts, "status: in transition")
38+
} else {
39+
parts = append(parts, "status: incomplete")
40+
}
41+
}
42+
43+
if len(parts) == 0 {
44+
return "reconcile completed successfully"
45+
}
46+
47+
return strings.Join(parts, "; ")
48+
}
49+
50+
// summarizePhases creates a summary of phase results, focusing on problems
51+
func summarizePhases(phases []machinery.PhaseResult) string {
52+
var problemPhases []string
53+
var successfulPhases []string
54+
55+
for _, phase := range phases {
56+
phaseName := phase.GetName()
57+
if phaseName == "" {
58+
phaseName = "unnamed"
59+
}
60+
61+
// Check for validation errors (using error interface)
62+
if verr := phase.GetValidationError(); verr != nil {
63+
problemPhases = append(problemPhases, fmt.Sprintf("%s: validation error", phaseName))
64+
continue
65+
}
66+
67+
// Check for object issues
68+
objects := phase.GetObjects()
69+
if len(objects) > 0 {
70+
objectSummary := summarizeObjects(objects)
71+
if objectSummary.hasIssues {
72+
problemPhases = append(problemPhases, fmt.Sprintf("%s: %s", phaseName, objectSummary.summary))
73+
} else if phase.IsComplete() {
74+
successfulPhases = append(successfulPhases, phaseName)
75+
}
76+
}
77+
78+
// Check phase completion status
79+
if !phase.IsComplete() && len(objects) == 0 {
80+
problemPhases = append(problemPhases, fmt.Sprintf("%s: incomplete", phaseName))
81+
}
82+
}
83+
84+
var parts []string
85+
if len(problemPhases) > 0 {
86+
parts = append(parts, fmt.Sprintf("phases with issues: %s", strings.Join(problemPhases, ", ")))
87+
}
88+
if len(successfulPhases) > 0 && len(problemPhases) == 0 {
89+
parts = append(parts, fmt.Sprintf("%d phase(s) successful", len(successfulPhases)))
90+
}
91+
92+
return strings.Join(parts, "; ")
93+
}
94+
95+
type objectSummary struct {
96+
hasIssues bool
97+
summary string
98+
}
99+
100+
// summarizeObjects creates a summary of object results
101+
func summarizeObjects(objects []machinery.ObjectResult) objectSummary {
102+
var collisions []string
103+
var failures []string
104+
var probeFailures []string
105+
successCount := 0
106+
107+
for _, obj := range objects {
108+
action := obj.Action()
109+
success := obj.Success()
110+
objInfo := getObjectInfo(obj.Object())
111+
112+
switch action {
113+
case machinery.ActionCollision:
114+
collisions = append(collisions, objInfo)
115+
default:
116+
if !success {
117+
failures = append(failures, fmt.Sprintf("%s (action: %s)", objInfo, action))
118+
} else {
119+
// Check probe results
120+
probes := obj.Probes()
121+
for probeName, probeResult := range probes {
122+
if !probeResult.Success {
123+
// Only include the probe name and status, as probeResult.Message doesn't exist
124+
probeFailures = append(probeFailures, fmt.Sprintf("%s probe '%s' failed", objInfo, probeName))
125+
}
126+
}
127+
if len(probes) == 0 || allProbesSuccessful(probes) {
128+
successCount++
129+
}
130+
}
131+
}
132+
}
133+
134+
var parts []string
135+
if len(collisions) > 0 {
136+
// Limit to first 3 collisions to avoid verbose output
137+
displayed := collisions
138+
if len(collisions) > 3 {
139+
displayed = collisions[:3]
140+
parts = append(parts, fmt.Sprintf("%d collision(s) [showing first 3: %s]", len(collisions), strings.Join(displayed, ", ")))
141+
} else {
142+
parts = append(parts, fmt.Sprintf("%d collision(s): %s", len(collisions), strings.Join(displayed, ", ")))
143+
}
144+
}
145+
if len(failures) > 0 {
146+
// Limit to first 3 failures
147+
displayed := failures
148+
if len(failures) > 3 {
149+
displayed = failures[:3]
150+
parts = append(parts, fmt.Sprintf("%d failed object(s) [showing first 3: %s]", len(failures), strings.Join(displayed, ", ")))
151+
} else {
152+
parts = append(parts, fmt.Sprintf("%d failed object(s): %s", len(failures), strings.Join(displayed, ", ")))
153+
}
154+
}
155+
if len(probeFailures) > 0 {
156+
// Limit to first 3 probe failures
157+
displayed := probeFailures
158+
if len(probeFailures) > 3 {
159+
displayed = probeFailures[:3]
160+
parts = append(parts, fmt.Sprintf("%d probe failure(s) [showing first 3: %s]", len(probeFailures), strings.Join(displayed, ", ")))
161+
} else {
162+
parts = append(parts, fmt.Sprintf("%d probe failure(s): %s", len(probeFailures), strings.Join(displayed, ", ")))
163+
}
164+
}
165+
166+
hasIssues := len(collisions) > 0 || len(failures) > 0 || len(probeFailures) > 0
167+
summary := strings.Join(parts, "; ")
168+
169+
if !hasIssues && successCount > 0 {
170+
summary = fmt.Sprintf("%d object(s) applied successfully", successCount)
171+
}
172+
173+
return objectSummary{
174+
hasIssues: hasIssues,
175+
summary: summary,
176+
}
177+
}
178+
179+
// getObjectInfo extracts a human-readable identifier from an object
180+
func getObjectInfo(obj machinery.Object) string {
181+
if obj == nil {
182+
return "unknown object"
183+
}
184+
185+
gvk := obj.GetObjectKind().GroupVersionKind()
186+
name := obj.GetName()
187+
namespace := obj.GetNamespace()
188+
189+
kind := gvk.Kind
190+
if kind == "" {
191+
kind = "unknown"
192+
}
193+
194+
if namespace != "" {
195+
return fmt.Sprintf("%s %s/%s", kind, namespace, name)
196+
}
197+
return fmt.Sprintf("%s %s", kind, name)
198+
}
199+
200+
// allProbesSuccessful checks if all probes passed
201+
func allProbesSuccessful(probes map[string]machinery.ObjectProbeResult) bool {
202+
for _, result := range probes {
203+
if !result.Success {
204+
return false
205+
}
206+
}
207+
return true
208+
}
209+
210+
// SummarizeRevisionTeardownResult creates a concise summary of a teardown result
211+
func SummarizeRevisionTeardownResult(result machinery.RevisionTeardownResult) string {
212+
if result == nil {
213+
return ""
214+
}
215+
216+
if result.IsComplete() {
217+
return "teardown completed successfully"
218+
}
219+
220+
var parts []string
221+
222+
// Check waiting phases
223+
waitingPhases := result.GetWaitingPhaseNames()
224+
if len(waitingPhases) > 0 {
225+
parts = append(parts, fmt.Sprintf("waiting on phases: %s", strings.Join(waitingPhases, ", ")))
226+
}
227+
228+
// Summarize phase teardown
229+
phases := result.GetPhases()
230+
if len(phases) > 0 {
231+
phaseSummary := summarizeTeardownPhases(phases)
232+
if phaseSummary != "" {
233+
parts = append(parts, phaseSummary)
234+
}
235+
}
236+
237+
if len(parts) == 0 {
238+
return "teardown in progress"
239+
}
240+
241+
return strings.Join(parts, "; ")
242+
}
243+
244+
// summarizeTeardownPhases creates a summary of phase teardown results
245+
func summarizeTeardownPhases(phases []machinery.PhaseTeardownResult) string {
246+
var incompletePhases []string
247+
completedCount := 0
248+
249+
for _, phase := range phases {
250+
phaseName := phase.GetName()
251+
if phaseName == "" {
252+
phaseName = "unnamed"
253+
}
254+
255+
if !phase.IsComplete() {
256+
incompletePhases = append(incompletePhases, phaseName)
257+
} else {
258+
completedCount++
259+
}
260+
}
261+
262+
var parts []string
263+
if len(incompletePhases) > 0 {
264+
parts = append(parts, fmt.Sprintf("incomplete phases: %s", strings.Join(incompletePhases, ", ")))
265+
}
266+
if completedCount > 0 {
267+
parts = append(parts, fmt.Sprintf("%d phase(s) completed", completedCount))
268+
}
269+
270+
return strings.Join(parts, "; ")
271+
}

0 commit comments

Comments
 (0)