diff --git a/tools/events/event_recorder.go b/tools/events/event_recorder.go index ba2ec7be4..08fa57d9f 100644 --- a/tools/events/event_recorder.go +++ b/tools/events/event_recorder.go @@ -41,9 +41,14 @@ type recorderImpl struct { } var _ EventRecorder = &recorderImpl{} +var _ AnnotatedEventRecorder = &recorderImpl{} func (recorder *recorderImpl) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { - recorder.eventf(klog.Background(), regarding, related, eventtype, reason, action, note, args...) + recorder.eventf(klog.Background(), regarding, related, nil, eventtype, reason, action, note, args...) +} + +func (recorder *recorderImpl) AnnotatedEventf(regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) { + recorder.eventf(klog.Background(), regarding, related, annotations, eventtype, reason, action, note, args...) } type recorderImplLogger struct { @@ -54,14 +59,18 @@ type recorderImplLogger struct { var _ EventRecorderLogger = &recorderImplLogger{} func (recorder *recorderImplLogger) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { - recorder.eventf(recorder.logger, regarding, related, eventtype, reason, action, note, args...) + recorder.eventf(recorder.logger, regarding, related, nil, eventtype, reason, action, note, args...) +} + +func (recorder *recorderImplLogger) AnnotatedEventf(regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) { + recorder.eventf(recorder.logger, regarding, related, annotations, eventtype, reason, action, note, args...) } func (recorder *recorderImplLogger) WithLogger(logger klog.Logger) EventRecorderLogger { return &recorderImplLogger{recorderImpl: recorder.recorderImpl, logger: logger} } -func (recorder *recorderImpl) eventf(logger klog.Logger, regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { +func (recorder *recorderImpl) eventf(logger klog.Logger, regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) { timestamp := metav1.MicroTime{Time: time.Now()} message := fmt.Sprintf(note, args...) refRegarding, err := reference.GetReference(recorder.scheme, regarding) @@ -81,14 +90,14 @@ func (recorder *recorderImpl) eventf(logger klog.Logger, regarding runtime.Objec logger.Error(nil, "Unsupported event type", "eventType", eventtype) return } - event := recorder.makeEvent(refRegarding, refRelated, timestamp, eventtype, reason, message, recorder.reportingController, recorder.reportingInstance, action) + event := recorder.makeEvent(refRegarding, refRelated, timestamp, annotations, eventtype, reason, message, recorder.reportingController, recorder.reportingInstance, action) go func() { defer utilruntime.HandleCrash() recorder.Action(watch.Added, event) }() } -func (recorder *recorderImpl) makeEvent(refRegarding *v1.ObjectReference, refRelated *v1.ObjectReference, timestamp metav1.MicroTime, eventtype, reason, message string, reportingController string, reportingInstance string, action string) *eventsv1.Event { +func (recorder *recorderImpl) makeEvent(refRegarding *v1.ObjectReference, refRelated *v1.ObjectReference, timestamp metav1.MicroTime, annotations map[string]string, eventtype, reason, message string, reportingController string, reportingInstance string, action string) *eventsv1.Event { t := metav1.Time{Time: recorder.clock.Now()} namespace := refRegarding.Namespace if namespace == "" { @@ -96,8 +105,9 @@ func (recorder *recorderImpl) makeEvent(refRegarding *v1.ObjectReference, refRel } return &eventsv1.Event{ ObjectMeta: metav1.ObjectMeta{ - Name: util.GenerateEventName(refRegarding.Name, t.UnixNano()), - Namespace: namespace, + Name: util.GenerateEventName(refRegarding.Name, t.UnixNano()), + Namespace: namespace, + Annotations: annotations, }, EventTime: timestamp, Series: nil, diff --git a/tools/events/eventseries_test.go b/tools/events/eventseries_test.go index 526101ca6..b8c590d05 100644 --- a/tools/events/eventseries_test.go +++ b/tools/events/eventseries_test.go @@ -299,7 +299,7 @@ func TestFinishSeries(t *testing.T) { cache := map[eventKey]*eventsv1.Event{} eventBroadcaster := newBroadcaster(&testEvents, 0, cache).(*eventBroadcasterImpl) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, "k8s.io/kube-foo").(*recorderImplLogger) - cachedEvent := recorder.makeEvent(regarding, related, metav1.MicroTime{Time: time.Now()}, v1.EventTypeNormal, "test", "some verbose message: 1", "eventTest", "eventTest-"+hostname, "started") + cachedEvent := recorder.makeEvent(regarding, related, metav1.MicroTime{Time: time.Now()}, nil, v1.EventTypeNormal, "test", "some verbose message: 1", "eventTest", "eventTest-"+hostname, "started") nonFinishedEvent := cachedEvent.DeepCopy() nonFinishedEvent.ReportingController = "nonFinished-controller" cachedEvent.Series = &eventsv1.EventSeries{ @@ -386,7 +386,7 @@ func TestRefreshExistingEventSeries(t *testing.T) { cache := map[eventKey]*eventsv1.Event{} eventBroadcaster := newBroadcaster(&testEvents, 0, cache).(*eventBroadcasterImpl) recorder := eventBroadcaster.NewRecorder(scheme.Scheme, "k8s.io/kube-foo").(*recorderImplLogger) - cachedEvent := recorder.makeEvent(regarding, related, metav1.MicroTime{Time: time.Now()}, v1.EventTypeNormal, "test", "some verbose message: 1", "eventTest", "eventTest-"+hostname, "started") + cachedEvent := recorder.makeEvent(regarding, related, metav1.MicroTime{Time: time.Now()}, nil, v1.EventTypeNormal, "test", "some verbose message: 1", "eventTest", "eventTest-"+hostname, "started") cachedEvent.Series = &eventsv1.EventSeries{ Count: 10, LastObservedTime: LastObservedTime, diff --git a/tools/events/fake.go b/tools/events/fake.go index e26826d6c..5c1892a76 100644 --- a/tools/events/fake.go +++ b/tools/events/fake.go @@ -27,26 +27,71 @@ import ( // when created manually and not by NewFakeRecorder, however all events may be // thrown away in this case. type FakeRecorder struct { - Events chan string + Events chan string + Verbose bool } var _ EventRecorderLogger = &FakeRecorder{} +var _ AnnotatedEventRecorder = &FakeRecorder{} // Eventf emits an event func (f *FakeRecorder) Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) { - if f.Events != nil { - f.Events <- fmt.Sprintf(eventtype+" "+reason+" "+note, args...) - } + f.writeEvent(regarding, related, nil, eventtype, reason, action, note, args...) +} + +// AnnotatedEventf emits an event like Eventf, but with annotations attached +func (f *FakeRecorder) AnnotatedEventf(regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) { + f.writeEvent(regarding, related, annotations, eventtype, reason, action, note, args...) } func (f *FakeRecorder) WithLogger(logger klog.Logger) EventRecorderLogger { return f } +// writeEvent constructs a string from the event parameters and sends +// it to the Events channel +func (f *FakeRecorder) writeEvent(regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) { + if f.Events != nil { + msg := fmt.Sprintf(eventtype+" "+reason+" "+note, args...) + if f.Verbose { + msg = eventtype + " " + reason + " " + action + " " + fmt.Sprintf(note, args...) + + objectString(regarding) + objectString(related) + annotationsString(annotations) + } + f.Events <- msg + } +} + +// annotationsString returns a formatted string of the annotations map, +// or empty string if none +func annotationsString(annotations map[string]string) string { + annotationString := "" + if len(annotations) > 0 { + annotationString = " " + fmt.Sprint(annotations) + } + return annotationString +} + +// objectString returns a formatted string with the object's kind and +// apiVersion +func objectString(object runtime.Object) string { + objectString := "" + if object != nil { + gvk := object.GetObjectKind().GroupVersionKind() + if !gvk.Empty() { + objectString = fmt.Sprintf(" {kind=%s,apiVersion=%s}", + gvk.Kind, + gvk.GroupVersion(), + ) + } + } + return objectString +} + // NewFakeRecorder creates new fake event recorder with event channel with // buffer of given size. func NewFakeRecorder(bufferSize int) *FakeRecorder { return &FakeRecorder{ - Events: make(chan string, bufferSize), + Events: make(chan string, bufferSize), + Verbose: false, } } diff --git a/tools/events/interfaces.go b/tools/events/interfaces.go index bb6109f62..209b3c0d5 100644 --- a/tools/events/interfaces.go +++ b/tools/events/interfaces.go @@ -28,6 +28,7 @@ import ( type EventRecorder = internalevents.EventRecorder type EventRecorderLogger = internalevents.EventRecorderLogger +type AnnotatedEventRecorder = internalevents.AnnotatedEventRecorder // EventBroadcaster knows how to receive events and send them to any EventSink, watcher, or log. type EventBroadcaster interface { diff --git a/tools/internal/events/interfaces.go b/tools/internal/events/interfaces.go index be6261b53..50bb4d2e8 100644 --- a/tools/internal/events/interfaces.go +++ b/tools/internal/events/interfaces.go @@ -43,6 +43,15 @@ type EventRecorder interface { Eventf(regarding runtime.Object, related runtime.Object, eventtype, reason, action, note string, args ...interface{}) } +// AnnotatedEventRecorder is an optional extension of EventRecorder that +// supports attaching annotations to events at creation time. +// Annotations are intended for low-frequency correlation metadata, not as a general purpose structured-data channel. +// Kubernetes events are best-effort, supplemental observability data and may be dropped, deduped or aggregated at any +// point in pipeline. Do not rely on annotations for event-driven control flow. +type AnnotatedEventRecorder interface { + AnnotatedEventf(regarding runtime.Object, related runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) +} + // EventRecorderLogger extends EventRecorder such that a logger can // be set for methods in EventRecorder. Normally, those methods // uses the global default logger to record errors and debug messages. diff --git a/tools/record/event.go b/tools/record/event.go index 305a92438..e88eaef37 100644 --- a/tools/record/event.go +++ b/tools/record/event.go @@ -176,6 +176,12 @@ func (a *EventRecorderAdapter) Eventf(regarding, _ runtime.Object, eventtype, re a.recorder.Eventf(regarding, eventtype, reason, note, args...) } +// AnnotatedEventf is a wrapper around v1 AnnotatedEventf +func (a *EventRecorderAdapter) AnnotatedEventf(regarding, _ runtime.Object, annotations map[string]string, eventtype, reason, action, note string, args ...interface{}) { + //nolint:forbidigo // Legacy usage + a.recorder.AnnotatedEventf(regarding, annotations, eventtype, reason, note, args...) +} + func (a *EventRecorderAdapter) WithLogger(logger klog.Logger) internalevents.EventRecorderLogger { return &EventRecorderAdapter{ recorder: a.recorder.WithLogger(logger),