Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ type Controller struct {
logger *zap.SugaredLogger
canaries *sync.Map
jobs map[string]CanaryJob
// namespaces we have previously reported a canary_total value for;
// used to remove stale per-namespace series when a namespace no
// longer contains any Canary resources.
trackedNamespaces map[string]struct{}
recorder metrics.Recorder
notifier notifier.Interface
canaryFactory *canary.Factory
Expand Down Expand Up @@ -123,6 +127,7 @@ func NewController(
logger: logger,
canaries: new(sync.Map),
jobs: map[string]CanaryJob{},
trackedNamespaces: map[string]struct{}{},
flaggerWindow: flaggerWindow,
observerFactory: observerFactory,
recorder: recorder,
Expand Down Expand Up @@ -180,6 +185,7 @@ func NewController(
if ok {
ctrl.logger.Infof("Deleting %s.%s from cache", r.Name, r.Namespace)
ctrl.canaries.Delete(fmt.Sprintf("%s.%s", r.Name, r.Namespace))
ctrl.recorder.DeleteFor(r.Name, r.Namespace)
}
},
})
Expand Down
21 changes: 21 additions & 0 deletions pkg/controller/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ func (c *Controller) scheduleCanaries() {
for k, v := range stats {
c.recorder.SetTotal(k, v)
}

// remove canary_total series for namespaces that no longer contain any canaries
for ns := range c.trackedNamespaces {
if _, ok := stats[ns]; !ok {
c.recorder.DeleteTotalFor(ns)
delete(c.trackedNamespaces, ns)
}
}
for ns := range stats {
c.trackedNamespaces[ns] = struct{}{}
}
}

func (c *Controller) advanceCanary(name string, namespace string) {
Expand Down Expand Up @@ -286,6 +297,12 @@ func (c *Controller) advanceCanary(name string, namespace string) {

if !shouldAdvance {
c.recorder.SetStatus(cd, cd.Status.Phase)
// keep the weight gauge populated in steady state (e.g. after a
// controller restart or right after initialization), otherwise it
// is only emitted once a canary starts progressing
if primaryWeight, canaryWeight, _, err := meshRouter.GetRoutes(cd); err == nil {
c.recorder.SetWeight(cd, primaryWeight, canaryWeight)
}
return
}

Expand Down Expand Up @@ -410,6 +427,7 @@ func (c *Controller) advanceCanary(name string, namespace string) {

c.recorder.SetStatus(cd, flaggerv1.CanaryPhaseSucceeded)
c.recorder.IncSuccesses(metrics.CanaryMetricLabels{
Canary: cd.Name,
Name: cd.Spec.TargetRef.Name,
Namespace: cd.Namespace,
DeploymentStrategy: cd.DeploymentStrategy(),
Expand Down Expand Up @@ -832,6 +850,7 @@ func (c *Controller) shouldSkipAnalysis(canary *flaggerv1.Canary, canaryControll
// notify
c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseSucceeded)
c.recorder.IncSuccesses(metrics.CanaryMetricLabels{
Canary: canary.Name,
Name: canary.Spec.TargetRef.Name,
Namespace: canary.Namespace,
DeploymentStrategy: canary.DeploymentStrategy(),
Expand Down Expand Up @@ -989,6 +1008,7 @@ func (c *Controller) rollback(canary *flaggerv1.Canary, canaryController canary.

c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseFailed)
c.recorder.IncFailures(metrics.CanaryMetricLabels{
Canary: canary.Name,
Name: canary.Spec.TargetRef.Name,
Namespace: canary.Namespace,
DeploymentStrategy: canary.DeploymentStrategy(),
Expand Down Expand Up @@ -1026,6 +1046,7 @@ func (c *Controller) handleFailedPromotion(canary *flaggerv1.Canary, canaryContr

c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseFailed)
c.recorder.IncFailures(metrics.CanaryMetricLabels{
Canary: canary.Name,
Name: canary.Spec.TargetRef.Name,
Namespace: canary.Namespace,
DeploymentStrategy: canary.DeploymentStrategy(),
Expand Down
48 changes: 36 additions & 12 deletions pkg/controller/scheduler_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func TestController_MetricsStateTransition(t *testing.T) {
mocks.makePrimaryReady(t)
mocks.ctrl.advanceCanary("podinfo", "default")

actualStatus := testutil.ToFloat64(mocks.ctrl.recorder.GetStatusMetric().WithLabelValues("podinfo", "default"))
actualStatus := testutil.ToFloat64(mocks.ctrl.recorder.GetStatusMetric().WithLabelValues("podinfo", "podinfo", "default"))
assert.Equal(t, float64(1), actualStatus)

actualTotal := testutil.ToFloat64(mocks.ctrl.recorder.GetTotalMetric().WithLabelValues("default"))
Expand All @@ -207,11 +207,11 @@ func TestController_MetricsStateTransition(t *testing.T) {
mocks.makeCanaryReady(t)
mocks.ctrl.advanceCanary("podinfo", "default")

actualStatus = testutil.ToFloat64(mocks.ctrl.recorder.GetStatusMetric().WithLabelValues("podinfo", "default"))
actualStatus = testutil.ToFloat64(mocks.ctrl.recorder.GetStatusMetric().WithLabelValues("podinfo", "podinfo", "default"))
assert.Equal(t, float64(0), actualStatus)

actualPrimaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo-primary", "default"))
actualCanaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "default"))
actualPrimaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "podinfo-primary", "default"))
actualCanaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "podinfo", "default"))

t.Logf("Progression weights - Primary: %f, Canary: %f", actualPrimaryWeight, actualCanaryWeight)
assert.GreaterOrEqual(t, actualPrimaryWeight, float64(50))
Expand All @@ -232,7 +232,7 @@ func TestController_MetricsStateTransition(t *testing.T) {
}
}

successCount := testutil.ToFloat64(mocks.ctrl.recorder.GetSuccessesMetric().WithLabelValues("podinfo", "default", "canary", "completed"))
successCount := testutil.ToFloat64(mocks.ctrl.recorder.GetSuccessesMetric().WithLabelValues("podinfo", "podinfo", "default", "canary", "completed"))
assert.Equal(t, float64(1), successCount)
})

Expand Down Expand Up @@ -267,15 +267,15 @@ func TestController_MetricsStateTransition(t *testing.T) {
mocks.ctrl.advanceCanary("podinfo", "default")
mocks.ctrl.advanceCanary("podinfo", "default")

actualStatus := testutil.ToFloat64(mocks.ctrl.recorder.GetStatusMetric().WithLabelValues("podinfo", "default"))
actualStatus := testutil.ToFloat64(mocks.ctrl.recorder.GetStatusMetric().WithLabelValues("podinfo", "podinfo", "default"))
assert.Equal(t, float64(2), actualStatus)

actualPrimaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo-primary", "default"))
actualCanaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "default"))
actualPrimaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "podinfo-primary", "default"))
actualCanaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "podinfo", "default"))
assert.Equal(t, float64(100), actualPrimaryWeight)
assert.Equal(t, float64(0), actualCanaryWeight)

failureCount := testutil.ToFloat64(mocks.ctrl.recorder.GetFailuresMetric().WithLabelValues("podinfo", "default", "canary", "completed"))
failureCount := testutil.ToFloat64(mocks.ctrl.recorder.GetFailuresMetric().WithLabelValues("podinfo", "podinfo", "default", "canary", "completed"))
assert.Equal(t, float64(1), failureCount)
})

Expand Down Expand Up @@ -303,9 +303,33 @@ func TestController_MetricsStateTransition(t *testing.T) {
mocks.makeCanaryReady(t)
mocks.ctrl.advanceCanary("podinfo", "default")

successCount := testutil.ToFloat64(mocks.ctrl.recorder.GetSuccessesMetric().WithLabelValues("podinfo", "default", "canary", "skipped"))
successCount := testutil.ToFloat64(mocks.ctrl.recorder.GetSuccessesMetric().WithLabelValues("podinfo", "podinfo", "default", "canary", "skipped"))
assert.Equal(t, float64(1), successCount)
})

t.Run("weight is emitted for an initialized canary before progression", func(t *testing.T) {
mocks := newDeploymentFixture(nil)

// drive the canary to the Initialized phase
mocks.ctrl.advanceCanary("podinfo", "default")
mocks.makePrimaryReady(t)
mocks.ctrl.advanceCanary("podinfo", "default")

c, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
require.Equal(t, flaggerv1.CanaryPhaseInitialized, c.Status.Phase)

// a steady-state reconcile (no changes, shouldAdvance == false) must still
// populate the weight gauge, otherwise it is absent until the canary progresses
mocks.ctrl.advanceCanary("podinfo", "default")

assert.Equal(t, 2, testutil.CollectAndCount(mocks.ctrl.recorder.GetWeightMetric(), "flagger_canary_weight"))

primaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "podinfo-primary", "default"))
canaryWeight := testutil.ToFloat64(mocks.ctrl.recorder.GetWeightMetric().WithLabelValues("podinfo", "podinfo", "default"))
assert.Equal(t, float64(100), primaryWeight)
assert.Equal(t, float64(0), canaryWeight)
})
}

func TestController_AnalysisMetricsRecording(t *testing.T) {
Expand Down Expand Up @@ -347,10 +371,10 @@ func TestController_AnalysisMetricsRecording(t *testing.T) {
result := mocks.ctrl.runMetricChecks(canary)
assert.True(t, result)

successRateMetric := mocks.ctrl.recorder.GetAnalysisMetric().WithLabelValues("podinfo", "default", "request-success-rate")
successRateMetric := mocks.ctrl.recorder.GetAnalysisMetric().WithLabelValues("podinfo", "podinfo", "default", "request-success-rate")
assert.NotNil(t, successRateMetric)

durationMetric := mocks.ctrl.recorder.GetAnalysisMetric().WithLabelValues("podinfo", "default", "request-duration")
durationMetric := mocks.ctrl.recorder.GetAnalysisMetric().WithLabelValues("podinfo", "podinfo", "default", "request-duration")
assert.NotNil(t, durationMetric)
})
}
46 changes: 34 additions & 12 deletions pkg/metrics/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const (

// CanaryMetricLabels holds labels for canary metrics
type CanaryMetricLabels struct {
// Canary is the name of the Canary resource
Canary string
// Name is the name of the target workload (spec.targetRef.name)
Name string
Namespace string
DeploymentStrategy string
Expand All @@ -47,7 +50,7 @@ type CanaryMetricLabels struct {

// Values returns label values as a slice for Prometheus metrics
func (c CanaryMetricLabels) Values() []string {
return []string{c.Name, c.Namespace, c.DeploymentStrategy, c.AnalysisStatus}
return []string{c.Canary, c.Name, c.Namespace, c.DeploymentStrategy, c.AnalysisStatus}
}

// Recorder records the canary analysis as Prometheus metrics
Expand Down Expand Up @@ -75,7 +78,7 @@ func NewRecorder(controller string, register bool) Recorder {
Name: "canary_duration_seconds",
Help: "Seconds spent performing canary analysis.",
Buckets: prometheus.DefBuckets,
}, []string{"name", "namespace"})
}, []string{"canary", "name", "namespace"})

total := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: controller,
Expand All @@ -88,31 +91,31 @@ func NewRecorder(controller string, register bool) Recorder {
Subsystem: controller,
Name: "canary_status",
Help: "Last canary analysis result",
}, []string{"name", "namespace"})
}, []string{"canary", "name", "namespace"})

weight := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: controller,
Name: "canary_weight",
Help: "The virtual service destination weight current value",
}, []string{"workload", "namespace"})
}, []string{"canary", "workload", "namespace"})

analysis := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: controller,
Name: "canary_metric_analysis",
Help: "Last canary analysis result per metric",
}, []string{"name", "namespace", "metric"})
}, []string{"canary", "name", "namespace", "metric"})

successes := prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: controller,
Name: "canary_successes_total",
Help: "Total number of canary successes",
}, []string{"name", "namespace", "deployment_strategy", "analysis_status"})
}, []string{"canary", "name", "namespace", "deployment_strategy", "analysis_status"})

failures := prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: controller,
Name: "canary_failures_total",
Help: "Total number of canary failures",
}, []string{"name", "namespace", "deployment_strategy", "analysis_status"})
}, []string{"canary", "name", "namespace", "deployment_strategy", "analysis_status"})

if register {
prometheus.MustRegister(info)
Expand Down Expand Up @@ -144,7 +147,7 @@ func (cr *Recorder) SetInfo(version string, meshProvider string) {

// SetDuration sets the time spent in seconds performing canary analysis
func (cr *Recorder) SetDuration(cd *flaggerv1.Canary, duration time.Duration) {
cr.duration.WithLabelValues(cd.Spec.TargetRef.Name, cd.Namespace).Observe(duration.Seconds())
cr.duration.WithLabelValues(cd.Name, cd.Spec.TargetRef.Name, cd.Namespace).Observe(duration.Seconds())
}

// SetTotal sets the total number of canaries per namespace
Expand All @@ -153,7 +156,7 @@ func (cr *Recorder) SetTotal(namespace string, total int) {
}

func (cr *Recorder) SetAnalysis(cd *flaggerv1.Canary, metricTemplateName string, val float64) {
cr.analysis.WithLabelValues(cd.Spec.TargetRef.Name, cd.Namespace, metricTemplateName).Set(val)
cr.analysis.WithLabelValues(cd.Name, cd.Spec.TargetRef.Name, cd.Namespace, metricTemplateName).Set(val)
}

// SetStatus sets the last known canary analysis status
Expand All @@ -167,13 +170,13 @@ func (cr *Recorder) SetStatus(cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase)
default:
status = 1
}
cr.status.WithLabelValues(cd.Spec.TargetRef.Name, cd.Namespace).Set(float64(status))
cr.status.WithLabelValues(cd.Name, cd.Spec.TargetRef.Name, cd.Namespace).Set(float64(status))
}

// SetWeight sets the weight values for primary and canary destinations
func (cr *Recorder) SetWeight(cd *flaggerv1.Canary, primary int, canary int) {
cr.weight.WithLabelValues(fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name), cd.Namespace).Set(float64(primary))
cr.weight.WithLabelValues(cd.Spec.TargetRef.Name, cd.Namespace).Set(float64(canary))
cr.weight.WithLabelValues(cd.Name, fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name), cd.Namespace).Set(float64(primary))
cr.weight.WithLabelValues(cd.Name, cd.Spec.TargetRef.Name, cd.Namespace).Set(float64(canary))
}

// IncSuccesses increments the total number of canary successes
Expand All @@ -186,6 +189,25 @@ func (cr *Recorder) IncFailures(labels CanaryMetricLabels) {
cr.failures.WithLabelValues(labels.Values()...).Inc()
}

// DeleteFor removes all per-canary metric series matching the given
// name and namespace. Called when a Canary resource is deleted so that
// stale series do not linger in the Prometheus registry.
func (cr *Recorder) DeleteFor(name, namespace string) {
labels := prometheus.Labels{"canary": name, "namespace": namespace}
cr.duration.DeletePartialMatch(labels)
cr.status.DeletePartialMatch(labels)
cr.weight.DeletePartialMatch(labels)
cr.analysis.DeletePartialMatch(labels)
cr.successes.DeletePartialMatch(labels)
cr.failures.DeletePartialMatch(labels)
}

// DeleteTotalFor removes the per-namespace canary_total series. Called
// once a namespace no longer contains any Canary resources.
func (cr *Recorder) DeleteTotalFor(namespace string) {
cr.total.DeleteLabelValues(namespace)
}

// GetStatusMetric returns the status metric
func (cr *Recorder) GetStatusMetric() *prometheus.GaugeVec {
return cr.status
Expand Down
Loading