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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions pkg/metrics/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package metrics

import (
"sync"
"time"

"github.com/sirupsen/logrus"
)

const (
EventsPluginName = "events"
)

type EventLevel string

const (
EventLevelInfo EventLevel = "Info"
EventLevelWarning EventLevel = "Warning"
EventLevelError EventLevel = "Error"
)

type Event struct {
Level EventLevel `json:"level"`
Source string `json:"source"`
Locator EventLocator `json:"locator"`
Message EventMessage `json:"message"`
From time.Time `json:"from"`
To time.Time `json:"to"`
Timestamp time.Time `json:"timestamp"`
}

type EventLocator struct {
Type string `json:"type"`
Name string `json:"name"`
Container *string `json:"container,omitempty"`
Keys map[string]any `json:"keys,omitempty"`
}

type EventMessage struct {
Reason string `json:"reason"`
Cause string `json:"cause"`
HumanMessage string `json:"humanMessage"`
Annotations map[string]any `json:"annotations,omitempty"`
}

func (e *Event) SetTimestamp(t time.Time) {
e.Timestamp = t
}

func NewEvent(level EventLevel, locator EventLocator, message EventMessage, source string, from, to time.Time) *Event {
return &Event{Level: level, Source: source, From: from, To: to, Locator: locator, Message: message}
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Timestamp field is not set in the NewEvent constructor. The Event struct includes a Timestamp field (line 29) which remains zero-valued when events are created through this constructor. Consider either setting it in the constructor or documenting that callers must use SetTimestamp separately.

Suggested change
return &Event{Level: level, Source: source, From: from, To: to, Locator: locator, Message: message}
return &Event{
Level: level,
Source: source,
From: from,
To: to,
Locator: locator,
Message: message,
Timestamp: time.Now(),
}

Copilot uses AI. Check for mistakes.
}

type eventsPlugin struct {
mu sync.Mutex
logger *logrus.Entry
events []MetricsEvent
}

func newEventsPlugin(logger *logrus.Entry) *eventsPlugin {
return &eventsPlugin{logger: logger.WithField("plugin", EventsPluginName)}
}

func (p *eventsPlugin) Name() string { return EventsPluginName }

func (p *eventsPlugin) Record(ev MetricsEvent) {
if _, ok := ev.(*Event); !ok {
return
}
p.mu.Lock()
p.events = append(p.events, ev)
p.mu.Unlock()
}

func (p *eventsPlugin) Events() []MetricsEvent {
return p.events
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Events() method returns the slice without mutex protection, which could lead to race conditions if called concurrently with Record(). Consider acquiring the mutex before returning the slice or returning a copy to ensure thread safety.

Suggested change
return p.events
p.mu.Lock()
defer p.mu.Unlock()
eventsCopy := make([]MetricsEvent, len(p.events))
copy(eventsCopy, p.events)
return eventsCopy

Copilot uses AI. Check for mistakes.
}
19 changes: 12 additions & 7 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type MetricsAgent struct {
client ctrlruntimeclient.Client

insightsPlugin *insightsPlugin
eventsPlugin *eventsPlugin
buildPlugin *buildPlugin
nodesPlugin *nodesMetricsPlugin
leasePlugin *leasesPlugin
Expand Down Expand Up @@ -68,6 +69,7 @@ func NewMetricsAgent(ctx context.Context, clusterConfig *rest.Config) (*MetricsA
logger: logger,
client: client,
insightsPlugin: newInsightsPlugin(logger),
eventsPlugin: newEventsPlugin(logger),
buildPlugin: newBuildPlugin(ctx, logger, client),
nodesPlugin: newNodesMetricsPlugin(ctx, logger, client, metricsClient, nodesCh),
leasePlugin: newLeasesPlugin(logger),
Expand Down Expand Up @@ -99,6 +101,7 @@ func (ma *MetricsAgent) Run() {
}
// Record the event to all plugins
ma.insightsPlugin.Record(ev)
ma.eventsPlugin.Record(ev)
ma.buildPlugin.Record(ev)
ma.nodesPlugin.Record(ev)
ma.leasePlugin.Record(ev)
Expand Down Expand Up @@ -132,13 +135,15 @@ func (ma *MetricsAgent) Stop() {

// flush writes the accumulated events to a JSON file in the artifacts directory.
func (ma *MetricsAgent) flush() {
output := make(map[string]any, 6)
output[ma.insightsPlugin.Name()] = ma.insightsPlugin.Events()
output[ma.buildPlugin.Name()] = ma.buildPlugin.Events()
output[ma.nodesPlugin.Name()] = ma.nodesPlugin.Events()
output[ma.leasePlugin.Name()] = ma.leasePlugin.Events()
output[ma.imagesPlugin.Name()] = ma.imagesPlugin.Events()
output[ma.podPlugin.Name()] = ma.podPlugin.Events()
output := map[string]any{
ma.insightsPlugin.Name(): ma.insightsPlugin.Events(),
ma.eventsPlugin.Name(): ma.eventsPlugin.Events(),
ma.buildPlugin.Name(): ma.buildPlugin.Events(),
ma.nodesPlugin.Name(): ma.nodesPlugin.Events(),
ma.leasePlugin.Name(): ma.leasePlugin.Events(),
ma.imagesPlugin.Name(): ma.imagesPlugin.Events(),
ma.podPlugin.Name(): ma.podPlugin.Events(),
}

data, err := json.MarshalIndent(output, "", " ")
if err != nil {
Expand Down