Skip to content

Commit 98c2932

Browse files
committed
Add count metrics for canary successes and failures
Implement flagger_canary_successes_total and flagger_canary_failures_total counter metrics with deployment strategy detection and analysis status tracking for better observability of canary deployment outcomes. Signed-off-by: cappyzawa <[email protected]>
1 parent 8dee08e commit 98c2932

File tree

7 files changed

+222
-124
lines changed

7 files changed

+222
-124
lines changed

pkg/apis/flagger/v1beta1/canary.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ const (
3535
MetricInterval = "1m"
3636
)
3737

38+
// Deployment strategies
39+
const (
40+
DeploymentStrategyCanary = "canary"
41+
DeploymentStrategyBlueGreen = "blue-green"
42+
DeploymentStrategyABTesting = "ab-testing"
43+
)
44+
3845
// +genclient
3946
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
4047

@@ -640,3 +647,24 @@ func (c *Canary) SkipAnalysis() bool {
640647
}
641648
return c.Spec.SkipAnalysis
642649
}
650+
651+
// DeploymentStrategy returns the deployment strategy based on canary analysis configuration
652+
func (c *Canary) DeploymentStrategy() string {
653+
analysis := c.GetAnalysis()
654+
if analysis == nil {
655+
return DeploymentStrategyCanary
656+
}
657+
658+
// A/B Testing: has match conditions and iterations
659+
if len(analysis.Match) > 0 && analysis.Iterations > 0 {
660+
return DeploymentStrategyABTesting
661+
}
662+
663+
// Blue/Green: has iterations but no match conditions
664+
if analysis.Iterations > 0 {
665+
return DeploymentStrategyBlueGreen
666+
}
667+
668+
// Canary Release: default (has maxWeight, stepWeight, or stepWeights)
669+
return DeploymentStrategyCanary
670+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
Copyright 2025 The Flux authors
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+
17+
package v1beta1
18+
19+
import (
20+
"testing"
21+
22+
istiov1alpha1 "github.com/fluxcd/flagger/pkg/apis/istio/common/v1alpha1"
23+
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
24+
"github.com/stretchr/testify/assert"
25+
)
26+
27+
func TestCanary_GetDeploymentStrategy(t *testing.T) {
28+
tests := []struct {
29+
name string
30+
analysis *CanaryAnalysis
31+
expected string
32+
}{
33+
{
34+
name: "canary strategy with maxWeight",
35+
analysis: &CanaryAnalysis{
36+
MaxWeight: 30,
37+
StepWeight: 10,
38+
},
39+
expected: DeploymentStrategyCanary,
40+
},
41+
{
42+
name: "canary strategy with stepWeights",
43+
analysis: &CanaryAnalysis{
44+
StepWeights: []int{10, 20, 30},
45+
},
46+
expected: DeploymentStrategyCanary,
47+
},
48+
{
49+
name: "blue-green strategy with iterations",
50+
analysis: &CanaryAnalysis{
51+
Iterations: 5,
52+
},
53+
expected: DeploymentStrategyBlueGreen,
54+
},
55+
{
56+
name: "ab-testing strategy with iterations and match",
57+
analysis: &CanaryAnalysis{
58+
Iterations: 10,
59+
Match: []istiov1beta1.HTTPMatchRequest{
60+
{
61+
Headers: map[string]istiov1alpha1.StringMatch{
62+
"x-canary": {
63+
Exact: "insider",
64+
},
65+
},
66+
},
67+
},
68+
},
69+
expected: DeploymentStrategyABTesting,
70+
},
71+
{
72+
name: "default to canary when analysis is nil",
73+
analysis: nil,
74+
expected: DeploymentStrategyCanary,
75+
},
76+
{
77+
name: "default to canary when analysis is empty",
78+
analysis: &CanaryAnalysis{},
79+
expected: DeploymentStrategyCanary,
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
canary := &Canary{
86+
Spec: CanarySpec{
87+
Analysis: tt.analysis,
88+
},
89+
}
90+
result := canary.DeploymentStrategy()
91+
assert.Equal(t, tt.expected, result)
92+
})
93+
}
94+
}

pkg/controller/events.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -209,30 +209,35 @@ func alertMetadata(canary *flaggerv1.Canary) []notifier.Field {
209209
},
210210
)
211211

212-
if canary.GetAnalysis().StepWeight > 0 {
213-
fields = append(fields, notifier.Field{
214-
Name: "Traffic routing",
215-
Value: fmt.Sprintf("Weight step: %v max: %v",
216-
canary.GetAnalysis().StepWeight,
217-
canary.GetAnalysis().MaxWeight),
218-
})
219-
} else if len(canary.GetAnalysis().StepWeights) > 0 {
220-
fields = append(fields, notifier.Field{
221-
Name: "Traffic routing",
222-
Value: fmt.Sprintf("Weight steps: %s max: %v",
223-
strings.Trim(strings.Join(strings.Fields(fmt.Sprint(canary.GetAnalysis().StepWeights)), ","), "[]"),
224-
canary.GetAnalysis().MaxWeight),
225-
})
226-
} else if len(canary.GetAnalysis().Match) > 0 {
212+
strategy := canary.DeploymentStrategy()
213+
switch strategy {
214+
case flaggerv1.DeploymentStrategyABTesting:
227215
fields = append(fields, notifier.Field{
228216
Name: "Traffic routing",
229217
Value: "A/B Testing",
230218
})
231-
} else if canary.GetAnalysis().Iterations > 0 {
219+
case flaggerv1.DeploymentStrategyBlueGreen:
232220
fields = append(fields, notifier.Field{
233221
Name: "Traffic routing",
234222
Value: "Blue/Green",
235223
})
224+
default:
225+
// Canary strategy
226+
if canary.GetAnalysis().StepWeight > 0 {
227+
fields = append(fields, notifier.Field{
228+
Name: "Traffic routing",
229+
Value: fmt.Sprintf("Weight step: %v max: %v",
230+
canary.GetAnalysis().StepWeight,
231+
canary.GetAnalysis().MaxWeight),
232+
})
233+
} else if len(canary.GetAnalysis().StepWeights) > 0 {
234+
fields = append(fields, notifier.Field{
235+
Name: "Traffic routing",
236+
Value: fmt.Sprintf("Weight steps: %s max: %v",
237+
strings.Trim(strings.Join(strings.Fields(fmt.Sprint(canary.GetAnalysis().StepWeights)), ","), "[]"),
238+
canary.GetAnalysis().MaxWeight),
239+
})
240+
}
236241
}
237242
return fields
238243
}

pkg/controller/scheduler.go

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,6 @@ func (c *Controller) min(a int, b int) int {
3838
return b
3939
}
4040

41-
// getDeploymentStrategy determines the deployment strategy based on canary analysis configuration
42-
func (c *Controller) getDeploymentStrategy(canary *flaggerv1.Canary) string {
43-
analysis := canary.GetAnalysis()
44-
if analysis == nil {
45-
return metrics.CanaryStrategy
46-
}
47-
48-
// A/B Testing: has match conditions and iterations
49-
if len(analysis.Match) > 0 && analysis.Iterations > 0 {
50-
return metrics.ABTestingStrategy
51-
}
52-
53-
// Blue/Green: has iterations but no match conditions
54-
if analysis.Iterations > 0 {
55-
return metrics.BlueGreenStrategy
56-
}
57-
58-
// Canary Release: default (has maxWeight, stepWeight, or stepWeights)
59-
return metrics.CanaryStrategy
60-
}
61-
6241
func (c *Controller) maxWeight(canary *flaggerv1.Canary) int {
6342
var stepWeightsLen = len(canary.GetAnalysis().StepWeights)
6443
if stepWeightsLen > 0 {
@@ -425,7 +404,7 @@ func (c *Controller) advanceCanary(name string, namespace string) {
425404
c.recorder.IncSuccesses(metrics.CanaryMetricLabels{
426405
Name: cd.Spec.TargetRef.Name,
427406
Namespace: cd.Namespace,
428-
DeploymentStrategy: c.getDeploymentStrategy(cd),
407+
DeploymentStrategy: cd.DeploymentStrategy(),
429408
AnalysisStatus: metrics.AnalysisStatusCompleted,
430409
})
431410
c.runPostRolloutHooks(cd, flaggerv1.CanaryPhaseSucceeded)
@@ -488,14 +467,13 @@ func (c *Controller) advanceCanary(name string, namespace string) {
488467
}
489468
}
490469

491-
// strategy: A/B testing
492-
if len(cd.GetAnalysis().Match) > 0 && cd.GetAnalysis().Iterations > 0 {
470+
// check deployment strategy
471+
strategy := cd.DeploymentStrategy()
472+
switch strategy {
473+
case flaggerv1.DeploymentStrategyABTesting:
493474
c.runAB(cd, canaryController, meshRouter)
494475
return
495-
}
496-
497-
// strategy: Blue/Green
498-
if cd.GetAnalysis().Iterations > 0 {
476+
case flaggerv1.DeploymentStrategyBlueGreen:
499477
c.runBlueGreen(cd, canaryController, meshRouter, provider, mirrored)
500478
return
501479
}
@@ -845,7 +823,7 @@ func (c *Controller) shouldSkipAnalysis(canary *flaggerv1.Canary, canaryControll
845823
c.recorder.IncSuccesses(metrics.CanaryMetricLabels{
846824
Name: canary.Spec.TargetRef.Name,
847825
Namespace: canary.Namespace,
848-
DeploymentStrategy: c.getDeploymentStrategy(canary),
826+
DeploymentStrategy: canary.DeploymentStrategy(),
849827
AnalysisStatus: metrics.AnalysisStatusSkipped,
850828
})
851829
c.recordEventInfof(canary, "Promotion completed! Canary analysis was skipped for %s.%s",
@@ -998,7 +976,7 @@ func (c *Controller) rollback(canary *flaggerv1.Canary, canaryController canary.
998976
c.recorder.IncFailures(metrics.CanaryMetricLabels{
999977
Name: canary.Spec.TargetRef.Name,
1000978
Namespace: canary.Namespace,
1001-
DeploymentStrategy: c.getDeploymentStrategy(canary),
979+
DeploymentStrategy: canary.DeploymentStrategy(),
1002980
AnalysisStatus: metrics.AnalysisStatusCompleted,
1003981
})
1004982
c.runPostRolloutHooks(canary, flaggerv1.CanaryPhaseFailed)

0 commit comments

Comments
 (0)