Repository: weaveworks/flagger
Pull request #429 (2020-02-10 15:23:21)

Implement canary alerts and alert providers
This PR implements alerts on a per canary basis as described in #426 

Changes:
- Implement AlertProvider API (CRD, clientset, informer, RBAC)
- Implement Discord and Rocket notifiers
- Add alerts to the canary analysts spec
- Filter alerts based on severity fix: #427
- Add unit tests for alerts
- Refactor unit tests and fill informers cache
- Use informers cache to query for metric templates and alert providers

Fix: #426 


Patch:
diff --git a/artifacts/flagger/account.yaml b/artifacts/flagger/account.yaml
index fa85d632..68ffc3a0 100644
--- a/artifacts/flagger/account.yaml
+++ b/artifacts/flagger/account.yaml
@@ -44,6 +44,8 @@ rules:
       - canaries/status
       - metrictemplates
       - metrictemplates/status
+      - alertproviders
+      - alertproviders/status
     verbs: ["*"]
   - apiGroups:
       - networking.istio.io
diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml
index c97962e4..5bbd94de 100644
--- a/artifacts/flagger/crd.yaml
+++ b/artifacts/flagger/crd.yaml
@@ -591,3 +591,62 @@ spec:
             query:
               description: Query of this metric template
               type: string
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: alertproviders.flagger.app
+  annotations:
+    helm.sh/resource-policy: keep
+spec:
+  group: flagger.app
+  version: v1beta1
+  versions:
+    - name: v1beta1
+      served: true
+      storage: true
+  names:
+    plural: alertproviders
+    singular: alertprovider
+    kind: AlertProvider
+    categories:
+      - all
+  scope: Namespaced
+  subresources:
+    status: {}
+  additionalPrinterColumns:
+    - name: Type
+      type: string
+      JSONPath: .spec.type
+  validation:
+    openAPIV3Schema:
+      properties:
+        spec:
+          oneOf:
+            - required:
+              - type
+              - address
+            - required:
+              - type
+              - secretRef
+          properties:
+            type:
+              description: Type of this provider
+              type: string
+              enum:
+                - slack
+                - msteams
+                - discord
+                - rocket
+            address:
+              description: Hook URL address of this provider
+              type: string
+            secretRef:
+              description: Kubernetes secret reference containing the provider address
+              type: object
+              required:
+                - name
+              properties:
+                name:
+                  description: Name of the Kubernetes secret
+                  type: string
diff --git a/charts/flagger/templates/crd.yaml b/charts/flagger/templates/crd.yaml
index 360e2d67..5e3e7ebc 100644
--- a/charts/flagger/templates/crd.yaml
+++ b/charts/flagger/templates/crd.yaml
@@ -592,4 +592,63 @@ spec:
             query:
               description: Query of this metric template
               type: string
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: alertproviders.flagger.app
+  annotations:
+    helm.sh/resource-policy: keep
+spec:
+  group: flagger.app
+  version: v1beta1
+  versions:
+    - name: v1beta1
+      served: true
+      storage: true
+  names:
+    plural: alertproviders
+    singular: alertprovider
+    kind: AlertProvider
+    categories:
+      - all
+  scope: Namespaced
+  subresources:
+    status: {}
+  additionalPrinterColumns:
+    - name: Type
+      type: string
+      JSONPath: .spec.type
+  validation:
+    openAPIV3Schema:
+      properties:
+        spec:
+          oneOf:
+            - required:
+              - type
+              - address
+            - required:
+              - type
+              - secretRef
+          properties:
+            type:
+              description: Type of this provider
+              type: string
+              enum:
+                - slack
+                - msteams
+                - discord
+                - rocket
+            address:
+              description: Hook URL address of this provider
+              type: string
+            secretRef:
+              description: Kubernetes secret reference containing the provider address
+              type: object
+              required:
+                - name
+              properties:
+                name:
+                  description: Name of the Kubernetes secret
+                  type: string
 {{- end }}
diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml
index f2b40f0a..2013fe91 100644
--- a/charts/flagger/templates/rbac.yaml
+++ b/charts/flagger/templates/rbac.yaml
@@ -40,6 +40,8 @@ rules:
       - canaries/status
       - metrictemplates
       - metrictemplates/status
+      - alertproviders
+      - alertproviders/status
     verbs: ["*"]
   - apiGroups:
       - networking.istio.io
diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go
index 5b8ef376..fe5d79c8 100644
--- a/cmd/flagger/main.go
+++ b/cmd/flagger/main.go
@@ -129,6 +129,7 @@ func main() {
 
 	verifyCRDs(flaggerClient, logger)
 	verifyKubernetesVersion(kubeClient, logger)
+	infos := startInformers(flaggerClient, logger, stopCh)
 
 	labels := strings.Split(selectorLabels, ",")
 	if len(labels) < 1 {
@@ -157,23 +158,6 @@ func main() {
 	// start HTTP server
 	go server.ListenAndServe(port, 3*time.Second, logger, stopCh)
 
-	// start informers
-	flaggerInformerFactory := informers.NewSharedInformerFactoryWithOptions(flaggerClient, time.Second*30, informers.WithNamespace(namespace))
-
-	logger.Info("Waiting for canary informer cache to sync")
-	canaryInformer := flaggerInformerFactory.Flagger().V1beta1().Canaries()
-	go canaryInformer.Informer().Run(stopCh)
-	if ok := cache.WaitForNamedCacheSync("flagger", stopCh, canaryInformer.Informer().HasSynced); !ok {
-		logger.Fatalf("failed to wait for cache to sync")
-	}
-
-	logger.Info("Waiting for metric template informer cache to sync")
-	metricInformer := flaggerInformerFactory.Flagger().V1beta1().MetricTemplates()
-	go metricInformer.Informer().Run(stopCh)
-	if ok := cache.WaitForNamedCacheSync("flagger", stopCh, metricInformer.Informer().HasSynced); !ok {
-		logger.Fatalf("failed to wait for cache to sync")
-	}
-
 	routerFactory := router.NewFactory(cfg, kubeClient, flaggerClient, ingressAnnotationsPrefix, logger, meshClient)
 
 	var configTracker canary.Tracker
@@ -193,7 +177,7 @@ func main() {
 		kubeClient,
 		meshClient,
 		flaggerClient,
-		canaryInformer,
+		infos,
 		controlLoopInterval,
 		logger,
 		notifierClient,
@@ -237,6 +221,37 @@ func main() {
 	}
 }
 
+func startInformers(flaggerClient clientset.Interface, logger *zap.SugaredLogger, stopCh <-chan struct{}) controller.Informers {
+	flaggerInformerFactory := informers.NewSharedInformerFactoryWithOptions(flaggerClient, time.Second*30, informers.WithNamespace(namespace))
+
+	logger.Info("Waiting for canary informer cache to sync")
+	canaryInformer := flaggerInformerFactory.Flagger().V1beta1().Canaries()
+	go canaryInformer.Informer().Run(stopCh)
+	if ok := cache.WaitForNamedCacheSync("flagger", stopCh, canaryInformer.Informer().HasSynced); !ok {
+		logger.Fatalf("failed to wait for cache to sync")
+	}
+
+	logger.Info("Waiting for metric template informer cache to sync")
+	metricInformer := flaggerInformerFactory.Flagger().V1beta1().MetricTemplates()
+	go metricInformer.Informer().Run(stopCh)
+	if ok := cache.WaitForNamedCacheSync("flagger", stopCh, metricInformer.Informer().HasSynced); !ok {
+		logger.Fatalf("failed to wait for cache to sync")
+	}
+
+	logger.Info("Waiting for alert provider informer cache to sync")
+	alertInformer := flaggerInformerFactory.Flagger().V1beta1().AlertProviders()
+	go alertInformer.Informer().Run(stopCh)
+	if ok := cache.WaitForNamedCacheSync("flagger", stopCh, alertInformer.Informer().HasSynced); !ok {
+		logger.Fatalf("failed to wait for cache to sync")
+	}
+
+	return controller.Informers{
+		CanaryInformer: canaryInformer,
+		MetricInformer: metricInformer,
+		AlertInformer:  alertInformer,
+	}
+}
+
 func startLeaderElection(ctx context.Context, run func(), ns string, kubeClient kubernetes.Interface, logger *zap.SugaredLogger) {
 	configMapName := "flagger-leader-election"
 	id, err := os.Hostname()
@@ -322,6 +337,11 @@ func verifyCRDs(flaggerClient clientset.Interface, logger *zap.SugaredLogger) {
 	if err != nil {
 		logger.Fatalf("MetricTemplate CRD is not registered %v", err)
 	}
+
+	_, err = flaggerClient.FlaggerV1beta1().AlertProviders(namespace).List(metav1.ListOptions{Limit: 1})
+	if err != nil {
+		logger.Fatalf("AlertProvider CRD is not registered %v", err)
+	}
 }
 
 func verifyKubernetesVersion(kubeClient kubernetes.Interface, logger *zap.SugaredLogger) {
diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml
index c97962e4..5bbd94de 100644
--- a/kustomize/base/flagger/crd.yaml
+++ b/kustomize/base/flagger/crd.yaml
@@ -591,3 +591,62 @@ spec:
             query:
               description: Query of this metric template
               type: string
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: alertproviders.flagger.app
+  annotations:
+    helm.sh/resource-policy: keep
+spec:
+  group: flagger.app
+  version: v1beta1
+  versions:
+    - name: v1beta1
+      served: true
+      storage: true
+  names:
+    plural: alertproviders
+    singular: alertprovider
+    kind: AlertProvider
+    categories:
+      - all
+  scope: Namespaced
+  subresources:
+    status: {}
+  additionalPrinterColumns:
+    - name: Type
+      type: string
+      JSONPath: .spec.type
+  validation:
+    openAPIV3Schema:
+      properties:
+        spec:
+          oneOf:
+            - required:
+              - type
+              - address
+            - required:
+              - type
+              - secretRef
+          properties:
+            type:
+              description: Type of this provider
+              type: string
+              enum:
+                - slack
+                - msteams
+                - discord
+                - rocket
+            address:
+              description: Hook URL address of this provider
+              type: string
+            secretRef:
+              description: Kubernetes secret reference containing the provider address
+              type: object
+              required:
+                - name
+              properties:
+                name:
+                  description: Name of the Kubernetes secret
+                  type: string
diff --git a/kustomize/base/flagger/rbac.yaml b/kustomize/base/flagger/rbac.yaml
index 93c90c08..b4bf38f0 100644
--- a/kustomize/base/flagger/rbac.yaml
+++ b/kustomize/base/flagger/rbac.yaml
@@ -34,6 +34,8 @@ rules:
       - canaries/status
       - metrictemplates
       - metrictemplates/status
+      - alertproviders
+      - alertproviders/status
     verbs: ["*"]
   - apiGroups:
       - networking.istio.io
diff --git a/pkg/apis/flagger/v1beta1/alert.go b/pkg/apis/flagger/v1beta1/alert.go
new file mode 100644
index 00000000..90a5c923
--- /dev/null
+++ b/pkg/apis/flagger/v1beta1/alert.go
@@ -0,0 +1,95 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	AlertProviderKind = "AlertProvider"
+)
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// AlertProvider is the configuration of alerting for a specific provider
+type AlertProvider struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   AlertProviderSpec   `json:"spec"`
+	Status AlertProviderStatus `json:"status"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// AlertProviderList is a list of alert provider resources
+type AlertProviderList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []AlertProvider `json:"items"`
+}
+
+// AlertProviderSpec is the specification of the desired behavior of the AlertProvider
+type AlertProviderSpec struct {
+	// Type of provider
+	Type string `json:"type"`
+
+	// Alert channel for this provider
+	// +optional
+	Channel string `json:"channel,omitempty"`
+
+	// Bot username for this provider
+	// +optional
+	Username string `json:"username,omitempty"`
+
+	// HTTP(S) webhook address of this provider
+	// +optional
+	Address string `json:"address,omitempty"`
+
+	// Secret reference containing the provider webhook URL
+	// +optional
+	SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`
+}
+
+type AlertProviderStatus struct {
+	// Conditions of this status
+	Conditions []AlertProviderCondition `json:"conditions,omitempty"`
+}
+
+type AlertProviderCondition struct {
+	// Type of this condition
+	Type string `json:"type"`
+
+	// Status of this condition
+	Status corev1.ConditionStatus `json:"status"`
+
+	// LastUpdateTime of this condition
+	LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"`
+
+	// LastTransitionTime of this condition
+	LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
+
+	// Reason for the current status of this condition
+	Reason string `json:"reason,omitempty"`
+
+	// Message associated with this condition
+	Message string `json:"message,omitempty"`
+}
diff --git a/pkg/apis/flagger/v1beta1/canary.go b/pkg/apis/flagger/v1beta1/canary.go
index 1e3dcc40..f370f55e 100644
--- a/pkg/apis/flagger/v1beta1/canary.go
+++ b/pkg/apis/flagger/v1beta1/canary.go
@@ -21,7 +21,6 @@ import (
 	"time"
 
 	istiov1alpha3 "github.com/weaveworks/flagger/pkg/apis/istio/v1alpha3"
-	hpav1 "k8s.io/api/autoscaling/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 )
@@ -36,7 +35,9 @@ const (
 // +genclient
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 
-// Canary is a specification for a Canary resource
+// Canary is the configuration for a canary release,
+// which automatically manages the bootstrap, analysis, traffic shifting,
+// promotion or rollback of an app revision
 type Canary struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -45,111 +46,217 @@ type Canary struct {
 	Status CanaryStatus `json:"status"`
 }
 
-// CanarySpec is the spec for a Canary resource
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// CanaryList is a list of Canary resources
+type CanaryList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []Canary `json:"items"`
+}
+
+// CanarySpec is the specification of the desired behavior of the Canary
 type CanarySpec struct {
-	// if specified overwrites the -mesh-provider flag for this particular canary
+	// Provider overwrites the -mesh-provider flag for this particular canary
 	// +optional
 	Provider string `json:"provider,omitempty"`
 
-	// if specified overwrites the -metrics-server flag for this particular canary
+	// MetricsServer overwrites the -metrics-server flag for this particular canary
 	// +optional
 	MetricsServer string `json:"metricsServer,omitempty"`
 
-	// reference to target resource
-	TargetRef hpav1.CrossVersionObjectReference `json:"targetRef"`
+	// TargetRef references a target resource
+	TargetRef CrossNamespaceObjectReference `json:"targetRef"`
 
-	// reference to autoscaling resource
+	// AutoscalerRef references an autoscaling resource
 	// +optional
-	AutoscalerRef *hpav1.CrossVersionObjectReference `json:"autoscalerRef,omitempty"`
+	AutoscalerRef *CrossNamespaceObjectReference `json:"autoscalerRef,omitempty"`
 
-	// reference to NGINX ingress resource
+	// Reference to NGINX ingress resource
 	// +optional
-	IngressRef *hpav1.CrossVersionObjectReference `json:"ingressRef,omitempty"`
+	IngressRef *CrossNamespaceObjectReference `json:"ingressRef,omitempty"`
 
-	// virtual service spec
+	// Service defines how ClusterIP services, service mesh or ingress routing objects are generated
 	Service CanaryService `json:"service"`
 
-	// metrics, thresholds and webhooks spec
+	// CanaryAnalysis defines how the analysis should be performed
 	CanaryAnalysis CanaryAnalysis `json:"canaryAnalysis"`
 
-	// the maximum time in seconds for a canary deployment to make progress
-	// before it is considered to be failed. Defaults to ten minutes.
+	// ProgressDeadlineSeconds represents the maximum time in seconds for a
+	// canary deployment to make progress before it is considered to be failed
+	// +optional
 	ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty"`
 
-	// promote the canary without analysing it
+	// SkipAnalysis promotes the canary without analysing it
 	// +optional
 	SkipAnalysis bool `json:"skipAnalysis,omitempty"`
 }
 
-// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// CanaryService defines how ClusterIP services, service mesh or ingress routing objects are generated
+type CanaryService struct {
+	// Name of the Kubernetes service generated by Flagger
+	// Defaults to CanarySpec.TargetRef.Name
+	// +optional
+	Name string `json:"name,omitempty"`
 
-// CanaryList is a list of Canary resources
-type CanaryList struct {
-	metav1.TypeMeta `json:",inline"`
-	metav1.ListMeta `json:"metadata"`
+	// Port of the generated Kubernetes service
+	Port int32 `json:"port"`
 
-	Items []Canary `json:"items"`
-}
+	// Port name of the generated Kubernetes service
+	// Defaults to http
+	// +optional
+	PortName string `json:"portName,omitempty"`
 
-// CanaryService is used to create ClusterIP services
-// and service mesh or ingress routing objects
-type CanaryService struct {
-	Name          string             `json:"name,omitempty"`
-	Port          int32              `json:"port"`
-	PortName      string             `json:"portName,omitempty"`
-	TargetPort    intstr.IntOrString `json:"targetPort,omitempty"`
-	PortDiscovery bool               `json:"portDiscovery"`
-	Timeout       string             `json:"timeout,omitempty"`
-	// Istio
-	Gateways      []string                         `json:"gateways,omitempty"`
-	Hosts         []string                         `json:"hosts,omitempty"`
-	TrafficPolicy *istiov1alpha3.TrafficPolicy     `json:"trafficPolicy,omitempty"`
-	Match         []istiov1alpha3.HTTPMatchRequest `json:"match,omitempty"`
-	Rewrite       *istiov1alpha3.HTTPRewrite       `json:"rewrite,omitempty"`
-	Retries       *istiov1alpha3.HTTPRetry         `json:"retries,omitempty"`
-	Headers       *istiov1alpha3.Headers           `json:"headers,omitempty"`
-	CorsPolicy    *istiov1alpha3.CorsPolicy        `json:"corsPolicy,omitempty"`
-	// App Mesh
-	MeshName string   `json:"meshName,omitempty"`
+	// Target port number or name of the generated Kubernetes service
+	// Defaults to CanaryService.Port
+	// +optional
+	TargetPort intstr.IntOrString `json:"targetPort,omitempty"`
+
+	// PortDiscovery adds all container ports to the generated Kubernetes service
+	PortDiscovery bool `json:"portDiscovery"`
+
+	// Timeout of the HTTP or gRPC request
+	// +optional
+	Timeout string `json:"timeout,omitempty"`
+
+	// Gateways attached to the generated Istio virtual service
+	// Defaults to the internal mesh gateway
+	// +optional
+	Gateways []string `json:"gateways,omitempty"`
+
+	// Hosts attached to the generated Istio virtual service
+	// Defaults to the service name
+	// +optional
+	Hosts []string `json:"hosts,omitempty"`
+
+	// TrafficPolicy attached to the generated Istio destination rules
+	// +optional
+	TrafficPolicy *istiov1alpha3.TrafficPolicy `json:"trafficPolicy,omitempty"`
+
+	// URI match conditions for the generated service
+	// +optional
+	Match []istiov1alpha3.HTTPMatchRequest `json:"match,omitempty"`
+
+	// Rewrite HTTP URIs for the generated service
+	// +optional
+	Rewrite *istiov1alpha3.HTTPRewrite `json:"rewrite,omitempty"`
+
+	// Retries policy for the generated virtual service
+	// +optional
+	Retries *istiov1alpha3.HTTPRetry `json:"retries,omitempty"`
+
+	// Headers operations for the generated Istio virtual service
+	// +optional
+	Headers *istiov1alpha3.Headers `json:"headers,omitempty"`
+
+	// Cross-Origin Resource Sharing policy for the generated Istio virtual service
+	// +optional
+	CorsPolicy *istiov1alpha3.CorsPolicy `json:"corsPolicy,omitempty"`
+
+	// Mesh name of the generated App Mesh virtual nodes and virtual service
+	// +optional
+	MeshName string `json:"meshName,omitempty"`
+
+	// Backends of the generated App Mesh virtual nodes
+	// +optional
 	Backends []string `json:"backends,omitempty"`
 }
 
 // CanaryAnalysis is used to describe how the analysis should be done
 type CanaryAnalysis struct {
-	Interval   string                           `json:"interval"`
-	Threshold  int                              `json:"threshold"`
-	MaxWeight  int                              `json:"maxWeight"`
-	Mirror     bool                             `json:"mirror,omitempty"`
-	StepWeight int                              `json:"stepWeight"`
-	Metrics    []CanaryMetric                   `json:"metrics,omitempty"`
-	Webhooks   []CanaryWebhook                  `json:"webhooks,omitempty"`
-	Match      []istiov1alpha3.HTTPMatchRequest `json:"match,omitempty"`
-	Iterations int                              `json:"iterations,omitempty"`
+	// Schedule interval for this canary analysis
+	Interval string `json:"interval"`
+
+	// Number of checks to run for A/B Testing and Blue/Green
+	// +optional
+	Iterations int `json:"iterations,omitempty"`
+
+	//Enable traffic mirroring for Blue/Green
+	// +optional
+	Mirror bool `json:"mirror,omitempty"`
+
+	// Max traffic percentage routed to canary
+	// +optional
+	MaxWeight int `json:"maxWeight,omitempty"`
+
+	// Incremental traffic percentage step
+	// +optional
+	StepWeight int `json:"stepWeight,omitempty"`
+
+	// Max number of failed checks before the canary is terminated
+	Threshold int `json:"threshold"`
+
+	// Alert list for this canary analysis
+	Alerts []CanaryAlert `json:"alerts,omitempty"`
+
+	// Metric check list for this canary analysis
+	// +optional
+	Metrics []CanaryMetric `json:"metrics,omitempty"`
+
+	// Webhook list for this canary  analysis
+	// +optional
+	Webhooks []CanaryWebhook `json:"webhooks,omitempty"`
+
+	// A/B testing HTTP header match conditions
+	// +optional
+	Match []istiov1alpha3.HTTPMatchRequest `json:"match,omitempty"`
 }
 
 // CanaryMetric holds the reference to metrics used for canary analysis
 type CanaryMetric struct {
-	Name      string  `json:"name"`
-	Interval  string  `json:"interval,omitempty"`
+	// Name of the metric
+	Name string `json:"name"`
+
+	// Interval represents the windows size
+	Interval string `json:"interval,omitempty"`
+
+	// Max value accepted for this metric
 	Threshold float64 `json:"threshold"`
+
+	// Range value accepted for this metric
 	// +optional
 	ThresholdRange *CanaryThresholdRange `json:"thresholdRange,omitempty"`
+
+	// Prometheus query for this metric (deprecated in favor of TemplateRef)
 	// +optional
 	Query string `json:"query,omitempty"`
+
+	// TemplateRef references a metric template object
 	// +optional
-	TemplateRef *MetricTemplateRef `json:"templateRef,omitempty"`
+	TemplateRef *CrossNamespaceObjectReference `json:"templateRef,omitempty"`
 }
 
 // CanaryThresholdRange defines the range used for metrics validation
 type CanaryThresholdRange struct {
+	// Minimum value
+	// +optional
 	Min *float64 `json:"min,omitempty"`
+
+	// Maximum value
+	// +optional
 	Max *float64 `json:"max,omitempty"`
 }
 
-type MetricTemplateRef struct {
+// AlertSeverity defines alert filtering based on severity levels
+type AlertSeverity string
+
+const (
+	SeverityInfo  AlertSeverity = "info"
+	SeverityWarn  AlertSeverity = "warn"
+	SeverityError AlertSeverity = "error"
+)
+
+// CanaryAlert defines an alert for this canary
+type CanaryAlert struct {
+	// Name of the alert
 	Name string `json:"name"`
-	// +optional
-	Namespace string `json:"namespace,omitempty"`
+
+	// Severity level: info, warn, error (default info)
+	Severity AlertSeverity `json:"severity,omitempty"`
+
+	// Alert provider reference
+	ProviderRef CrossNamespaceObjectReference `json:"providerRef"`
 }
 
 // HookType can be pre, post or during rollout
@@ -168,26 +275,61 @@ const (
 	ConfirmPromotionHook HookType = "confirm-promotion"
 	// EventHook dispatches Flagger events to the specified endpoint
 	EventHook HookType = "event"
-	// RollbackHook rollback canary anaylysis if webhook returns HTTP 200
+	// RollbackHook rollback canary analysis if webhook returns HTTP 200
 	RollbackHook HookType = "rollback"
 )
 
 // CanaryWebhook holds the reference to external checks used for canary analysis
 type CanaryWebhook struct {
-	Type    HookType `json:"type"`
-	Name    string   `json:"name"`
-	URL     string   `json:"url"`
-	Timeout string   `json:"timeout"`
+	// Type of this webhook
+	Type HookType `json:"type"`
+
+	// Name of this webhook
+	Name string `json:"name"`
+
+	// URL address of this webhook
+	URL string `json:"url"`
+
+	// Request timeout for this webhook
+	Timeout string `json:"timeout"`
+
+	// Metadata (key-value pairs) for this webhook
 	// +optional
 	Metadata *map[string]string `json:"metadata,omitempty"`
 }
 
 // CanaryWebhookPayload holds the deployment info and metadata sent to webhooks
 type CanaryWebhookPayload struct {
-	Name      string            `json:"name"`
-	Namespace string            `json:"namespace"`
-	Phase     CanaryPhase       `json:"phase"`
-	Metadata  map[string]string `json:"metadata,omitempty"`
+	// Name of the canary
+	Name string `json:"name"`
+
+	// Namespace of the canary
+	Namespace string `json:"namespace"`
+
+	// Phase of the canary analysis
+	Phase CanaryPhase `json:"phase"`
+
+	// Metadata (key-value pairs) for this webhook
+	Metadata map[string]string `json:"metadata,omitempty"`
+}
+
+// CrossNamespaceObjectReference contains enough information to let you locate the
+// typed referenced object at cluster level
+type CrossNamespaceObjectReference struct {
+	// API version of the referent
+	// +optional
+	APIVersion string `json:"apiVersion,omitempty"`
+
+	// Kind of the referent
+	// +optional
+	Kind string `json:"kind,omitempty"`
+
+	// Name of the referent
+	Name string `json:"name"`
+
+	// Namespace of the referent
+	// +optional
+	Namespace string `json:"namespace,omitempty"`
 }
 
 // GetServiceNames returns the apex, primary and canary Kubernetes service names
diff --git a/pkg/apis/flagger/v1beta1/register.go b/pkg/apis/flagger/v1beta1/register.go
index d883b692..9c5f6f78 100755
--- a/pkg/apis/flagger/v1beta1/register.go
+++ b/pkg/apis/flagger/v1beta1/register.go
@@ -49,6 +49,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
 		&CanaryList{},
 		&MetricTemplate{},
 		&MetricTemplateList{},
+		&AlertProvider{},
+		&AlertProviderList{},
 	)
 	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
 	return nil
diff --git a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go
index c4d6e874..c825b121 100644
--- a/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/flagger/v1beta1/zz_generated.deepcopy.go
@@ -22,11 +22,133 @@ package v1beta1
 
 import (
 	v1alpha3 "github.com/weaveworks/flagger/pkg/apis/istio/v1alpha3"
-	v1 "k8s.io/api/autoscaling/v1"
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlertProvider) DeepCopyInto(out *AlertProvider) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertProvider.
+func (in *AlertProvider) DeepCopy() *AlertProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(AlertProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AlertProvider) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlertProviderCondition) DeepCopyInto(out *AlertProviderCondition) {
+	*out = *in
+	in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime)
+	in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertProviderCondition.
+func (in *AlertProviderCondition) DeepCopy() *AlertProviderCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(AlertProviderCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlertProviderList) DeepCopyInto(out *AlertProviderList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]AlertProvider, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertProviderList.
+func (in *AlertProviderList) DeepCopy() *AlertProviderList {
+	if in == nil {
+		return nil
+	}
+	out := new(AlertProviderList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AlertProviderList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlertProviderSpec) DeepCopyInto(out *AlertProviderSpec) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(v1.LocalObjectReference)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertProviderSpec.
+func (in *AlertProviderSpec) DeepCopy() *AlertProviderSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(AlertProviderSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AlertProviderStatus) DeepCopyInto(out *AlertProviderStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]AlertProviderCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertProviderStatus.
+func (in *AlertProviderStatus) DeepCopy() *AlertProviderStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(AlertProviderStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Canary) DeepCopyInto(out *Canary) {
 	*out = *in
@@ -55,9 +177,31 @@ func (in *Canary) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CanaryAlert) DeepCopyInto(out *CanaryAlert) {
+	*out = *in
+	out.ProviderRef = in.ProviderRef
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryAlert.
+func (in *CanaryAlert) DeepCopy() *CanaryAlert {
+	if in == nil {
+		return nil
+	}
+	out := new(CanaryAlert)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CanaryAnalysis) DeepCopyInto(out *CanaryAnalysis) {
 	*out = *in
+	if in.Alerts != nil {
+		in, out := &in.Alerts, &out.Alerts
+		*out = make([]CanaryAlert, len(*in))
+		copy(*out, *in)
+	}
 	if in.Metrics != nil {
 		in, out := &in.Metrics, &out.Metrics
 		*out = make([]CanaryMetric, len(*in))
@@ -153,7 +297,7 @@ func (in *CanaryMetric) DeepCopyInto(out *CanaryMetric) {
 	}
 	if in.TemplateRef != nil {
 		in, out := &in.TemplateRef, &out.TemplateRef
-		*out = new(MetricTemplateRef)
+		*out = new(CrossNamespaceObjectReference)
 		**out = **in
 	}
 	return
@@ -239,12 +383,12 @@ func (in *CanarySpec) DeepCopyInto(out *CanarySpec) {
 	out.TargetRef = in.TargetRef
 	if in.AutoscalerRef != nil {
 		in, out := &in.AutoscalerRef, &out.AutoscalerRef
-		*out = new(v1.CrossVersionObjectReference)
+		*out = new(CrossNamespaceObjectReference)
 		**out = **in
 	}
 	if in.IngressRef != nil {
 		in, out := &in.IngressRef, &out.IngressRef
-		*out = new(v1.CrossVersionObjectReference)
+		*out = new(CrossNamespaceObjectReference)
 		**out = **in
 	}
 	in.Service.DeepCopyInto(&out.Service)
@@ -378,6 +522,22 @@ func (in *CanaryWebhookPayload) DeepCopy() *CanaryWebhookPayload {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CrossNamespaceObjectReference) DeepCopyInto(out *CrossNamespaceObjectReference) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceObjectReference.
+func (in *CrossNamespaceObjectReference) DeepCopy() *CrossNamespaceObjectReference {
+	if in == nil {
+		return nil
+	}
+	out := new(CrossNamespaceObjectReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MetricTemplate) DeepCopyInto(out *MetricTemplate) {
 	*out = *in
@@ -478,7 +638,7 @@ func (in *MetricTemplateProvider) DeepCopyInto(out *MetricTemplateProvider) {
 	*out = *in
 	if in.SecretRef != nil {
 		in, out := &in.SecretRef, &out.SecretRef
-		*out = new(corev1.LocalObjectReference)
+		*out = new(v1.LocalObjectReference)
 		**out = **in
 	}
 	return
@@ -494,22 +654,6 @@ func (in *MetricTemplateProvider) DeepCopy() *MetricTemplateProvider {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *MetricTemplateRef) DeepCopyInto(out *MetricTemplateRef) {
-	*out = *in
-	return
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricTemplateRef.
-func (in *MetricTemplateRef) DeepCopy() *MetricTemplateRef {
-	if in == nil {
-		return nil
-	}
-	out := new(MetricTemplateRef)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MetricTemplateSpec) DeepCopyInto(out *MetricTemplateSpec) {
 	*out = *in
diff --git a/pkg/canary/mock.go b/pkg/canary/fixture.go
similarity index 95%
rename from pkg/canary/mock.go
rename to pkg/canary/fixture.go
index 6e93a5ad..49727de7 100644
--- a/pkg/canary/mock.go
+++ b/pkg/canary/fixture.go
@@ -4,7 +4,6 @@ import (
 	"github.com/weaveworks/flagger/pkg/logger"
 	"go.uber.org/zap"
 	appsv1 "k8s.io/api/apps/v1"
-	hpav1 "k8s.io/api/autoscaling/v1"
 	hpav2 "k8s.io/api/autoscaling/v2beta1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -16,7 +15,7 @@ import (
 	fakeFlagger "github.com/weaveworks/flagger/pkg/client/clientset/versioned/fake"
 )
 
-type Mocks struct {
+type fixture struct {
 	canary        *flaggerv1.Canary
 	kubeClient    kubernetes.Interface
 	flaggerClient clientset.Interface
@@ -24,7 +23,7 @@ type Mocks struct {
 	logger        *zap.SugaredLogger
 }
 
-func SetupMocks() Mocks {
+func newFixture() fixture {
 	// init canary
 	canary := newTestCanary()
 	flaggerClient := fakeFlagger.NewSimpleClientset(canary)
@@ -55,7 +54,7 @@ func SetupMocks() Mocks {
 		},
 	}
 
-	return Mocks{
+	return fixture{
 		canary:        canary,
 		deployer:      deployer,
 		logger:        logger,
@@ -181,12 +180,12 @@ func newTestCanary() *flaggerv1.Canary {
 			Name:      "podinfo",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
 			},
-			AutoscalerRef: &hpav1.CrossVersionObjectReference{
+			AutoscalerRef: &flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "autoscaling/v2beta1",
 				Kind:       "HorizontalPodAutoscaler",
@@ -196,18 +195,6 @@ func newTestCanary() *flaggerv1.Canary {
 				Threshold:  10,
 				StepWeight: 10,
 				MaxWeight:  50,
-				Metrics: []flaggerv1.CanaryMetric{
-					{
-						Name:      "istio_requests_total",
-						Threshold: 99,
-						Interval:  "1m",
-					},
-					{
-						Name:      "istio_request_duration_seconds_bucket",
-						Threshold: 500,
-						Interval:  "1m",
-					},
-				},
 			},
 		},
 	}
diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/alertprovider.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/alertprovider.go
new file mode 100644
index 00000000..99ba1478
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/alertprovider.go
@@ -0,0 +1,191 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	"time"
+
+	v1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	scheme "github.com/weaveworks/flagger/pkg/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// AlertProvidersGetter has a method to return a AlertProviderInterface.
+// A group's client should implement this interface.
+type AlertProvidersGetter interface {
+	AlertProviders(namespace string) AlertProviderInterface
+}
+
+// AlertProviderInterface has methods to work with AlertProvider resources.
+type AlertProviderInterface interface {
+	Create(*v1beta1.AlertProvider) (*v1beta1.AlertProvider, error)
+	Update(*v1beta1.AlertProvider) (*v1beta1.AlertProvider, error)
+	UpdateStatus(*v1beta1.AlertProvider) (*v1beta1.AlertProvider, error)
+	Delete(name string, options *v1.DeleteOptions) error
+	DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
+	Get(name string, options v1.GetOptions) (*v1beta1.AlertProvider, error)
+	List(opts v1.ListOptions) (*v1beta1.AlertProviderList, error)
+	Watch(opts v1.ListOptions) (watch.Interface, error)
+	Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.AlertProvider, err error)
+	AlertProviderExpansion
+}
+
+// alertProviders implements AlertProviderInterface
+type alertProviders struct {
+	client rest.Interface
+	ns     string
+}
+
+// newAlertProviders returns a AlertProviders
+func newAlertProviders(c *FlaggerV1beta1Client, namespace string) *alertProviders {
+	return &alertProviders{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the alertProvider, and returns the corresponding alertProvider object, and an error if there is any.
+func (c *alertProviders) Get(name string, options v1.GetOptions) (result *v1beta1.AlertProvider, err error) {
+	result = &v1beta1.AlertProvider{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of AlertProviders that match those selectors.
+func (c *alertProviders) List(opts v1.ListOptions) (result *v1beta1.AlertProviderList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1beta1.AlertProviderList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do().
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested alertProviders.
+func (c *alertProviders) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch()
+}
+
+// Create takes the representation of a alertProvider and creates it.  Returns the server's representation of the alertProvider, and an error, if there is any.
+func (c *alertProviders) Create(alertProvider *v1beta1.AlertProvider) (result *v1beta1.AlertProvider, err error) {
+	result = &v1beta1.AlertProvider{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		Body(alertProvider).
+		Do().
+		Into(result)
+	return
+}
+
+// Update takes the representation of a alertProvider and updates it. Returns the server's representation of the alertProvider, and an error, if there is any.
+func (c *alertProviders) Update(alertProvider *v1beta1.AlertProvider) (result *v1beta1.AlertProvider, err error) {
+	result = &v1beta1.AlertProvider{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		Name(alertProvider.Name).
+		Body(alertProvider).
+		Do().
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+
+func (c *alertProviders) UpdateStatus(alertProvider *v1beta1.AlertProvider) (result *v1beta1.AlertProvider, err error) {
+	result = &v1beta1.AlertProvider{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		Name(alertProvider.Name).
+		SubResource("status").
+		Body(alertProvider).
+		Do().
+		Into(result)
+	return
+}
+
+// Delete takes name of the alertProvider and deletes it. Returns an error if one occurs.
+func (c *alertProviders) Delete(name string, options *v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		Name(name).
+		Body(options).
+		Do().
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *alertProviders) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	var timeout time.Duration
+	if listOptions.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("alertproviders").
+		VersionedParams(&listOptions, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(options).
+		Do().
+		Error()
+}
+
+// Patch applies the patch and returns the patched alertProvider.
+func (c *alertProviders) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.AlertProvider, err error) {
+	result = &v1beta1.AlertProvider{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("alertproviders").
+		SubResource(subresources...).
+		Name(name).
+		Body(data).
+		Do().
+		Into(result)
+	return
+}
diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_alertprovider.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_alertprovider.go
new file mode 100644
index 00000000..f746e39e
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_alertprovider.go
@@ -0,0 +1,140 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeAlertProviders implements AlertProviderInterface
+type FakeAlertProviders struct {
+	Fake *FakeFlaggerV1beta1
+	ns   string
+}
+
+var alertprovidersResource = schema.GroupVersionResource{Group: "flagger.app", Version: "v1beta1", Resource: "alertproviders"}
+
+var alertprovidersKind = schema.GroupVersionKind{Group: "flagger.app", Version: "v1beta1", Kind: "AlertProvider"}
+
+// Get takes name of the alertProvider, and returns the corresponding alertProvider object, and an error if there is any.
+func (c *FakeAlertProviders) Get(name string, options v1.GetOptions) (result *v1beta1.AlertProvider, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(alertprovidersResource, c.ns, name), &v1beta1.AlertProvider{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.AlertProvider), err
+}
+
+// List takes label and field selectors, and returns the list of AlertProviders that match those selectors.
+func (c *FakeAlertProviders) List(opts v1.ListOptions) (result *v1beta1.AlertProviderList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(alertprovidersResource, alertprovidersKind, c.ns, opts), &v1beta1.AlertProviderList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta1.AlertProviderList{ListMeta: obj.(*v1beta1.AlertProviderList).ListMeta}
+	for _, item := range obj.(*v1beta1.AlertProviderList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested alertProviders.
+func (c *FakeAlertProviders) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(alertprovidersResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a alertProvider and creates it.  Returns the server's representation of the alertProvider, and an error, if there is any.
+func (c *FakeAlertProviders) Create(alertProvider *v1beta1.AlertProvider) (result *v1beta1.AlertProvider, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(alertprovidersResource, c.ns, alertProvider), &v1beta1.AlertProvider{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.AlertProvider), err
+}
+
+// Update takes the representation of a alertProvider and updates it. Returns the server's representation of the alertProvider, and an error, if there is any.
+func (c *FakeAlertProviders) Update(alertProvider *v1beta1.AlertProvider) (result *v1beta1.AlertProvider, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(alertprovidersResource, c.ns, alertProvider), &v1beta1.AlertProvider{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.AlertProvider), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeAlertProviders) UpdateStatus(alertProvider *v1beta1.AlertProvider) (*v1beta1.AlertProvider, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(alertprovidersResource, "status", c.ns, alertProvider), &v1beta1.AlertProvider{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.AlertProvider), err
+}
+
+// Delete takes name of the alertProvider and deletes it. Returns an error if one occurs.
+func (c *FakeAlertProviders) Delete(name string, options *v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(alertprovidersResource, c.ns, name), &v1beta1.AlertProvider{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeAlertProviders) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(alertprovidersResource, c.ns, listOptions)
+
+	_, err := c.Fake.Invokes(action, &v1beta1.AlertProviderList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched alertProvider.
+func (c *FakeAlertProviders) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.AlertProvider, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(alertprovidersResource, c.ns, name, pt, data, subresources...), &v1beta1.AlertProvider{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta1.AlertProvider), err
+}
diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go
index 7655b98a..b75844eb 100644
--- a/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go
+++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/fake/fake_flagger_client.go
@@ -28,6 +28,10 @@ type FakeFlaggerV1beta1 struct {
 	*testing.Fake
 }
 
+func (c *FakeFlaggerV1beta1) AlertProviders(namespace string) v1beta1.AlertProviderInterface {
+	return &FakeAlertProviders{c, namespace}
+}
+
 func (c *FakeFlaggerV1beta1) Canaries(namespace string) v1beta1.CanaryInterface {
 	return &FakeCanaries{c, namespace}
 }
diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go
index 3caaafa3..6d2aee20 100644
--- a/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go
+++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/flagger_client.go
@@ -26,6 +26,7 @@ import (
 
 type FlaggerV1beta1Interface interface {
 	RESTClient() rest.Interface
+	AlertProvidersGetter
 	CanariesGetter
 	MetricTemplatesGetter
 }
@@ -35,6 +36,10 @@ type FlaggerV1beta1Client struct {
 	restClient rest.Interface
 }
 
+func (c *FlaggerV1beta1Client) AlertProviders(namespace string) AlertProviderInterface {
+	return newAlertProviders(c, namespace)
+}
+
 func (c *FlaggerV1beta1Client) Canaries(namespace string) CanaryInterface {
 	return newCanaries(c, namespace)
 }
diff --git a/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go b/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go
index 08afc9e4..2d9f05ef 100644
--- a/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go
+++ b/pkg/client/clientset/versioned/typed/flagger/v1beta1/generated_expansion.go
@@ -18,6 +18,8 @@ limitations under the License.
 
 package v1beta1
 
+type AlertProviderExpansion interface{}
+
 type CanaryExpansion interface{}
 
 type MetricTemplateExpansion interface{}
diff --git a/pkg/client/informers/externalversions/flagger/v1beta1/alertprovider.go b/pkg/client/informers/externalversions/flagger/v1beta1/alertprovider.go
new file mode 100644
index 00000000..88b34223
--- /dev/null
+++ b/pkg/client/informers/externalversions/flagger/v1beta1/alertprovider.go
@@ -0,0 +1,89 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	time "time"
+
+	flaggerv1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	versioned "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+	v1beta1 "github.com/weaveworks/flagger/pkg/client/listers/flagger/v1beta1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// AlertProviderInformer provides access to a shared informer and lister for
+// AlertProviders.
+type AlertProviderInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta1.AlertProviderLister
+}
+
+type alertProviderInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewAlertProviderInformer constructs a new informer for AlertProvider type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewAlertProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredAlertProviderInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredAlertProviderInformer constructs a new informer for AlertProvider type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredAlertProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.FlaggerV1beta1().AlertProviders(namespace).List(options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.FlaggerV1beta1().AlertProviders(namespace).Watch(options)
+			},
+		},
+		&flaggerv1beta1.AlertProvider{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *alertProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredAlertProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *alertProviderInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&flaggerv1beta1.AlertProvider{}, f.defaultInformer)
+}
+
+func (f *alertProviderInformer) Lister() v1beta1.AlertProviderLister {
+	return v1beta1.NewAlertProviderLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/flagger/v1beta1/interface.go b/pkg/client/informers/externalversions/flagger/v1beta1/interface.go
index a15b3589..ae1b83dd 100644
--- a/pkg/client/informers/externalversions/flagger/v1beta1/interface.go
+++ b/pkg/client/informers/externalversions/flagger/v1beta1/interface.go
@@ -24,6 +24,8 @@ import (
 
 // Interface provides access to all the informers in this group version.
 type Interface interface {
+	// AlertProviders returns a AlertProviderInformer.
+	AlertProviders() AlertProviderInformer
 	// Canaries returns a CanaryInformer.
 	Canaries() CanaryInformer
 	// MetricTemplates returns a MetricTemplateInformer.
@@ -41,6 +43,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
 }
 
+// AlertProviders returns a AlertProviderInformer.
+func (v *version) AlertProviders() AlertProviderInformer {
+	return &alertProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
 // Canaries returns a CanaryInformer.
 func (v *version) Canaries() CanaryInformer {
 	return &canaryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index deb747c3..f5ed4731 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -66,6 +66,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Appmesh().V1beta1().VirtualServices().Informer()}, nil
 
 		// Group=flagger.app, Version=v1beta1
+	case flaggerv1beta1.SchemeGroupVersion.WithResource("alertproviders"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Flagger().V1beta1().AlertProviders().Informer()}, nil
 	case flaggerv1beta1.SchemeGroupVersion.WithResource("canaries"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Flagger().V1beta1().Canaries().Informer()}, nil
 	case flaggerv1beta1.SchemeGroupVersion.WithResource("metrictemplates"):
diff --git a/pkg/client/listers/flagger/v1beta1/alertprovider.go b/pkg/client/listers/flagger/v1beta1/alertprovider.go
new file mode 100644
index 00000000..fa3ce43e
--- /dev/null
+++ b/pkg/client/listers/flagger/v1beta1/alertprovider.go
@@ -0,0 +1,94 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	v1beta1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// AlertProviderLister helps list AlertProviders.
+type AlertProviderLister interface {
+	// List lists all AlertProviders in the indexer.
+	List(selector labels.Selector) (ret []*v1beta1.AlertProvider, err error)
+	// AlertProviders returns an object that can list and get AlertProviders.
+	AlertProviders(namespace string) AlertProviderNamespaceLister
+	AlertProviderListerExpansion
+}
+
+// alertProviderLister implements the AlertProviderLister interface.
+type alertProviderLister struct {
+	indexer cache.Indexer
+}
+
+// NewAlertProviderLister returns a new AlertProviderLister.
+func NewAlertProviderLister(indexer cache.Indexer) AlertProviderLister {
+	return &alertProviderLister{indexer: indexer}
+}
+
+// List lists all AlertProviders in the indexer.
+func (s *alertProviderLister) List(selector labels.Selector) (ret []*v1beta1.AlertProvider, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.AlertProvider))
+	})
+	return ret, err
+}
+
+// AlertProviders returns an object that can list and get AlertProviders.
+func (s *alertProviderLister) AlertProviders(namespace string) AlertProviderNamespaceLister {
+	return alertProviderNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// AlertProviderNamespaceLister helps list and get AlertProviders.
+type AlertProviderNamespaceLister interface {
+	// List lists all AlertProviders in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1beta1.AlertProvider, err error)
+	// Get retrieves the AlertProvider from the indexer for a given namespace and name.
+	Get(name string) (*v1beta1.AlertProvider, error)
+	AlertProviderNamespaceListerExpansion
+}
+
+// alertProviderNamespaceLister implements the AlertProviderNamespaceLister
+// interface.
+type alertProviderNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all AlertProviders in the indexer for a given namespace.
+func (s alertProviderNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.AlertProvider, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta1.AlertProvider))
+	})
+	return ret, err
+}
+
+// Get retrieves the AlertProvider from the indexer for a given namespace and name.
+func (s alertProviderNamespaceLister) Get(name string) (*v1beta1.AlertProvider, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta1.Resource("alertprovider"), name)
+	}
+	return obj.(*v1beta1.AlertProvider), nil
+}
diff --git a/pkg/client/listers/flagger/v1beta1/expansion_generated.go b/pkg/client/listers/flagger/v1beta1/expansion_generated.go
index 625cbd66..d26fc6d2 100644
--- a/pkg/client/listers/flagger/v1beta1/expansion_generated.go
+++ b/pkg/client/listers/flagger/v1beta1/expansion_generated.go
@@ -18,6 +18,14 @@ limitations under the License.
 
 package v1beta1
 
+// AlertProviderListerExpansion allows custom methods to be added to
+// AlertProviderLister.
+type AlertProviderListerExpansion interface{}
+
+// AlertProviderNamespaceListerExpansion allows custom methods to be added to
+// AlertProviderNamespaceLister.
+type AlertProviderNamespaceListerExpansion interface{}
+
 // CanaryListerExpansion allows custom methods to be added to
 // CanaryLister.
 type CanaryListerExpansion interface{}
diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go
index 6239ecbd..e3677d71 100644
--- a/pkg/controller/controller.go
+++ b/pkg/controller/controller.go
@@ -24,7 +24,6 @@ import (
 	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
 	flaggerscheme "github.com/weaveworks/flagger/pkg/client/clientset/versioned/scheme"
 	flaggerinformers "github.com/weaveworks/flagger/pkg/client/informers/externalversions/flagger/v1beta1"
-	flaggerlisters "github.com/weaveworks/flagger/pkg/client/listers/flagger/v1beta1"
 	"github.com/weaveworks/flagger/pkg/metrics"
 	"github.com/weaveworks/flagger/pkg/metrics/observers"
 	"github.com/weaveworks/flagger/pkg/notifier"
@@ -35,31 +34,37 @@ const controllerAgentName = "flagger"
 
 // Controller is managing the canary objects and schedules canary deployments
 type Controller struct {
-	kubeClient      kubernetes.Interface
-	istioClient     clientset.Interface
-	flaggerClient   clientset.Interface
-	flaggerLister   flaggerlisters.CanaryLister
-	flaggerSynced   cache.InformerSynced
-	flaggerWindow   time.Duration
-	workqueue       workqueue.RateLimitingInterface
-	eventRecorder   record.EventRecorder
-	logger          *zap.SugaredLogger
-	canaries        *sync.Map
-	jobs            map[string]CanaryJob
-	recorder        metrics.Recorder
-	notifier        notifier.Interface
-	canaryFactory   *canary.Factory
-	routerFactory   *router.Factory
-	observerFactory *observers.Factory
-	meshProvider    string
-	eventWebhook    string
+	kubeClient       kubernetes.Interface
+	istioClient      clientset.Interface
+	flaggerClient    clientset.Interface
+	flaggerInformers Informers
+	flaggerSynced    cache.InformerSynced
+	flaggerWindow    time.Duration
+	workqueue        workqueue.RateLimitingInterface
+	eventRecorder    record.EventRecorder
+	logger           *zap.SugaredLogger
+	canaries         *sync.Map
+	jobs             map[string]CanaryJob
+	recorder         metrics.Recorder
+	notifier         notifier.Interface
+	canaryFactory    *canary.Factory
+	routerFactory    *router.Factory
+	observerFactory  *observers.Factory
+	meshProvider     string
+	eventWebhook     string
+}
+
+type Informers struct {
+	CanaryInformer flaggerinformers.CanaryInformer
+	MetricInformer flaggerinformers.MetricTemplateInformer
+	AlertInformer  flaggerinformers.AlertProviderInformer
 }
 
 func NewController(
 	kubeClient kubernetes.Interface,
 	istioClient clientset.Interface,
 	flaggerClient clientset.Interface,
-	flaggerInformer flaggerinformers.CanaryInformer,
+	flaggerInformers Informers,
 	flaggerWindow time.Duration,
 	logger *zap.SugaredLogger,
 	notifier notifier.Interface,
@@ -83,27 +88,27 @@ func NewController(
 	recorder.SetInfo(version, meshProvider)
 
 	ctrl := &Controller{
-		kubeClient:      kubeClient,
-		istioClient:     istioClient,
-		flaggerClient:   flaggerClient,
-		flaggerLister:   flaggerInformer.Lister(),
-		flaggerSynced:   flaggerInformer.Informer().HasSynced,
-		workqueue:       workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName),
-		eventRecorder:   eventRecorder,
-		logger:          logger,
-		canaries:        new(sync.Map),
-		jobs:            map[string]CanaryJob{},
-		flaggerWindow:   flaggerWindow,
-		observerFactory: observerFactory,
-		recorder:        recorder,
-		notifier:        notifier,
-		canaryFactory:   canaryFactory,
-		routerFactory:   routerFactory,
-		meshProvider:    meshProvider,
-		eventWebhook:    eventWebhook,
+		kubeClient:       kubeClient,
+		istioClient:      istioClient,
+		flaggerClient:    flaggerClient,
+		flaggerInformers: flaggerInformers,
+		flaggerSynced:    flaggerInformers.CanaryInformer.Informer().HasSynced,
+		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName),
+		eventRecorder:    eventRecorder,
+		logger:           logger,
+		canaries:         new(sync.Map),
+		jobs:             map[string]CanaryJob{},
+		flaggerWindow:    flaggerWindow,
+		observerFactory:  observerFactory,
+		recorder:         recorder,
+		notifier:         notifier,
+		canaryFactory:    canaryFactory,
+		routerFactory:    routerFactory,
+		meshProvider:     meshProvider,
+		eventWebhook:     eventWebhook,
 	}
 
-	flaggerInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+	flaggerInformers.CanaryInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 		AddFunc: ctrl.enqueue,
 		UpdateFunc: func(old, new interface{}) {
 			oldCanary, ok := checkCustomResourceType(old, logger)
@@ -209,7 +214,7 @@ func (c *Controller) syncHandler(key string) error {
 		utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
 		return nil
 	}
-	cd, err := c.flaggerLister.Canaries(namespace).Get(name)
+	cd, err := c.flaggerInformers.CanaryInformer.Lister().Canaries(namespace).Get(name)
 	if errors.IsNotFound(err) {
 		utilruntime.HandleError(fmt.Errorf("%s in work queue no longer exists", key))
 		return nil
@@ -256,94 +261,6 @@ func checkCustomResourceType(obj interface{}, logger *zap.SugaredLogger) (flagge
 	return *roll, true
 }
 
-func (c *Controller) sendEventToWebhook(r *flaggerv1.Canary, eventtype, template string, args []interface{}) {
-	webhookOverride := false
-	if len(r.Spec.CanaryAnalysis.Webhooks) > 0 {
-		for _, canaryWebhook := range r.Spec.CanaryAnalysis.Webhooks {
-			if canaryWebhook.Type == flaggerv1.EventHook {
-				webhookOverride = true
-				err := CallEventWebhook(r, canaryWebhook.URL, fmt.Sprintf(template, args...), eventtype)
-				if err != nil {
-					c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Errorf("error sending event to webhook: %s", err)
-				}
-			}
-		}
-	}
-
-	if c.eventWebhook != "" && !webhookOverride {
-		err := CallEventWebhook(r, c.eventWebhook, fmt.Sprintf(template, args...), eventtype)
-		if err != nil {
-			c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Errorf("error sending event to webhook: %s", err)
-		}
-	}
-}
-
-func (c *Controller) recordEventInfof(r *flaggerv1.Canary, template string, args ...interface{}) {
-	c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Infof(template, args...)
-	c.eventRecorder.Event(r, corev1.EventTypeNormal, "Synced", fmt.Sprintf(template, args...))
-	c.sendEventToWebhook(r, corev1.EventTypeNormal, template, args)
-}
-
-func (c *Controller) recordEventErrorf(r *flaggerv1.Canary, template string, args ...interface{}) {
-	c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Errorf(template, args...)
-	c.eventRecorder.Event(r, corev1.EventTypeWarning, "Synced", fmt.Sprintf(template, args...))
-	c.sendEventToWebhook(r, corev1.EventTypeWarning, template, args)
-}
-
-func (c *Controller) recordEventWarningf(r *flaggerv1.Canary, template string, args ...interface{}) {
-	c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Infof(template, args...)
-	c.eventRecorder.Event(r, corev1.EventTypeWarning, "Synced", fmt.Sprintf(template, args...))
-	c.sendEventToWebhook(r, corev1.EventTypeWarning, template, args)
-}
-
-func (c *Controller) sendNotification(cd *flaggerv1.Canary, message string, metadata bool, warn bool) {
-	if c.notifier == nil {
-		return
-	}
-
-	var fields []notifier.Field
-
-	if metadata {
-		fields = append(fields,
-			notifier.Field{
-				Name:  "Target",
-				Value: fmt.Sprintf("%s/%s.%s", cd.Spec.TargetRef.Kind, cd.Spec.TargetRef.Name, cd.Namespace),
-			},
-			notifier.Field{
-				Name:  "Failed checks threshold",
-				Value: fmt.Sprintf("%v", cd.Spec.CanaryAnalysis.Threshold),
-			},
-			notifier.Field{
-				Name:  "Progress deadline",
-				Value: fmt.Sprintf("%vs", cd.GetProgressDeadlineSeconds()),
-			},
-		)
-
-		if cd.Spec.CanaryAnalysis.StepWeight > 0 {
-			fields = append(fields, notifier.Field{
-				Name: "Traffic routing",
-				Value: fmt.Sprintf("Weight step: %v max: %v",
-					cd.Spec.CanaryAnalysis.StepWeight,
-					cd.Spec.CanaryAnalysis.MaxWeight),
-			})
-		} else if len(cd.Spec.CanaryAnalysis.Match) > 0 {
-			fields = append(fields, notifier.Field{
-				Name:  "Traffic routing",
-				Value: "A/B Testing",
-			})
-		} else if cd.Spec.CanaryAnalysis.Iterations > 0 {
-			fields = append(fields, notifier.Field{
-				Name:  "Traffic routing",
-				Value: "Blue/Green",
-			})
-		}
-	}
-	err := c.notifier.Post(cd.Name, cd.Namespace, message, fields, warn)
-	if err != nil {
-		c.logger.Error(err)
-	}
-}
-
 func int32p(i int32) *int32 {
 	return &i
 }
diff --git a/pkg/controller/events.go b/pkg/controller/events.go
new file mode 100644
index 00000000..00fe49a1
--- /dev/null
+++ b/pkg/controller/events.go
@@ -0,0 +1,191 @@
+package controller
+
+import (
+	"fmt"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	corev1 "k8s.io/api/core/v1"
+
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	"github.com/weaveworks/flagger/pkg/notifier"
+)
+
+func (c *Controller) recordEventInfof(r *flaggerv1.Canary, template string, args ...interface{}) {
+	c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Infof(template, args...)
+	c.eventRecorder.Event(r, corev1.EventTypeNormal, "Synced", fmt.Sprintf(template, args...))
+	c.sendEventToWebhook(r, corev1.EventTypeNormal, template, args)
+}
+
+func (c *Controller) recordEventErrorf(r *flaggerv1.Canary, template string, args ...interface{}) {
+	c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Errorf(template, args...)
+	c.eventRecorder.Event(r, corev1.EventTypeWarning, "Synced", fmt.Sprintf(template, args...))
+	c.sendEventToWebhook(r, corev1.EventTypeWarning, template, args)
+}
+
+func (c *Controller) recordEventWarningf(r *flaggerv1.Canary, template string, args ...interface{}) {
+	c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Infof(template, args...)
+	c.eventRecorder.Event(r, corev1.EventTypeWarning, "Synced", fmt.Sprintf(template, args...))
+	c.sendEventToWebhook(r, corev1.EventTypeWarning, template, args)
+}
+
+func (c *Controller) sendEventToWebhook(r *flaggerv1.Canary, eventType, template string, args []interface{}) {
+	webhookOverride := false
+	if len(r.Spec.CanaryAnalysis.Webhooks) > 0 {
+		for _, canaryWebhook := range r.Spec.CanaryAnalysis.Webhooks {
+			if canaryWebhook.Type == flaggerv1.EventHook {
+				webhookOverride = true
+				err := CallEventWebhook(r, canaryWebhook.URL, fmt.Sprintf(template, args...), eventType)
+				if err != nil {
+					c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Errorf("error sending event to webhook: %s", err)
+				}
+			}
+		}
+	}
+
+	if c.eventWebhook != "" && !webhookOverride {
+		err := CallEventWebhook(r, c.eventWebhook, fmt.Sprintf(template, args...), eventType)
+		if err != nil {
+			c.logger.With("canary", fmt.Sprintf("%s.%s", r.Name, r.Namespace)).Errorf("error sending event to webhook: %s", err)
+		}
+	}
+}
+
+func (c *Controller) alert(canary *flaggerv1.Canary, message string, metadata bool, severity flaggerv1.AlertSeverity) {
+	if c.notifier == nil && len(canary.Spec.CanaryAnalysis.Alerts) == 0 {
+		return
+	}
+
+	var fields []notifier.Field
+	if metadata {
+		fields = alertMetadata(canary)
+	}
+
+	// send alert with the global notifier
+	if len(canary.Spec.CanaryAnalysis.Alerts) == 0 {
+		err := c.notifier.Post(canary.Name, canary.Namespace, message, fields, string(severity))
+		if err != nil {
+			c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Errorf("alert can't be sent: %v", err)
+			return
+		}
+		return
+	}
+
+	// send canary alerts
+	for _, alert := range canary.Spec.CanaryAnalysis.Alerts {
+		// determine if alert should be sent based on severity level
+		shouldAlert := false
+		if alert.Severity == flaggerv1.SeverityInfo {
+			shouldAlert = true
+		} else {
+			if severity == alert.Severity {
+				shouldAlert = true
+			}
+			if severity == flaggerv1.SeverityWarn && alert.Severity == flaggerv1.SeverityError {
+				shouldAlert = true
+			}
+		}
+		if !shouldAlert {
+			continue
+		}
+
+		// determine alert provider namespace
+		providerNamespace := canary.GetNamespace()
+		if alert.ProviderRef.Namespace != "" {
+			providerNamespace = alert.ProviderRef.Namespace
+		}
+
+		// find alert provider
+		provider, err := c.flaggerInformers.AlertInformer.Lister().AlertProviders(providerNamespace).Get(alert.ProviderRef.Name)
+		if err != nil {
+			c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Errorf("alert provider %s.%s error: %v", alert.ProviderRef.Name, providerNamespace, err)
+			continue
+		}
+
+		// set hook URL address
+		url := provider.Spec.Address
+
+		// extract address from secret
+		if provider.Spec.SecretRef != nil {
+			secret, err := c.kubeClient.CoreV1().Secrets(providerNamespace).Get(provider.Spec.SecretRef.Name, metav1.GetOptions{})
+			if err != nil {
+				c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+					Errorf("alert provider %s.%s secretRef error: %v", alert.ProviderRef.Name, providerNamespace, err)
+				continue
+			}
+			if address, ok := secret.Data["address"]; ok {
+				url = string(address)
+			} else {
+				c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+					Errorf("alert provider %s.%s secret does not contain an address", alert.ProviderRef.Name, providerNamespace)
+				continue
+			}
+		}
+
+		// set defaults
+		username := "flagger"
+		if provider.Spec.Username != "" {
+			username = provider.Spec.Username
+		}
+		channel := "general"
+		if provider.Spec.Channel != "" {
+			channel = provider.Spec.Channel
+		}
+
+		// create notifier based on provider type
+		f := notifier.NewFactory(url, username, channel)
+		n, err := f.Notifier(provider.Spec.Type)
+		if err != nil {
+			c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Errorf("alert provider %s.%s error: %v", alert.ProviderRef.Name, providerNamespace, err)
+			continue
+		}
+
+		// send alert
+		err = n.Post(canary.Name, canary.Namespace, message, fields, string(severity))
+		if err != nil {
+			c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Errorf("alert provider $s.%s send error: %v", alert.ProviderRef.Name, providerNamespace, err)
+		}
+
+	}
+}
+
+func alertMetadata(canary *flaggerv1.Canary) []notifier.Field {
+	var fields []notifier.Field
+	fields = append(fields,
+		notifier.Field{
+			Name:  "Target",
+			Value: fmt.Sprintf("%s/%s.%s", canary.Spec.TargetRef.Kind, canary.Spec.TargetRef.Name, canary.Namespace),
+		},
+		notifier.Field{
+			Name:  "Failed checks threshold",
+			Value: fmt.Sprintf("%v", canary.Spec.CanaryAnalysis.Threshold),
+		},
+		notifier.Field{
+			Name:  "Progress deadline",
+			Value: fmt.Sprintf("%vs", canary.GetProgressDeadlineSeconds()),
+		},
+	)
+
+	if canary.Spec.CanaryAnalysis.StepWeight > 0 {
+		fields = append(fields, notifier.Field{
+			Name: "Traffic routing",
+			Value: fmt.Sprintf("Weight step: %v max: %v",
+				canary.Spec.CanaryAnalysis.StepWeight,
+				canary.Spec.CanaryAnalysis.MaxWeight),
+		})
+	} else if len(canary.Spec.CanaryAnalysis.Match) > 0 {
+		fields = append(fields, notifier.Field{
+			Name:  "Traffic routing",
+			Value: "A/B Testing",
+		})
+	} else if canary.Spec.CanaryAnalysis.Iterations > 0 {
+		fields = append(fields, notifier.Field{
+			Name:  "Traffic routing",
+			Value: "Blue/Green",
+		})
+	}
+	return fields
+}
diff --git a/pkg/controller/scheduler.go b/pkg/controller/scheduler.go
index 72749a6b..1fd4a54f 100644
--- a/pkg/controller/scheduler.go
+++ b/pkg/controller/scheduler.go
@@ -230,7 +230,7 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 		cd.Status.Phase == flaggerv1.CanaryPhaseWaiting {
 		if ok := c.runRollbackHooks(cd, cd.Status.Phase); ok {
 			c.recordEventWarningf(cd, "Rolling back %s.%s manual webhook invoked", cd.Name, cd.Namespace)
-			c.sendNotification(cd, "Rolling back manual webhook invoked", false, true)
+			c.alert(cd, "Rolling back manual webhook invoked", false, flaggerv1.SeverityWarn)
 			c.rollback(cd, canaryController, meshRouter)
 			return
 		}
@@ -271,8 +271,8 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 		c.recorder.SetStatus(cd, flaggerv1.CanaryPhaseSucceeded)
 		c.runPostRolloutHooks(cd, flaggerv1.CanaryPhaseSucceeded)
 		c.recordEventInfof(cd, "Promotion completed! Scaling down %s.%s", cd.Spec.TargetRef.Name, cd.Namespace)
-		c.sendNotification(cd, "Canary analysis completed successfully, promotion finished.",
-			false, false)
+		c.alert(cd, "Canary analysis completed successfully, promotion finished.",
+			false, flaggerv1.SeverityInfo)
 		return
 	}
 
@@ -282,8 +282,8 @@ func (c *Controller) advanceCanary(name string, namespace string, skipLivenessCh
 		if !retriable {
 			c.recordEventWarningf(cd, "Rolling back %s.%s progress deadline exceeded %v",
 				cd.Name, cd.Namespace, err)
-			c.sendNotification(cd, fmt.Sprintf("Progress deadline exceeded %v", err),
-				false, true)
+			c.alert(cd, fmt.Sprintf("Progress deadline exceeded %v", err),
+				false, flaggerv1.SeverityError)
 		}
 		c.rollback(cd, canaryController, meshRouter)
 		return
@@ -569,8 +569,8 @@ func (c *Controller) shouldSkipAnalysis(canary *flaggerv1.Canary, canaryControll
 	c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseSucceeded)
 	c.recordEventInfof(canary, "Promotion completed! Canary analysis was skipped for %s.%s",
 		canary.Spec.TargetRef.Name, canary.Namespace)
-	c.sendNotification(canary, "Canary analysis was skipped, promotion finished.",
-		false, false)
+	c.alert(canary, "Canary analysis was skipped, promotion finished.",
+		false, flaggerv1.SeverityInfo)
 
 	return true
 }
@@ -617,8 +617,8 @@ func (c *Controller) checkCanaryStatus(canary *flaggerv1.Canary, canaryControlle
 		}
 		c.recorder.SetStatus(canary, flaggerv1.CanaryPhaseInitialized)
 		c.recordEventInfof(canary, "Initialization done! %s.%s", canary.Name, canary.Namespace)
-		c.sendNotification(canary, "New deployment detected, initialization completed.",
-			true, false)
+		c.alert(canary, "New deployment detected, initialization completed.",
+			true, flaggerv1.SeverityInfo)
 		return false
 	}
 
@@ -626,8 +626,8 @@ func (c *Controller) checkCanaryStatus(canary *flaggerv1.Canary, canaryControlle
 		canaryPhaseProgressing := canary.DeepCopy()
 		canaryPhaseProgressing.Status.Phase = flaggerv1.CanaryPhaseProgressing
 		c.recordEventInfof(canaryPhaseProgressing, "New revision detected! Scaling up %s.%s", canaryPhaseProgressing.Spec.TargetRef.Name, canaryPhaseProgressing.Namespace)
-		c.sendNotification(canaryPhaseProgressing, "New revision detected, starting canary analysis.",
-			true, false)
+		c.alert(canaryPhaseProgressing, "New revision detected, starting canary analysis.",
+			true, flaggerv1.SeverityInfo)
 
 		if err := canaryController.ScaleFromZero(canary); err != nil {
 			c.recordEventErrorf(canary, "%v", err)
@@ -666,7 +666,7 @@ func (c *Controller) runConfirmRolloutHooks(canary *flaggerv1.Canary, canaryCont
 					}
 					c.recordEventWarningf(canary, "Halt %s.%s advancement waiting for approval %s",
 						canary.Name, canary.Namespace, webhook.Name)
-					c.sendNotification(canary, "Canary is waiting for approval.", false, false)
+					c.alert(canary, "Canary is waiting for approval.", false, flaggerv1.SeverityWarn)
 				}
 				return false
 			} else {
@@ -691,7 +691,7 @@ func (c *Controller) runConfirmPromotionHooks(canary *flaggerv1.Canary) bool {
 			if err != nil {
 				c.recordEventWarningf(canary, "Halt %s.%s advancement waiting for promotion approval %s",
 					canary.Name, canary.Namespace, webhook.Name)
-				c.sendNotification(canary, "Canary promotion is waiting for approval.", false, false)
+				c.alert(canary, "Canary promotion is waiting for approval.", false, flaggerv1.SeverityWarn)
 				return false
 			} else {
 				c.recordEventInfof(canary, "Confirm-promotion check %s passed", webhook.Name)
@@ -921,7 +921,7 @@ func (c *Controller) runMetricChecks(canary *flaggerv1.Canary) bool {
 				namespace = metric.TemplateRef.Namespace
 			}
 
-			template, err := c.flaggerClient.FlaggerV1beta1().MetricTemplates(namespace).Get(metric.TemplateRef.Name, metav1.GetOptions{})
+			template, err := c.flaggerInformers.MetricInformer.Lister().MetricTemplates(namespace).Get(metric.TemplateRef.Name)
 			if err != nil {
 				c.recordEventErrorf(canary, "Metric template %s.%s error: %v", metric.TemplateRef.Name, namespace, err)
 				return false
@@ -1010,8 +1010,8 @@ func (c *Controller) rollback(canary *flaggerv1.Canary, canaryController canary.
 	if canary.Status.FailedChecks >= canary.Spec.CanaryAnalysis.Threshold {
 		c.recordEventWarningf(canary, "Rolling back %s.%s failed checks threshold reached %v",
 			canary.Name, canary.Namespace, canary.Status.FailedChecks)
-		c.sendNotification(canary, fmt.Sprintf("Failed checks threshold reached %v", canary.Status.FailedChecks),
-			false, true)
+		c.alert(canary, fmt.Sprintf("Failed checks threshold reached %v", canary.Status.FailedChecks),
+			false, flaggerv1.SeverityError)
 	}
 
 	// route all traffic back to primary
diff --git a/pkg/notifier/discord.go b/pkg/notifier/discord.go
new file mode 100644
index 00000000..952f45e9
--- /dev/null
+++ b/pkg/notifier/discord.go
@@ -0,0 +1,81 @@
+package notifier
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"path"
+	"strings"
+)
+
+// Discord holds the hook URL
+type Discord struct {
+	URL      string
+	Username string
+	Channel  string
+}
+
+// NewDiscord validates the URL and returns a Discord object
+func NewDiscord(hookURL string, username string, channel string) (*Discord, error) {
+	webhook, err := url.ParseRequestURI(hookURL)
+	if err != nil {
+		return nil, fmt.Errorf("invalid Discord hook URL %s", hookURL)
+	}
+
+	// use Slack formatting
+	// https://birdie0.github.io/discord-webhooks-guide/other/slack_formatting.html
+	if !strings.HasSuffix(hookURL, "/slack") {
+		webhook.Path = path.Join(webhook.Path, "slack")
+		hookURL = webhook.String()
+	}
+
+	if username == "" {
+		return nil, errors.New("empty Discord username")
+	}
+
+	if channel == "" {
+		return nil, errors.New("empty Discord channel")
+	}
+
+	return &Discord{
+		Channel:  channel,
+		URL:      hookURL,
+		Username: username,
+	}, nil
+}
+
+// Post Discord message
+func (s *Discord) Post(workload string, namespace string, message string, fields []Field, severity string) error {
+	payload := SlackPayload{
+		Channel:   s.Channel,
+		Username:  s.Username,
+		IconEmoji: ":rocket:",
+	}
+
+	color := "good"
+	if severity == "error" {
+		color = "danger"
+	}
+
+	sfields := make([]SlackField, 0, len(fields))
+	for _, f := range fields {
+		sfields = append(sfields, SlackField{f.Name, f.Value, false})
+	}
+
+	a := SlackAttachment{
+		Color:      color,
+		AuthorName: fmt.Sprintf("%s.%s", workload, namespace),
+		Text:       message,
+		MrkdwnIn:   []string{"text"},
+		Fields:     sfields,
+	}
+
+	payload.Attachments = []SlackAttachment{a}
+
+	err := postMessage(s.URL, payload)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go
index d7ef2ec1..deabe4b8 100644
--- a/pkg/notifier/factory.go
+++ b/pkg/notifier/factory.go
@@ -1,5 +1,7 @@
 package notifier
 
+import "fmt"
+
 type Factory struct {
 	URL      string
 	Username string
@@ -18,9 +20,13 @@ func (f Factory) Notifier(provider string) (Interface, error) {
 	switch {
 	case provider == "slack":
 		return NewSlack(f.URL, f.Username, f.Channel)
+	case provider == "discord":
+		return NewDiscord(f.URL, f.Username, f.Channel)
+	case provider == "rocket":
+		return NewRocket(f.URL, f.Username, f.Channel)
 	case provider == "msteams":
 		return NewMSTeams(f.URL)
 	}
 
-	return nil, nil
+	return nil, fmt.Errorf("provider %s not supported", provider)
 }
diff --git a/pkg/notifier/notifier.go b/pkg/notifier/notifier.go
index c014b030..4b7fd977 100644
--- a/pkg/notifier/notifier.go
+++ b/pkg/notifier/notifier.go
@@ -1,7 +1,7 @@
 package notifier
 
 type Interface interface {
-	Post(workload string, namespace string, message string, fields []Field, warn bool) error
+	Post(workload string, namespace string, message string, fields []Field, severity string) error
 }
 
 type Field struct {
diff --git a/pkg/notifier/rocket.go b/pkg/notifier/rocket.go
new file mode 100644
index 00000000..4bdf549f
--- /dev/null
+++ b/pkg/notifier/rocket.go
@@ -0,0 +1,72 @@
+package notifier
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+)
+
+// Rocket holds the hook URL
+type Rocket struct {
+	URL      string
+	Username string
+	Channel  string
+}
+
+// NewRocket validates the Rocket URL and returns a Rocket object
+func NewRocket(hookURL string, username string, channel string) (*Rocket, error) {
+	_, err := url.ParseRequestURI(hookURL)
+	if err != nil {
+		return nil, fmt.Errorf("invalid Rocket hook URL %s", hookURL)
+	}
+
+	if username == "" {
+		return nil, errors.New("empty Rocket username")
+	}
+
+	if channel == "" {
+		return nil, errors.New("empty Rocket channel")
+	}
+
+	return &Rocket{
+		Channel:  channel,
+		URL:      hookURL,
+		Username: username,
+	}, nil
+}
+
+// Post Rocket message
+func (s *Rocket) Post(workload string, namespace string, message string, fields []Field, severity string) error {
+	payload := SlackPayload{
+		Channel:   s.Channel,
+		Username:  s.Username,
+		IconEmoji: ":rocket:",
+	}
+
+	color := "#0076D7"
+	if severity == "error" {
+		color = "#FF0000"
+	}
+
+	sfields := make([]SlackField, 0, len(fields))
+	for _, f := range fields {
+		sfields = append(sfields, SlackField{f.Name, f.Value, false})
+	}
+
+	a := SlackAttachment{
+		Color:      color,
+		AuthorName: fmt.Sprintf("%s.%s", workload, namespace),
+		Text:       message,
+		MrkdwnIn:   []string{"text"},
+		Fields:     sfields,
+	}
+
+	payload.Attachments = []SlackAttachment{a}
+
+	err := postMessage(s.URL, payload)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/notifier/slack.go b/pkg/notifier/slack.go
index f4a3d5ce..52084e3e 100644
--- a/pkg/notifier/slack.go
+++ b/pkg/notifier/slack.go
@@ -8,10 +8,9 @@ import (
 
 // Slack holds the hook URL
 type Slack struct {
-	URL       string
-	Username  string
-	Channel   string
-	IconEmoji string
+	URL      string
+	Username string
+	Channel  string
 }
 
 // SlackPayload holds the channel and attachments
@@ -55,22 +54,22 @@ func NewSlack(hookURL string, username string, channel string) (*Slack, error) {
 	}
 
 	return &Slack{
-		Channel:   channel,
-		URL:       hookURL,
-		Username:  username,
-		IconEmoji: ":rocket:",
+		Channel:  channel,
+		URL:      hookURL,
+		Username: username,
 	}, nil
 }
 
 // Post Slack message
-func (s *Slack) Post(workload string, namespace string, message string, fields []Field, warn bool) error {
+func (s *Slack) Post(workload string, namespace string, message string, fields []Field, severity string) error {
 	payload := SlackPayload{
-		Channel:  s.Channel,
-		Username: s.Username,
+		Channel:   s.Channel,
+		Username:  s.Username,
+		IconEmoji: ":rocket:",
 	}
 
 	color := "good"
-	if warn {
+	if severity == "error" {
 		color = "danger"
 	}
 
diff --git a/pkg/notifier/teams.go b/pkg/notifier/teams.go
index 01050f0a..63e6ade3 100644
--- a/pkg/notifier/teams.go
+++ b/pkg/notifier/teams.go
@@ -44,7 +44,7 @@ func NewMSTeams(hookURL string) (*MSTeams, error) {
 }
 
 // Post MS Teams message
-func (s *MSTeams) Post(workload string, namespace string, message string, fields []Field, warn bool) error {
+func (s *MSTeams) Post(workload string, namespace string, message string, fields []Field, severity string) error {
 	facts := make([]MSTeamsField, 0, len(fields))
 	for _, f := range fields {
 		facts = append(facts, MSTeamsField{f.Name, f.Value})
@@ -64,7 +64,7 @@ func (s *MSTeams) Post(workload string, namespace string, message string, fields
 		},
 	}
 
-	if warn {
+	if severity == "error" {
 		payload.ThemeColor = "FF0000"
 	}
 


Test patch:
diff --git a/pkg/canary/deployment_controller_test.go b/pkg/canary/deployment_controller_test.go
index 04923aa7..8e1d3ff2 100644
--- a/pkg/canary/deployment_controller_test.go
+++ b/pkg/canary/deployment_controller_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestCanaryDeployer_Sync(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -101,7 +101,7 @@ func TestCanaryDeployer_Sync(t *testing.T) {
 }
 
 func TestCanaryDeployer_IsNewSpec(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -124,7 +124,7 @@ func TestCanaryDeployer_IsNewSpec(t *testing.T) {
 }
 
 func TestCanaryDeployer_Promote(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -190,7 +190,7 @@ func TestCanaryDeployer_Promote(t *testing.T) {
 }
 
 func TestCanaryDeployer_IsReady(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Error("Expected primary readiness check to fail")
@@ -208,7 +208,7 @@ func TestCanaryDeployer_IsReady(t *testing.T) {
 }
 
 func TestCanaryDeployer_SetFailedChecks(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -230,7 +230,7 @@ func TestCanaryDeployer_SetFailedChecks(t *testing.T) {
 }
 
 func TestCanaryDeployer_SetState(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -252,7 +252,7 @@ func TestCanaryDeployer_SetState(t *testing.T) {
 }
 
 func TestCanaryDeployer_SyncStatus(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -291,7 +291,7 @@ func TestCanaryDeployer_SyncStatus(t *testing.T) {
 }
 
 func TestCanaryDeployer_Scale(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	err := mocks.deployer.Initialize(mocks.canary, true)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -310,7 +310,7 @@ func TestCanaryDeployer_Scale(t *testing.T) {
 }
 
 func TestCanaryDeployer_NoConfigTracking(t *testing.T) {
-	mocks := SetupMocks()
+	mocks := newFixture()
 	mocks.deployer.configTracker = &NopTracker{}
 
 	err := mocks.deployer.Initialize(mocks.canary, true)
diff --git a/pkg/controller/controller_test.go b/pkg/controller/fixture.go
similarity index 84%
rename from pkg/controller/controller_test.go
rename to pkg/controller/fixture.go
index f9f88fc0..8b5c94ad 100644
--- a/pkg/controller/controller_test.go
+++ b/pkg/controller/fixture.go
@@ -7,7 +7,6 @@ import (
 
 	"go.uber.org/zap"
 	appsv1 "k8s.io/api/apps/v1"
-	hpav1 "k8s.io/api/autoscaling/v1"
 	hpav2 "k8s.io/api/autoscaling/v2beta1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -34,7 +33,7 @@ var (
 	noResyncPeriodFunc = func() time.Duration { return 0 }
 )
 
-type Mocks struct {
+type fixture struct {
 	canary        *flaggerv1.Canary
 	kubeClient    kubernetes.Interface
 	meshClient    clientset.Interface
@@ -45,30 +44,42 @@ type Mocks struct {
 	router        router.Interface
 }
 
-func SetupMocks(c *flaggerv1.Canary) Mocks {
+func newFixture(c *flaggerv1.Canary) fixture {
 	if c == nil {
 		c = newTestCanary()
 	}
-	flaggerClient := fakeFlagger.NewSimpleClientset(c, newTestMetricTemplate())
 
-	// init kube clientset and register mock objects
+	// init Flagger clientset and register objects
+	flaggerClient := fakeFlagger.NewSimpleClientset(
+		c,
+		newTestMetricTemplate(),
+		newTestAlertProvider(),
+	)
+
+	// init Kubernetes clientset and register objects
 	kubeClient := fake.NewSimpleClientset(
 		newTestDeployment(),
 		newTestService(),
 		newTestHPA(),
-		NewTestConfigMap(),
-		NewTestConfigMapEnv(),
-		NewTestConfigMapVol(),
-		NewTestSecret(),
-		NewTestSecretEnv(),
-		NewTestSecretVol(),
+		newTestConfigMap(),
+		newTestConfigMapEnv(),
+		newTestConfigMapVol(),
+		newTestSecret(),
+		newTestSecretEnv(),
+		newTestSecretVol(),
+		newTestAlertProviderSecret(),
 	)
 
 	logger, _ := logger.NewLogger("debug")
 
 	// init controller
-	flaggerInformerFactory := informers.NewSharedInformerFactory(flaggerClient, noResyncPeriodFunc())
-	flaggerInformer := flaggerInformerFactory.Flagger().V1beta1().Canaries()
+	flaggerInformerFactory := informers.NewSharedInformerFactory(flaggerClient, 0)
+
+	fi := Informers{
+		CanaryInformer: flaggerInformerFactory.Flagger().V1beta1().Canaries(),
+		MetricInformer: flaggerInformerFactory.Flagger().V1beta1().MetricTemplates(),
+		AlertInformer:  flaggerInformerFactory.Flagger().V1beta1().AlertProviders(),
+	}
 
 	// init router
 	rf := router.NewFactory(nil, kubeClient, flaggerClient, "annotationsPrefix", logger, flaggerClient)
@@ -85,26 +96,29 @@ func SetupMocks(c *flaggerv1.Canary) Mocks {
 	canaryFactory := canary.NewFactory(kubeClient, flaggerClient, configTracker, []string{"app", "name"}, logger)
 
 	ctrl := &Controller{
-		kubeClient:      kubeClient,
-		istioClient:     flaggerClient,
-		flaggerClient:   flaggerClient,
-		flaggerLister:   flaggerInformer.Lister(),
-		flaggerSynced:   flaggerInformer.Informer().HasSynced,
-		workqueue:       workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName),
-		eventRecorder:   &record.FakeRecorder{},
-		logger:          logger,
-		canaries:        new(sync.Map),
-		flaggerWindow:   time.Second,
-		canaryFactory:   canaryFactory,
-		observerFactory: observerFactory,
-		recorder:        metrics.NewRecorder(controllerAgentName, false),
-		routerFactory:   rf,
+		kubeClient:       kubeClient,
+		istioClient:      flaggerClient,
+		flaggerClient:    flaggerClient,
+		flaggerInformers: fi,
+		flaggerSynced:    fi.CanaryInformer.Informer().HasSynced,
+		workqueue:        workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), controllerAgentName),
+		eventRecorder:    &record.FakeRecorder{},
+		logger:           logger,
+		canaries:         new(sync.Map),
+		flaggerWindow:    time.Second,
+		canaryFactory:    canaryFactory,
+		observerFactory:  observerFactory,
+		recorder:         metrics.NewRecorder(controllerAgentName, false),
+		routerFactory:    rf,
 	}
 	ctrl.flaggerSynced = alwaysReady
+	ctrl.flaggerInformers.CanaryInformer.Informer().GetIndexer().Add(c)
+	ctrl.flaggerInformers.MetricInformer.Informer().GetIndexer().Add(newTestMetricTemplate())
+	ctrl.flaggerInformers.AlertInformer.Informer().GetIndexer().Add(newTestAlertProvider())
 
 	meshRouter := rf.MeshRouter("istio")
 
-	return Mocks{
+	return fixture{
 		canary:        c,
 		deployer:      canaryFactory.Controller("Deployment"),
 		logger:        logger,
@@ -116,7 +130,7 @@ func SetupMocks(c *flaggerv1.Canary) Mocks {
 	}
 }
 
-func NewTestConfigMap() *corev1.ConfigMap {
+func newTestConfigMap() *corev1.ConfigMap {
 	return &corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -129,7 +143,7 @@ func NewTestConfigMap() *corev1.ConfigMap {
 	}
 }
 
-func NewTestConfigMapV2() *corev1.ConfigMap {
+func newTestConfigMapV2() *corev1.ConfigMap {
 	return &corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -143,7 +157,7 @@ func NewTestConfigMapV2() *corev1.ConfigMap {
 	}
 }
 
-func NewTestConfigMapEnv() *corev1.ConfigMap {
+func newTestConfigMapEnv() *corev1.ConfigMap {
 	return &corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -156,7 +170,7 @@ func NewTestConfigMapEnv() *corev1.ConfigMap {
 	}
 }
 
-func NewTestConfigMapVol() *corev1.ConfigMap {
+func newTestConfigMapVol() *corev1.ConfigMap {
 	return &corev1.ConfigMap{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -169,7 +183,7 @@ func NewTestConfigMapVol() *corev1.ConfigMap {
 	}
 }
 
-func NewTestSecret() *corev1.Secret {
+func newTestSecret() *corev1.Secret {
 	return &corev1.Secret{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -185,7 +199,7 @@ func NewTestSecret() *corev1.Secret {
 	}
 }
 
-func NewTestSecretV2() *corev1.Secret {
+func newTestSecretV2() *corev1.Secret {
 	return &corev1.Secret{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -201,7 +215,7 @@ func NewTestSecretV2() *corev1.Secret {
 	}
 }
 
-func NewTestSecretEnv() *corev1.Secret {
+func newTestSecretEnv() *corev1.Secret {
 	return &corev1.Secret{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -215,7 +229,7 @@ func NewTestSecretEnv() *corev1.Secret {
 	}
 }
 
-func NewTestSecretVol() *corev1.Secret {
+func newTestSecretVol() *corev1.Secret {
 	return &corev1.Secret{
 		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
 		ObjectMeta: metav1.ObjectMeta{
@@ -237,12 +251,12 @@ func newTestCanary() *flaggerv1.Canary {
 			Name:      "podinfo",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
 			},
-			AutoscalerRef: &hpav1.CrossVersionObjectReference{
+			AutoscalerRef: &flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "autoscaling/v2beta1",
 				Kind:       "HorizontalPodAutoscaler",
@@ -273,7 +287,7 @@ func newTestCanary() *flaggerv1.Canary {
 							Max: toFloatPtr(100),
 						},
 						Interval: "1m",
-						TemplateRef: &flaggerv1.MetricTemplateRef{
+						TemplateRef: &flaggerv1.CrossNamespaceObjectReference{
 							Name:      "envoy",
 							Namespace: "default",
 						},
@@ -304,12 +318,12 @@ func newTestCanaryAB() *flaggerv1.Canary {
 			Name:      "podinfo",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
 			},
-			AutoscalerRef: &hpav1.CrossVersionObjectReference{
+			AutoscalerRef: &flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "autoscaling/v2beta1",
 				Kind:       "HorizontalPodAutoscaler",
@@ -696,3 +710,34 @@ func newTestMetricTemplate() *flaggerv1.MetricTemplate {
 	}
 	return template
 }
+
+func newTestAlertProviderSecret() *corev1.Secret {
+	return &corev1.Secret{
+		TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "alert-secret",
+		},
+		Type: corev1.SecretTypeOpaque,
+		Data: map[string][]byte{
+			"address": []byte("http://mock.slack"),
+		},
+	}
+}
+
+func newTestAlertProvider() *flaggerv1.AlertProvider {
+	return &flaggerv1.AlertProvider{
+		TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "default",
+			Name:      "slack",
+		},
+		Spec: flaggerv1.AlertProviderSpec{
+			Type:    "slack",
+			Address: "http://fake.slack",
+			SecretRef: &corev1.LocalObjectReference{
+				Name: "alert-secret",
+			},
+		},
+	}
+}
diff --git a/pkg/controller/scheduler_svc_test.go b/pkg/controller/scheduler_svc_test.go
index bbe41a6c..fa2a4691 100644
--- a/pkg/controller/scheduler_svc_test.go
+++ b/pkg/controller/scheduler_svc_test.go
@@ -3,14 +3,13 @@ package controller
 import (
 	"testing"
 
-	hpav1 "k8s.io/api/autoscaling/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
 )
 
 func TestScheduler_ServicePromotion(t *testing.T) {
-	mocks := SetupMocks(newTestServiceCanary())
+	mocks := newFixture(newTestServiceCanary())
 
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
@@ -135,7 +134,7 @@ func newTestServiceCanary() *flaggerv1.Canary {
 			Name:      "podinfo",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "core/v1",
 				Kind:       "Service",
@@ -149,13 +148,13 @@ func newTestServiceCanary() *flaggerv1.Canary {
 				MaxWeight:  50,
 				Metrics: []flaggerv1.CanaryMetric{
 					{
-						Name:      "istio_requests_total",
+						Name:      "request-success-rate",
 						Threshold: 99,
 						Interval:  "1m",
 					},
 					{
-						Name:      "istio_request_duration_seconds_bucket",
-						Threshold: 500,
+						Name:      "request-duration",
+						Threshold: 500000,
 						Interval:  "1m",
 					},
 				},
diff --git a/pkg/controller/scheduler_test.go b/pkg/controller/scheduler_test.go
index 96ee6d89..f49a44f5 100644
--- a/pkg/controller/scheduler_test.go
+++ b/pkg/controller/scheduler_test.go
@@ -1,17 +1,22 @@
 package controller
 
 import (
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 
 	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	"github.com/weaveworks/flagger/pkg/notifier"
 )
 
 func TestScheduler_Init(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
 	_, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo-primary", metav1.GetOptions{})
@@ -21,7 +26,7 @@ func TestScheduler_Init(t *testing.T) {
 }
 
 func TestScheduler_NewRevision(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
 	// update
@@ -45,7 +50,7 @@ func TestScheduler_NewRevision(t *testing.T) {
 }
 
 func TestScheduler_Rollback(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
@@ -99,7 +104,7 @@ func TestScheduler_Rollback(t *testing.T) {
 }
 
 func TestScheduler_SkipAnalysis(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
@@ -140,7 +145,7 @@ func TestScheduler_SkipAnalysis(t *testing.T) {
 }
 
 func TestScheduler_NewRevisionReset(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
@@ -202,7 +207,7 @@ func TestScheduler_NewRevisionReset(t *testing.T) {
 }
 
 func TestScheduler_Promotion(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
@@ -227,13 +232,13 @@ func TestScheduler_Promotion(t *testing.T) {
 	// detect pod spec changes
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
-	config2 := NewTestConfigMapV2()
+	config2 := newTestConfigMapV2()
 	_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(config2)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
 
-	secret2 := NewTestSecretV2()
+	secret2 := newTestSecretV2()
 	_, err = mocks.kubeClient.CoreV1().Secrets("default").Update(secret2)
 	if err != nil {
 		t.Fatal(err.Error())
@@ -353,7 +358,7 @@ func TestScheduler_Promotion(t *testing.T) {
 }
 
 func TestScheduler_Mirroring(t *testing.T) {
-	mocks := SetupMocks(newTestCanaryMirror())
+	mocks := newFixture(newTestCanaryMirror())
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
@@ -411,7 +416,7 @@ func TestScheduler_Mirroring(t *testing.T) {
 }
 
 func TestScheduler_ABTesting(t *testing.T) {
-	mocks := SetupMocks(newTestCanaryAB())
+	mocks := newFixture(newTestCanaryAB())
 	// init
 	mocks.ctrl.advanceCanary("podinfo", "default", true)
 
@@ -499,7 +504,7 @@ func TestScheduler_ABTesting(t *testing.T) {
 }
 
 func TestScheduler_PortDiscovery(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 
 	// enable port discovery
 	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
@@ -543,7 +548,7 @@ func TestScheduler_PortDiscovery(t *testing.T) {
 }
 
 func TestScheduler_TargetPortNumber(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 
 	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
 	if err != nil {
@@ -588,7 +593,7 @@ func TestScheduler_TargetPortNumber(t *testing.T) {
 }
 
 func TestScheduler_TargetPortName(t *testing.T) {
-	mocks := SetupMocks(nil)
+	mocks := newFixture(nil)
 
 	cd, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
 	if err != nil {
@@ -631,3 +636,53 @@ func TestScheduler_TargetPortName(t *testing.T) {
 
 	}
 }
+
+func TestScheduler_Alerts(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		b, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		var payload = notifier.SlackPayload{}
+		err = json.Unmarshal(b, &payload)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if payload.Attachments[0].AuthorName != "podinfo.default" {
+			t.Fatal("wrong author name")
+		}
+	}))
+	defer ts.Close()
+
+	canary := newTestCanary()
+	canary.Spec.CanaryAnalysis.Alerts = []flaggerv1.CanaryAlert{
+		{
+			Name:     "slack-dev",
+			Severity: "info",
+			ProviderRef: flaggerv1.CrossNamespaceObjectReference{
+				Name:      "slack",
+				Namespace: "default",
+			},
+		},
+		{
+			Name:     "slack-prod",
+			Severity: "info",
+			ProviderRef: flaggerv1.CrossNamespaceObjectReference{
+				Name: "slack",
+			},
+		},
+	}
+	mocks := newFixture(canary)
+
+	secret := newTestAlertProviderSecret()
+	secret.Data = map[string][]byte{
+		"address": []byte(ts.URL),
+	}
+	_, err := mocks.kubeClient.CoreV1().Secrets("default").Update(secret)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// init canary and send alerts
+	mocks.ctrl.advanceCanary("podinfo", "default", true)
+}
diff --git a/pkg/notifier/discord_test.go b/pkg/notifier/discord_test.go
new file mode 100644
index 00000000..b74b6673
--- /dev/null
+++ b/pkg/notifier/discord_test.go
@@ -0,0 +1,49 @@
+package notifier
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestDiscord_Post(t *testing.T) {
+	fields := []Field{
+		{Name: "name1", Value: "value1"},
+		{Name: "name2", Value: "value2"},
+	}
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		b, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		var payload = SlackPayload{}
+		err = json.Unmarshal(b, &payload)
+
+		if payload.Attachments[0].AuthorName != "podinfo.test" {
+			t.Fatal("wrong author name")
+		}
+
+		if len(payload.Attachments[0].Fields) != len(fields) {
+			t.Fatal("wrong facts")
+		}
+	}))
+	defer ts.Close()
+
+	discord, err := NewDiscord(ts.URL, "test", "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !strings.HasSuffix(discord.URL, "/slack") {
+		t.Error("Invalid Discord URL, expected to have /slack prefix")
+	}
+
+	err = discord.Post("podinfo", "test", "test", fields, "warn")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/pkg/notifier/rocket_test.go b/pkg/notifier/rocket_test.go
new file mode 100644
index 00000000..2d4daab8
--- /dev/null
+++ b/pkg/notifier/rocket_test.go
@@ -0,0 +1,44 @@
+package notifier
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+func TestSlack_Post(t *testing.T) {
+	fields := []Field{
+		{Name: "name1", Value: "value1"},
+		{Name: "name2", Value: "value2"},
+	}
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		b, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		var payload = SlackPayload{}
+		err = json.Unmarshal(b, &payload)
+
+		if payload.Attachments[0].AuthorName != "podinfo.test" {
+			t.Fatal("wrong author name")
+		}
+
+		if len(payload.Attachments[0].Fields) != len(fields) {
+			t.Fatal("wrong facts")
+		}
+	}))
+	defer ts.Close()
+
+	slack, err := NewSlack(ts.URL, "test", "test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = slack.Post("podinfo", "test", "test", fields, "error")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/pkg/notifier/slack_test.go b/pkg/notifier/slack_test.go
index 3194f0fb..4399f2a6 100644
--- a/pkg/notifier/slack_test.go
+++ b/pkg/notifier/slack_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 )
 
-func TestSlack_Post(t *testing.T) {
+func TestRocket_Post(t *testing.T) {
 	fields := []Field{
 		{Name: "name1", Value: "value1"},
 		{Name: "name2", Value: "value2"},
@@ -27,17 +27,17 @@ func TestSlack_Post(t *testing.T) {
 		}
 
 		if len(payload.Attachments[0].Fields) != len(fields) {
-			t.Fatal("wrong facts")
+			t.Fatal("wrong field")
 		}
 	}))
 	defer ts.Close()
 
-	slack, err := NewSlack(ts.URL, "test", "test")
+	rocket, err := NewRocket(ts.URL, "test", "test")
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	err = slack.Post("podinfo", "test", "test", fields, true)
+	err = rocket.Post("podinfo", "test", "test", fields, "error")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/notifier/teams_test.go b/pkg/notifier/teams_test.go
index 4045d216..75d08d6b 100644
--- a/pkg/notifier/teams_test.go
+++ b/pkg/notifier/teams_test.go
@@ -37,7 +37,7 @@ func TestTeams_Post(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	err = teams.Post("podinfo", "test", "test", fields, true)
+	err = teams.Post("podinfo", "test", "test", fields, "info")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/router/appmesh_test.go b/pkg/router/appmesh_test.go
index 2f036c4f..b7e3ac42 100644
--- a/pkg/router/appmesh_test.go
+++ b/pkg/router/appmesh_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestAppmeshRouter_Reconcile(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &AppMeshRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -151,7 +151,7 @@ func TestAppmeshRouter_Reconcile(t *testing.T) {
 }
 
 func TestAppmeshRouter_GetSetRoutes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &AppMeshRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -188,7 +188,7 @@ func TestAppmeshRouter_GetSetRoutes(t *testing.T) {
 }
 
 func TestAppmeshRouter_ABTest(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &AppMeshRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -230,7 +230,7 @@ func TestAppmeshRouter_ABTest(t *testing.T) {
 }
 
 func TestAppmeshRouter_Gateway(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &AppMeshRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
diff --git a/pkg/router/contour_test.go b/pkg/router/contour_test.go
index 54b69a39..13d2b699 100644
--- a/pkg/router/contour_test.go
+++ b/pkg/router/contour_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func TestContourRouter_Reconcile(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &ContourRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -116,7 +116,7 @@ func TestContourRouter_Reconcile(t *testing.T) {
 }
 
 func TestContourRouter_Routes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &ContourRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
diff --git a/pkg/router/gloo_test.go b/pkg/router/gloo_test.go
index dad25a59..d46867ee 100644
--- a/pkg/router/gloo_test.go
+++ b/pkg/router/gloo_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestGlooRouter_Sync(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &GlooRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -43,7 +43,7 @@ func TestGlooRouter_Sync(t *testing.T) {
 }
 
 func TestGlooRouter_SetRoutes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &GlooRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -100,7 +100,7 @@ func TestGlooRouter_SetRoutes(t *testing.T) {
 }
 
 func TestGlooRouter_GetRoutes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &GlooRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
diff --git a/pkg/router/ingress_test.go b/pkg/router/ingress_test.go
index b37fb27b..91079927 100644
--- a/pkg/router/ingress_test.go
+++ b/pkg/router/ingress_test.go
@@ -8,7 +8,7 @@ import (
 )
 
 func TestIngressRouter_Reconcile(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IngressRouter{
 		logger:            mocks.logger,
 		kubeClient:        mocks.kubeClient,
@@ -44,7 +44,7 @@ func TestIngressRouter_Reconcile(t *testing.T) {
 }
 
 func TestIngressRouter_GetSetRoutes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IngressRouter{
 		logger:            mocks.logger,
 		kubeClient:        mocks.kubeClient,
diff --git a/pkg/router/istio_test.go b/pkg/router/istio_test.go
index 1e24b99e..de09c480 100644
--- a/pkg/router/istio_test.go
+++ b/pkg/router/istio_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func TestIstioRouter_Sync(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IstioRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -108,7 +108,7 @@ func TestIstioRouter_Sync(t *testing.T) {
 }
 
 func TestIstioRouter_SetRoutes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IstioRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -213,7 +213,7 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
 }
 
 func TestIstioRouter_GetRoutes(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IstioRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -309,7 +309,7 @@ func TestIstioRouter_GetRoutes(t *testing.T) {
 }
 
 func TestIstioRouter_HTTPRequestHeaders(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IstioRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -338,7 +338,7 @@ func TestIstioRouter_HTTPRequestHeaders(t *testing.T) {
 }
 
 func TestIstioRouter_CORS(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IstioRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
@@ -371,7 +371,7 @@ func TestIstioRouter_CORS(t *testing.T) {
 }
 
 func TestIstioRouter_ABTest(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &IstioRouter{
 		logger:        mocks.logger,
 		flaggerClient: mocks.flaggerClient,
diff --git a/pkg/router/kubernetes_deployment_test.go b/pkg/router/kubernetes_deployment_test.go
index 8268f3ec..d4aae335 100644
--- a/pkg/router/kubernetes_deployment_test.go
+++ b/pkg/router/kubernetes_deployment_test.go
@@ -7,7 +7,7 @@ import (
 )
 
 func TestServiceRouter_Create(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &KubernetesDeploymentRouter{
 		kubeClient:    mocks.kubeClient,
 		flaggerClient: mocks.flaggerClient,
@@ -52,7 +52,7 @@ func TestServiceRouter_Create(t *testing.T) {
 }
 
 func TestServiceRouter_Update(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &KubernetesDeploymentRouter{
 		kubeClient:    mocks.kubeClient,
 		flaggerClient: mocks.flaggerClient,
@@ -103,7 +103,7 @@ func TestServiceRouter_Update(t *testing.T) {
 }
 
 func TestServiceRouter_Undo(t *testing.T) {
-	mocks := setupfakeClients()
+	mocks := newFixture()
 	router := &KubernetesDeploymentRouter{
 		kubeClient:    mocks.kubeClient,
 		flaggerClient: mocks.flaggerClient,
diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go
index fbe9fc43..1389a690 100644
--- a/pkg/router/router_test.go
+++ b/pkg/router/router_test.go
@@ -3,7 +3,6 @@ package router
 import (
 	"go.uber.org/zap"
 	appsv1 "k8s.io/api/apps/v1"
-	hpav1 "k8s.io/api/autoscaling/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/api/extensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -19,7 +18,7 @@ import (
 	"github.com/weaveworks/flagger/pkg/logger"
 )
 
-type fakeClients struct {
+type fixture struct {
 	canary        *flaggerv1.Canary
 	abtest        *flaggerv1.Canary
 	appmeshCanary *flaggerv1.Canary
@@ -30,7 +29,7 @@ type fakeClients struct {
 	logger        *zap.SugaredLogger
 }
 
-func setupfakeClients() fakeClients {
+func newFixture() fixture {
 	canary := newMockCanary()
 	abtest := newMockABTest()
 	appmeshCanary := newMockCanaryAppMesh()
@@ -42,7 +41,7 @@ func setupfakeClients() fakeClients {
 	meshClient := fakeFlagger.NewSimpleClientset()
 	logger, _ := logger.NewLogger("debug")
 
-	return fakeClients{
+	return fixture{
 		canary:        canary,
 		abtest:        abtest,
 		appmeshCanary: appmeshCanary,
@@ -62,7 +61,7 @@ func newMockCanaryAppMesh() *flaggerv1.Canary {
 			Name:      "appmesh",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
@@ -108,7 +107,7 @@ func newMockCanary() *flaggerv1.Canary {
 			Name:      "podinfo",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
@@ -173,7 +172,7 @@ func newMockABTest() *flaggerv1.Canary {
 			Name:      "abtest",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "abtest",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
@@ -309,12 +308,12 @@ func newMockCanaryIngress() *flaggerv1.Canary {
 			Name:      "nginx",
 		},
 		Spec: flaggerv1.CanarySpec{
-			TargetRef: hpav1.CrossVersionObjectReference{
+			TargetRef: flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "apps/v1",
 				Kind:       "Deployment",
 			},
-			IngressRef: &hpav1.CrossVersionObjectReference{
+			IngressRef: &flaggerv1.CrossNamespaceObjectReference{
 				Name:       "podinfo",
 				APIVersion: "extensions/v1beta1",
 				Kind:       "Ingress",