blob: 6fb8d33b259b4678ba2324487ce44a23b7ea4e8c [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package qlog
import (
"bytes"
"fmt"
"io"
"log/slog"
"strconv"
"sync"
"time"
)
// A jsonWriter writes JSON-SEQ (RFC 7464).
//
// A JSON-SEQ file consists of a series of JSON text records,
// each beginning with an RS (0x1e) character and ending with LF (0x0a).
type jsonWriter struct {
mu sync.Mutex
w io.WriteCloser
buf bytes.Buffer
}
// writeRecordStart writes the start of a JSON-SEQ record.
func (w *jsonWriter) writeRecordStart() {
w.mu.Lock()
w.buf.WriteByte(0x1e)
w.buf.WriteByte('{')
}
// writeRecordEnd finishes writing a JSON-SEQ record.
func (w *jsonWriter) writeRecordEnd() {
w.buf.WriteByte('}')
w.buf.WriteByte('\n')
w.w.Write(w.buf.Bytes())
w.buf.Reset()
w.mu.Unlock()
}
func (w *jsonWriter) writeAttrs(attrs []slog.Attr) {
w.buf.WriteByte('{')
for _, a := range attrs {
w.writeAttr(a)
}
w.buf.WriteByte('}')
}
func (w *jsonWriter) writeAttr(a slog.Attr) {
if a.Key == "" {
return
}
w.writeName(a.Key)
w.writeValue(a.Value)
}
// writeAttr writes a []slog.Attr as an object field.
func (w *jsonWriter) writeAttrsField(name string, attrs []slog.Attr) {
w.writeName(name)
w.writeAttrs(attrs)
}
func (w *jsonWriter) writeValue(v slog.Value) {
v = v.Resolve()
switch v.Kind() {
case slog.KindAny:
switch v := v.Any().(type) {
case []slog.Value:
w.writeArray(v)
case interface{ AppendJSON([]byte) []byte }:
w.buf.Write(v.AppendJSON(w.buf.AvailableBuffer()))
default:
w.writeString(fmt.Sprint(v))
}
case slog.KindBool:
w.writeBool(v.Bool())
case slog.KindDuration:
w.writeDuration(v.Duration())
case slog.KindFloat64:
w.writeFloat64(v.Float64())
case slog.KindInt64:
w.writeInt64(v.Int64())
case slog.KindString:
w.writeString(v.String())
case slog.KindTime:
w.writeTime(v.Time())
case slog.KindUint64:
w.writeUint64(v.Uint64())
case slog.KindGroup:
w.writeAttrs(v.Group())
default:
w.writeString("unhandled kind")
}
}
// writeName writes an object field name followed by a colon.
func (w *jsonWriter) writeName(name string) {
if b := w.buf.Bytes(); len(b) > 0 && b[len(b)-1] != '{' {
// Add the comma separating this from the previous field.
w.buf.WriteByte(',')
}
w.writeString(name)
w.buf.WriteByte(':')
}
func (w *jsonWriter) writeObject(f func()) {
w.buf.WriteByte('{')
f()
w.buf.WriteByte('}')
}
// writeObject writes an object-valued object field.
// The function f is called to write the contents.
func (w *jsonWriter) writeObjectField(name string, f func()) {
w.writeName(name)
w.writeObject(f)
}
func (w *jsonWriter) writeArray(vals []slog.Value) {
w.buf.WriteByte('[')
for i, v := range vals {
if i != 0 {
w.buf.WriteByte(',')
}
w.writeValue(v)
}
w.buf.WriteByte(']')
}
func (w *jsonWriter) writeRaw(v string) {
w.buf.WriteString(v)
}
// writeRawField writes a field with a raw JSON value.
func (w *jsonWriter) writeRawField(name, v string) {
w.writeName(name)
w.writeRaw(v)
}
func (w *jsonWriter) writeBool(v bool) {
if v {
w.buf.WriteString("true")
} else {
w.buf.WriteString("false")
}
}
// writeBoolField writes a bool-valued object field.
func (w *jsonWriter) writeBoolField(name string, v bool) {
w.writeName(name)
w.writeBool(v)
}
// writeDuration writes a duration as milliseconds.
func (w *jsonWriter) writeDuration(v time.Duration) {
if v < 0 {
w.buf.WriteByte('-')
v = -v
}
fmt.Fprintf(&w.buf, "%d.%06d", v.Milliseconds(), v%time.Millisecond)
}
// writeDurationField writes a millisecond duration-valued object field.
func (w *jsonWriter) writeDurationField(name string, v time.Duration) {
w.writeName(name)
w.writeDuration(v)
}
func (w *jsonWriter) writeFloat64(v float64) {
w.buf.Write(strconv.AppendFloat(w.buf.AvailableBuffer(), v, 'f', -1, 64))
}
// writeFloat64Field writes an float64-valued object field.
func (w *jsonWriter) writeFloat64Field(name string, v float64) {
w.writeName(name)
w.writeFloat64(v)
}
func (w *jsonWriter) writeInt64(v int64) {
w.buf.Write(strconv.AppendInt(w.buf.AvailableBuffer(), v, 10))
}
// writeInt64Field writes an int64-valued object field.
func (w *jsonWriter) writeInt64Field(name string, v int64) {
w.writeName(name)
w.writeInt64(v)
}
func (w *jsonWriter) writeUint64(v uint64) {
w.buf.Write(strconv.AppendUint(w.buf.AvailableBuffer(), v, 10))
}
// writeUint64Field writes a uint64-valued object field.
func (w *jsonWriter) writeUint64Field(name string, v uint64) {
w.writeName(name)
w.writeUint64(v)
}
// writeTime writes a time as seconds since the Unix epoch.
func (w *jsonWriter) writeTime(v time.Time) {
fmt.Fprintf(&w.buf, "%d.%06d", v.UnixMilli(), v.Nanosecond()%int(time.Millisecond))
}
// writeTimeField writes a time-valued object field.
func (w *jsonWriter) writeTimeField(name string, v time.Time) {
w.writeName(name)
w.writeTime(v)
}
func jsonSafeSet(c byte) bool {
// mask is a 128-bit bitmap with 1s for allowed bytes,
// so that the byte c can be tested with a shift and an and.
// If c > 128, then 1<<c and 1<<(c-64) will both be zero,
// and this function will return false.
const mask = 0 |
(1<<(0x22-0x20)-1)<<0x20 |
(1<<(0x5c-0x23)-1)<<0x23 |
(1<<(0x7f-0x5d)-1)<<0x5d
return ((uint64(1)<<c)&(mask&(1<<64-1)) |
(uint64(1)<<(c-64))&(mask>>64)) != 0
}
func jsonNeedsEscape(s string) bool {
for i := range s {
if !jsonSafeSet(s[i]) {
return true
}
}
return false
}
// writeString writes an ASCII string.
//
// qlog fields should never contain anything that isn't ASCII,
// so we do the bare minimum to avoid producing invalid output if we
// do write something unexpected.
func (w *jsonWriter) writeString(v string) {
w.buf.WriteByte('"')
if !jsonNeedsEscape(v) {
w.buf.WriteString(v)
} else {
for i := range v {
if jsonSafeSet(v[i]) {
w.buf.WriteByte(v[i])
} else {
fmt.Fprintf(&w.buf, `\u%04x`, v[i])
}
}
}
w.buf.WriteByte('"')
}
// writeStringField writes a string-valued object field.
func (w *jsonWriter) writeStringField(name, v string) {
w.writeName(name)
w.writeString(v)
}