github.com/containers/podman/v4@v4.9.4/libpod/events/journal_linux.go (about) 1 //go:build systemd 2 // +build systemd 3 4 package events 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "strconv" 12 "time" 13 14 "github.com/containers/podman/v4/pkg/rootless" 15 "github.com/containers/podman/v4/pkg/util" 16 "github.com/coreos/go-systemd/v22/journal" 17 "github.com/coreos/go-systemd/v22/sdjournal" 18 "github.com/sirupsen/logrus" 19 ) 20 21 // DefaultEventerType is journald when systemd is available 22 const DefaultEventerType = Journald 23 24 // EventJournalD is the journald implementation of an eventer 25 type EventJournalD struct { 26 options EventerOptions 27 } 28 29 // newEventJournalD creates a new journald Eventer 30 func newEventJournalD(options EventerOptions) (Eventer, error) { 31 return EventJournalD{options}, nil 32 } 33 34 // Write to journald 35 func (e EventJournalD) Write(ee Event) error { 36 m := make(map[string]string) 37 m["SYSLOG_IDENTIFIER"] = "podman" 38 m["PODMAN_EVENT"] = ee.Status.String() 39 m["PODMAN_TYPE"] = ee.Type.String() 40 m["PODMAN_TIME"] = ee.Time.Format(time.RFC3339Nano) 41 42 // Add specialized information based on the podman type 43 switch ee.Type { 44 case Image: 45 m["PODMAN_NAME"] = ee.Name 46 m["PODMAN_ID"] = ee.ID 47 case Container, Pod: 48 m["PODMAN_IMAGE"] = ee.Image 49 m["PODMAN_NAME"] = ee.Name 50 m["PODMAN_ID"] = ee.ID 51 if ee.ContainerExitCode != 0 { 52 m["PODMAN_EXIT_CODE"] = strconv.Itoa(ee.ContainerExitCode) 53 } 54 if ee.PodID != "" { 55 m["PODMAN_POD_ID"] = ee.PodID 56 } 57 // If we have container labels, we need to convert them to a string so they 58 // can be recorded with the event 59 if len(ee.Details.Attributes) > 0 { 60 b, err := json.Marshal(ee.Details.Attributes) 61 if err != nil { 62 return err 63 } 64 m["PODMAN_LABELS"] = string(b) 65 } 66 m["PODMAN_HEALTH_STATUS"] = ee.HealthStatus 67 68 if len(ee.Details.ContainerInspectData) > 0 { 69 m["PODMAN_CONTAINER_INSPECT_DATA"] = ee.Details.ContainerInspectData 70 } 71 case Network: 72 m["PODMAN_ID"] = ee.ID 73 m["PODMAN_NETWORK_NAME"] = ee.Network 74 case Volume: 75 m["PODMAN_NAME"] = ee.Name 76 } 77 78 // starting with commit 7e6e267329 we set LogLevel=notice for the systemd healthcheck unit 79 // This so it doesn't log the started/stopped unit messages al the time which spam the 80 // journal if a small interval is used. That however broke the healthcheck event as it no 81 // longer showed up in podman events when running as root as we only send the event on info 82 // level. To fix this we have to send the event on notice level. 83 // https://github.com/containers/podman/issues/20342 84 prio := journal.PriInfo 85 if len(ee.HealthStatus) > 0 { 86 prio = journal.PriNotice 87 } 88 89 return journal.Send(ee.ToHumanReadable(false), prio, m) 90 } 91 92 // Read reads events from the journal and sends qualified events to the event channel 93 func (e EventJournalD) Read(ctx context.Context, options ReadOptions) error { 94 defer close(options.EventChannel) 95 filterMap, err := generateEventFilters(options.Filters, options.Since, options.Until) 96 if err != nil { 97 return fmt.Errorf("failed to parse event filters: %w", err) 98 } 99 100 var untilTime time.Time 101 if len(options.Until) > 0 { 102 untilTime, err = util.ParseInputTime(options.Until, false) 103 if err != nil { 104 return err 105 } 106 } 107 108 j, err := sdjournal.NewJournal() 109 if err != nil { 110 return err 111 } 112 defer func() { 113 if err := j.Close(); err != nil { 114 logrus.Errorf("Unable to close journal :%v", err) 115 } 116 }() 117 err = j.SetDataThreshold(0) 118 if err != nil { 119 logrus.Warnf("cannot set data threshold: %v", err) 120 } 121 // match only podman journal entries 122 podmanJournal := sdjournal.Match{Field: "SYSLOG_IDENTIFIER", Value: "podman"} 123 if err := j.AddMatch(podmanJournal.String()); err != nil { 124 return fmt.Errorf("failed to add SYSLOG_IDENTIFIER journal filter for event log: %w", err) 125 } 126 127 // make sure we only read events for the current user 128 uidMatch := sdjournal.Match{Field: "_UID", Value: strconv.Itoa(rootless.GetRootlessUID())} 129 if err := j.AddMatch(uidMatch.String()); err != nil { 130 return fmt.Errorf("failed to add _UID journal filter for event log: %w", err) 131 } 132 133 if len(options.Since) == 0 && len(options.Until) == 0 && options.Stream { 134 if err := j.SeekTail(); err != nil { 135 return fmt.Errorf("failed to seek end of journal: %w", err) 136 } 137 // After SeekTail calling Next moves to a random entry. 138 // To prevent this we have to call Previous first. 139 // see: https://bugs.freedesktop.org/show_bug.cgi?id=64614 140 if _, err := j.Previous(); err != nil { 141 return fmt.Errorf("failed to move journal cursor to previous entry: %w", err) 142 } 143 } else if len(options.Since) > 0 { 144 since, err := util.ParseInputTime(options.Since, true) 145 if err != nil { 146 return err 147 } 148 // seek based on time which helps to reduce unnecessary event reads 149 if err := j.SeekRealtimeUsec(uint64(since.UnixMicro())); err != nil { 150 return err 151 } 152 } 153 154 for { 155 entry, err := GetNextEntry(ctx, j, options.Stream, untilTime) 156 if err != nil { 157 return err 158 } 159 // no entry == we hit the end 160 if entry == nil { 161 return nil 162 } 163 164 newEvent, err := newEventFromJournalEntry(entry) 165 if err != nil { 166 // We can't decode this event. 167 // Don't fail hard - that would make events unusable. 168 // Instead, log and continue. 169 if !errors.Is(err, ErrEventTypeBlank) { 170 logrus.Errorf("Unable to decode event: %v", err) 171 } 172 continue 173 } 174 if applyFilters(newEvent, filterMap) { 175 options.EventChannel <- newEvent 176 } 177 } 178 } 179 180 func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { 181 newEvent := Event{} 182 eventType, err := StringToType(entry.Fields["PODMAN_TYPE"]) 183 if err != nil { 184 return nil, err 185 } 186 eventTime, err := time.Parse(time.RFC3339Nano, entry.Fields["PODMAN_TIME"]) 187 if err != nil { 188 return nil, err 189 } 190 eventStatus, err := StringToStatus(entry.Fields["PODMAN_EVENT"]) 191 if err != nil { 192 return nil, err 193 } 194 newEvent.Type = eventType 195 newEvent.Time = eventTime 196 newEvent.Status = eventStatus 197 newEvent.Name = entry.Fields["PODMAN_NAME"] 198 199 switch eventType { 200 case Container, Pod: 201 newEvent.ID = entry.Fields["PODMAN_ID"] 202 newEvent.Image = entry.Fields["PODMAN_IMAGE"] 203 newEvent.PodID = entry.Fields["PODMAN_POD_ID"] 204 if code, ok := entry.Fields["PODMAN_EXIT_CODE"]; ok { 205 intCode, err := strconv.Atoi(code) 206 if err != nil { 207 logrus.Errorf("Parsing event exit code %s", code) 208 } else { 209 newEvent.ContainerExitCode = intCode 210 } 211 } 212 213 // we need to check for the presence of labels recorded to a container event 214 if stringLabels, ok := entry.Fields["PODMAN_LABELS"]; ok && len(stringLabels) > 0 { 215 labels := make(map[string]string, 0) 216 if err := json.Unmarshal([]byte(stringLabels), &labels); err != nil { 217 return nil, err 218 } 219 220 // if we have labels, add them to the event 221 if len(labels) > 0 { 222 newEvent.Attributes = labels 223 } 224 } 225 newEvent.HealthStatus = entry.Fields["PODMAN_HEALTH_STATUS"] 226 newEvent.Details.ContainerInspectData = entry.Fields["PODMAN_CONTAINER_INSPECT_DATA"] 227 case Network: 228 newEvent.ID = entry.Fields["PODMAN_ID"] 229 newEvent.Network = entry.Fields["PODMAN_NETWORK_NAME"] 230 case Image: 231 newEvent.ID = entry.Fields["PODMAN_ID"] 232 } 233 return &newEvent, nil 234 } 235 236 // String returns a string representation of the logger 237 func (e EventJournalD) String() string { 238 return Journald.String() 239 } 240 241 // GetNextEntry returns the next entry in the journal. If the end of the 242 // journal is reached and stream is not set or the current time is after 243 // the until time this function returns nil,nil. 244 func GetNextEntry(ctx context.Context, j *sdjournal.Journal, stream bool, untilTime time.Time) (*sdjournal.JournalEntry, error) { 245 for { 246 select { 247 case <-ctx.Done(): 248 // the consumer has cancelled 249 return nil, nil 250 default: 251 // fallthrough 252 } 253 // the api requires a next|prev before reading the event 254 ret, err := j.Next() 255 if err != nil { 256 return nil, fmt.Errorf("failed to move journal cursor to next entry: %w", err) 257 } 258 // ret == 0 equals EOF, see sd_journal_next(3) 259 if ret == 0 { 260 if !stream || (!untilTime.IsZero() && time.Now().After(untilTime)) { 261 // we hit the end and should not keep streaming 262 return nil, nil 263 } 264 // keep waiting for the next entry 265 // j.Wait() is blocking, this would cause the goroutine to hang forever 266 // if no more journal entries are generated and thus if the client 267 // has closed the connection in the meantime to leak memory. 268 // Waiting only 5 seconds makes sure we can check if the client closed in the 269 // meantime at least every 5 seconds. 270 t := 5 * time.Second 271 if !untilTime.IsZero() { 272 until := time.Until(untilTime) 273 if until < t { 274 t = until 275 } 276 } 277 _ = j.Wait(t) 278 continue 279 } 280 281 entry, err := j.GetEntry() 282 if err != nil { 283 return nil, fmt.Errorf("failed to read journal entry: %w", err) 284 } 285 return entry, nil 286 } 287 }