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