From 7bf7c177ab3f67d5de1689842204c258fca083e4 Mon Sep 17 00:00:00 2001
From: baude <bbaude@redhat.com>
Date: Wed, 27 Mar 2019 13:50:54 -0500
Subject: 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>
---
 libpod/events/config.go             | 149 ++++++++++++++++++++++++++++++++++++
 libpod/events/events.go             | 148 ++++++-----------------------------
 libpod/events/events_linux.go       |  20 +++++
 libpod/events/events_unsupported.go |  10 +++
 libpod/events/filters.go            | 114 +++++++++++++++++++++++++++
 libpod/events/journal_linux.go      | 131 +++++++++++++++++++++++++++++++
 libpod/events/logfile.go            |  65 ++++++++++++++++
 libpod/events/nullout.go            |  23 ++++++
 8 files changed, 536 insertions(+), 124 deletions(-)
 create mode 100644 libpod/events/config.go
 create mode 100644 libpod/events/events_linux.go
 create mode 100644 libpod/events/events_unsupported.go
 create mode 100644 libpod/events/filters.go
 create mode 100644 libpod/events/journal_linux.go
 create mode 100644 libpod/events/logfile.go
 create mode 100644 libpod/events/nullout.go

(limited to 'libpod/events')

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
+}
-- 
cgit v1.2.3-54-g00ecf