13
"github.com/containers/podman/v5/pkg/rootless"
14
"github.com/containers/podman/v5/pkg/util"
15
"github.com/coreos/go-systemd/v22/journal"
16
"github.com/coreos/go-systemd/v22/sdjournal"
17
"github.com/sirupsen/logrus"
20
// DefaultEventerType is journald when systemd is available
21
const DefaultEventerType = Journald
23
// EventJournalD is the journald implementation of an eventer
24
type EventJournalD struct {
25
options EventerOptions
28
// newEventJournalD creates a new journald Eventer
29
func newEventJournalD(options EventerOptions) (Eventer, error) {
30
return EventJournalD{options}, nil
34
func (e EventJournalD) Write(ee Event) error {
35
m := make(map[string]string)
36
m["SYSLOG_IDENTIFIER"] = "podman"
37
m["PODMAN_EVENT"] = ee.Status.String()
38
m["PODMAN_TYPE"] = ee.Type.String()
39
m["PODMAN_TIME"] = ee.Time.Format(time.RFC3339Nano)
41
// Add specialized information based on the podman type
44
m["PODMAN_NAME"] = ee.Name
45
m["PODMAN_ID"] = ee.ID
50
m["PODMAN_IMAGE"] = ee.Image
51
m["PODMAN_NAME"] = ee.Name
52
m["PODMAN_ID"] = ee.ID
53
if ee.ContainerExitCode != nil {
54
m["PODMAN_EXIT_CODE"] = strconv.Itoa(*ee.ContainerExitCode)
57
m["PODMAN_POD_ID"] = ee.PodID
59
// If we have container labels, we need to convert them to a string so they
60
// can be recorded with the event
61
if len(ee.Details.Attributes) > 0 {
62
b, err := json.Marshal(ee.Details.Attributes)
66
m["PODMAN_LABELS"] = string(b)
68
m["PODMAN_HEALTH_STATUS"] = ee.HealthStatus
70
if len(ee.Details.ContainerInspectData) > 0 {
71
m["PODMAN_CONTAINER_INSPECT_DATA"] = ee.Details.ContainerInspectData
74
m["PODMAN_ID"] = ee.ID
75
m["PODMAN_NETWORK_NAME"] = ee.Network
77
m["PODMAN_NAME"] = ee.Name
80
// starting with commit 7e6e267329 we set LogLevel=notice for the systemd healthcheck unit
81
// This so it doesn't log the started/stopped unit messages al the time which spam the
82
// journal if a small interval is used. That however broke the healthcheck event as it no
83
// longer showed up in podman events when running as root as we only send the event on info
84
// level. To fix this we have to send the event on notice level.
85
// https://github.com/containers/podman/issues/20342
86
prio := journal.PriInfo
87
if len(ee.HealthStatus) > 0 {
88
prio = journal.PriNotice
91
return journal.Send(ee.ToHumanReadable(false), prio, m)
94
// Read reads events from the journal and sends qualified events to the event channel
95
func (e EventJournalD) Read(ctx context.Context, options ReadOptions) error {
96
defer close(options.EventChannel)
97
filterMap, err := generateEventFilters(options.Filters, options.Since, options.Until)
99
return fmt.Errorf("failed to parse event filters: %w", err)
102
var untilTime time.Time
103
if len(options.Until) > 0 {
104
untilTime, err = util.ParseInputTime(options.Until, false)
110
j, err := sdjournal.NewJournal()
115
if err := j.Close(); err != nil {
116
logrus.Errorf("Unable to close journal :%v", err)
119
err = j.SetDataThreshold(0)
121
logrus.Warnf("cannot set data threshold: %v", err)
123
// match only podman journal entries
124
podmanJournal := sdjournal.Match{Field: "SYSLOG_IDENTIFIER", Value: "podman"}
125
if err := j.AddMatch(podmanJournal.String()); err != nil {
126
return fmt.Errorf("failed to add SYSLOG_IDENTIFIER journal filter for event log: %w", err)
129
// make sure we only read events for the current user
130
uidMatch := sdjournal.Match{Field: "_UID", Value: strconv.Itoa(rootless.GetRootlessUID())}
131
if err := j.AddMatch(uidMatch.String()); err != nil {
132
return fmt.Errorf("failed to add _UID journal filter for event log: %w", err)
135
if len(options.Since) == 0 && len(options.Until) == 0 && options.Stream {
136
if err := j.SeekTail(); err != nil {
137
return fmt.Errorf("failed to seek end of journal: %w", err)
139
// After SeekTail calling Next moves to a random entry.
140
// To prevent this we have to call Previous first.
141
// see: https://bugs.freedesktop.org/show_bug.cgi?id=64614
142
if _, err := j.Previous(); err != nil {
143
return fmt.Errorf("failed to move journal cursor to previous entry: %w", err)
145
} else if len(options.Since) > 0 {
146
since, err := util.ParseInputTime(options.Since, true)
150
// seek based on time which helps to reduce unnecessary event reads
151
if err := j.SeekRealtimeUsec(uint64(since.UnixMicro())); err != nil {
157
entry, err := GetNextEntry(ctx, j, options.Stream, untilTime)
161
// no entry == we hit the end
166
newEvent, err := newEventFromJournalEntry(entry)
168
// We can't decode this event.
169
// Don't fail hard - that would make events unusable.
170
// Instead, log and continue.
171
if !errors.Is(err, ErrEventTypeBlank) {
172
logrus.Errorf("Unable to decode event: %v", err)
176
if applyFilters(newEvent, filterMap) {
177
options.EventChannel <- newEvent
182
func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) {
184
eventType, err := StringToType(entry.Fields["PODMAN_TYPE"])
188
eventTime, err := time.Parse(time.RFC3339Nano, entry.Fields["PODMAN_TIME"])
192
eventStatus, err := StringToStatus(entry.Fields["PODMAN_EVENT"])
196
newEvent.Type = eventType
197
newEvent.Time = eventTime
198
newEvent.Status = eventStatus
199
newEvent.Name = entry.Fields["PODMAN_NAME"]
203
newEvent.ID = entry.Fields["PODMAN_ID"]
204
newEvent.Image = entry.Fields["PODMAN_IMAGE"]
205
newEvent.PodID = entry.Fields["PODMAN_POD_ID"]
206
if code, ok := entry.Fields["PODMAN_EXIT_CODE"]; ok {
207
intCode, err := strconv.Atoi(code)
209
logrus.Errorf("Parsing event exit code %s", code)
211
newEvent.ContainerExitCode = &intCode
215
// we need to check for the presence of labels recorded to a container event
216
if stringLabels, ok := entry.Fields["PODMAN_LABELS"]; ok && len(stringLabels) > 0 {
217
labels := make(map[string]string, 0)
218
if err := json.Unmarshal([]byte(stringLabels), &labels); err != nil {
222
// if we have labels, add them to the event
224
newEvent.Attributes = labels
227
newEvent.HealthStatus = entry.Fields["PODMAN_HEALTH_STATUS"]
228
newEvent.Details.ContainerInspectData = entry.Fields["PODMAN_CONTAINER_INSPECT_DATA"]
230
newEvent.ID = entry.Fields["PODMAN_ID"]
231
newEvent.Network = entry.Fields["PODMAN_NETWORK_NAME"]
233
newEvent.ID = entry.Fields["PODMAN_ID"]
234
if val, ok := entry.Fields["ERROR"]; ok {
238
return &newEvent, nil
241
// String returns a string representation of the logger
242
func (e EventJournalD) String() string {
243
return Journald.String()
246
// GetNextEntry returns the next entry in the journal. If the end of the
247
// journal is reached and stream is not set or the current time is after
248
// the until time this function returns nil,nil.
249
func GetNextEntry(ctx context.Context, j *sdjournal.Journal, stream bool, untilTime time.Time) (*sdjournal.JournalEntry, error) {
253
// the consumer has cancelled
258
// the api requires a next|prev before reading the event
261
return nil, fmt.Errorf("failed to move journal cursor to next entry: %w", err)
263
// ret == 0 equals EOF, see sd_journal_next(3)
265
if !stream || (!untilTime.IsZero() && time.Now().After(untilTime)) {
266
// we hit the end and should not keep streaming
269
// keep waiting for the next entry
270
// j.Wait() is blocking, this would cause the goroutine to hang forever
271
// if no more journal entries are generated and thus if the client
272
// has closed the connection in the meantime to leak memory.
273
// Waiting only 5 seconds makes sure we can check if the client closed in the
274
// meantime at least every 5 seconds.
276
if !untilTime.IsZero() {
277
until := time.Until(untilTime)
286
entry, err := j.GetEntry()
288
return nil, fmt.Errorf("failed to read journal entry: %w", err)