Skip to content

Commit b02adb3

Browse files
Spend less time preparing to write log lines when the logger is disabled (#352)
* Skip some logging work when log message would be ignored In callers like HashiCorp Terraform, the plugin logger is completely disabled (hclog.Off) by default, but previously go-plugin would nonetheless waste time trying to parse log lines to decide what log level to use when calling into the logger, creating memory allocation garbage and spending time in the JSON parser only to immediately discard the results. The hclog.Logger API exposes information about the log level so that callers can skip doing expensive preparation work unless it will actually be used. We'll now use that to totally skip all of the log preparation when the logger is completely disabled, and to skip _some_ of the work if the log line's level is not sufficient to match the logger's level. * Log entries use flat KVPairs, instead of slice of pointers Previously the JSON log line parsing code would allocate separately for each additional key/value pair in a log line. Using a flat slice of logEntryKV instead of indirection through pointers amortizes that memory allocation cost a little better, for a marginal decrease in allocations per log line. The resulting logEntryKV objects are always passed and used all together anyway, so there is no clear advantage to splitting each into a separate memory allocation.
1 parent a938e9a commit b02adb3

File tree

3 files changed

+89
-8
lines changed

3 files changed

+89
-8
lines changed

client.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,10 @@ func (c *Client) getGRPCMuxer(addr net.Addr) (*grpcmux.GRPCClientMuxer, error) {
11671167
func (c *Client) logStderr(name string, r io.Reader) {
11681168
defer c.clientWaitGroup.Done()
11691169
defer c.pipesWaitGroup.Done()
1170+
11701171
l := c.logger.Named(filepath.Base(name))
1172+
loggerLevel := l.GetLevel()
1173+
loggerDisabled := loggerLevel == hclog.Off
11711174

11721175
reader := bufio.NewReaderSize(r, c.config.PluginLogBufferSize)
11731176
// continuation indicates the previous line was a prefix
@@ -1203,6 +1206,18 @@ func (c *Client) logStderr(name string, r io.Reader) {
12031206

12041207
_, _ = c.config.Stderr.Write([]byte{'\n'})
12051208

1209+
//
1210+
// Any side-effects other than writing to the hclog logger must be
1211+
// above this point!
1212+
//
1213+
1214+
if loggerDisabled {
1215+
// If the logger we'd be writing to is completely disabled then
1216+
// we can skip all of the parsing work to decide what log level
1217+
// we'd use to write this line.
1218+
continue
1219+
}
1220+
12061221
entry, err := parseJSON(line)
12071222
// If output is not JSON format, print directly to Debug
12081223
if err != nil {
@@ -1236,10 +1251,17 @@ func (c *Client) logStderr(name string, r io.Reader) {
12361251
}
12371252
} else {
12381253
panic = false
1239-
out := flattenKVPairs(entry.KVPairs)
12401254

1255+
logLevel := hclog.LevelFromString(entry.Level)
1256+
if logLevel != hclog.NoLevel && logLevel < loggerLevel {
1257+
// The logger will ignore this log entry anyway, so we
1258+
// won't spend any more time preparing it.
1259+
continue
1260+
}
1261+
1262+
out := flattenKVPairs(entry.KVPairs)
12411263
out = append(out, "timestamp", entry.Timestamp.Format(hclog.TimeFormat))
1242-
switch hclog.LevelFromString(entry.Level) {
1264+
switch logLevel {
12431265
case hclog.Trace:
12441266
l.Trace(entry.Message, out...)
12451267
case hclog.Debug:

client_logger_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package plugin
2+
3+
import (
4+
"io"
5+
"sync"
6+
"testing"
7+
8+
"github.com/hashicorp/go-hclog"
9+
)
10+
11+
func BenchmarkClientLogging(b *testing.B) {
12+
// We're not actually going to start the process in this benchmark,
13+
// so this is just a placeholder to satisfy the ClientConfig.
14+
process := helperProcess("bad-version")
15+
16+
tests := map[string]hclog.Level{
17+
"off": hclog.Off,
18+
"error": hclog.Error,
19+
"trace": hclog.Trace,
20+
}
21+
22+
for name, logLevel := range tests {
23+
b.Run(name, func(b *testing.B) {
24+
logger := hclog.New(&hclog.LoggerOptions{
25+
Name: "test-logger",
26+
Level: logLevel,
27+
Output: io.Discard,
28+
Mutex: new(sync.Mutex),
29+
})
30+
31+
c := NewClient(&ClientConfig{
32+
Cmd: process,
33+
Stderr: io.Discard,
34+
HandshakeConfig: testHandshake,
35+
Logger: logger,
36+
Plugins: testPluginMap,
37+
})
38+
39+
r, w := io.Pipe()
40+
41+
c.clientWaitGroup.Add(1)
42+
c.pipesWaitGroup.Add(1)
43+
// logStderr calls Done() on both waitgroups
44+
go c.logStderr("test", r)
45+
46+
fakeLogLine := []byte("{\"@level\":\"debug\",\"@timestamp\":\"2006-01-02T15:04:05.000000Z\",\"@message\":\"hello\",\"extra\":\"hi\",\"numbers\":[1,2,3]}\n")
47+
for b.Loop() {
48+
n, err := w.Write(fakeLogLine)
49+
if err != nil || n != len(fakeLogLine) {
50+
b.Fatal("failed to write to pipe")
51+
}
52+
}
53+
err := w.Close() // causes the c.logStderr goroutine to exit
54+
if err != nil {
55+
b.Fatalf("failed to close write end of pipe: %s", err)
56+
}
57+
})
58+
}
59+
}

log_entry.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import (
1010

1111
// logEntry is the JSON payload that gets sent to Stderr from the plugin to the host
1212
type logEntry struct {
13-
Message string `json:"@message"`
14-
Level string `json:"@level"`
15-
Timestamp time.Time `json:"timestamp"`
16-
KVPairs []*logEntryKV `json:"kv_pairs"`
13+
Message string `json:"@message"`
14+
Level string `json:"@level"`
15+
Timestamp time.Time `json:"timestamp"`
16+
KVPairs []logEntryKV `json:"kv_pairs"`
1717
}
1818

1919
// logEntryKV is a key value pair within the Output payload
@@ -24,7 +24,7 @@ type logEntryKV struct {
2424

2525
// flattenKVPairs is used to flatten KVPair slice into []interface{}
2626
// for hclog consumption.
27-
func flattenKVPairs(kvs []*logEntryKV) []interface{} {
27+
func flattenKVPairs(kvs []logEntryKV) []interface{} {
2828
var result []interface{}
2929
for _, kv := range kvs {
3030
result = append(result, kv.Key)
@@ -66,7 +66,7 @@ func parseJSON(input []byte) (*logEntry, error) {
6666

6767
// Parse dynamic KV args from the hclog payload.
6868
for k, v := range raw {
69-
entry.KVPairs = append(entry.KVPairs, &logEntryKV{
69+
entry.KVPairs = append(entry.KVPairs, logEntryKV{
7070
Key: k,
7171
Value: v,
7272
})

0 commit comments

Comments
 (0)