From 939a16620f9048d51ba5353b927dc40eeb1e4a65 Mon Sep 17 00:00:00 2001
From: Matheus Pimenta <matheuscscp@gmail.com>
Date: Mon, 23 Dec 2024 16:46:36 +0000
Subject: [PATCH] [RFC-0008] Custom Event Metadata from Annotations

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
---
 api/v1beta3/alert_types.go                    |   3 +
 ...notification.toolkit.fluxcd.io_alerts.yaml |   5 +-
 docs/api/v1beta3/notification.md              |   6 +-
 docs/spec/v1beta3/alerts.md                   |  93 ++++++++++++--
 go.mod                                        |   2 +-
 go.sum                                        |   4 +-
 internal/server/event_handlers.go             | 101 ++++++++++++---
 internal/server/event_handlers_test.go        | 116 ++++++++++++++----
 internal/server/event_server.go               |  30 +++--
 internal/server/event_server_test.go          |  12 +-
 10 files changed, 291 insertions(+), 81 deletions(-)

diff --git a/api/v1beta3/alert_types.go b/api/v1beta3/alert_types.go
index 722d98999..660f836b3 100644
--- a/api/v1beta3/alert_types.go
+++ b/api/v1beta3/alert_types.go
@@ -64,8 +64,11 @@ type AlertSpec struct {
 	ExclusionList []string `json:"exclusionList,omitempty"`
 
 	// Summary holds a short description of the impact and affected cluster.
+	// Deprecated: Use EventMetadata instead.
+	//
 	// +kubebuilder:validation:MaxLength:=255
 	// +optional
+	// +deprecated
 	Summary string `json:"summary,omitempty"`
 
 	// Suspend tells the controller to suspend subsequent
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
index 0de250303..0ff9c2196 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_alerts.yaml
@@ -557,8 +557,9 @@ spec:
                 - name
                 type: object
               summary:
-                description: Summary holds a short description of the impact and affected
-                  cluster.
+                description: |-
+                  Summary holds a short description of the impact and affected cluster.
+                  Deprecated: Use EventMetadata instead.
                 maxLength: 255
                 type: string
               suspend:
diff --git a/docs/api/v1beta3/notification.md b/docs/api/v1beta3/notification.md
index cb972e603..34410f85d 100644
--- a/docs/api/v1beta3/notification.md
+++ b/docs/api/v1beta3/notification.md
@@ -161,7 +161,8 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>Summary holds a short description of the impact and affected cluster.</p>
+<p>Summary holds a short description of the impact and affected cluster.
+Deprecated: Use EventMetadata instead.</p>
 </td>
 </tr>
 <tr>
@@ -492,7 +493,8 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>Summary holds a short description of the impact and affected cluster.</p>
+<p>Summary holds a short description of the impact and affected cluster.
+Deprecated: Use EventMetadata instead.</p>
 </td>
 </tr>
 <tr>
diff --git a/docs/spec/v1beta3/alerts.md b/docs/spec/v1beta3/alerts.md
index 6932e195d..0e579cea6 100644
--- a/docs/spec/v1beta3/alerts.md
+++ b/docs/spec/v1beta3/alerts.md
@@ -28,9 +28,13 @@ metadata:
   name: slack
   namespace: flux-system
 spec:
-  summary: "Cluster addons impacted in us-east-2"
   providerRef:
     name: slack-bot
+  eventMetadata:
+    summary: Cluster addons impacted
+    env: prod
+    cluster: my-cluster
+    region: us-east-2
   eventSeverity: error
   eventSources:
     - kind: GitRepository
@@ -51,7 +55,7 @@ In the above example:
   all GitRepositories and Kustomizations in the `flux-system` namespace.
 - When an event with severity `error` is received, the controller posts
   a message on Slack channel from `.spec.channel`,
-  containing the `summary` text and the reconciliation error.
+  containing the metadata and the reconciliation error.
 
 You can run this example by saving the manifests into `slack-alerts.yaml`.
 
@@ -78,10 +82,15 @@ An Alert also needs a
 
 ### Summary
 
-`.spec.summary` is an optional field to specify a short description of the
-impact and affected cluster.
+`.spec.summary` is an optional field to specify a short description of the impact.
+
+The summary max length can't be greater than 255 characters.
 
-The summary max length can't be greater than 255 characters. 
+**Warning:** Support for `.spec.summary` has been deprecated and will be removed in
+Alert API v1 GA. If you have any Alerts using this field, the controller will log a
+deprecation warning. Please use [`.spec.eventMetadata.summary`](#event-metadata) or
+[object annotations](#event-metadata-from-object-annotations) for defining alert
+summary instead.
 
 ### Provider reference
 
@@ -146,10 +155,11 @@ preventing tenants from subscribing to another tenant's events.
 ### Event metadata
 
 `.spec.eventMetadata` is an optional field for adding metadata to events dispatched by
-the controller. This can be used for enhancing the context of the event. If a field
-would override one already present on the original event as generated by the emitter,
-then the override doesn't happen, i.e. the original value is preserved, and an info
-log is printed.
+the controller. This can be used for enhancing the context of the event, e.g. with
+cluster-level information.
+
+For all the event metadata sources and their precedence order, please refer to
+[Event metadata from object annotations](#event-metadata-from-object-annotations).
 
 #### Example
 
@@ -168,9 +178,68 @@ spec:
   inclusionList:
     - ".*succeeded.*"
   eventMetadata:
-    app.kubernetes.io/env: "production"
-    app.kubernetes.io/cluster: "my-cluster"
-    app.kubernetes.io/region: "us-east-1"
+    env: production
+    cluster: my-cluster
+    region: us-east-1
+```
+
+### Event metadata from object annotations
+
+Event metadata has four sources. They are listed below in order of precedence,
+from lowest to highest:
+
+1. User-defined metadata on Flux objects, set with the `event.toolkit.fluxcd.io/`
+prefix in the keys of the object's `.metadata.annotations`.
+2. User-defined metadata on the Alert object, set with [`.spec.eventMetadata`](#event-metadata).
+3. User-defined summary on the Alert object, set with [`.spec.summary`](#summary) (deprecated, see docs).
+4. Controller-defined metadata, set with the `<controller group>.toolkit.fluxcd.io/`
+prefix in the metadata keys of the event payload.
+
+If there are any metadata key conflicts between the sources, the higher
+precedence source will override the lower precedence source, and a warning
+log and Kubernetes event will be emitted.
+
+#### Example
+
+```yaml
+---
+apiVersion: notification.toolkit.fluxcd.io/v1beta3
+kind: Alert
+metadata:
+  name: <name>
+spec:
+  eventSources:
+    - kind: HelmRelease
+      name: '*'
+  eventMetadata:
+    env: production
+    cluster: my-cluster
+    region: us-east-1
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: my-webapp
+  annotations:
+    event.toolkit.fluxcd.io/summary: "my-webapp impacted. Playbook: <URL to playbook>"
+    event.toolkit.fluxcd.io/deploymentID: e076e315-5a48-41c3-81c8-8d8bdee7d74d
+spec:
+  ... # fields omitted for brevity
+```
+
+In the above example, the event payload dispatched by the controller will look like this
+(most fields omitted for highlighting the metadata):
+
+```json
+{
+  "metadata": {
+    "env": "production",
+    "cluster": "my-cluster",
+    "region": "us-east-1",
+    "summary": "my-webapp impacted. Playbook: <URL to playbook>",
+    "deploymentID": "e076e315-5a48-41c3-81c8-8d8bdee7d74d"
+  }
+}
 ```
 
 ### Event severity
diff --git a/go.mod b/go.mod
index 2a8e2a015..dec38ff71 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/containrrr/shoutrrr v0.8.0
 	github.com/fluxcd/cli-utils v0.36.0-flux.11
 	github.com/fluxcd/notification-controller/api v1.4.0
-	github.com/fluxcd/pkg/apis/event v0.12.0
+	github.com/fluxcd/pkg/apis/event v0.13.0
 	github.com/fluxcd/pkg/apis/meta v1.9.0
 	github.com/fluxcd/pkg/git v0.23.0
 	github.com/fluxcd/pkg/masktoken v0.6.0
diff --git a/go.sum b/go.sum
index 44baecd26..c36dcb037 100644
--- a/go.sum
+++ b/go.sum
@@ -142,8 +142,8 @@ github.com/fluxcd/cli-utils v0.36.0-flux.11 h1:W0y2uvCVkcE8bgV9jgoGSjzWbLFiNq1Aj
 github.com/fluxcd/cli-utils v0.36.0-flux.11/go.mod h1:WZ7xUpZbK+O6HBxA5UWqzWTLSSltdmj4wS1LstS5Dqs=
 github.com/fluxcd/pkg/apis/acl v0.5.0 h1:+ykKezgerKUlZwSYFUy03lPMOIAyWlqvMNNLIWWqOhk=
 github.com/fluxcd/pkg/apis/acl v0.5.0/go.mod h1:IVDZx3MAoDWjlLrJHMF9Z27huFuXAEQlnbWw0M6EcTs=
-github.com/fluxcd/pkg/apis/event v0.12.0 h1:+zQVefTG3+THYRS48dtZkoA1rdbZZNx3t6wnbzprFIE=
-github.com/fluxcd/pkg/apis/event v0.12.0/go.mod h1:aRK2AONnjjSNW61B6Iy3SW4YHozACntnJeGm3fFqDqA=
+github.com/fluxcd/pkg/apis/event v0.13.0 h1:m5qHAhYIC0+mRFy5OC8FZxBVBGJM3qxJ/sEg2Vgx4T8=
+github.com/fluxcd/pkg/apis/event v0.13.0/go.mod h1:aRK2AONnjjSNW61B6Iy3SW4YHozACntnJeGm3fFqDqA=
 github.com/fluxcd/pkg/apis/meta v1.9.0 h1:wPgm7bWNJZ/ImS5GqikOxt362IgLPFBG73dZ27uWRiQ=
 github.com/fluxcd/pkg/apis/meta v1.9.0/go.mod h1:pMea8eEZcsFSI7ngRnTHFtDZk2CEZGgtrueNgI6Iu70=
 github.com/fluxcd/pkg/auth v0.2.0 h1:Df3pHGMDJjpr8AcGKgPvudXF3Lb3SuBlkAmhrkp7U1k=
diff --git a/internal/server/event_handlers.go b/internal/server/event_handlers.go
index 685948fd0..b8a57f2f7 100644
--- a/internal/server/event_handlers.go
+++ b/internal/server/event_handlers.go
@@ -24,6 +24,7 @@ import (
 	"net/http"
 	"net/url"
 	"regexp"
+	"slices"
 	"strings"
 	"time"
 
@@ -256,7 +257,7 @@ func (s *EventServer) getNotificationParams(ctx context.Context, event *eventv1.
 	}
 
 	notification := *event.DeepCopy()
-	s.enhanceEventWithAlertMetadata(ctx, &notification, alert)
+	s.combineEventMetadata(ctx, &notification, alert)
 
 	return sender, &notification, token, provider.GetTimeout(), nil
 }
@@ -418,30 +419,90 @@ func (s *EventServer) eventMatchesAlertSource(ctx context.Context, event *eventv
 	return sel.Matches(labels.Set(obj.GetLabels()))
 }
 
-// enhanceEventWithAlertMetadata enhances the event with Alert metadata.
-func (s *EventServer) enhanceEventWithAlertMetadata(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert) {
-	meta := event.Metadata
-	if meta == nil {
-		meta = make(map[string]string)
+// combineEventMetadata combines all the sources of metadata for the event
+// according to the precedence order defined in RFC 0008. From lowest to
+// highest precedence, the sources are:
+//
+// 1) Event metadata keys prefixed with the Event API Group stripped of the prefix.
+//
+// 2) Alert .spec.eventMetadata with the keys as they are.
+//
+// 3) Alert .spec.summary with the key "summary".
+//
+// 4) Event metadata keys prefixed with the involved object's API Group stripped of the prefix.
+//
+// At the end of the process key conflicts are detected and a single
+// info-level log is emitted to warn users about all the conflicts,
+// but only if at least one conflict is found.
+func (s *EventServer) combineEventMetadata(ctx context.Context, event *eventv1.Event, alert *apiv1beta3.Alert) {
+	const (
+		sourceEventGroup         = "involved object annotations"
+		sourceAlertEventMetadata = "Alert object .spec.eventMetadata"
+		sourceAlertSummary       = "Alert object .spec.summary"
+		sourceObjectGroup        = "involved object controller metadata"
+
+		summaryKey = "summary"
+	)
+
+	l := log.FromContext(ctx)
+	metadata := make(map[string]string)
+	metadataSources := make(map[string][]string)
+
+	// 1) Event metadata keys prefixed with the Event API Group stripped of the prefix.
+	const eventGroupPrefix = eventv1.Group + "/"
+	for k, v := range event.Metadata {
+		if strings.HasPrefix(k, eventGroupPrefix) {
+			key := strings.TrimPrefix(k, eventGroupPrefix)
+			metadata[key] = v
+			metadataSources[key] = append(metadataSources[key], sourceEventGroup)
+		}
 	}
 
-	for key, value := range alert.Spec.EventMetadata {
-		if _, alreadyPresent := meta[key]; !alreadyPresent {
-			meta[key] = value
-		} else {
-			log.FromContext(ctx).
-				Info("metadata key found in the existing set of metadata", "key", key)
-			s.Eventf(alert, corev1.EventTypeWarning, "MetadataAppendFailed",
-				"metadata key found in the existing set of metadata for '%s' in %s", key, involvedObjectString(event.InvolvedObject))
-		}
+	// 2) Alert .spec.eventMetadata with the keys as they are.
+	for k, v := range alert.Spec.EventMetadata {
+		metadata[k] = v
+		metadataSources[k] = append(metadataSources[k], sourceAlertEventMetadata)
 	}
 
+	// 3) Alert .spec.summary with the key "summary".
 	if alert.Spec.Summary != "" {
-		meta["summary"] = alert.Spec.Summary
+		metadata[summaryKey] = alert.Spec.Summary
+		metadataSources[summaryKey] = append(metadataSources[summaryKey], sourceAlertSummary)
+		l.Info("warning: specifying an alert summary with '.spec.summary' is deprecated, use '.spec.eventMetadata.summary' instead")
+	}
+
+	// 4) Event metadata keys prefixed with the involved object's API Group stripped of the prefix.
+	objectGroupPrefix := event.InvolvedObject.GroupVersionKind().Group + "/"
+	for k, v := range event.Metadata {
+		if strings.HasPrefix(k, objectGroupPrefix) {
+			key := strings.TrimPrefix(k, objectGroupPrefix)
+			metadata[key] = v
+			metadataSources[key] = append(metadataSources[key], sourceObjectGroup)
+		}
+	}
+
+	// Detect key conflicts and emit warnings if any.
+	type keyConflict struct {
+		Key     string   `json:"key"`
+		Sources []string `json:"sources"`
+	}
+	var conflictingKeys []*keyConflict
+	conflictEventAnnotations := make(map[string]string)
+	for key, sources := range metadataSources {
+		if len(sources) > 1 {
+			conflictingKeys = append(conflictingKeys, &keyConflict{key, sources})
+			conflictEventAnnotations[key] = strings.Join(sources, ", ")
+		}
+	}
+	if len(conflictingKeys) > 0 {
+		const msg = "metadata key conflicts detected (please refer to the Alert API docs and Flux RFC 0008 for more information)"
+		slices.SortFunc(conflictingKeys, func(a, b *keyConflict) int { return strings.Compare(a.Key, b.Key) })
+		l.Info("warning: "+msg, "conflictingKeys", conflictingKeys)
+		s.AnnotatedEventf(alert, conflictEventAnnotations, corev1.EventTypeWarning, "MetadataAppendFailed", "%s", msg)
 	}
 
-	if len(meta) > 0 {
-		event.Metadata = meta
+	if len(metadata) > 0 {
+		event.Metadata = metadata
 	}
 }
 
@@ -450,7 +511,9 @@ func excludeInternalMetadata(event *eventv1.Event) {
 	if len(event.Metadata) == 0 {
 		return
 	}
-	excludeList := []string{eventv1.MetaTokenKey}
+	objectGroup := event.InvolvedObject.GetObjectKind().GroupVersionKind().Group
+	tokenKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaTokenKey)
+	excludeList := []string{tokenKey}
 	for _, key := range excludeList {
 		delete(event.Metadata, key)
 	}
diff --git a/internal/server/event_handlers_test.go b/internal/server/event_handlers_test.go
index 0b9f7aef6..9820ddf37 100644
--- a/internal/server/event_handlers_test.go
+++ b/internal/server/event_handlers_test.go
@@ -515,7 +515,7 @@ func TestGetNotificationParams(t *testing.T) {
 				g.Expect(n.Metadata["summary"]).To(Equal(tt.alertSummary))
 			}
 			// NOTE: This is performing simple check. Thorough test for event
-			// metadata is performed in TestEnhanceEventWithAlertMetadata.
+			// metadata is performed in TestCombineEventMetadata.
 			if tt.alertEventMetadata != nil {
 				for k, v := range tt.alertEventMetadata {
 					g.Expect(n.Metadata).To(HaveKeyWithValue(k, v))
@@ -977,84 +977,143 @@ func TestEventMatchesAlert(t *testing.T) {
 	}
 }
 
-func TestEnhanceEventWithAlertMetadata(t *testing.T) {
-	s := &EventServer{
-		logger:        log.Log,
-		EventRecorder: record.NewFakeRecorder(32),
-	}
-
+func TestCombineEventMetadata(t *testing.T) {
 	for name, tt := range map[string]struct {
 		event            eventv1.Event
 		alert            apiv1beta3.Alert
 		expectedMetadata map[string]string
+		conflictEvent    string
 	}{
 		"empty metadata": {
 			event:            eventv1.Event{},
 			alert:            apiv1beta3.Alert{},
 			expectedMetadata: nil,
 		},
-		"enhanced with summary": {
-			event: eventv1.Event{},
+		"all metadata sources work": {
+			event: eventv1.Event{
+				Metadata: map[string]string{
+					"kustomize.toolkit.fluxcd.io/controllerMetadata1": "controllerMetadataValue1",
+					"kustomize.toolkit.fluxcd.io/controllerMetadata2": "controllerMetadataValue2",
+					"event.toolkit.fluxcd.io/objectMetadata1":         "objectMetadataValue1",
+					"event.toolkit.fluxcd.io/objectMetadata2":         "objectMetadataValue2",
+				},
+			},
 			alert: apiv1beta3.Alert{
 				Spec: apiv1beta3.AlertSpec{
-					Summary: "summary",
+					Summary: "summaryValue",
+					EventMetadata: map[string]string{
+						"foo": "bar",
+						"baz": "qux",
+					},
 				},
 			},
 			expectedMetadata: map[string]string{
-				"summary": "summary",
+				"foo":                 "bar",
+				"baz":                 "qux",
+				"controllerMetadata1": "controllerMetadataValue1",
+				"controllerMetadata2": "controllerMetadataValue2",
+				"summary":             "summaryValue",
+				"objectMetadata1":     "objectMetadataValue1",
+				"objectMetadata2":     "objectMetadataValue2",
 			},
 		},
-		"overriden with summary": {
+		"object metadata is overriden by summary": {
 			event: eventv1.Event{
 				Metadata: map[string]string{
-					"summary": "original summary",
+					"event.toolkit.fluxcd.io/summary": "objectSummary",
 				},
 			},
 			alert: apiv1beta3.Alert{
 				Spec: apiv1beta3.AlertSpec{
-					Summary: "summary",
+					Summary: "alertSummary",
 				},
 			},
 			expectedMetadata: map[string]string{
-				"summary": "summary",
+				"summary": "alertSummary",
 			},
+			conflictEvent: "Warning MetadataAppendFailed metadata key conflicts detected (please refer to the Alert API docs and Flux RFC 0008 for more information) map[summary:involved object annotations, Alert object .spec.summary]",
 		},
-		"enhanced with metadata": {
+		"alert event metadata is overriden by summary": {
 			event: eventv1.Event{},
 			alert: apiv1beta3.Alert{
 				Spec: apiv1beta3.AlertSpec{
+					Summary: "alertSummary",
 					EventMetadata: map[string]string{
-						"foo": "bar",
+						"summary": "eventMetadataSummary",
 					},
 				},
 			},
 			expectedMetadata: map[string]string{
-				"foo": "bar",
+				"summary": "alertSummary",
+			},
+			conflictEvent: "Warning MetadataAppendFailed metadata key conflicts detected (please refer to the Alert API docs and Flux RFC 0008 for more information) map[summary:Alert object .spec.eventMetadata, Alert object .spec.summary]",
+		},
+		"summary is overriden by controller metadata": {
+			event: eventv1.Event{
+				Metadata: map[string]string{
+					"kustomize.toolkit.fluxcd.io/summary": "controllerSummary",
+				},
+			},
+			alert: apiv1beta3.Alert{
+				Spec: apiv1beta3.AlertSpec{
+					Summary: "alertSummary",
+				},
+			},
+			expectedMetadata: map[string]string{
+				"summary": "controllerSummary",
 			},
+			conflictEvent: "Warning MetadataAppendFailed metadata key conflicts detected (please refer to the Alert API docs and Flux RFC 0008 for more information) map[summary:Alert object .spec.summary, involved object controller metadata]",
 		},
-		"skipped override with metadata": {
+		"precedence order in RFC 0008 is honered": {
 			event: eventv1.Event{
 				Metadata: map[string]string{
-					"foo": "baz",
+					"kustomize.toolkit.fluxcd.io/objectMetadataOverridenByController": "controllerMetadataValue1",
+					"kustomize.toolkit.fluxcd.io/alertMetadataOverridenByController":  "controllerMetadataValue2",
+					"kustomize.toolkit.fluxcd.io/controllerMetadata":                  "controllerMetadataValue3",
+					"event.toolkit.fluxcd.io/objectMetadata":                          "objectMetadataValue1",
+					"event.toolkit.fluxcd.io/objectMetadataOverridenByAlert":          "objectMetadataValue2",
+					"event.toolkit.fluxcd.io/objectMetadataOverridenByController":     "objectMetadataValue3",
 				},
 			},
 			alert: apiv1beta3.Alert{
 				Spec: apiv1beta3.AlertSpec{
 					EventMetadata: map[string]string{
-						"foo": "bar",
+						"objectMetadataOverridenByAlert":     "alertMetadataValue1",
+						"alertMetadata":                      "alertMetadataValue2",
+						"alertMetadataOverridenByController": "alertMetadataValue3",
 					},
 				},
 			},
 			expectedMetadata: map[string]string{
-				"foo": "baz",
-			},
+				"objectMetadata":                      "objectMetadataValue1",
+				"objectMetadataOverridenByAlert":      "alertMetadataValue1",
+				"objectMetadataOverridenByController": "controllerMetadataValue1",
+				"alertMetadata":                       "alertMetadataValue2",
+				"alertMetadataOverridenByController":  "controllerMetadataValue2",
+				"controllerMetadata":                  "controllerMetadataValue3",
+			},
+			conflictEvent: "Warning MetadataAppendFailed metadata key conflicts detected (please refer to the Alert API docs and Flux RFC 0008 for more information) map[alertMetadataOverridenByController:Alert object .spec.eventMetadata, involved object controller metadata objectMetadataOverridenByAlert:involved object annotations, Alert object .spec.eventMetadata objectMetadataOverridenByController:involved object annotations, involved object controller metadata]",
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
 			g := NewGomegaWithT(t)
 
-			s.enhanceEventWithAlertMetadata(context.Background(), &tt.event, &tt.alert)
+			eventRecorder := record.NewFakeRecorder(1)
+			s := &EventServer{
+				logger:        log.Log,
+				EventRecorder: eventRecorder,
+			}
+
+			tt.event.InvolvedObject.APIVersion = "kustomize.toolkit.fluxcd.io/v1"
+			s.combineEventMetadata(context.Background(), &tt.event, &tt.alert)
 			g.Expect(tt.event.Metadata).To(BeEquivalentTo(tt.expectedMetadata))
+
+			var event string
+			select {
+			case event = <-eventRecorder.Events:
+			default:
+			}
+			g.Expect(event).To(Equal(tt.conflictEvent))
 		})
 	}
 }
@@ -1071,13 +1130,16 @@ func Test_excludeInternalMetadata(t *testing.T) {
 		{
 			name: "internal metadata",
 			event: eventv1.Event{
+				InvolvedObject: corev1.ObjectReference{
+					APIVersion: "kustomize.toolkit.fluxcd.io/v1",
+				},
 				Metadata: map[string]string{
-					eventv1.MetaTokenKey:    "aaaa",
-					eventv1.MetaRevisionKey: "bbbb",
+					"kustomize.toolkit.fluxcd.io/" + eventv1.MetaTokenKey:    "aaaa",
+					"kustomize.toolkit.fluxcd.io/" + eventv1.MetaRevisionKey: "bbbb",
 				},
 			},
 			wantMetadata: map[string]string{
-				eventv1.MetaRevisionKey: "bbbb",
+				"kustomize.toolkit.fluxcd.io/" + eventv1.MetaRevisionKey: "bbbb",
 			},
 		},
 	}
diff --git a/internal/server/event_server.go b/internal/server/event_server.go
index 9aa5eb110..0f78cce93 100644
--- a/internal/server/event_server.go
+++ b/internal/server/event_server.go
@@ -156,21 +156,23 @@ func (s *EventServer) eventMiddleware(h http.Handler) http.Handler {
 }
 
 // cleanupMetadata removes metadata entries which are not used for alerting.
+// In particular, it removes the checksum and digest metadata entries and
+// keeps only the metadata entries that are prefixed with either the event
+// group prefix or the involved object's group prefix.
 func cleanupMetadata(event *eventv1.Event) {
-	group := event.InvolvedObject.GetObjectKind().GroupVersionKind().Group
+	const eventGroupPrefix = eventv1.Group + "/"
+	objectGroupPrefix := event.InvolvedObject.GetObjectKind().GroupVersionKind().Group + "/"
 	excludeList := []string{
-		fmt.Sprintf("%s/%s", group, eventv1.MetaChecksumKey),
-		fmt.Sprintf("%s/%s", group, eventv1.MetaDigestKey),
+		fmt.Sprintf("%s%s", objectGroupPrefix, eventv1.MetaChecksumKey),
+		fmt.Sprintf("%s%s", objectGroupPrefix, eventv1.MetaDigestKey),
 	}
 
+	// Filter other meta based on group prefix, while filtering out excludes
 	meta := make(map[string]string)
-	if event.Metadata != nil && len(event.Metadata) > 0 {
-		// Filter other meta based on group prefix, while filtering out excludes
-		for key, val := range event.Metadata {
-			if strings.HasPrefix(key, group) && !inList(excludeList, key) {
-				newKey := strings.TrimPrefix(key, fmt.Sprintf("%s/", group))
-				meta[newKey] = val
-			}
+	for key, val := range event.Metadata {
+		if !inList(excludeList, key) &&
+			(strings.HasPrefix(key, eventGroupPrefix) || strings.HasPrefix(key, objectGroupPrefix)) {
+			meta[key] = val
 		}
 	}
 
@@ -229,12 +231,16 @@ func eventKeyFunc(r *http.Request) (string, error) {
 		"message=" + event.Message,
 	}
 
-	revision, ok := event.Metadata[eventv1.MetaRevisionKey]
+	objectGroup := event.InvolvedObject.GetObjectKind().GroupVersionKind().Group
+
+	revisionKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaRevisionKey)
+	revision, ok := event.Metadata[revisionKey]
 	if ok {
 		comps = append(comps, "revision="+revision)
 	}
 
-	token, ok := event.Metadata[eventv1.MetaTokenKey]
+	tokenKey := fmt.Sprintf("%s/%s", objectGroup, eventv1.MetaTokenKey)
+	token, ok := event.Metadata[tokenKey]
 	if ok {
 		comps = append(comps, "token="+token)
 	}
diff --git a/internal/server/event_server_test.go b/internal/server/event_server_test.go
index 1b47a3269..ac1e327cc 100644
--- a/internal/server/event_server_test.go
+++ b/internal/server/event_server_test.go
@@ -562,13 +562,17 @@ func TestCleanupMetadata(t *testing.T) {
 					"source.toolkit.fluxcd.io/baz":        "bazval",
 					group + "/zzz":                        "zzzz",
 					group + "/aa/bb":                      "cc",
+					"event.toolkit.fluxcd.io/baz":         "foo",
+					"event.toolkit.fluxcd.io/bazfoo":      "foobaz",
 				},
 			},
 			wantMeta: map[string]string{
-				"foo":   "fooval",
-				"bar":   "barval",
-				"zzz":   "zzzz",
-				"aa/bb": "cc",
+				"kustomize.toolkit.fluxcd.io/foo":   "fooval",
+				"kustomize.toolkit.fluxcd.io/bar":   "barval",
+				"kustomize.toolkit.fluxcd.io/zzz":   "zzzz",
+				"kustomize.toolkit.fluxcd.io/aa/bb": "cc",
+				"event.toolkit.fluxcd.io/baz":       "foo",
+				"event.toolkit.fluxcd.io/bazfoo":    "foobaz",
 			},
 		},
 	}