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  }