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 }