types/logger, logtail: add mechanism to do structured JSON logs

e.g. the change to ipnlocal in this commit ultimately logs out:

{"logtail":{"client_time":"2022-02-17T20:40:30.511381153-08:00","server_time":"2022-02-18T04:40:31.057771504Z"},"type":"Hostinfo","val":{"GoArch":"amd64","Hostname":"tsdev","IPNVersion":"1.21.0-date.20220107","OS":"linux","OSVersion":"Debian 11.2 (bullseye); kernel=5.10.0-10-amd64"},"v":1}

Change-Id: I668646b19aeae4a2fed05170d7b279456829c844
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2022-02-17 20:41:49 -08:00
committed by Brad Fitzpatrick
parent 8c3c5e80b7
commit 84138450a4
5 changed files with 83 additions and 1 deletions

View File

@@ -9,7 +9,9 @@ package logger
import (
"bufio"
"bytes"
"container/list"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@@ -35,6 +37,49 @@ type Context context.Context
type logfKey struct{}
// jenc is a json.Encode + bytes.Buffer pair wired up to be reused in a pool.
type jenc struct {
buf bytes.Buffer
enc *json.Encoder
}
var jencPool = &sync.Pool{New: func() interface{} {
je := new(jenc)
je.enc = json.NewEncoder(&je.buf)
return je
}}
// JSON marshals v as JSON and writes it to logf formatted with the annotation to make logtail
// treat it as a structured log.
//
// The recType is the record type and becomes the key of the wrapper
// JSON object that is logged. That is, if recType is "foo" and v is
// 123, the value logged is {"foo":123}.
//
// Do not use recType "logtail" or "v", with any case. Those are
// reserved for the logging system.
//
// The level can be from 0 to 9. Levels from 1 to 9 are included in
// the logged JSON object, like {"foo":123,"v":2}.
func (logf Logf) JSON(level int, recType string, v interface{}) {
je := jencPool.Get().(*jenc)
defer jencPool.Put(je)
je.buf.Reset()
je.buf.WriteByte('{')
je.enc.Encode(recType)
je.buf.Truncate(je.buf.Len() - 1) // remove newline from prior Encode
je.buf.WriteByte(':')
if err := je.enc.Encode(v); err != nil {
logf("[unexpected]: failed to encode structured JSON log record of type %q / %T: %v", recType, v, err)
return
}
je.buf.Truncate(je.buf.Len() - 1) // remove newline from prior Encode
je.buf.WriteByte('}')
// Magic prefix recognized by logtail:
logf("[v\x00JSON]%d%s", level%10, je.buf.Bytes())
}
// FromContext extracts a log function from ctx.
func FromContext(ctx Context) Logf {
v := ctx.Value(logfKey{})

View File

@@ -15,6 +15,7 @@ import (
"time"
qt "github.com/frankban/quicktest"
"tailscale.com/tailcfg"
)
func TestFuncWriter(t *testing.T) {
@@ -211,3 +212,13 @@ func TestContext(t *testing.T) {
logf("a")
c.Assert(called, qt.IsTrue)
}
func TestJSON(t *testing.T) {
var buf bytes.Buffer
var logf Logf = func(f string, a ...interface{}) { fmt.Fprintf(&buf, f, a...) }
logf.JSON(1, "foo", &tailcfg.Hostinfo{})
want := "[v\x00JSON]1" + `{"foo":{"OS":"","Hostname":""}}`
if got := buf.String(); got != want {
t.Errorf("mismatch\n got: %q\nwant: %q\n", got, want)
}
}