github.com/moby/docker@v26.1.3+incompatible/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 "github.com/google/uuid" 26 "gotest.tools/v3/assert" 27 28 "github.com/docker/docker/daemon/logger/journald/internal/export" 29 ) 30 31 // The systemd-journal-remote command is not conventionally installed on $PATH. 32 // The manpage from upstream systemd lists the command as 33 // /usr/lib/systemd/systemd-journal-remote, but Debian installs it to 34 // /lib/systemd instead. 35 var cmdPaths = []string{ 36 "/usr/lib/systemd/systemd-journal-remote", 37 "/lib/systemd/systemd-journal-remote", 38 "systemd-journal-remote", // Check $PATH anyway, just in case. 39 } 40 41 // ErrCommandNotFound is returned when the systemd-journal-remote command could 42 // not be located at the well-known paths or $PATH. 43 var ErrCommandNotFound = errors.New("systemd-journal-remote command not found") 44 45 // JournalRemoteCmdPath searches for the systemd-journal-remote command in 46 // well-known paths and the directories named in the $PATH environment variable. 47 func JournalRemoteCmdPath() (string, error) { 48 for _, p := range cmdPaths { 49 if path, err := exec.LookPath(p); err == nil { 50 return path, nil 51 } 52 } 53 return "", ErrCommandNotFound 54 } 55 56 // Sender fakes github.com/coreos/go-systemd/v22/journal.Send, writing journal 57 // entries to an arbitrary journal file without depending on a running journald 58 // process. 59 type Sender struct { 60 CmdName string 61 OutputPath string 62 63 // Clock for timestamping sent messages. 64 Clock clock.Clock 65 // Whether to assign the event's realtime timestamp to the time 66 // specified by the SYSLOG_TIMESTAMP variable value. This is roughly 67 // analogous to journald receiving the event and assigning it a 68 // timestamp in zero time after the SYSLOG_TIMESTAMP value was set, 69 // which is higly unrealistic in practice. 70 AssignEventTimestampFromSyslogTimestamp bool 71 // Boot ID for journal entries. Required by systemd-journal-remote as of 72 // https://github.com/systemd/systemd/commit/1eede158519e4e5ed22738c90cb57a91dbecb7f2 73 // (systemd 255). 74 BootID uuid.UUID 75 76 // When set, Send will act as a test helper and redirect 77 // systemd-journal-remote command output to the test log. 78 TB testing.TB 79 } 80 81 // New constructs a new Sender which will write journal entries to outpath. The 82 // file name must end in '.journal' and the directory must already exist. The 83 // journal file will be created if it does not exist. An existing journal file 84 // will be appended to. 85 func New(outpath string) (*Sender, error) { 86 p, err := JournalRemoteCmdPath() 87 if err != nil { 88 return nil, err 89 } 90 sender := &Sender{ 91 CmdName: p, 92 OutputPath: outpath, 93 Clock: clock.NewClock(), 94 BootID: uuid.New(), // UUIDv4, like systemd itself generates for sd_id128 values. 95 } 96 return sender, nil 97 } 98 99 // NewT is like New but will skip the test if the systemd-journal-remote command 100 // is not available. 101 func NewT(t *testing.T, outpath string) *Sender { 102 t.Helper() 103 s, err := New(outpath) 104 if errors.Is(err, ErrCommandNotFound) { 105 t.Skip(err) 106 } 107 assert.NilError(t, err) 108 s.TB = t 109 return s 110 } 111 112 var validVarName = regexp.MustCompile("^[A-Z0-9][A-Z0-9_]*$") 113 114 // Send is a drop-in replacement for 115 // github.com/coreos/go-systemd/v22/journal.Send. 116 func (s *Sender) Send(message string, priority journal.Priority, vars map[string]string) error { 117 if s.TB != nil { 118 s.TB.Helper() 119 } 120 var buf bytes.Buffer 121 // https://systemd.io/JOURNAL_EXPORT_FORMATS/ says "if you are 122 // generating this format you shouldn’t care about these special 123 // double-underscore fields," yet systemd-journal-remote treats entries 124 // without a __REALTIME_TIMESTAMP as invalid and discards them. 125 // Reported upstream: https://github.com/systemd/systemd/issues/22411 126 var ts time.Time 127 if sts := vars["SYSLOG_TIMESTAMP"]; s.AssignEventTimestampFromSyslogTimestamp && sts != "" { 128 var err error 129 if ts, err = time.Parse(time.RFC3339Nano, sts); err != nil { 130 return fmt.Errorf("fake: error parsing SYSLOG_TIMESTAMP value %q: %w", ts, err) 131 } 132 } else { 133 ts = s.Clock.Now() 134 } 135 if err := export.WriteField(&buf, "__REALTIME_TIMESTAMP", strconv.FormatInt(ts.UnixMicro(), 10)); err != nil { 136 return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err) 137 } 138 if err := export.WriteField(&buf, "_BOOT_ID", fmt.Sprintf("%x", [16]byte(s.BootID))); err != nil { 139 return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err) 140 } 141 if err := export.WriteField(&buf, "MESSAGE", message); err != nil { 142 return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err) 143 } 144 if err := export.WriteField(&buf, "PRIORITY", strconv.Itoa(int(priority))); err != nil { 145 return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err) 146 } 147 for k, v := range vars { 148 if !validVarName.MatchString(k) { 149 return fmt.Errorf("fake: invalid journal-entry variable name %q", k) 150 } 151 if err := export.WriteField(&buf, k, v); err != nil { 152 return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err) 153 } 154 } 155 if err := export.WriteEndOfEntry(&buf); err != nil { 156 return fmt.Errorf("fake: error writing entry to systemd-journal-remote: %w", err) 157 } 158 159 // Invoke the command separately for each entry to ensure that the entry 160 // has been flushed to disk when Send returns. 161 cmd := exec.Command(s.CmdName, "--output", s.OutputPath, "-") 162 cmd.Stdin = &buf 163 164 if s.TB != nil { 165 out, err := cmd.CombinedOutput() 166 s.TB.Logf("[systemd-journal-remote] %s", out) 167 var exitErr *exec.ExitError 168 if errors.As(err, &exitErr) { 169 s.TB.Logf("systemd-journal-remote exit status: %d", exitErr.ExitCode()) 170 } 171 return err 172 } 173 cmd.Stdout = os.Stdout 174 cmd.Stderr = os.Stderr 175 return cmd.Run() 176 }