diff options
author | baude <bbaude@redhat.com> | 2019-03-27 13:50:54 -0500 |
---|---|---|
committer | baude <bbaude@redhat.com> | 2019-04-24 16:00:04 -0500 |
commit | 7bf7c177ab3f67d5de1689842204c258fca083e4 (patch) | |
tree | 960fd1365913209fec61e3226b96a6f8bb1fd051 /libpod/events | |
parent | d75543fcd2ce87a9b87b8883400f355979004e91 (diff) | |
download | podman-7bf7c177ab3f67d5de1689842204c258fca083e4.tar.gz podman-7bf7c177ab3f67d5de1689842204c258fca083e4.tar.bz2 podman-7bf7c177ab3f67d5de1689842204c258fca083e4.zip |
journald event logging
add the ability for podman to read and write events to journald instead
of just a logfile. This can be controlled in libpod.conf with the
`events_logger` attribute of `journald` or `file`. The default will be
set to `journald`.
Signed-off-by: baude <bbaude@redhat.com>
Diffstat (limited to 'libpod/events')
-rw-r--r-- | libpod/events/config.go | 149 | ||||
-rw-r--r-- | libpod/events/events.go | 148 | ||||
-rw-r--r-- | libpod/events/events_linux.go | 20 | ||||
-rw-r--r-- | libpod/events/events_unsupported.go | 10 | ||||
-rw-r--r-- | libpod/events/filters.go | 114 | ||||
-rw-r--r-- | libpod/events/journal_linux.go | 131 | ||||
-rw-r--r-- | libpod/events/logfile.go | 65 | ||||
-rw-r--r-- | libpod/events/nullout.go | 23 |
8 files changed, 536 insertions, 124 deletions
diff --git a/libpod/events/config.go b/libpod/events/config.go new file mode 100644 index 000000000..d3b6d8c50 --- /dev/null +++ b/libpod/events/config.go @@ -0,0 +1,149 @@ +package events + +import ( + "time" +) + +// EventerType ... +type EventerType int + +const ( + // LogFile indicates the event logger will be a logfile + LogFile EventerType = iota + // Journald indicates journald should be used to log events + Journald EventerType = iota +) + +// Event describes the attributes of a libpod event +type Event struct { + // ContainerExitCode is for storing the exit code of a container which can + // be used for "internal" event notification + ContainerExitCode int + // ID can be for the container, image, volume, etc + ID string + // Image used where applicable + Image string + // Name where applicable + Name string + // Status describes the event that occurred + Status Status + // Time the event occurred + Time time.Time + // Type of event that occurred + Type Type +} + +// EventerOptions describe options that need to be passed to create +// an eventer +type EventerOptions struct { + // EventerType describes whether to use journald or a file + EventerType string + // LogFilePath is the path to where the log file should reside if using + // the file logger + LogFilePath string +} + +// Eventer is the interface for journald or file event logging +type Eventer interface { + // Write an event to a backend + Write(event Event) error + // Read an event from the backend + Read(options ReadOptions) error +} + +// ReadOptions describe the attributes needed to read event logs +type ReadOptions struct { + // EventChannel is the comm path back to user + EventChannel chan *Event + // Filters are key/value pairs that describe to limit output + Filters []string + // FromStart means you start reading from the start of the logs + FromStart bool + // Since reads "since" the given time + Since string + // Stream is follow + Stream bool + // Until reads "until" the given time + Until string +} + +// Type of event that occurred (container, volume, image, pod, etc) +type Type string + +// Status describes the actual event action (stop, start, create, kill) +type Status string + +const ( + // If you add or subtract any values to the following lists, make sure you also update + // the switch statements below and the enums for EventType or EventStatus in the + // varlink description file. + + // Container - event is related to containers + Container Type = "container" + // Image - event is related to images + Image Type = "image" + // Pod - event is related to pods + Pod Type = "pod" + // Volume - event is related to volumes + Volume Type = "volume" + + // Attach ... + Attach Status = "attach" + // Checkpoint ... + Checkpoint Status = "checkpoint" + // Cleanup ... + Cleanup Status = "cleanup" + // Commit ... + Commit Status = "commit" + // Create ... + Create Status = "create" + // Exec ... + Exec Status = "exec" + // Exited indicates that a container's process died + Exited Status = "died" + // Export ... + Export Status = "export" + // History ... + History Status = "history" + // Import ... + Import Status = "import" + // Init ... + Init Status = "init" + // Kill ... + Kill Status = "kill" + // LoadFromArchive ... + LoadFromArchive Status = "loadfromarchive" + // Mount ... + Mount Status = "mount" + // Pause ... + Pause Status = "pause" + // Prune ... + Prune Status = "prune" + // Pull ... + Pull Status = "pull" + // Push ... + Push Status = "push" + // Remove ... + Remove Status = "remove" + // Restore ... + Restore Status = "restore" + // Save ... + Save Status = "save" + // Start ... + Start Status = "start" + // Stop ... + Stop Status = "stop" + // Sync ... + Sync Status = "sync" + // Tag ... + Tag Status = "tag" + // Unmount ... + Unmount Status = "unmount" + // Unpause ... + Unpause Status = "unpause" + // Untag ... + Untag Status = "untag" +) + +// EventFilter for filtering events +type EventFilter func(*Event) bool diff --git a/libpod/events/events.go b/libpod/events/events.go index 074a3ba5b..e8c61faa0 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -6,109 +6,18 @@ import ( "os" "time" - "github.com/containers/storage" + "github.com/hpcloud/tail" "github.com/pkg/errors" ) -// Event describes the attributes of a libpod event -type Event struct { - // ContainerExitCode is for storing the exit code of a container which can - // be used for "internal" event notification - ContainerExitCode int - // ID can be for the container, image, volume, etc - ID string - // Image used where applicable - Image string - // Name where applicable - Name string - // Status describes the event that occurred - Status Status - // Time the event occurred - Time time.Time - // Type of event that occurred - Type Type -} - -// Type of event that occurred (container, volume, image, pod, etc) -type Type string - -// Status describes the actual event action (stop, start, create, kill) -type Status string - -const ( - // If you add or subtract any values to the following lists, make sure you also update - // the switch statements below and the enums for EventType or EventStatus in the - // varlink description file. - - // Container - event is related to containers - Container Type = "container" - // Image - event is related to images - Image Type = "image" - // Pod - event is related to pods - Pod Type = "pod" - // Volume - event is related to volumes - Volume Type = "volume" +// String returns a string representation of EventerType +func (et EventerType) String() string { + if et == LogFile { + return "file" - // Attach ... - Attach Status = "attach" - // Checkpoint ... - Checkpoint Status = "checkpoint" - // Cleanup ... - Cleanup Status = "cleanup" - // Commit ... - Commit Status = "commit" - // Create ... - Create Status = "create" - // Exec ... - Exec Status = "exec" - // Exited indicates that a container's process died - Exited Status = "died" - // Export ... - Export Status = "export" - // History ... - History Status = "history" - // Import ... - Import Status = "import" - // Init ... - Init Status = "init" - // Kill ... - Kill Status = "kill" - // LoadFromArchive ... - LoadFromArchive Status = "status" - // Mount ... - Mount Status = "mount" - // Pause ... - Pause Status = "pause" - // Prune ... - Prune Status = "prune" - // Pull ... - Pull Status = "pull" - // Push ... - Push Status = "push" - // Remove ... - Remove Status = "remove" - // Restore ... - Restore Status = "restore" - // Save ... - Save Status = "save" - // Start ... - Start Status = "start" - // Stop ... - Stop Status = "stop" - // Sync ... - Sync Status = "sync" - // Tag ... - Tag Status = "tag" - // Unmount ... - Unmount Status = "unmount" - // Unpause ... - Unpause Status = "unpause" - // Untag ... - Untag Status = "untag" -) - -// EventFilter for filtering events -type EventFilter func(*Event) bool + } + return "journald" +} // NewEvent creates a event struct and populates with // the given status and time. @@ -119,30 +28,6 @@ func NewEvent(status Status) Event { } } -// Write will record the event to the given path -func (e *Event) Write(path string) error { - // We need to lock events file - lock, err := storage.GetLockfile(path + ".lock") - if err != nil { - return err - } - lock.Lock() - defer lock.Unlock() - f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700) - if err != nil { - return err - } - defer f.Close() - eventJSONString, err := e.ToJSONString() - if err != nil { - return err - } - if _, err := f.WriteString(fmt.Sprintf("%s\n", eventJSONString)); err != nil { - return err - } - return nil -} - // Recycle checks if the event log has reach a limit and if so // renames the current log and starts a new one. The remove bool // indicates the old log file should be deleted. @@ -172,7 +57,7 @@ func (e *Event) ToHumanReadable() string { // NewEventFromString takes stringified json and converts // it to an event -func NewEventFromString(event string) (*Event, error) { +func newEventFromJSONString(event string) (*Event, error) { e := Event{} if err := json.Unmarshal([]byte(event), &e); err != nil { return nil, err @@ -222,6 +107,7 @@ func StringToStatus(name string) (Status, error) { case Commit.String(): return Commit, nil case Create.String(): + return Create, nil case Exec.String(): return Exec, nil @@ -270,3 +156,17 @@ func StringToStatus(name string) (Status, error) { } return "", errors.Errorf("unknown event status %s", name) } + +func (e EventLogFile) getTail(options ReadOptions) (*tail.Tail, error) { + reopen := true + seek := tail.SeekInfo{Offset: 0, Whence: os.SEEK_END} + if options.FromStart || !options.Stream { + seek.Whence = 0 + reopen = false + } + stream := options.Stream + if len(options.Until) > 0 { + stream = false + } + return tail.TailFile(e.options.LogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger}) +} diff --git a/libpod/events/events_linux.go b/libpod/events/events_linux.go new file mode 100644 index 000000000..d6898145c --- /dev/null +++ b/libpod/events/events_linux.go @@ -0,0 +1,20 @@ +package events + +import ( + "github.com/pkg/errors" + "strings" +) + +// NewEventer creates an eventer based on the eventer type +func NewEventer(options EventerOptions) (Eventer, error) { + var eventer Eventer + switch strings.ToUpper(options.EventerType) { + case strings.ToUpper(Journald.String()): + eventer = EventJournalD{options} + case strings.ToUpper(LogFile.String()): + eventer = EventLogFile{options} + default: + return eventer, errors.Errorf("unknown event logger type: %s", strings.ToUpper(options.EventerType)) + } + return eventer, nil +} diff --git a/libpod/events/events_unsupported.go b/libpod/events/events_unsupported.go new file mode 100644 index 000000000..5b32a1b4b --- /dev/null +++ b/libpod/events/events_unsupported.go @@ -0,0 +1,10 @@ +// +build !linux + +package events + +import "github.com/pkg/errors" + +// NewEventer creates an eventer based on the eventer type +func NewEventer(options EventerOptions) (Eventer, error) { + return nil, errors.New("this function is not available for your platform") +} diff --git a/libpod/events/filters.go b/libpod/events/filters.go new file mode 100644 index 000000000..9a64082d1 --- /dev/null +++ b/libpod/events/filters.go @@ -0,0 +1,114 @@ +package events + +import ( + "fmt" + "strings" + "time" + + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +func generateEventFilter(filter, filterValue string) (func(e *Event) bool, error) { + switch strings.ToUpper(filter) { + case "CONTAINER": + return func(e *Event) bool { + if e.Type != Container { + return false + } + if e.Name == filterValue { + return true + } + return strings.HasPrefix(e.ID, filterValue) + }, nil + case "EVENT", "STATUS": + return func(e *Event) bool { + return fmt.Sprintf("%s", e.Status) == filterValue + }, nil + case "IMAGE": + return func(e *Event) bool { + if e.Type != Image { + return false + } + if e.Name == filterValue { + return true + } + return strings.HasPrefix(e.ID, filterValue) + }, nil + case "POD": + return func(e *Event) bool { + if e.Type != Pod { + return false + } + if e.Name == filterValue { + return true + } + return strings.HasPrefix(e.ID, filterValue) + }, nil + case "VOLUME": + return func(e *Event) bool { + if e.Type != Volume { + return false + } + return strings.HasPrefix(e.ID, filterValue) + }, nil + case "TYPE": + return func(e *Event) bool { + return fmt.Sprintf("%s", e.Type) == filterValue + }, nil + } + return nil, errors.Errorf("%s is an invalid filter", filter) +} + +func generateEventSinceOption(timeSince time.Time) func(e *Event) bool { + return func(e *Event) bool { + return e.Time.After(timeSince) + } +} + +func generateEventUntilOption(timeUntil time.Time) func(e *Event) bool { + return func(e *Event) bool { + return e.Time.Before(timeUntil) + + } +} + +func parseFilter(filter string) (string, string, error) { + filterSplit := strings.Split(filter, "=") + if len(filterSplit) != 2 { + return "", "", errors.Errorf("%s is an invalid filter", filter) + } + return filterSplit[0], filterSplit[1], nil +} + +func generateEventOptions(filters []string, since, until string) ([]EventFilter, error) { + var options []EventFilter + for _, filter := range filters { + key, val, err := parseFilter(filter) + if err != nil { + return nil, err + } + funcFilter, err := generateEventFilter(key, val) + if err != nil { + return nil, err + } + options = append(options, funcFilter) + } + + if len(since) > 0 { + timeSince, err := util.ParseInputTime(since) + if err != nil { + return nil, errors.Wrapf(err, "unable to convert since time of %s", since) + } + options = append(options, generateEventSinceOption(timeSince)) + } + + if len(until) > 0 { + timeUntil, err := util.ParseInputTime(until) + if err != nil { + return nil, errors.Wrapf(err, "unable to convert until time of %s", until) + } + options = append(options, generateEventUntilOption(timeUntil)) + } + return options, nil +} diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go new file mode 100644 index 000000000..e6b54db1d --- /dev/null +++ b/libpod/events/journal_linux.go @@ -0,0 +1,131 @@ +package events + +import ( + "fmt" + "time" + + "github.com/coreos/go-systemd/journal" + "github.com/coreos/go-systemd/sdjournal" + "github.com/pkg/errors" +) + +// EventJournalD is the journald implementation of an eventer +type EventJournalD struct { + options EventerOptions +} + +// Write to journald +func (e EventJournalD) Write(ee Event) error { + m := make(map[string]string) + m["SYSLOG_IDENTIFIER"] = "podman" + m["PODMAN_EVENT"] = ee.Status.String() + m["PODMAN_TYPE"] = ee.Type.String() + m["PODMAN_TIME"] = ee.Time.Format(time.RFC3339Nano) + + // Add specialized information based on the podman type + switch ee.Type { + case Image: + m["PODMAN_NAME"] = ee.Name + m["PODMAN_ID"] = ee.ID + case Container, Pod: + m["PODMAN_IMAGE"] = ee.Image + m["PODMAN_NAME"] = ee.Name + m["PODMAN_ID"] = ee.ID + case Volume: + m["PODMAN_NAME"] = ee.Name + } + return journal.Send(fmt.Sprintf("%s", ee.ToHumanReadable()), journal.PriInfo, m) +} + +// Read reads events from the journal and sends qualified events to the event channel +func (e EventJournalD) Read(options ReadOptions) error { + eventOptions, err := generateEventOptions(options.Filters, options.Since, options.Until) + if err != nil { + return errors.Wrapf(err, "failed to generate event options") + } + podmanJournal := sdjournal.Match{Field: "SYSLOG_IDENTIFIER", Value: "podman"} //nolint + j, err := sdjournal.NewJournal() //nolint + if err != nil { + return err + } + if err := j.AddMatch(podmanJournal.String()); err != nil { + return errors.Wrap(err, "failed to add filter for event log") + } + if len(options.Since) == 0 && len(options.Until) == 0 && options.Stream { + if err := j.SeekTail(); err != nil { + return errors.Wrap(err, "failed to seek end of journal") + } + } + // the api requires a next|prev before getting a cursor + if _, err := j.Next(); err != nil { + return err + } + prevCursor, err := j.GetCursor() + if err != nil { + return err + } + defer close(options.EventChannel) + for { + if _, err := j.Next(); err != nil { + return err + } + newCursor, err := j.GetCursor() + if err != nil { + return err + } + if prevCursor == newCursor { + if len(options.Until) > 0 || !options.Stream { + break + } + _ = j.Wait(sdjournal.IndefiniteWait) //nolint + continue + } + prevCursor = newCursor + entry, err := j.GetEntry() + if err != nil { + return err + } + newEvent, err := newEventFromJournalEntry(entry) + if err != nil { + return err + } + include := true + for _, filter := range eventOptions { + include = include && filter(newEvent) + } + if include { + options.EventChannel <- newEvent + } + } + return nil + +} + +func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { //nolint + newEvent := Event{} + eventType, err := StringToType(entry.Fields["PODMAN_TYPE"]) + if err != nil { + return nil, err + } + eventTime, err := time.Parse(time.RFC3339Nano, entry.Fields["PODMAN_TIME"]) + if err != nil { + return nil, err + } + eventStatus, err := StringToStatus(entry.Fields["PODMAN_EVENT"]) + if err != nil { + return nil, err + } + newEvent.Type = eventType + newEvent.Time = eventTime + newEvent.Status = eventStatus + newEvent.Name = entry.Fields["PODMAN_NAME"] + + switch eventType { + case Container, Pod: + newEvent.ID = entry.Fields["PODMAN_ID"] + newEvent.Image = entry.Fields["PODMAN_IMAGE"] + case Image: + newEvent.ID = entry.Fields["PODMAN_ID"] + } + return &newEvent, nil +} diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go new file mode 100644 index 000000000..3232b86d0 --- /dev/null +++ b/libpod/events/logfile.go @@ -0,0 +1,65 @@ +package events + +import ( + "fmt" + "os" + + "github.com/pkg/errors" +) + +// EventLogFile is the structure for event writing to a logfile. It contains the eventer +// options and the event itself. Methods for reading and writing are also defined from it. +type EventLogFile struct { + options EventerOptions +} + +// Writes to the log file +func (e EventLogFile) Write(ee Event) error { + f, err := os.OpenFile(e.options.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0700) + if err != nil { + return err + } + defer f.Close() + eventJSONString, err := ee.ToJSONString() + if err != nil { + return err + } + if _, err := f.WriteString(fmt.Sprintf("%s\n", eventJSONString)); err != nil { + return err + } + return nil + +} + +// Reads from the log file +func (e EventLogFile) Read(options ReadOptions) error { + eventOptions, err := generateEventOptions(options.Filters, options.Since, options.Until) + if err != nil { + return errors.Wrapf(err, "unable to generate event options") + } + t, err := e.getTail(options) + if err != nil { + return err + } + for line := range t.Lines { + event, err := newEventFromJSONString(line.Text) + if err != nil { + return err + } + switch event.Type { + case Image, Volume, Pod, Container: + // no-op + default: + return errors.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath) + } + include := true + for _, filter := range eventOptions { + include = include && filter(event) + } + if include { + options.EventChannel <- event + } + } + close(options.EventChannel) + return nil +} diff --git a/libpod/events/nullout.go b/libpod/events/nullout.go new file mode 100644 index 000000000..7d811a9c7 --- /dev/null +++ b/libpod/events/nullout.go @@ -0,0 +1,23 @@ +package events + +// EventToNull is an eventer type that only performs write operations +// and only writes to /dev/null. It is meant for unittests only +type EventToNull struct{} + +// Write eats the event and always returns nil +func (e EventToNull) Write(ee Event) error { + return nil +} + +// Read does nothing. Do not use it. +func (e EventToNull) Read(options ReadOptions) error { + return nil +} + +// NewNullEventer returns a new null eventer. You should only do this for +// the purposes on internal libpod testing. +func NewNullEventer() Eventer { + var e Eventer + e = EventToNull{} + return e +} |