github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/logger/journald/internal/fake/sender.go (about)

     1  // Package fake implements a journal writer for testing which is decoupled from
     2  // the system's journald.
     3  //
     4  // The systemd project does not have any facilities to support testing of
     5  // journal reader clients (although it has been requested:
     6  // https://github.com/systemd/systemd/issues/14120) so we have to get creative.
     7  // The systemd-journal-remote command reads serialized journal entries in the
     8  // Journal Export Format and writes them to journal files. This format is
     9  // well-documented and straightforward to generate.
    10  package fake // import "github.com/docker/docker/daemon/logger/journald/internal/fake"
    11  
    12  import (
    13  	"bytes"
    14  	"errors"
    15  	"fmt"
    16  	"os"
    17  	"os/exec"
    18  	"regexp"
    19  	"strconv"
    20  	"testing"
    21  	"time"
    22  
    23  	"code.cloudfoundry.org/clock"
    24  	"github.com/coreos/go-systemd/v22/journal"
    25  	"gotest.tools/v3/assert"
    26  
    27  	"github.com/docker/docker/daemon/logger/journald/internal/export"
    28  )
    29  
    30  // The systemd-journal-remote command is not conventionally installed on $PATH.
    31  // The manpage from upstream systemd lists the command as
    32  // /usr/lib/systemd/systemd-journal-remote, but Debian installs it to
    33  // /lib/systemd instead.
    34  var cmdPaths = []string{
    35  	"/usr/lib/systemd/systemd-journal-remote",
    36  	"/lib/systemd/systemd-journal-remote",
    37  	"systemd-journal-remote", // Check $PATH anyway, just in case.
    38  }
    39  
    40  // ErrCommandNotFound is returned when the systemd-journal-remote command could
    41  // not be located at the well-known paths or $PATH.
    42  var ErrCommandNotFound = errors.New("systemd-journal-remote command not found")
    43  
    44  // JournalRemoteCmdPath searches for the systemd-journal-remote command in
    45  // well-known paths and the directories named in the $PATH environment variable.
    46  func JournalRemoteCmdPath() (string, error) {
    47  	for _, p := range cmdPaths {
    48  		if path, err := exec.LookPath(p); err == nil {
    49  			return path, nil
    50  		}
    51  	}
    52  	return "", ErrCommandNotFound
    53  }
    54  
    55  // Sender fakes github.com/coreos/go-systemd/v22/journal.Send, writing journal
    56  // entries to an arbitrary journal file without depending on a running journald
    57  // process.
    58  type Sender struct {
    59  	CmdName    string
    60  	OutputPath string
    61  
    62  	// Clock for timestamping sent messages.
    63  	Clock clock.Clock
    64  	// Whether to assign the event's realtime timestamp to the time
    65  	// specified by the SYSLOG_TIMESTAMP variable value. This is roughly
    66  	// analogous to journald receiving the event and assigning it a
    67  	// timestamp in zero time after the SYSLOG_TIMESTAMP value was set,
    68  	// which is higly unrealistic in practice.
    69  	AssignEventTimestampFromSyslogTimestamp bool
    70  }
    71  
    72  // New constructs a new Sender which will write journal entries to outpath. The
    73  // file name must end in '.journal' and the directory must already exist. The
    74  // journal file will be created if it does not exist. An existing journal file
    75  // will be appended to.
    76  func New(outpath string) (*Sender, error) {
    77  	p, err := JournalRemoteCmdPath()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	sender := &Sender{
    82  		CmdName:    p,
    83  		OutputPath: outpath,
    84  		Clock:      clock.NewClock(),
    85  	}
    86  	return sender, nil
    87  }
    88  
    89  // NewT is like New but will skip the test if the systemd-journal-remote command
    90  // is not available.
    91  func NewT(t *testing.T, outpath string) *Sender {
    92  	t.Helper()
    93  	s, err := New(outpath)
    94  	if errors.Is(err, ErrCommandNotFound) {
    95  		t.Skip(err)
    96  	}
    97  	assert.NilError(t, err)
    98  	return s
    99  }
   100  
   101  var validVarName = regexp.MustCompile("^[A-Z0-9][A-Z0-9_]*$")
   102  
   103  // Send is a drop-in replacement for
   104  // github.com/coreos/go-systemd/v22/journal.Send.
   105  func (s *Sender) Send(message string, priority journal.Priority, vars map[string]string) error {
   106  	var buf bytes.Buffer
   107  	// https://systemd.io/JOURNAL_EXPORT_FORMATS/ says "if you are
   108  	// generating this format you shouldn’t care about these special
   109  	// double-underscore fields," yet systemd-journal-remote treats entries
   110  	// without a __REALTIME_TIMESTAMP as invalid and discards them.
   111  	// Reported upstream: https://github.com/systemd/systemd/issues/22411
   112  	var ts time.Time
   113  	if sts := vars["SYSLOG_TIMESTAMP"]; s.AssignEventTimestampFromSyslogTimestamp && sts != "" {
   114  		var err error
   115  		if ts, err = time.Parse(time.RFC3339Nano, sts); err != nil {
   116  			return fmt.Errorf("fake: error parsing SYSLOG_TIMESTAMP value %q: %w", ts, err)
   117  		}
   118  	} else {
   119  		ts = s.Clock.Now()
   120  	}
   121  	if err := export.WriteField(&buf, "__REALTIME_TIMESTAMP", strconv.FormatInt(ts.UnixMicro(), 10)); err != nil {
   122  		return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err)
   123  	}
   124  	if err := export.WriteField(&buf, "MESSAGE", message); err != nil {
   125  		return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err)
   126  	}
   127  	if err := export.WriteField(&buf, "PRIORITY", strconv.Itoa(int(priority))); err != nil {
   128  		return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err)
   129  	}
   130  	for k, v := range vars {
   131  		if !validVarName.MatchString(k) {
   132  			return fmt.Errorf("fake: invalid journal-entry variable name %q", k)
   133  		}
   134  		if err := export.WriteField(&buf, k, v); err != nil {
   135  			return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err)
   136  		}
   137  	}
   138  	if err := export.WriteEndOfEntry(&buf); err != nil {
   139  		return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err)
   140  	}
   141  
   142  	// Invoke the command separately for each entry to ensure that the entry
   143  	// has been flushed to disk when Send returns.
   144  	cmd := exec.Command(s.CmdName, "--output", s.OutputPath, "-")
   145  	cmd.Stdin = &buf
   146  	cmd.Stdout = os.Stdout
   147  	cmd.Stderr = os.Stderr
   148  	return cmd.Run()
   149  }