diff --git a/example/client/main.go b/example/client/main.go index 538bcfe4c..da696fe95 100644 --- a/example/client/main.go +++ b/example/client/main.go @@ -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() { diff --git a/example/main.go b/example/main.go index bf87f8107..4dec11533 100644 --- a/example/main.go +++ b/example/main.go @@ -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 diff --git a/http3/conn.go b/http3/conn.go index 874aaed35..4e9cdeb2c 100644 --- a/http3/conn.go +++ b/http3/conn.go @@ -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{ diff --git a/http3/http3_helper_test.go b/http3/http3_helper_test.go index eac88ae3e..c698cbf71 100644 --- a/http3/http3_helper_test.go +++ b/http3/http3_helper_test.go @@ -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 } diff --git a/http3/qlog/qlog_dir.go b/http3/qlog/qlog_dir.go new file mode 100644 index 000000000..898d17c6b --- /dev/null +++ b/http3/qlog/qlog_dir.go @@ -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}) +} diff --git a/http3/qlog/qlog_dir_test.go b/http3/qlog/qlog_dir_test.go new file mode 100644 index 000000000..ea7d8469f --- /dev/null +++ b/http3/qlog/qlog_dir_test.go @@ -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) +} diff --git a/http3/server.go b/http3/server.go index 67070c3e8..3d42016ed 100644 --- a/http3/server.go +++ b/http3/server.go @@ -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() } diff --git a/integrationtests/self/self_test.go b/integrationtests/self/self_test.go index 584626494..87631cc6a 100644 --- a/integrationtests/self/self_test.go +++ b/integrationtests/self/self_test.go @@ -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{} diff --git a/integrationtests/tools/qlog.go b/integrationtests/tools/qlog.go index 19917f25e..a35c33104 100644 --- a/integrationtests/tools/qlog.go +++ b/integrationtests/tools/qlog.go @@ -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 diff --git a/integrationtests/versionnegotiation/test_helper_test.go b/integrationtests/versionnegotiation/test_helper_test.go index 3f9c6dfd7..9b9a5814b 100644 --- a/integrationtests/versionnegotiation/test_helper_test.go +++ b/integrationtests/versionnegotiation/test_helper_test.go @@ -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 { diff --git a/interop/utils/logging.go b/interop/utils/logging.go index 53bac8d2d..baa77010a 100644 --- a/interop/utils/logging.go +++ b/interop/utils/logging.go @@ -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 diff --git a/qlog/qlog_dir.go b/qlog/qlog_dir.go index c014a1c3f..83bb72b3f 100644 --- a/qlog/qlog_dir.go +++ b/qlog/qlog_dir.go @@ -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 _.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 diff --git a/qlog/qlog_dir_test.go b/qlog/qlog_dir_test.go index 02f0ab136..9a59d5fe7 100644 --- a/qlog/qlog_dir_test.go +++ b/qlog/qlog_dir_test.go @@ -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) { diff --git a/qlogwriter/writer.go b/qlogwriter/writer.go index 5a78fd85f..390039d61 100644 --- a/qlogwriter/writer.go +++ b/qlogwriter/writer.go @@ -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() diff --git a/testutils/events/event_recorder.go b/testutils/events/event_recorder.go index feea651bc..01f047607 100644 --- a/testutils/events/event_recorder.go +++ b/testutils/events/event_recorder.go @@ -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 {