In the previous article we covered WorkQueue — the retry and rate-limiting backbone of controllers. In this article we look at a different but equally important mechanism: EventBroadcaster. It's how Kubernetes components record what they're doing, and how you can do the same in your own controllers.
Source: k8s.io/client-go/tools/record/
1. What Is a Kubernetes Event?
A Kubernetes Event is a first-class resource object — stored in etcd, queryable via kubectl, and subject to the same API machinery as Pods or Deployments.
# View recent events in a namespace
kubectl get events -n default
# View events for a specific Pod (shown in describe output)
kubectl describe pod nginx-deployment-7d8ff89d87-25kb4
LAST SEEN TYPE REASON OBJECT MESSAGE
2m Normal Scheduled pod/nginx-7d8ff89d87-25kb4 Successfully assigned default/nginx to node-1
2m Normal Pulling pod/nginx-7d8ff89d87-25kb4 Pulling image "nginx:1.21"
1m Normal Pulled pod/nginx-7d8ff89d87-25kb4 Successfully pulled image in 3.2s
1m Normal Created pod/nginx-7d8ff89d87-25kb4 Created container nginx
1m Normal Started pod/nginx-7d8ff89d87-25kb4 Started container nginx
⚠️ Important distinction: Kubernetes Events are resource objects managed by the API Server. They are NOT the same as the etcd watch callback events used internally by Informer/Reflector. Don't confuse the two.
2. The Event Resource Structure
// k8s.io/api/core/v1/types.go
type Event struct {
metav1.TypeMeta
metav1.ObjectMeta
// The object this event is about (e.g. the Pod that was scheduled)
InvolvedObject ObjectReference
// Short machine-readable reason string (e.g. "Scheduled", "Pulling", "Failed")
Reason string
// Human-readable description of what happened
Message string
// Component that generated this event
Source EventSource // {Component: "scheduler", Host: "master-1"}
// Timing
FirstTimestamp metav1.Time // when this event first occurred
LastTimestamp metav1.Time // most recent occurrence
Count int32 // how many times this event has occurred
// "Normal" or "Warning"
Type string
// High-precision timestamp (for newer event API)
EventTime metav1.MicroTime
// For aggregated/series events
Series *EventSeries
// What action was taken
Action string
// Secondary object involved (e.g. the Node a Pod was scheduled to)
Related *ObjectReference
// For audit: which controller generated this event
ReportingController string
ReportingInstance string
}
Key fields explained
| Field | Example value | Purpose |
|---|---|---|
InvolvedObject |
{Kind:"Pod", Name:"nginx-xxx"} |
The resource this event describes |
Reason |
"Scheduled", "Pulling", "BackOff"
|
Machine-readable event category |
Message |
"Successfully assigned to node-1" |
Human-readable detail |
Type |
"Normal" or "Warning"
|
Severity indicator |
Count |
5 |
Deduplication counter — same event aggregated |
FirstTimestamp / LastTimestamp
|
timestamps | When it first/last occurred |
Source.Component |
"kubelet", "scheduler"
|
Which Kubernetes component fired it |
3. Event Retention Policy
Since Events are stored in etcd, unchecked growth would fill disk space. Kubernetes enforces a strict retention policy:
┌─────────────────────────────────────────────────────────┐
│ Event Retention Rules │
│ │
│ • Default TTL: 1 hour after last occurrence │
│ • kubectl get events shows only last-hour events │
│ • Identical events are AGGREGATED (Count++ instead │
│ of creating duplicate objects) │
│ • etcd stores events in a separate keyspace to │
│ prevent them from crowding out other resources │
└─────────────────────────────────────────────────────────┘
This is why kubectl describe pod shows "Events: " for Pods that have been running stably for more than an hour — the events existed, but have been garbage collected.
4. The EventBroadcaster Pipeline
EventBroadcaster is the mechanism that takes an event generated by a component and delivers it to one or more sinks (e.g. the API Server, a log file, stdout).
┌──────────────────────────────────────────────────────────────────┐
│ EventBroadcaster Pipeline │
│ │
│ Your Controller │
│ │ │
│ │ recorder.Eventf(obj, type, reason, message) │
│ ▼ │
│ ┌──────────────┐ │
│ │EventRecorder │ generates Event object │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ EventBroadcaster │ fan-out to multiple sinks │
│ └──────┬───────────┘ │
│ │ │
│ ┌────┴──────────────────────────┐ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ API Server Sink │ │ Logging Sink │ │
│ │ (writes Event │ │ (prints to stdout/log) │ │
│ │ to etcd) │ │ │ │
│ └──────────────────┘ └──────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
The three actors
| Actor | Role |
|---|---|
| EventRecorder | The interface your code calls — Event(), Eventf(), AnnotatedEventf()
|
| EventBroadcaster | Receives events from recorders, fans out to registered sinks |
| EventSink | The destination — API Server (etcd), log output, or custom handler |
5. Using EventBroadcaster in a Custom Controller
Here's the complete setup pattern used in real Kubernetes controllers:
Step 1 — Create the broadcaster and recorder
import (
"k8s.io/client-go/tools/record"
"k8s.io/client-go/kubernetes/scheme"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func NewController(clientset kubernetes.Interface, ...) *Controller {
// Create the broadcaster
eventBroadcaster := record.NewBroadcaster()
// Sink 1: write events to the API Server (persisted in etcd)
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{
Interface: clientset.CoreV1().Events(""),
})
// Sink 2: also log events to stdout (useful for debugging)
eventBroadcaster.StartEventWatcher(func(e *corev1.Event) {
klog.V(4).Infof("Event: %s %s %s", e.Type, e.Reason, e.Message)
})
// Create a recorder scoped to this controller
// "my-canary-controller" will appear in Event.Source.Component
recorder := eventBroadcaster.NewRecorder(
scheme.Scheme,
corev1.EventSource{Component: "my-canary-controller"},
)
return &Controller{
recorder: recorder,
...
}
}
Step 2 — Emit events during reconciliation
func (c *Controller) reconcile(key string) error {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
// Fetch the resource from local cache
canary, err := c.canaryLister.Canaries(namespace).Get(name)
if err != nil {
return err
}
// --- Normal event: record a successful action ---
c.recorder.Event(
canary, // InvolvedObject
corev1.EventTypeNormal,
"TrafficShifted", // Reason
"Shifted 10% traffic to canary deployment",
)
// --- Warning event: record a problem ---
if canary.Status.ErrorRate > threshold {
c.recorder.Eventf(
canary,
corev1.EventTypeWarning,
"HighErrorRate",
"Canary error rate %.2f%% exceeds threshold %.2f%%, rolling back",
canary.Status.ErrorRate,
threshold,
)
return c.rollback(canary)
}
return nil
}
What the emitted Event looks like in kubectl
LAST SEEN TYPE REASON OBJECT MESSAGE
30s Normal TrafficShifted canary/my-app-canary Shifted 10% traffic to canary deployment
10s Warning HighErrorRate canary/my-app-canary Canary error rate 5.23% exceeds threshold 1.00%, rolling back
6. Pod Lifecycle Events: The Most Important Events in Practice
Since Kubernetes is Pod-centric — Deployments, StatefulSets, DaemonSets, CronJobs all ultimately create Pods — Pod events are the most frequently used events in production operations.
Pod lifecycle → key events:
┌─────────────────────────────────────────────────────────────────┐
│ Phase │ Reason │ Type │ Source │
├─────────────────┼──────────────────┼─────────┼──────────────────┤
│ Scheduling │ Scheduled │ Normal │ scheduler │
│ │ FailedScheduling │ Warning │ scheduler │
├─────────────────┼──────────────────┼─────────┼──────────────────┤
│ Image pull │ Pulling │ Normal │ kubelet │
│ │ Pulled │ Normal │ kubelet │
│ │ Failed │ Warning │ kubelet │
│ │ ErrImagePull │ Warning │ kubelet │
├─────────────────┼──────────────────┼─────────┼──────────────────┤
│ Container │ Created │ Normal │ kubelet │
│ lifecycle │ Started │ Normal │ kubelet │
│ │ Killing │ Normal │ kubelet │
│ │ BackOff │ Warning │ kubelet │
├─────────────────┼──────────────────┼─────────┼──────────────────┤
│ Node │ Evicted │ Warning │ kubelet │
│ │ OOMKilling │ Warning │ kernel/kubelet │
└─────────────────┴──────────────────┴─────────┴──────────────────┘
Practical use cases:
-
Slow startup diagnosis: Compare
Pulling→Pulledtimestamps to measure image pull time -
Crash analysis:
BackOffevents with increasing intervals signalCrashLoopBackOff -
Scheduling failures:
FailedSchedulingwith reason message explains why a Pod isPending -
Eviction tracking:
Evictedevents identify nodes under memory pressure
7. Summary
EventBroadcaster flow:
recorder.Eventf(obj, type, reason, msg)
│
▼
EventBroadcaster.ActionOrDrop(event)
│
├── StartRecordingToSink() → clientset.CoreV1().Events().Create/Patch
│ (aggregates duplicate events via Count++)
│
└── StartEventWatcher() → custom handler (logging, metrics, alerting)
| Concept | Detail |
|---|---|
| Event | A core API resource — stored in etcd, queryable via kubectl |
| Type |
Normal (informational) or Warning (something needs attention) |
| Reason | Short machine-readable string — used for filtering and alerting |
| Count | Identical events are aggregated — prevents etcd flooding |
| Retention | Events are deleted 1 hour after last occurrence |
| EventRecorder | Your controller's interface for emitting events |
| EventBroadcaster | Fan-out hub — delivers events to all registered sinks |
| EventSink | Destination: API Server (etcd), stdout log, or custom handler |
EventBroadcaster closes the observability loop for Kubernetes controllers. By emitting structured events at key decision points in your reconciliation logic, you give operators the same visibility into your custom controller that they have for built-in Kubernetes components — making your controller production-ready and debuggable.
Next in this series: Controller: The Central Hub of the Informer Framework (Part 7)
Follow the series for more deep dives into Kubernetes development.
Top comments (0)