http3: add the qlog event schema to trace header (#5383)

This commit is contained in:
Marten Seemann
2025-10-15 12:02:05 +08:00
committed by GitHub
parent 378d867241
commit 5e5100b40c
15 changed files with 129 additions and 9 deletions

View File

@@ -13,8 +13,8 @@ import (
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/internal/testdata"
"github.com/quic-go/quic-go/qlog"
)
func main() {

View File

@@ -17,8 +17,8 @@ import (
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/internal/testdata"
"github.com/quic-go/quic-go/qlog"
)
type binds []string

View File

@@ -66,7 +66,7 @@ func newConnection(
idleTimeout time.Duration,
) *Conn {
var qlogger qlogwriter.Recorder
if qlogTrace := quicConn.QlogTrace(); qlogTrace != nil {
if qlogTrace := quicConn.QlogTrace(); qlogTrace != nil && qlogTrace.SupportsSchemas(qlog.EventSchema) {
qlogger = qlogTrace.AddProducer()
}
c := &Conn{

View File

@@ -143,6 +143,8 @@ type qlogTrace struct {
recorder qlogwriter.Recorder
}
func (t *qlogTrace) SupportsSchemas(schema string) bool { return true }
func (t *qlogTrace) AddProducer() qlogwriter.Recorder {
return t.recorder
}

15
http3/qlog/qlog_dir.go Normal file
View File

@@ -0,0 +1,15 @@
package qlog
import (
"context"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/qlog"
"github.com/quic-go/quic-go/qlogwriter"
)
const EventSchema = "urn:ietf:params:qlog:events:http3-12"
func DefaultConnectionTracer(ctx context.Context, isClient bool, connID quic.ConnectionID) qlogwriter.Trace {
return qlog.DefaultConnectionTracerWithSchemas(ctx, isClient, connID, []string{qlog.EventSchema, EventSchema})
}

View File

@@ -0,0 +1,41 @@
package qlog
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/qlog"
"github.com/stretchr/testify/require"
)
func TestQLOGDIRSet(t *testing.T) {
tmpDir := t.TempDir()
connID := quic.ConnectionIDFromBytes([]byte{1, 2, 3, 4})
qlogDir := filepath.Join(tmpDir, "qlogs")
t.Setenv("QLOGDIR", qlogDir)
tracer := DefaultConnectionTracer(context.Background(), true, connID)
require.NotNil(t, tracer)
// adding and closing a producer makes the tracer close the file
recorder := tracer.AddProducer()
recorder.Close()
_, err := os.Stat(qlogDir)
qlogDirCreated := !os.IsNotExist(err)
require.True(t, qlogDirCreated)
entries, err := os.ReadDir(qlogDir)
require.NoError(t, err)
require.Len(t, entries, 1)
data, err := os.ReadFile(filepath.Join(qlogDir, entries[0].Name()))
require.NoError(t, err)
require.Contains(t, string(data), EventSchema)
require.Contains(t, string(data), qlog.EventSchema)
}

View File

@@ -441,7 +441,7 @@ func (s *Server) removeListener(l *QUICListener) {
// It blocks until all HTTP handlers for all streams have returned.
func (s *Server) handleConn(conn *quic.Conn) error {
var qlogger qlogwriter.Recorder
if qlogTrace := conn.QlogTrace(); qlogTrace != nil {
if qlogTrace := conn.QlogTrace(); qlogTrace != nil && qlogTrace.SupportsSchemas(qlog.EventSchema) {
qlogger = qlogTrace.AddProducer()
}

View File

@@ -137,6 +137,10 @@ func (t *multiplexedTrace) AddProducer() qlogwriter.Recorder {
return &multiplexedRecorder{Recorders: recorders}
}
func (t *multiplexedTrace) SupportsSchemas(schema string) bool {
return true
}
func getQuicConfig(conf *quic.Config) *quic.Config {
if conf == nil {
conf = &quic.Config{}

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/quic-go/quic-go"
h3qlog "github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/qlog"
"github.com/quic-go/quic-go/qlogwriter"
@@ -46,7 +47,7 @@ func NewQlogConnectionTracer(logger io.Writer) func(ctx context.Context, isClien
utils.NewBufferedWriteCloser(bufio.NewWriter(f), f),
isClient,
connID,
[]string{qlog.EventSchema},
[]string{qlog.EventSchema, h3qlog.EventSchema},
)
go fileSeq.Run()
return fileSeq

View File

@@ -74,6 +74,8 @@ type multiplexedTrace struct {
var _ qlogwriter.Trace = &multiplexedTrace{}
func (t *multiplexedTrace) SupportsSchemas(schema string) bool { return true }
func (t *multiplexedTrace) AddProducer() qlogwriter.Recorder {
recorders := make([]qlogwriter.Recorder, 0, len(t.Traces))
for _, tr := range t.Traces {

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/quic-go/quic-go"
h3qlog "github.com/quic-go/quic-go/http3/qlog"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/qlog"
"github.com/quic-go/quic-go/qlogwriter"
@@ -50,7 +51,7 @@ func NewQLOGConnectionTracer(_ context.Context, isClient bool, connID quic.Conne
utils.NewBufferedWriteCloser(bufio.NewWriter(f), f),
isClient,
connID,
[]string{qlog.EventSchema},
[]string{qlog.EventSchema, h3qlog.EventSchema},
)
go fileSeq.Run()
return fileSeq

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log"
"os"
"slices"
"strings"
"github.com/quic-go/quic-go/internal/utils"
@@ -19,6 +20,17 @@ const EventSchema = "urn:ietf:params:qlog:events:quic-12"
// File names are <odcid>_<perspective>.sqlog.
// Returns nil if QLOGDIR is not set.
func DefaultConnectionTracer(_ context.Context, isClient bool, connID ConnectionID) qlogwriter.Trace {
return defaultConnectionTracerWithSchemas(isClient, connID, []string{EventSchema})
}
func DefaultConnectionTracerWithSchemas(_ context.Context, isClient bool, connID ConnectionID, eventSchemas []string) qlogwriter.Trace {
if !slices.Contains(eventSchemas, EventSchema) {
eventSchemas = append([]string{EventSchema}, eventSchemas...)
}
return defaultConnectionTracerWithSchemas(isClient, connID, eventSchemas)
}
func defaultConnectionTracerWithSchemas(isClient bool, connID ConnectionID, eventSchemas []string) qlogwriter.Trace {
qlogDir := os.Getenv("QLOGDIR")
if qlogDir == "" {
return nil
@@ -42,7 +54,7 @@ func DefaultConnectionTracer(_ context.Context, isClient bool, connID Connection
utils.NewBufferedWriteCloser(bufio.NewWriter(f), f),
isClient,
connID,
[]string{EventSchema},
eventSchemas,
)
go fileSeq.Run()
return fileSeq

View File

@@ -2,11 +2,14 @@ package qlog
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/qlogwriter"
"github.com/stretchr/testify/require"
)
@@ -17,10 +20,21 @@ func TestQLOGDIRSet(t *testing.T) {
qlogDir := filepath.Join(tmpDir, "qlogs")
t.Setenv("QLOGDIR", qlogDir)
tracer := DefaultConnectionTracer(context.Background(), true, connID)
t.Run("default connection tracer", func(t *testing.T) {
tracer := DefaultConnectionTracer(context.Background(), true, connID)
testQLOGDIRSet(t, qlogDir, tracer, []string{EventSchema})
})
t.Run("default connection tracer with schemas", func(t *testing.T) {
tracer := DefaultConnectionTracerWithSchemas(context.Background(), true, connID, []string{"urn:ietf:params:qlog:events:foobar"})
testQLOGDIRSet(t, qlogDir, tracer, []string{EventSchema, "urn:ietf:params:qlog:events:foobar"})
})
}
func testQLOGDIRSet(t *testing.T, qlogDir string, tracer qlogwriter.Trace, expectedEventSchemas []string) {
require.NotNil(t, tracer)
// adddng and closing a producer makes the tracer close the file
// adding and closing a producer makes the tracer close the file
recorder := tracer.AddProducer()
recorder.Close()
@@ -31,6 +45,20 @@ func TestQLOGDIRSet(t *testing.T) {
entries, err := os.ReadDir(qlogDir)
require.NoError(t, err)
require.Len(t, entries, 1)
data, err := os.ReadFile(filepath.Join(qlogDir, entries[0].Name()))
require.NoError(t, err)
var obj map[string]any
require.NoError(t, json.Unmarshal([]byte(strings.Split(string(data), "\n")[0])[1:], &obj))
require.Contains(t, obj, "trace")
require.IsType(t, obj["trace"], map[string]any{})
require.Contains(t, obj["trace"], "event_schemas")
var eventSchemas []string
for _, v := range obj["trace"].(map[string]any)["event_schemas"].([]any) {
eventSchemas = append(eventSchemas, v.(string))
}
require.Equal(t, eventSchemas, expectedEventSchemas)
}
func TestQLOGDIRNotSet(t *testing.T) {

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"log"
"slices"
"sync"
"time"
@@ -18,6 +19,9 @@ 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.
@@ -67,6 +71,8 @@ type FileSeq struct {
mx sync.Mutex
producers int
closed bool
eventSchemas []string
}
var _ Trace = &FileSeq{}
@@ -112,6 +118,10 @@ func newFileSeq(w io.WriteCloser, pers string, odcid *ConnectionID, 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()

View File

@@ -26,6 +26,10 @@ func (t *Trace) AddProducer() qlogwriter.Recorder {
return t.Recorder
}
func (t *Trace) SupportsSchemas(string) bool {
return true
}
// Recorder is a qlog.Recorder that records events.
// Events can be retrieved using the Events method.
type Recorder struct {