aboutsummaryrefslogtreecommitdiff
path: root/libpod/events
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2019-03-27 13:50:54 -0500
committerbaude <bbaude@redhat.com>2019-04-24 16:00:04 -0500
commit7bf7c177ab3f67d5de1689842204c258fca083e4 (patch)
tree960fd1365913209fec61e3226b96a6f8bb1fd051 /libpod/events
parentd75543fcd2ce87a9b87b8883400f355979004e91 (diff)
downloadpodman-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.go149
-rw-r--r--libpod/events/events.go148
-rw-r--r--libpod/events/events_linux.go20
-rw-r--r--libpod/events/events_unsupported.go10
-rw-r--r--libpod/events/filters.go114
-rw-r--r--libpod/events/journal_linux.go131
-rw-r--r--libpod/events/logfile.go65
-rw-r--r--libpod/events/nullout.go23
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
+}