Files
quic-go/qlogwriter/writer.go
2025-11-18 17:42:33 +03:00

217 lines
5.2 KiB
Go

package qlogwriter
import (
"bytes"
"fmt"
"io"
"log"
"slices"
"sync"
"time"
"git.geeks-team.ru/gr1ffon/quic-go/qlogwriter/jsontext"
)
// Trace represents a qlog trace that can have multiple event producers.
// Each producer can record events to the trace independently.
// When the last producer is closed, the underlying trace is closed as well.
type Trace interface {
// AddProducer creates a new Recorder for this trace.
// Each Recorder can record events independently.
AddProducer() Recorder
// SupportsSchemas returns true if the trace supports the given schema.
SupportsSchemas(schema string) bool
}
// Recorder is used to record events to a qlog trace.
// It is safe for concurrent use by multiple goroutines.
type Recorder interface {
// RecordEvent records a single Event to the trace.
RecordEvent(Event)
// Close signals that this producer is done recording events.
// When all producers are closed, the underlying trace is closed.
io.Closer
}
// Event represents a qlog event that can be encoded to JSON.
// Each event must provide its name and a method to encode itself using a jsontext.Encoder.
type Event interface {
// Name returns the name of the event, as it should appear in the qlog output
Name() string
// Encode writes the event's data to the provided jsontext.Encoder
Encode(encoder *jsontext.Encoder, eventTime time.Time) error
}
// RecordSeparator is the record separator byte for the JSON-SEQ format
const RecordSeparator byte = 0x1e
var recordSeparator = []byte{RecordSeparator}
type event struct {
Time time.Time
Event Event
}
const eventChanSize = 50
// FileSeq represents a qlog trace using the JSON-SEQ format,
// https://www.ietf.org/archive/id/draft-ietf-quic-qlog-main-schema-12.html#section-5
// qlog event producers can be created by calling AddProducer.
// The underlying io.WriteCloser is closed when the last producer is removed.
type FileSeq struct {
w io.WriteCloser
enc *jsontext.Encoder
referenceTime time.Time
runStopped chan struct{}
encodeErr error
events chan event
mx sync.Mutex
producers int
closed bool
eventSchemas []string
}
var _ Trace = &FileSeq{}
// NewFileSeq creates a new JSON-SEQ qlog trace to log transport events.
func NewFileSeq(w io.WriteCloser) *FileSeq {
return newFileSeq(w, "transport", nil, nil)
}
// NewConnectionFileSeq creates a new qlog trace to log connection events.
func NewConnectionFileSeq(w io.WriteCloser, isClient bool, odcid ConnectionID, eventSchemas []string) *FileSeq {
pers := "server"
if isClient {
pers = "client"
}
return newFileSeq(w, pers, &odcid, eventSchemas)
}
func newFileSeq(w io.WriteCloser, pers string, odcid *ConnectionID, eventSchemas []string) *FileSeq {
now := time.Now()
buf := &bytes.Buffer{}
enc := jsontext.NewEncoder(buf)
if _, err := buf.Write(recordSeparator); err != nil {
panic(fmt.Sprintf("qlog encoding into a bytes.Buffer failed: %s", err))
}
if err := (&traceHeader{
VantagePointType: pers,
GroupID: odcid,
ReferenceTime: now,
EventSchemas: eventSchemas,
}).Encode(enc); err != nil {
panic(fmt.Sprintf("qlog encoding into a bytes.Buffer failed: %s", err))
}
_, encodeErr := w.Write(buf.Bytes())
return &FileSeq{
w: w,
referenceTime: now,
enc: jsontext.NewEncoder(w),
runStopped: make(chan struct{}),
encodeErr: encodeErr,
events: make(chan event, eventChanSize),
eventSchemas: eventSchemas,
}
}
func (t *FileSeq) SupportsSchemas(schema string) bool {
return slices.Contains(t.eventSchemas, schema)
}
func (t *FileSeq) AddProducer() Recorder {
t.mx.Lock()
defer t.mx.Unlock()
if t.closed {
return nil
}
t.producers++
return &Writer{t: t}
}
func (t *FileSeq) record(eventTime time.Time, details Event) {
t.mx.Lock()
if t.closed {
t.mx.Unlock()
return
}
t.mx.Unlock()
t.events <- event{Time: eventTime, Event: details}
}
func (t *FileSeq) Run() {
defer close(t.runStopped)
enc := jsontext.NewEncoder(t.w)
for e := range t.events {
if t.encodeErr != nil { // if encoding failed, just continue draining the event channel
continue
}
if _, err := t.w.Write(recordSeparator); err != nil {
t.encodeErr = err
continue
}
h := encoderHelper{enc: enc}
h.WriteToken(jsontext.BeginObject)
h.WriteToken(jsontext.String("time"))
h.WriteToken(jsontext.Float(float64(e.Time.Sub(t.referenceTime).Nanoseconds()) / 1e6))
h.WriteToken(jsontext.String("name"))
h.WriteToken(jsontext.String(e.Event.Name()))
h.WriteToken(jsontext.String("data"))
if err := e.Event.Encode(enc, e.Time); err != nil {
t.encodeErr = err
continue
}
h.WriteToken(jsontext.EndObject)
if h.err != nil {
t.encodeErr = h.err
}
}
}
func (t *FileSeq) removeProducer() {
t.mx.Lock()
defer t.mx.Unlock()
if t.closed {
return
}
t.producers--
if t.producers == 0 {
t.closed = true
t.close()
t.w.Close()
}
}
func (t *FileSeq) close() {
close(t.events)
<-t.runStopped
if t.encodeErr != nil {
log.Printf("exporting qlog failed: %s\n", t.encodeErr)
return
}
}
type Writer struct {
t *FileSeq
}
func (w *Writer) Close() error {
w.t.removeProducer()
return nil
}
func (w *Writer) RecordEvent(ev Event) {
w.t.record(time.Now(), ev)
}