forked from quic-go/quic-go
217 lines
5.2 KiB
Go
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)
|
|
}
|