Repository: quic-go/quic-go
Pull request #4640 (2024-08-24 04:27:36)

qlog: migrate tests away from Ginkgo
Done with Claude 3.5, plus a bit of manual cleanup.

Part of #3652.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/4640?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.83%. Comparing base [(`17fb3b9`)](https://app.codecov.io/gh/quic-go/quic-go/commit/17fb3b96ba07f820519560a00a82369f8ef6a368?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) to head [(`f7f9fa4`)](https://app.codecov.io/gh/quic-go/quic-go/commit/f7f9fa4aa10f8a9512aabf3a3243f9f12385a1ef?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4640      +/-   ##
==========================================
- Coverage   84.93%   84.83%   -0.10%     
==========================================
  Files         150      147       -3     
  Lines       14864    14586     -278     
==========================================
- Hits        12624    12373     -251     
+ Misses       1715     1705      -10     
+ Partials      525      508      -17     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quic-go/quic-go/pull/4640?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 7c4b71c0..63b40cc3 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -43,3 +43,4 @@ issues:
     - path: _test\.go
       linters:
         - exhaustive
+        - prealloc
diff --git a/go.mod b/go.mod
index 17db49c1..5e1ee107 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/onsi/gomega v1.27.6
 	github.com/prometheus/client_golang v1.19.1
 	github.com/quic-go/qpack v0.5.1
+	github.com/stretchr/testify v1.9.0
 	go.uber.org/mock v0.4.0
 	golang.org/x/crypto v0.26.0
 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
@@ -20,10 +21,12 @@ require (
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_model v0.5.0 // indirect
 	github.com/prometheus/common v0.48.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect


Test patch:
diff --git a/qlog/connection_tracer_test.go b/qlog/connection_tracer_test.go
index ef21904f..a193223a 100644
--- a/qlog/connection_tracer_test.go
+++ b/qlog/connection_tracer_test.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"net"
 	"net/netip"
+	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go"
@@ -13,8 +14,7 @@ import (
 	"github.com/quic-go/quic-go/internal/utils"
 	"github.com/quic-go/quic-go/logging"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
 type nopWriteCloserImpl struct{ io.Writer }
@@ -25,903 +25,884 @@ func nopWriteCloser(w io.Writer) io.WriteCloser {
 	return &nopWriteCloserImpl{Writer: w}
 }
 
-type entry struct {
-	Time  time.Time
-	Name  string
-	Event map[string]interface{}
+func newConnectionTracer() (*logging.ConnectionTracer, *bytes.Buffer) {
+	buf := &bytes.Buffer{}
+	tracer := NewConnectionTracer(
+		nopWriteCloser(buf),
+		logging.PerspectiveServer,
+		protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
+	)
+	return tracer, buf
 }
 
-func exportAndParse(buf *bytes.Buffer) []entry {
+func TestConnectionTraceMetadata(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.Close()
+
 	m := make(map[string]interface{})
-	line, err := buf.ReadBytes('\n')
-	Expect(err).ToNot(HaveOccurred())
-	Expect(unmarshal(line, &m)).To(Succeed())
-	Expect(m).To(HaveKey("trace"))
-	var entries []entry
+	require.NoError(t, unmarshal(buf.Bytes(), &m))
+	require.Equal(t, "0.3", m["qlog_version"])
+	require.Contains(t, m, "title")
+	require.Contains(t, m, "trace")
 	trace := m["trace"].(map[string]interface{})
-	Expect(trace).To(HaveKey("common_fields"))
+	require.Contains(t, trace, "common_fields")
 	commonFields := trace["common_fields"].(map[string]interface{})
-	Expect(commonFields).To(HaveKey("reference_time"))
+	require.Equal(t, "deadbeef", commonFields["ODCID"])
+	require.Equal(t, "deadbeef", commonFields["group_id"])
+	require.Contains(t, commonFields, "reference_time")
 	referenceTime := time.Unix(0, int64(commonFields["reference_time"].(float64)*1e6))
-	Expect(trace).ToNot(HaveKey("events"))
-
-	for buf.Len() > 0 {
-		line, err := buf.ReadBytes('\n')
-		Expect(err).ToNot(HaveOccurred())
-		ev := make(map[string]interface{})
-		Expect(unmarshal(line, &ev)).To(Succeed())
-		Expect(ev).To(HaveLen(3))
-		Expect(ev).To(HaveKey("time"))
-		Expect(ev).To(HaveKey("name"))
-		Expect(ev).To(HaveKey("data"))
-		entries = append(entries, entry{
-			Time:  referenceTime.Add(time.Duration(ev["time"].(float64)*1e6) * time.Nanosecond),
-			Name:  ev["name"].(string),
-			Event: ev["data"].(map[string]interface{}),
-		})
-	}
-	return entries
+	require.WithinDuration(t, time.Now(), referenceTime, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "relative", commonFields["time_format"])
+	require.Contains(t, trace, "vantage_point")
+	vantagePoint := trace["vantage_point"].(map[string]interface{})
+	require.Equal(t, "server", vantagePoint["type"])
 }
 
-func exportAndParseSingle(buf *bytes.Buffer) entry {
-	entries := exportAndParse(buf)
-	Expect(entries).To(HaveLen(1))
-	return entries[0]
+func TestConnectionStarts(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.StartedConnection(
+		&net.UDPAddr{IP: net.IPv4(192, 168, 13, 37), Port: 42},
+		&net.UDPAddr{IP: net.IPv4(192, 168, 12, 34), Port: 24},
+		protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
+		protocol.ParseConnectionID([]byte{5, 6, 7, 8}),
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_started", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "ipv4", ev["ip_version"])
+	require.Equal(t, "192.168.13.37", ev["src_ip"])
+	require.Equal(t, float64(42), ev["src_port"])
+	require.Equal(t, "192.168.12.34", ev["dst_ip"])
+	require.Equal(t, float64(24), ev["dst_port"])
+	require.Equal(t, "01020304", ev["src_cid"])
+	require.Equal(t, "05060708", ev["dst_cid"])
 }
 
-var _ = Describe("Tracing", func() {
-	var (
-		tracer *logging.ConnectionTracer
-		buf    *bytes.Buffer
-	)
+func TestVersionNegotiation(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.NegotiatedVersion(0x1337, nil, nil)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:version_information", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 1)
+	require.Equal(t, "1337", ev["chosen_version"])
+}
+
+func TestVersionNegotiationWithPriorAttempts(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.NegotiatedVersion(0x1337, []logging.Version{1, 2, 3}, []logging.Version{4, 5, 6})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:version_information", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 3)
+	require.Equal(t, "1337", ev["chosen_version"])
+	require.Equal(t, []interface{}{"1", "2", "3"}, ev["client_versions"])
+	require.Equal(t, []interface{}{"4", "5", "6"}, ev["server_versions"])
+}
+
+func TestIdleTimeouts(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ClosedConnection(&quic.IdleTimeoutError{})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_closed", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 2)
+	require.Equal(t, "local", ev["owner"])
+	require.Equal(t, "idle_timeout", ev["trigger"])
+}
 
-	BeforeEach(func() {
-		buf = &bytes.Buffer{}
-		tracer = NewConnectionTracer(
-			nopWriteCloser(buf),
-			logging.PerspectiveServer,
-			protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
-		)
+func TestHandshakeTimeouts(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ClosedConnection(&quic.HandshakeTimeoutError{})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_closed", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 2)
+	require.Equal(t, "local", ev["owner"])
+	require.Equal(t, "handshake_timeout", ev["trigger"])
+}
+
+func TestReceivedStatelessResetPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ClosedConnection(&quic.StatelessResetError{
+		Token: protocol.StatelessResetToken{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
 	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_closed", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 3)
+	require.Equal(t, "remote", ev["owner"])
+	require.Equal(t, "stateless_reset", ev["trigger"])
+	require.Equal(t, "00112233445566778899aabbccddeeff", ev["stateless_reset_token"])
+}
 
-	It("exports a trace that has the right metadata", func() {
-		tracer.Close()
-
-		m := make(map[string]interface{})
-		Expect(unmarshal(buf.Bytes(), &m)).To(Succeed())
-		Expect(m).To(HaveKeyWithValue("qlog_version", "0.3"))
-		Expect(m).To(HaveKey("title"))
-		Expect(m).To(HaveKey("trace"))
-		trace := m["trace"].(map[string]interface{})
-		Expect(trace).To(HaveKey("common_fields"))
-		commonFields := trace["common_fields"].(map[string]interface{})
-		Expect(commonFields).To(HaveKeyWithValue("ODCID", "deadbeef"))
-		Expect(commonFields).To(HaveKeyWithValue("group_id", "deadbeef"))
-		Expect(commonFields).To(HaveKey("reference_time"))
-		referenceTime := time.Unix(0, int64(commonFields["reference_time"].(float64)*1e6))
-		Expect(referenceTime).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-		Expect(commonFields).To(HaveKeyWithValue("time_format", "relative"))
-		Expect(trace).To(HaveKey("vantage_point"))
-		vantagePoint := trace["vantage_point"].(map[string]interface{})
-		Expect(vantagePoint).To(HaveKeyWithValue("type", "server"))
+func TestVersionNegotiationFailure(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ClosedConnection(&quic.VersionNegotiationError{})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_closed", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 1)
+	require.Equal(t, "version_mismatch", ev["trigger"])
+}
+
+func TestApplicationErrors(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ClosedConnection(&quic.ApplicationError{
+		Remote:       true,
+		ErrorCode:    1337,
+		ErrorMessage: "foobar",
 	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_closed", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 3)
+	require.Equal(t, "remote", ev["owner"])
+	require.Equal(t, float64(1337), ev["application_code"])
+	require.Equal(t, "foobar", ev["reason"])
+}
 
-	Context("Events", func() {
-		It("records connection starts", func() {
-			tracer.StartedConnection(
-				&net.UDPAddr{IP: net.IPv4(192, 168, 13, 37), Port: 42},
-				&net.UDPAddr{IP: net.IPv4(192, 168, 12, 34), Port: 24},
-				protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
-				protocol.ParseConnectionID([]byte{5, 6, 7, 8}),
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_started"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("ip_version", "ipv4"))
-			Expect(ev).To(HaveKeyWithValue("src_ip", "192.168.13.37"))
-			Expect(ev).To(HaveKeyWithValue("src_port", float64(42)))
-			Expect(ev).To(HaveKeyWithValue("dst_ip", "192.168.12.34"))
-			Expect(ev).To(HaveKeyWithValue("dst_port", float64(24)))
-			Expect(ev).To(HaveKeyWithValue("src_cid", "01020304"))
-			Expect(ev).To(HaveKeyWithValue("dst_cid", "05060708"))
-		})
-
-		It("records the version, if no version negotiation happened", func() {
-			tracer.NegotiatedVersion(0x1337, nil, nil)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:version_information"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(1))
-			Expect(ev).To(HaveKeyWithValue("chosen_version", "1337"))
-		})
-
-		It("records the version, if version negotiation happened", func() {
-			tracer.NegotiatedVersion(0x1337, []logging.Version{1, 2, 3}, []logging.Version{4, 5, 6})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:version_information"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(3))
-			Expect(ev).To(HaveKeyWithValue("chosen_version", "1337"))
-			Expect(ev).To(HaveKey("client_versions"))
-			Expect(ev["client_versions"].([]interface{})).To(Equal([]interface{}{"1", "2", "3"}))
-			Expect(ev).To(HaveKey("server_versions"))
-			Expect(ev["server_versions"].([]interface{})).To(Equal([]interface{}{"4", "5", "6"}))
-		})
-
-		It("records idle timeouts", func() {
-			tracer.ClosedConnection(&quic.IdleTimeoutError{})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_closed"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(2))
-			Expect(ev).To(HaveKeyWithValue("owner", "local"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "idle_timeout"))
-		})
-
-		It("records handshake timeouts", func() {
-			tracer.ClosedConnection(&quic.HandshakeTimeoutError{})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_closed"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(2))
-			Expect(ev).To(HaveKeyWithValue("owner", "local"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "handshake_timeout"))
-		})
-
-		It("records a received stateless reset packet", func() {
-			tracer.ClosedConnection(&quic.StatelessResetError{
-				Token: protocol.StatelessResetToken{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_closed"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(3))
-			Expect(ev).To(HaveKeyWithValue("owner", "remote"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "stateless_reset"))
-			Expect(ev).To(HaveKeyWithValue("stateless_reset_token", "00112233445566778899aabbccddeeff"))
-		})
-
-		It("records connection closing due to version negotiation failure", func() {
-			tracer.ClosedConnection(&quic.VersionNegotiationError{})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_closed"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(1))
-			Expect(ev).To(HaveKeyWithValue("trigger", "version_mismatch"))
-		})
-
-		It("records application errors", func() {
-			tracer.ClosedConnection(&quic.ApplicationError{
-				Remote:       true,
-				ErrorCode:    1337,
-				ErrorMessage: "foobar",
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_closed"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(3))
-			Expect(ev).To(HaveKeyWithValue("owner", "remote"))
-			Expect(ev).To(HaveKeyWithValue("application_code", float64(1337)))
-			Expect(ev).To(HaveKeyWithValue("reason", "foobar"))
-		})
-
-		It("records transport errors", func() {
-			tracer.ClosedConnection(&quic.TransportError{
-				ErrorCode:    qerr.AEADLimitReached,
-				ErrorMessage: "foobar",
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:connection_closed"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(3))
-			Expect(ev).To(HaveKeyWithValue("owner", "local"))
-			Expect(ev).To(HaveKeyWithValue("connection_code", "aead_limit_reached"))
-			Expect(ev).To(HaveKeyWithValue("reason", "foobar"))
-		})
-
-		It("records sent transport parameters", func() {
-			rcid := protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad})
-			tracer.SentTransportParameters(&logging.TransportParameters{
-				InitialMaxStreamDataBidiLocal:   1000,
-				InitialMaxStreamDataBidiRemote:  2000,
-				InitialMaxStreamDataUni:         3000,
-				InitialMaxData:                  4000,
-				MaxBidiStreamNum:                10,
-				MaxUniStreamNum:                 20,
-				MaxAckDelay:                     123 * time.Millisecond,
-				AckDelayExponent:                12,
-				DisableActiveMigration:          true,
-				MaxUDPPayloadSize:               1234,
-				MaxIdleTimeout:                  321 * time.Millisecond,
-				StatelessResetToken:             &protocol.StatelessResetToken{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00},
-				OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde}),
-				InitialSourceConnectionID:       protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
-				RetrySourceConnectionID:         &rcid,
-				ActiveConnectionIDLimit:         7,
-				MaxDatagramFrameSize:            protocol.InvalidByteCount,
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_set"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("owner", "local"))
-			Expect(ev).To(HaveKeyWithValue("original_destination_connection_id", "deadc0de"))
-			Expect(ev).To(HaveKeyWithValue("initial_source_connection_id", "deadbeef"))
-			Expect(ev).To(HaveKeyWithValue("retry_source_connection_id", "decafbad"))
-			Expect(ev).To(HaveKeyWithValue("stateless_reset_token", "112233445566778899aabbccddeeff00"))
-			Expect(ev).To(HaveKeyWithValue("max_idle_timeout", float64(321)))
-			Expect(ev).To(HaveKeyWithValue("max_udp_payload_size", float64(1234)))
-			Expect(ev).To(HaveKeyWithValue("ack_delay_exponent", float64(12)))
-			Expect(ev).To(HaveKeyWithValue("active_connection_id_limit", float64(7)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_data", float64(4000)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_stream_data_bidi_local", float64(1000)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_stream_data_bidi_remote", float64(2000)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_stream_data_uni", float64(3000)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_streams_bidi", float64(10)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_streams_uni", float64(20)))
-			Expect(ev).ToNot(HaveKey("preferred_address"))
-			Expect(ev).ToNot(HaveKey("max_datagram_frame_size"))
-		})
-
-		It("records the server's transport parameters, without a stateless reset token", func() {
-			tracer.SentTransportParameters(&logging.TransportParameters{
-				OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde}),
-				ActiveConnectionIDLimit:         7,
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_set"))
-			ev := entry.Event
-			Expect(ev).ToNot(HaveKey("stateless_reset_token"))
-		})
-
-		It("records transport parameters without retry_source_connection_id", func() {
-			tracer.SentTransportParameters(&logging.TransportParameters{
-				StatelessResetToken: &protocol.StatelessResetToken{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00},
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_set"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("owner", "local"))
-			Expect(ev).ToNot(HaveKey("retry_source_connection_id"))
-		})
-
-		It("records transport parameters with a preferred address", func() {
-			tracer.SentTransportParameters(&logging.TransportParameters{
-				PreferredAddress: &logging.PreferredAddress{
-					IPv4:                netip.AddrPortFrom(netip.AddrFrom4([4]byte{12, 34, 56, 78}), 123),
-					IPv6:                netip.AddrPortFrom(netip.AddrFrom16([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), 456),
-					ConnectionID:        protocol.ParseConnectionID([]byte{8, 7, 6, 5, 4, 3, 2, 1}),
-					StatelessResetToken: protocol.StatelessResetToken{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
-				},
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_set"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("owner", "local"))
-			Expect(ev).To(HaveKey("preferred_address"))
-			pa := ev["preferred_address"].(map[string]interface{})
-			Expect(pa).To(HaveKeyWithValue("ip_v4", "12.34.56.78"))
-			Expect(pa).To(HaveKeyWithValue("port_v4", float64(123)))
-			Expect(pa).To(HaveKeyWithValue("ip_v6", "102:304:506:708:90a:b0c:d0e:f10"))
-			Expect(pa).To(HaveKeyWithValue("port_v6", float64(456)))
-			Expect(pa).To(HaveKeyWithValue("connection_id", "0807060504030201"))
-			Expect(pa).To(HaveKeyWithValue("stateless_reset_token", "0f0e0d0c0b0a09080706050403020100"))
-		})
-
-		It("records transport parameters that enable the datagram extension", func() {
-			tracer.SentTransportParameters(&logging.TransportParameters{
-				MaxDatagramFrameSize: 1337,
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_set"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("max_datagram_frame_size", float64(1337)))
-		})
-
-		It("records received transport parameters", func() {
-			tracer.ReceivedTransportParameters(&logging.TransportParameters{})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_set"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("owner", "remote"))
-			Expect(ev).ToNot(HaveKey("original_destination_connection_id"))
-		})
-
-		It("records restored transport parameters", func() {
-			tracer.RestoredTransportParameters(&logging.TransportParameters{
-				InitialMaxStreamDataBidiLocal:  100,
-				InitialMaxStreamDataBidiRemote: 200,
-				InitialMaxStreamDataUni:        300,
-				InitialMaxData:                 400,
-				MaxIdleTimeout:                 123 * time.Millisecond,
-			})
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:parameters_restored"))
-			ev := entry.Event
-			Expect(ev).ToNot(HaveKey("owner"))
-			Expect(ev).ToNot(HaveKey("original_destination_connection_id"))
-			Expect(ev).ToNot(HaveKey("stateless_reset_token"))
-			Expect(ev).ToNot(HaveKey("retry_source_connection_id"))
-			Expect(ev).ToNot(HaveKey("initial_source_connection_id"))
-			Expect(ev).To(HaveKeyWithValue("max_idle_timeout", float64(123)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_data", float64(400)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_stream_data_bidi_local", float64(100)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_stream_data_bidi_remote", float64(200)))
-			Expect(ev).To(HaveKeyWithValue("initial_max_stream_data_uni", float64(300)))
-		})
-
-		It("records a sent long header packet, without an ACK", func() {
-			tracer.SentLongHeaderPacket(
-				&logging.ExtendedHeader{
-					Header: logging.Header{
-						Type:             protocol.PacketTypeHandshake,
-						DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
-						SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
-						Length:           1337,
-						Version:          protocol.Version1,
-					},
-					PacketNumber: 1337,
-				},
-				987,
-				logging.ECNCE,
-				nil,
-				[]logging.Frame{
-					&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
-					&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
-				},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_sent"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			raw := ev["raw"].(map[string]interface{})
-			Expect(raw).To(HaveKeyWithValue("length", float64(987)))
-			Expect(raw).To(HaveKeyWithValue("payload_length", float64(1337)))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "handshake"))
-			Expect(hdr).To(HaveKeyWithValue("packet_number", float64(1337)))
-			Expect(hdr).To(HaveKeyWithValue("scid", "04030201"))
-			Expect(ev).To(HaveKey("frames"))
-			Expect(ev).To(HaveKeyWithValue("ecn", "CE"))
-			frames := ev["frames"].([]interface{})
-			Expect(frames).To(HaveLen(2))
-			Expect(frames[0].(map[string]interface{})).To(HaveKeyWithValue("frame_type", "max_stream_data"))
-			Expect(frames[1].(map[string]interface{})).To(HaveKeyWithValue("frame_type", "stream"))
-		})
-
-		It("records a sent short header packet, without an ACK", func() {
-			tracer.SentShortHeaderPacket(
-				&logging.ShortHeader{
-					DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
-					PacketNumber:     1337,
-				},
-				123,
-				logging.ECNUnsupported,
-				&logging.AckFrame{AckRanges: []logging.AckRange{{Smallest: 1, Largest: 10}}},
-				[]logging.Frame{&logging.MaxDataFrame{MaximumData: 987}},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			ev := entry.Event
-			raw := ev["raw"].(map[string]interface{})
-			Expect(raw).To(HaveKeyWithValue("length", float64(123)))
-			Expect(raw).ToNot(HaveKey("payload_length"))
-			Expect(ev).To(HaveKey("header"))
-			Expect(ev).ToNot(HaveKey("ecn"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "1RTT"))
-			Expect(hdr).To(HaveKeyWithValue("packet_number", float64(1337)))
-			Expect(ev).To(HaveKey("frames"))
-			frames := ev["frames"].([]interface{})
-			Expect(frames).To(HaveLen(2))
-			Expect(frames[0].(map[string]interface{})).To(HaveKeyWithValue("frame_type", "ack"))
-			Expect(frames[1].(map[string]interface{})).To(HaveKeyWithValue("frame_type", "max_data"))
-		})
-
-		It("records a received Long Header packet", func() {
-			tracer.ReceivedLongHeaderPacket(
-				&logging.ExtendedHeader{
-					Header: logging.Header{
-						Type:             protocol.PacketTypeInitial,
-						DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
-						SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
-						Token:            []byte{0xde, 0xad, 0xbe, 0xef},
-						Length:           1234,
-						Version:          protocol.Version1,
-					},
-					PacketNumber: 1337,
-				},
-				789,
-				logging.ECT0,
-				[]logging.Frame{
-					&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
-					&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
-				},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_received"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			raw := ev["raw"].(map[string]interface{})
-			Expect(raw).To(HaveKeyWithValue("length", float64(789)))
-			Expect(raw).To(HaveKeyWithValue("payload_length", float64(1234)))
-			Expect(ev).To(HaveKeyWithValue("ecn", "ECT(0)"))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "initial"))
-			Expect(hdr).To(HaveKeyWithValue("packet_number", float64(1337)))
-			Expect(hdr).To(HaveKeyWithValue("scid", "04030201"))
-			Expect(hdr).To(HaveKey("token"))
-			token := hdr["token"].(map[string]interface{})
-			Expect(token).To(HaveKeyWithValue("data", "deadbeef"))
-			Expect(ev).To(HaveKey("frames"))
-			Expect(ev["frames"].([]interface{})).To(HaveLen(2))
-		})
-
-		It("records a received Short Header packet", func() {
-			shdr := &logging.ShortHeader{
-				DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
-				PacketNumber:     1337,
-				PacketNumberLen:  protocol.PacketNumberLen3,
-				KeyPhase:         protocol.KeyPhaseZero,
-			}
-			tracer.ReceivedShortHeaderPacket(
-				shdr,
-				789,
-				logging.ECT1,
-				[]logging.Frame{
-					&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
-					&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
-				},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_received"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			raw := ev["raw"].(map[string]interface{})
-			Expect(raw).To(HaveKeyWithValue("length", float64(789)))
-			Expect(raw).To(HaveKeyWithValue("payload_length", float64(789-(1+8+3))))
-			Expect(ev).To(HaveKeyWithValue("ecn", "ECT(1)"))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "1RTT"))
-			Expect(hdr).To(HaveKeyWithValue("packet_number", float64(1337)))
-			Expect(hdr).To(HaveKeyWithValue("key_phase_bit", "0"))
-			Expect(ev).To(HaveKey("frames"))
-			Expect(ev["frames"].([]interface{})).To(HaveLen(2))
-		})
-
-		It("records a received Retry packet", func() {
-			tracer.ReceivedRetry(
-				&logging.Header{
-					Type:             protocol.PacketTypeRetry,
-					DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
-					SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
-					Token:            []byte{0xde, 0xad, 0xbe, 0xef},
-					Version:          protocol.Version1,
-				},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_received"))
-			ev := entry.Event
-			Expect(ev).ToNot(HaveKey("raw"))
-			Expect(ev).To(HaveKey("header"))
-			header := ev["header"].(map[string]interface{})
-			Expect(header).To(HaveKeyWithValue("packet_type", "retry"))
-			Expect(header).ToNot(HaveKey("packet_number"))
-			Expect(header).To(HaveKey("version"))
-			Expect(header).To(HaveKey("dcid"))
-			Expect(header).To(HaveKey("scid"))
-			Expect(header).To(HaveKey("token"))
-			token := header["token"].(map[string]interface{})
-			Expect(token).To(HaveKeyWithValue("data", "deadbeef"))
-			Expect(ev).ToNot(HaveKey("frames"))
-		})
-
-		It("records a received Version Negotiation packet", func() {
-			tracer.ReceivedVersionNegotiationPacket(
-				protocol.ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8},
-				protocol.ArbitraryLenConnectionID{4, 3, 2, 1},
-				[]protocol.Version{0xdeadbeef, 0xdecafbad},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_received"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("header"))
-			Expect(ev).ToNot(HaveKey("frames"))
-			Expect(ev).To(HaveKey("supported_versions"))
-			Expect(ev["supported_versions"].([]interface{})).To(Equal([]interface{}{"deadbeef", "decafbad"}))
-			header := ev["header"]
-			Expect(header).To(HaveKeyWithValue("packet_type", "version_negotiation"))
-			Expect(header).ToNot(HaveKey("packet_number"))
-			Expect(header).ToNot(HaveKey("version"))
-			Expect(header).To(HaveKeyWithValue("dcid", "0102030405060708"))
-			Expect(header).To(HaveKeyWithValue("scid", "04030201"))
-		})
-
-		It("records buffered packets", func() {
-			tracer.BufferedPacket(logging.PacketTypeHandshake, 1337)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_buffered"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveLen(1))
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "handshake"))
-			Expect(ev).To(HaveKey("raw"))
-			Expect(ev["raw"].(map[string]interface{})).To(HaveKeyWithValue("length", float64(1337)))
-			Expect(ev).To(HaveKeyWithValue("trigger", "keys_unavailable"))
-		})
-
-		It("records dropped packets", func() {
-			tracer.DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, 1337, logging.PacketDropPayloadDecryptError)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_dropped"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			Expect(ev["raw"].(map[string]interface{})).To(HaveKeyWithValue("length", float64(1337)))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveLen(1))
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "retry"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "payload_decrypt_error"))
-		})
-
-		It("records dropped packets with a packet number", func() {
-			tracer.DroppedPacket(logging.PacketTypeHandshake, 42, 1337, logging.PacketDropDuplicate)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_dropped"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			Expect(ev["raw"].(map[string]interface{})).To(HaveKeyWithValue("length", float64(1337)))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveLen(2))
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "handshake"))
-			Expect(hdr).To(HaveKeyWithValue("packet_number", float64(42)))
-			Expect(ev).To(HaveKeyWithValue("trigger", "duplicate"))
-		})
-
-		It("records metrics updates", func() {
-			now := time.Now()
-			rttStats := utils.NewRTTStats()
-			rttStats.UpdateRTT(15*time.Millisecond, 0, now)
-			rttStats.UpdateRTT(20*time.Millisecond, 0, now)
-			rttStats.UpdateRTT(25*time.Millisecond, 0, now)
-			Expect(rttStats.MinRTT()).To(Equal(15 * time.Millisecond))
-			Expect(rttStats.SmoothedRTT()).To(And(
-				BeNumerically(">", 15*time.Millisecond),
-				BeNumerically("<", 25*time.Millisecond),
-			))
-			Expect(rttStats.LatestRTT()).To(Equal(25 * time.Millisecond))
-			tracer.UpdatedMetrics(
-				rttStats,
-				4321,
-				1234,
-				42,
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:metrics_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("min_rtt", float64(15)))
-			Expect(ev).To(HaveKeyWithValue("latest_rtt", float64(25)))
-			Expect(ev).To(HaveKey("smoothed_rtt"))
-			Expect(time.Duration(ev["smoothed_rtt"].(float64)) * time.Millisecond).To(BeNumerically("~", rttStats.SmoothedRTT(), time.Millisecond))
-			Expect(ev).To(HaveKey("rtt_variance"))
-			Expect(time.Duration(ev["rtt_variance"].(float64)) * time.Millisecond).To(BeNumerically("~", rttStats.MeanDeviation(), time.Millisecond))
-			Expect(ev).To(HaveKeyWithValue("congestion_window", float64(4321)))
-			Expect(ev).To(HaveKeyWithValue("bytes_in_flight", float64(1234)))
-			Expect(ev).To(HaveKeyWithValue("packets_in_flight", float64(42)))
-		})
-
-		It("only logs the diff between two metrics updates", func() {
-			now := time.Now()
-			rttStats := utils.NewRTTStats()
-			rttStats.UpdateRTT(15*time.Millisecond, 0, now)
-			rttStats.UpdateRTT(20*time.Millisecond, 0, now)
-			rttStats.UpdateRTT(25*time.Millisecond, 0, now)
-			Expect(rttStats.MinRTT()).To(Equal(15 * time.Millisecond))
-
-			rttStats2 := utils.NewRTTStats()
-			rttStats2.UpdateRTT(15*time.Millisecond, 0, now)
-			rttStats2.UpdateRTT(15*time.Millisecond, 0, now)
-			rttStats2.UpdateRTT(15*time.Millisecond, 0, now)
-			Expect(rttStats2.MinRTT()).To(Equal(15 * time.Millisecond))
-
-			Expect(rttStats.LatestRTT()).To(Equal(25 * time.Millisecond))
-			tracer.UpdatedMetrics(
-				rttStats,
-				4321,
-				1234,
-				42,
-			)
-			tracer.UpdatedMetrics(
-				rttStats2,
-				4321,
-				12345, // changed
-				42,
-			)
-			tracer.Close()
-			entries := exportAndParse(buf)
-			Expect(entries).To(HaveLen(2))
-			Expect(entries[0].Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entries[0].Name).To(Equal("recovery:metrics_updated"))
-			Expect(entries[0].Event).To(HaveLen(7))
-			Expect(entries[1].Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entries[1].Name).To(Equal("recovery:metrics_updated"))
-			ev := entries[1].Event
-			Expect(ev).ToNot(HaveKey("min_rtt"))
-			Expect(ev).ToNot(HaveKey("congestion_window"))
-			Expect(ev).ToNot(HaveKey("packets_in_flight"))
-			Expect(ev).To(HaveKeyWithValue("bytes_in_flight", float64(12345)))
-			Expect(ev).To(HaveKeyWithValue("smoothed_rtt", float64(15)))
-		})
-
-		It("records lost packets", func() {
-			tracer.LostPacket(protocol.EncryptionHandshake, 42, logging.PacketLossReorderingThreshold)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:packet_lost"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveLen(2))
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "handshake"))
-			Expect(hdr).To(HaveKeyWithValue("packet_number", float64(42)))
-			Expect(ev).To(HaveKeyWithValue("trigger", "reordering_threshold"))
-		})
-
-		It("records MTU discovery updates", func() {
-			tracer.UpdatedMTU(1337, true)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:mtu_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("mtu", float64(1337)))
-			Expect(ev).To(HaveKeyWithValue("done", true))
-		})
-
-		It("records congestion state updates", func() {
-			tracer.UpdatedCongestionState(logging.CongestionStateCongestionAvoidance)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:congestion_state_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("new", "congestion_avoidance"))
-		})
-
-		It("records PTO changes", func() {
-			tracer.UpdatedPTOCount(42)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:metrics_updated"))
-			Expect(entry.Event).To(HaveKeyWithValue("pto_count", float64(42)))
-		})
-
-		It("records TLS key updates", func() {
-			tracer.UpdatedKeyFromTLS(protocol.EncryptionHandshake, protocol.PerspectiveClient)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("security:key_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("key_type", "client_handshake_secret"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "tls"))
-			Expect(ev).ToNot(HaveKey("key_phase"))
-			Expect(ev).ToNot(HaveKey("old"))
-			Expect(ev).ToNot(HaveKey("new"))
-		})
-
-		It("records TLS key updates, for 1-RTT keys", func() {
-			tracer.UpdatedKeyFromTLS(protocol.Encryption1RTT, protocol.PerspectiveServer)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("security:key_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("key_type", "server_1rtt_secret"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "tls"))
-			Expect(ev).To(HaveKeyWithValue("key_phase", float64(0)))
-			Expect(ev).ToNot(HaveKey("old"))
-			Expect(ev).ToNot(HaveKey("new"))
-		})
-
-		It("records QUIC key updates", func() {
-			tracer.UpdatedKey(1337, true)
-			tracer.Close()
-			entries := exportAndParse(buf)
-			Expect(entries).To(HaveLen(2))
-			var keyTypes []string
-			for _, entry := range entries {
-				Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-				Expect(entry.Name).To(Equal("security:key_updated"))
-				ev := entry.Event
-				Expect(ev).To(HaveKeyWithValue("key_phase", float64(1337)))
-				Expect(ev).To(HaveKeyWithValue("trigger", "remote_update"))
-				Expect(ev).To(HaveKey("key_type"))
-				keyTypes = append(keyTypes, ev["key_type"].(string))
-			}
-			Expect(keyTypes).To(ContainElement("server_1rtt_secret"))
-			Expect(keyTypes).To(ContainElement("client_1rtt_secret"))
-		})
-
-		It("records dropped encryption levels", func() {
-			tracer.DroppedEncryptionLevel(protocol.EncryptionInitial)
-			tracer.Close()
-			entries := exportAndParse(buf)
-			Expect(entries).To(HaveLen(2))
-			var keyTypes []string
-			for _, entry := range entries {
-				Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-				Expect(entry.Name).To(Equal("security:key_discarded"))
-				ev := entry.Event
-				Expect(ev).To(HaveKeyWithValue("trigger", "tls"))
-				Expect(ev).To(HaveKey("key_type"))
-				keyTypes = append(keyTypes, ev["key_type"].(string))
-			}
-			Expect(keyTypes).To(ContainElement("server_initial_secret"))
-			Expect(keyTypes).To(ContainElement("client_initial_secret"))
-		})
-
-		It("records dropped 0-RTT keys", func() {
-			tracer.DroppedEncryptionLevel(protocol.Encryption0RTT)
-			tracer.Close()
-			entries := exportAndParse(buf)
-			Expect(entries).To(HaveLen(1))
-			entry := entries[0]
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("security:key_discarded"))
-			ev := entry.Event
-			Expect(ev).To(HaveKeyWithValue("trigger", "tls"))
-			Expect(ev).To(HaveKeyWithValue("key_type", "server_0rtt_secret"))
-		})
-
-		It("records dropped keys", func() {
-			tracer.DroppedKey(42)
-			tracer.Close()
-			entries := exportAndParse(buf)
-			Expect(entries).To(HaveLen(2))
-			var keyTypes []string
-			for _, entry := range entries {
-				Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-				Expect(entry.Name).To(Equal("security:key_discarded"))
-				ev := entry.Event
-				Expect(ev).To(HaveKeyWithValue("key_phase", float64(42)))
-				Expect(ev).ToNot(HaveKey("trigger"))
-				Expect(ev).To(HaveKey("key_type"))
-				keyTypes = append(keyTypes, ev["key_type"].(string))
-			}
-			Expect(keyTypes).To(ContainElement("server_1rtt_secret"))
-			Expect(keyTypes).To(ContainElement("client_1rtt_secret"))
-		})
-
-		It("records when the timer is set", func() {
-			timeout := time.Now().Add(137 * time.Millisecond)
-			tracer.SetLossTimer(logging.TimerTypePTO, protocol.EncryptionHandshake, timeout)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:loss_timer_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(4))
-			Expect(ev).To(HaveKeyWithValue("event_type", "set"))
-			Expect(ev).To(HaveKeyWithValue("timer_type", "pto"))
-			Expect(ev).To(HaveKeyWithValue("packet_number_space", "handshake"))
-			Expect(ev).To(HaveKey("delta"))
-			delta := time.Duration(ev["delta"].(float64)*1e6) * time.Nanosecond
-			Expect(entry.Time.Add(delta)).To(BeTemporally("~", timeout, scaleDuration(10*time.Microsecond)))
-		})
-
-		It("records when the loss timer expires", func() {
-			tracer.LossTimerExpired(logging.TimerTypeACK, protocol.Encryption1RTT)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:loss_timer_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(3))
-			Expect(ev).To(HaveKeyWithValue("event_type", "expired"))
-			Expect(ev).To(HaveKeyWithValue("timer_type", "ack"))
-			Expect(ev).To(HaveKeyWithValue("packet_number_space", "application_data"))
-		})
-
-		It("records when the timer is canceled", func() {
-			tracer.LossTimerCanceled()
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:loss_timer_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(1))
-			Expect(ev).To(HaveKeyWithValue("event_type", "cancelled"))
-		})
-
-		It("records an ECN state transition, without a trigger", func() {
-			tracer.ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:ecn_state_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(1))
-			Expect(ev).To(HaveKeyWithValue("new", "unknown"))
-		})
-
-		It("records an ECN state transition, with a trigger", func() {
-			tracer.ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedNoECNCounts)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("recovery:ecn_state_updated"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(2))
-			Expect(ev).To(HaveKeyWithValue("new", "failed"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "ACK doesn't contain ECN marks"))
-		})
-
-		It("records a generic event", func() {
-			tracer.Debug("foo", "bar")
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:foo"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(1))
-			Expect(ev).To(HaveKeyWithValue("details", "bar"))
-		})
+func TestTransportErrors(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ClosedConnection(&quic.TransportError{
+		ErrorCode:    qerr.AEADLimitReached,
+		ErrorMessage: "foobar",
+	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:connection_closed", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 3)
+	require.Equal(t, "local", ev["owner"])
+	require.Equal(t, "aead_limit_reached", ev["connection_code"])
+	require.Equal(t, "foobar", ev["reason"])
+}
+
+func TestSentTransportParameters(t *testing.T) {
+	rcid := protocol.ParseConnectionID([]byte{0xde, 0xca, 0xfb, 0xad})
+	tracer, buf := newConnectionTracer()
+	tracer.SentTransportParameters(&logging.TransportParameters{
+		InitialMaxStreamDataBidiLocal:   1000,
+		InitialMaxStreamDataBidiRemote:  2000,
+		InitialMaxStreamDataUni:         3000,
+		InitialMaxData:                  4000,
+		MaxBidiStreamNum:                10,
+		MaxUniStreamNum:                 20,
+		MaxAckDelay:                     123 * time.Millisecond,
+		AckDelayExponent:                12,
+		DisableActiveMigration:          true,
+		MaxUDPPayloadSize:               1234,
+		MaxIdleTimeout:                  321 * time.Millisecond,
+		StatelessResetToken:             &protocol.StatelessResetToken{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00},
+		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde}),
+		InitialSourceConnectionID:       protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
+		RetrySourceConnectionID:         &rcid,
+		ActiveConnectionIDLimit:         7,
+		MaxDatagramFrameSize:            protocol.InvalidByteCount,
+	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_set", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "local", ev["owner"])
+	require.Equal(t, "deadc0de", ev["original_destination_connection_id"])
+	require.Equal(t, "deadbeef", ev["initial_source_connection_id"])
+	require.Equal(t, "decafbad", ev["retry_source_connection_id"])
+	require.Equal(t, "112233445566778899aabbccddeeff00", ev["stateless_reset_token"])
+	require.Equal(t, float64(321), ev["max_idle_timeout"])
+	require.Equal(t, float64(1234), ev["max_udp_payload_size"])
+	require.Equal(t, float64(12), ev["ack_delay_exponent"])
+	require.Equal(t, float64(7), ev["active_connection_id_limit"])
+	require.Equal(t, float64(4000), ev["initial_max_data"])
+	require.Equal(t, float64(1000), ev["initial_max_stream_data_bidi_local"])
+	require.Equal(t, float64(2000), ev["initial_max_stream_data_bidi_remote"])
+	require.Equal(t, float64(3000), ev["initial_max_stream_data_uni"])
+	require.Equal(t, float64(10), ev["initial_max_streams_bidi"])
+	require.Equal(t, float64(20), ev["initial_max_streams_uni"])
+	require.NotContains(t, ev, "preferred_address")
+	require.NotContains(t, ev, "max_datagram_frame_size")
+}
+
+func TestServerTransportParametersWithoutStatelessResetToken(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.SentTransportParameters(&logging.TransportParameters{
+		OriginalDestinationConnectionID: protocol.ParseConnectionID([]byte{0xde, 0xad, 0xc0, 0xde}),
+		ActiveConnectionIDLimit:         7,
+	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_set", entry.Name)
+	ev := entry.Event
+	require.NotContains(t, ev, "stateless_reset_token")
+}
+
+func TestTransportParametersWithoutRetrySourceConnectionID(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.SentTransportParameters(&logging.TransportParameters{
+		StatelessResetToken: &protocol.StatelessResetToken{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00},
+	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_set", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "local", ev["owner"])
+	require.NotContains(t, ev, "retry_source_connection_id")
+}
+
+func TestTransportParametersWithPreferredAddress(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.SentTransportParameters(&logging.TransportParameters{
+		PreferredAddress: &logging.PreferredAddress{
+			IPv4:                netip.AddrPortFrom(netip.AddrFrom4([4]byte{12, 34, 56, 78}), 123),
+			IPv6:                netip.AddrPortFrom(netip.AddrFrom16([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}), 456),
+			ConnectionID:        protocol.ParseConnectionID([]byte{8, 7, 6, 5, 4, 3, 2, 1}),
+			StatelessResetToken: protocol.StatelessResetToken{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
+		},
+	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_set", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "local", ev["owner"])
+	require.Contains(t, ev, "preferred_address")
+	pa := ev["preferred_address"].(map[string]interface{})
+	require.Equal(t, "12.34.56.78", pa["ip_v4"])
+	require.Equal(t, float64(123), pa["port_v4"])
+	require.Equal(t, "102:304:506:708:90a:b0c:d0e:f10", pa["ip_v6"])
+	require.Equal(t, float64(456), pa["port_v6"])
+	require.Equal(t, "0807060504030201", pa["connection_id"])
+	require.Equal(t, "0f0e0d0c0b0a09080706050403020100", pa["stateless_reset_token"])
+}
+
+func TestTransportParametersWithDatagramExtension(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.SentTransportParameters(&logging.TransportParameters{
+		MaxDatagramFrameSize: 1337,
+	})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_set", entry.Name)
+	ev := entry.Event
+	require.Equal(t, float64(1337), ev["max_datagram_frame_size"])
+}
+
+func TestReceivedTransportParameters(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ReceivedTransportParameters(&logging.TransportParameters{})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_set", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "remote", ev["owner"])
+	require.NotContains(t, ev, "original_destination_connection_id")
+}
+
+func TestRestoredTransportParameters(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.RestoredTransportParameters(&logging.TransportParameters{
+		InitialMaxStreamDataBidiLocal:  100,
+		InitialMaxStreamDataBidiRemote: 200,
+		InitialMaxStreamDataUni:        300,
+		InitialMaxData:                 400,
+		MaxIdleTimeout:                 123 * time.Millisecond,
 	})
-})
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:parameters_restored", entry.Name)
+	ev := entry.Event
+	require.NotContains(t, ev, "owner")
+	require.NotContains(t, ev, "original_destination_connection_id")
+	require.NotContains(t, ev, "stateless_reset_token")
+	require.NotContains(t, ev, "retry_source_connection_id")
+	require.NotContains(t, ev, "initial_source_connection_id")
+	require.Equal(t, float64(123), ev["max_idle_timeout"])
+	require.Equal(t, float64(400), ev["initial_max_data"])
+	require.Equal(t, float64(100), ev["initial_max_stream_data_bidi_local"])
+	require.Equal(t, float64(200), ev["initial_max_stream_data_bidi_remote"])
+	require.Equal(t, float64(300), ev["initial_max_stream_data_uni"])
+}
+
+func TestSentLongHeaderPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.SentLongHeaderPacket(
+		&logging.ExtendedHeader{
+			Header: logging.Header{
+				Type:             protocol.PacketTypeHandshake,
+				DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+				SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
+				Length:           1337,
+				Version:          protocol.Version1,
+			},
+			PacketNumber: 1337,
+		},
+		987,
+		logging.ECNCE,
+		nil,
+		[]logging.Frame{
+			&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
+			&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
+		},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_sent", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	raw := ev["raw"].(map[string]interface{})
+	require.Equal(t, float64(987), raw["length"])
+	require.Equal(t, float64(1337), raw["payload_length"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Equal(t, "handshake", hdr["packet_type"])
+	require.Equal(t, float64(1337), hdr["packet_number"])
+	require.Equal(t, "04030201", hdr["scid"])
+	require.Contains(t, ev, "frames")
+	require.Equal(t, "CE", ev["ecn"])
+	frames := ev["frames"].([]interface{})
+	require.Len(t, frames, 2)
+	require.Equal(t, "max_stream_data", frames[0].(map[string]interface{})["frame_type"])
+	require.Equal(t, "stream", frames[1].(map[string]interface{})["frame_type"])
+}
+
+func TestSentShortHeaderPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.SentShortHeaderPacket(
+		&logging.ShortHeader{
+			DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4}),
+			PacketNumber:     1337,
+		},
+		123,
+		logging.ECNUnsupported,
+		&logging.AckFrame{AckRanges: []logging.AckRange{{Smallest: 1, Largest: 10}}},
+		[]logging.Frame{&logging.MaxDataFrame{MaximumData: 987}},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	ev := entry.Event
+	raw := ev["raw"].(map[string]interface{})
+	require.Equal(t, float64(123), raw["length"])
+	require.NotContains(t, raw, "payload_length")
+	require.Contains(t, ev, "header")
+	require.NotContains(t, ev, "ecn")
+	hdr := ev["header"].(map[string]interface{})
+	require.Equal(t, "1RTT", hdr["packet_type"])
+	require.Equal(t, float64(1337), hdr["packet_number"])
+	require.Contains(t, ev, "frames")
+	frames := ev["frames"].([]interface{})
+	require.Len(t, frames, 2)
+	require.Equal(t, "ack", frames[0].(map[string]interface{})["frame_type"])
+	require.Equal(t, "max_data", frames[1].(map[string]interface{})["frame_type"])
+}
+
+func TestReceivedLongHeaderPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ReceivedLongHeaderPacket(
+		&logging.ExtendedHeader{
+			Header: logging.Header{
+				Type:             protocol.PacketTypeInitial,
+				DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+				SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
+				Token:            []byte{0xde, 0xad, 0xbe, 0xef},
+				Length:           1234,
+				Version:          protocol.Version1,
+			},
+			PacketNumber: 1337,
+		},
+		789,
+		logging.ECT0,
+		[]logging.Frame{
+			&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
+			&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
+		},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_received", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	raw := ev["raw"].(map[string]interface{})
+	require.Equal(t, float64(789), raw["length"])
+	require.Equal(t, float64(1234), raw["payload_length"])
+	require.Equal(t, "ECT(0)", ev["ecn"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Equal(t, "initial", hdr["packet_type"])
+	require.Equal(t, float64(1337), hdr["packet_number"])
+	require.Equal(t, "04030201", hdr["scid"])
+	require.Contains(t, hdr, "token")
+	token := hdr["token"].(map[string]interface{})
+	require.Equal(t, "deadbeef", token["data"])
+	require.Contains(t, ev, "frames")
+	require.Len(t, ev["frames"].([]interface{}), 2)
+}
+
+func TestReceivedShortHeaderPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	shdr := &logging.ShortHeader{
+		DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+		PacketNumber:     1337,
+		PacketNumberLen:  protocol.PacketNumberLen3,
+		KeyPhase:         protocol.KeyPhaseZero,
+	}
+	tracer.ReceivedShortHeaderPacket(
+		shdr,
+		789,
+		logging.ECT1,
+		[]logging.Frame{
+			&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
+			&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
+		},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_received", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	raw := ev["raw"].(map[string]interface{})
+	require.Equal(t, float64(789), raw["length"])
+	require.Equal(t, float64(789-(1+8+3)), raw["payload_length"])
+	require.Equal(t, "ECT(1)", ev["ecn"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Equal(t, "1RTT", hdr["packet_type"])
+	require.Equal(t, float64(1337), hdr["packet_number"])
+	require.Equal(t, "0", hdr["key_phase_bit"])
+	require.Contains(t, ev, "frames")
+	require.Len(t, ev["frames"].([]interface{}), 2)
+}
+
+func TestReceivedRetryPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ReceivedRetry(
+		&logging.Header{
+			Type:             protocol.PacketTypeRetry,
+			DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+			SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
+			Token:            []byte{0xde, 0xad, 0xbe, 0xef},
+			Version:          protocol.Version1,
+		},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_received", entry.Name)
+	ev := entry.Event
+	require.NotContains(t, ev, "raw")
+	require.Contains(t, ev, "header")
+	header := ev["header"].(map[string]interface{})
+	require.Equal(t, "retry", header["packet_type"])
+	require.NotContains(t, header, "packet_number")
+	require.Contains(t, header, "version")
+	require.Contains(t, header, "dcid")
+	require.Contains(t, header, "scid")
+	require.Contains(t, header, "token")
+	token := header["token"].(map[string]interface{})
+	require.Equal(t, "deadbeef", token["data"])
+	require.NotContains(t, ev, "frames")
+}
+
+func TestReceivedVersionNegotiationPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ReceivedVersionNegotiationPacket(
+		protocol.ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8},
+		protocol.ArbitraryLenConnectionID{4, 3, 2, 1},
+		[]protocol.Version{0xdeadbeef, 0xdecafbad},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_received", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "header")
+	require.NotContains(t, ev, "frames")
+	require.Contains(t, ev, "supported_versions")
+	require.Equal(t, []interface{}{"deadbeef", "decafbad"}, ev["supported_versions"])
+	header := ev["header"].(map[string]interface{})
+	require.Equal(t, "version_negotiation", header["packet_type"])
+	require.NotContains(t, header, "packet_number")
+	require.NotContains(t, header, "version")
+	require.Equal(t, "0102030405060708", header["dcid"])
+	require.Equal(t, "04030201", header["scid"])
+}
+
+func TestBufferedPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.BufferedPacket(logging.PacketTypeHandshake, 1337)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_buffered", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Len(t, hdr, 1)
+	require.Equal(t, "handshake", hdr["packet_type"])
+	require.Contains(t, ev, "raw")
+	require.Equal(t, float64(1337), ev["raw"].(map[string]interface{})["length"])
+	require.Equal(t, "keys_unavailable", ev["trigger"])
+}
+
+func TestDroppedPacket(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.DroppedPacket(logging.PacketTypeRetry, protocol.InvalidPacketNumber, 1337, logging.PacketDropPayloadDecryptError)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_dropped", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	require.Equal(t, float64(1337), ev["raw"].(map[string]interface{})["length"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Len(t, hdr, 1)
+	require.Equal(t, "retry", hdr["packet_type"])
+	require.Equal(t, "payload_decrypt_error", ev["trigger"])
+}
+
+func TestDroppedPacketWithPacketNumber(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.DroppedPacket(logging.PacketTypeHandshake, 42, 1337, logging.PacketDropDuplicate)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_dropped", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	require.Equal(t, float64(1337), ev["raw"].(map[string]interface{})["length"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Len(t, hdr, 2)
+	require.Equal(t, "handshake", hdr["packet_type"])
+	require.Equal(t, float64(42), hdr["packet_number"])
+	require.Equal(t, "duplicate", ev["trigger"])
+}
+
+func TestUpdatedMetrics(t *testing.T) {
+	now := time.Now()
+	rttStats := utils.NewRTTStats()
+	rttStats.UpdateRTT(15*time.Millisecond, 0, now)
+	rttStats.UpdateRTT(20*time.Millisecond, 0, now)
+	rttStats.UpdateRTT(25*time.Millisecond, 0, now)
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedMetrics(
+		rttStats,
+		4321,
+		1234,
+		42,
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:metrics_updated", entry.Name)
+	ev := entry.Event
+	require.Equal(t, float64(15), ev["min_rtt"])
+	require.Equal(t, float64(25), ev["latest_rtt"])
+	require.Contains(t, ev, "smoothed_rtt")
+	require.InDelta(t, rttStats.SmoothedRTT().Milliseconds(), ev["smoothed_rtt"], float64(1))
+	require.Contains(t, ev, "rtt_variance")
+	require.InDelta(t, rttStats.MeanDeviation().Milliseconds(), ev["rtt_variance"], float64(1))
+	require.Equal(t, float64(4321), ev["congestion_window"])
+	require.Equal(t, float64(1234), ev["bytes_in_flight"])
+	require.Equal(t, float64(42), ev["packets_in_flight"])
+}
+
+func TestDiffForOnlyChangedMetrics(t *testing.T) {
+	now := time.Now()
+	rttStats := utils.NewRTTStats()
+	rttStats.UpdateRTT(15*time.Millisecond, 0, now)
+	rttStats.UpdateRTT(20*time.Millisecond, 0, now)
+	rttStats.UpdateRTT(25*time.Millisecond, 0, now)
+
+	rttStats2 := utils.NewRTTStats()
+	rttStats2.UpdateRTT(15*time.Millisecond, 0, now)
+	rttStats2.UpdateRTT(15*time.Millisecond, 0, now)
+	rttStats2.UpdateRTT(15*time.Millisecond, 0, now)
+
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedMetrics(
+		rttStats,
+		4321,
+		1234,
+		42,
+	)
+	tracer.UpdatedMetrics(
+		rttStats2,
+		4321,
+		12345, // changed
+		42,
+	)
+	tracer.Close()
+	entries := exportAndParse(t, buf)
+	require.Len(t, entries, 2)
+	require.WithinDuration(t, time.Now(), entries[0].Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:metrics_updated", entries[0].Name)
+	require.Len(t, entries[0].Event, 7)
+	require.WithinDuration(t, time.Now(), entries[1].Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:metrics_updated", entries[1].Name)
+	ev := entries[1].Event
+	require.NotContains(t, ev, "min_rtt")
+	require.NotContains(t, ev, "congestion_window")
+	require.NotContains(t, ev, "packets_in_flight")
+	require.Equal(t, float64(12345), ev["bytes_in_flight"])
+	require.Equal(t, float64(15), ev["smoothed_rtt"])
+}
+
+func TestLostPackets(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.LostPacket(protocol.EncryptionHandshake, 42, logging.PacketLossReorderingThreshold)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:packet_lost", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Len(t, hdr, 2)
+	require.Equal(t, "handshake", hdr["packet_type"])
+	require.Equal(t, float64(42), hdr["packet_number"])
+	require.Equal(t, "reordering_threshold", ev["trigger"])
+}
+
+func TestMTUUpdates(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedMTU(1337, true)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:mtu_updated", entry.Name)
+	ev := entry.Event
+	require.Equal(t, float64(1337), ev["mtu"])
+	require.Equal(t, true, ev["done"])
+}
+
+func TestCongestionStateUpdates(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedCongestionState(logging.CongestionStateCongestionAvoidance)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:congestion_state_updated", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "congestion_avoidance", ev["new"])
+}
+
+func TestPTOChanges(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedPTOCount(42)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:metrics_updated", entry.Name)
+	require.Equal(t, float64(42), entry.Event["pto_count"])
+}
+
+func TestTLSKeyUpdates(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedKeyFromTLS(protocol.EncryptionHandshake, protocol.PerspectiveClient)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "security:key_updated", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "client_handshake_secret", ev["key_type"])
+	require.Equal(t, "tls", ev["trigger"])
+	require.NotContains(t, ev, "key_phase")
+	require.NotContains(t, ev, "old")
+	require.NotContains(t, ev, "new")
+}
+
+func TestTLSKeyUpdatesFor1RTTKeys(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedKeyFromTLS(protocol.Encryption1RTT, protocol.PerspectiveServer)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "security:key_updated", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "server_1rtt_secret", ev["key_type"])
+	require.Equal(t, "tls", ev["trigger"])
+	require.Equal(t, float64(0), ev["key_phase"])
+	require.NotContains(t, ev, "old")
+	require.NotContains(t, ev, "new")
+}
+
+func TestQUICKeyUpdates(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.UpdatedKey(1337, true)
+	tracer.Close()
+	entries := exportAndParse(t, buf)
+	require.Len(t, entries, 2)
+	var keyTypes []string
+	for _, entry := range entries {
+		require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+		require.Equal(t, "security:key_updated", entry.Name)
+		ev := entry.Event
+		require.Equal(t, float64(1337), ev["key_phase"])
+		require.Equal(t, "remote_update", ev["trigger"])
+		require.Contains(t, ev, "key_type")
+		keyTypes = append(keyTypes, ev["key_type"].(string))
+	}
+	require.Contains(t, keyTypes, "server_1rtt_secret")
+	require.Contains(t, keyTypes, "client_1rtt_secret")
+}
+
+func TestDroppedEncryptionLevels(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.DroppedEncryptionLevel(protocol.EncryptionInitial)
+	tracer.Close()
+	entries := exportAndParse(t, buf)
+	require.Len(t, entries, 2)
+	var keyTypes []string
+	for _, entry := range entries {
+		require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+		require.Equal(t, "security:key_discarded", entry.Name)
+		ev := entry.Event
+		require.Equal(t, "tls", ev["trigger"])
+		require.Contains(t, ev, "key_type")
+		keyTypes = append(keyTypes, ev["key_type"].(string))
+	}
+	require.Contains(t, keyTypes, "server_initial_secret")
+	require.Contains(t, keyTypes, "client_initial_secret")
+}
+
+func TestDropped0RTTKeys(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.DroppedEncryptionLevel(protocol.Encryption0RTT)
+	tracer.Close()
+	entries := exportAndParse(t, buf)
+	require.Len(t, entries, 1)
+	entry := entries[0]
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "security:key_discarded", entry.Name)
+	ev := entry.Event
+	require.Equal(t, "tls", ev["trigger"])
+	require.Equal(t, "server_0rtt_secret", ev["key_type"])
+}
+
+func TestDroppedKeys(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.DroppedKey(42)
+	tracer.Close()
+	entries := exportAndParse(t, buf)
+	require.Len(t, entries, 2)
+	var keyTypes []string
+	for _, entry := range entries {
+		require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+		require.Equal(t, "security:key_discarded", entry.Name)
+		ev := entry.Event
+		require.Equal(t, float64(42), ev["key_phase"])
+		require.NotContains(t, ev, "trigger")
+		require.Contains(t, ev, "key_type")
+		keyTypes = append(keyTypes, ev["key_type"].(string))
+	}
+	require.Contains(t, keyTypes, "server_1rtt_secret")
+	require.Contains(t, keyTypes, "client_1rtt_secret")
+}
+
+func TestSetLossTimer(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	timeout := time.Now().Add(137 * time.Millisecond)
+	tracer.SetLossTimer(logging.TimerTypePTO, protocol.EncryptionHandshake, timeout)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:loss_timer_updated", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 4)
+	require.Equal(t, "set", ev["event_type"])
+	require.Equal(t, "pto", ev["timer_type"])
+	require.Equal(t, "handshake", ev["packet_number_space"])
+	require.Contains(t, ev, "delta")
+	delta := time.Duration(ev["delta"].(float64)*1e6) * time.Nanosecond
+	require.WithinDuration(t, timeout, entry.Time.Add(delta), scaleDuration(10*time.Microsecond))
+}
+
+func TestExpiredLossTimer(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.LossTimerExpired(logging.TimerTypeACK, protocol.Encryption1RTT)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:loss_timer_updated", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 3)
+	require.Equal(t, "expired", ev["event_type"])
+	require.Equal(t, "ack", ev["timer_type"])
+	require.Equal(t, "application_data", ev["packet_number_space"])
+}
+
+func TestCanceledLossTimer(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.LossTimerCanceled()
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:loss_timer_updated", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 1)
+	require.Equal(t, "cancelled", ev["event_type"])
+}
+
+func TestECNStateTransitionWithoutTrigger(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:ecn_state_updated", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 1)
+	require.Equal(t, "unknown", ev["new"])
+}
+
+func TestECNStateTransitionWithTrigger(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedNoECNCounts)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "recovery:ecn_state_updated", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 2)
+	require.Equal(t, "failed", ev["new"])
+	require.Equal(t, "ACK doesn't contain ECN marks", ev["trigger"])
+}
+
+func TestGenericConnectionTracerEvent(t *testing.T) {
+	tracer, buf := newConnectionTracer()
+	tracer.Debug("foo", "bar")
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:foo", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 1)
+	require.Equal(t, "bar", ev["details"])
+}
diff --git a/qlog/event_test.go b/qlog/event_test.go
index c31a7860..f523660f 100644
--- a/qlog/event_test.go
+++ b/qlog/event_test.go
@@ -3,12 +3,11 @@ package qlog
 import (
 	"bytes"
 	"encoding/json"
+	"testing"
 	"time"
 
 	"github.com/francoispqt/gojay"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
 type mevent struct{}
@@ -20,24 +19,26 @@ func (mevent) Name() string                         { return "mevent" }
 func (mevent) IsNil() bool                          { return false }
 func (mevent) MarshalJSONObject(enc *gojay.Encoder) { enc.StringKey("event", "details") }
 
-var _ = Describe("Events", func() {
-	It("marshals the fields before the event details", func() {
-		buf := &bytes.Buffer{}
-		enc := gojay.NewEncoder(buf)
-		Expect(enc.Encode(event{
-			RelativeTime: 1337 * time.Microsecond,
-			eventDetails: mevent{},
-		})).To(Succeed())
-
-		var decoded interface{}
-		Expect(json.Unmarshal(buf.Bytes(), &decoded)).To(Succeed())
-		Expect(decoded).To(HaveLen(3))
-
-		Expect(decoded).To(HaveKeyWithValue("time", 1.337))
-		Expect(decoded).To(HaveKeyWithValue("name", "connectivity:mevent"))
-		Expect(decoded).To(HaveKey("data"))
-		data := decoded.(map[string]interface{})["data"].(map[string]interface{})
-		Expect(data).To(HaveLen(1))
-		Expect(data).To(HaveKeyWithValue("event", "details"))
+func TestEventMarshaling(t *testing.T) {
+	buf := &bytes.Buffer{}
+	enc := gojay.NewEncoder(buf)
+	err := enc.Encode(event{
+		RelativeTime: 1337 * time.Microsecond,
+		eventDetails: mevent{},
 	})
-})
+	require.NoError(t, err)
+
+	var decoded map[string]interface{}
+	err = json.Unmarshal(buf.Bytes(), &decoded)
+	require.NoError(t, err)
+	require.Len(t, decoded, 3)
+
+	require.Equal(t, 1.337, decoded["time"])
+	require.Equal(t, "connectivity:mevent", decoded["name"])
+	require.Contains(t, decoded, "data")
+
+	data, ok := decoded["data"].(map[string]interface{})
+	require.True(t, ok)
+	require.Len(t, data, 1)
+	require.Equal(t, "details", data["event"])
+}
diff --git a/qlog/frame_test.go b/qlog/frame_test.go
index 0cee3847..dc7e55f3 100644
--- a/qlog/frame_test.go
+++ b/qlog/frame_test.go
@@ -3,90 +3,89 @@ package qlog
 import (
 	"bytes"
 	"encoding/json"
+	"testing"
 	"time"
 
+	"github.com/francoispqt/gojay"
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
 	"github.com/quic-go/quic-go/logging"
-
-	"github.com/francoispqt/gojay"
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Frames", func() {
-	check := func(f logging.Frame, expected map[string]interface{}) {
-		buf := &bytes.Buffer{}
-		enc := gojay.NewEncoder(buf)
-		ExpectWithOffset(1, enc.Encode(frame{Frame: f})).To(Succeed())
-		data := buf.Bytes()
-		ExpectWithOffset(1, json.Valid(data)).To(BeTrue())
-		checkEncoding(data, expected)
-	}
+func check(t *testing.T, f logging.Frame, expected map[string]interface{}) {
+	buf := &bytes.Buffer{}
+	enc := gojay.NewEncoder(buf)
+	err := enc.Encode(frame{Frame: f})
+	require.NoError(t, err)
+	data := buf.Bytes()
+	require.True(t, json.Valid(data))
+	checkEncoding(t, data, expected)
+}
 
-	It("marshals PING frames", func() {
-		check(
-			&logging.PingFrame{},
-			map[string]interface{}{
-				"frame_type": "ping",
-			},
-		)
-	})
+func TestPingFrame(t *testing.T) {
+	check(t,
+		&logging.PingFrame{},
+		map[string]interface{}{
+			"frame_type": "ping",
+		},
+	)
+}
 
-	It("marshals ACK frames with a range acknowledging a single packet", func() {
-		check(
-			&logging.AckFrame{
+func TestAckFrame(t *testing.T) {
+	tests := []struct {
+		name     string
+		frame    *logging.AckFrame
+		expected map[string]interface{}
+	}{
+		{
+			name: "with delay and single packet range",
+			frame: &logging.AckFrame{
 				DelayTime: 86 * time.Millisecond,
 				AckRanges: []logging.AckRange{{Smallest: 120, Largest: 120}},
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type":   "ack",
 				"ack_delay":    86,
 				"acked_ranges": [][]float64{{120}},
 			},
-		)
-	})
-
-	It("marshals ACK frames without a delay", func() {
-		check(
-			&logging.AckFrame{
+		},
+		{
+			name: "without delay",
+			frame: &logging.AckFrame{
 				AckRanges: []logging.AckRange{{Smallest: 120, Largest: 120}},
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type":   "ack",
 				"acked_ranges": [][]float64{{120}},
 			},
-		)
-	})
-
-	It("marshals ACK frames with ECN counts", func() {
-		check(
-			&logging.AckFrame{
+		},
+		{
+			name: "with ECN counts",
+			frame: &logging.AckFrame{
 				AckRanges: []logging.AckRange{{Smallest: 120, Largest: 120}},
 				ECT0:      10,
 				ECT1:      100,
 				ECNCE:     1000,
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type":   "ack",
 				"acked_ranges": [][]float64{{120}},
 				"ect0":         10,
 				"ect1":         100,
 				"ce":           1000,
 			},
-		)
-	})
-
-	It("marshals ACK frames with a range acknowledging ranges of packets", func() {
-		check(
-			&logging.AckFrame{
+		},
+		{
+			name: "with multiple ranges",
+			frame: &logging.AckFrame{
 				DelayTime: 86 * time.Millisecond,
 				AckRanges: []logging.AckRange{
 					{Smallest: 5, Largest: 50},
 					{Smallest: 100, Largest: 120},
 				},
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type": "ack",
 				"ack_delay":  86,
 				"acked_ranges": [][]float64{
@@ -94,283 +93,311 @@ var _ = Describe("Frames", func() {
 					{100, 120},
 				},
 			},
-		)
-	})
+		},
+	}
 
-	It("marshals RESET_STREAM frames", func() {
-		check(
-			&logging.ResetStreamFrame{
-				StreamID:  987,
-				FinalSize: 1234,
-				ErrorCode: 42,
-			},
-			map[string]interface{}{
-				"frame_type": "reset_stream",
-				"stream_id":  987,
-				"error_code": 42,
-				"final_size": 1234,
-			},
-		)
-	})
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			check(t, tt.frame, tt.expected)
+		})
+	}
+}
 
-	It("marshals STOP_SENDING frames", func() {
-		check(
-			&logging.StopSendingFrame{
-				StreamID:  987,
-				ErrorCode: 42,
-			},
-			map[string]interface{}{
-				"frame_type": "stop_sending",
-				"stream_id":  987,
-				"error_code": 42,
-			},
-		)
-	})
+func TestResetStreamFrame(t *testing.T) {
+	check(t,
+		&logging.ResetStreamFrame{
+			StreamID:  987,
+			FinalSize: 1234,
+			ErrorCode: 42,
+		},
+		map[string]interface{}{
+			"frame_type": "reset_stream",
+			"stream_id":  987,
+			"error_code": 42,
+			"final_size": 1234,
+		},
+	)
+}
 
-	It("marshals CRYPTO frames", func() {
-		check(
-			&logging.CryptoFrame{
-				Offset: 1337,
-				Length: 6,
-			},
-			map[string]interface{}{
-				"frame_type": "crypto",
-				"offset":     1337,
-				"length":     6,
-			},
-		)
-	})
+func TestStopSendingFrame(t *testing.T) {
+	check(t,
+		&logging.StopSendingFrame{
+			StreamID:  987,
+			ErrorCode: 42,
+		},
+		map[string]interface{}{
+			"frame_type": "stop_sending",
+			"stream_id":  987,
+			"error_code": 42,
+		},
+	)
+}
 
-	It("marshals NEW_TOKEN frames", func() {
-		check(
-			&logging.NewTokenFrame{
-				Token: []byte{0xde, 0xad, 0xbe, 0xef},
-			},
-			map[string]interface{}{
-				"frame_type": "new_token",
-				"token":      map[string]interface{}{"data": "deadbeef"},
-			},
-		)
-	})
+func TestCryptoFrame(t *testing.T) {
+	check(t,
+		&logging.CryptoFrame{
+			Offset: 1337,
+			Length: 6,
+		},
+		map[string]interface{}{
+			"frame_type": "crypto",
+			"offset":     1337,
+			"length":     6,
+		},
+	)
+}
+
+func TestNewTokenFrame(t *testing.T) {
+	check(t,
+		&logging.NewTokenFrame{
+			Token: []byte{0xde, 0xad, 0xbe, 0xef},
+		},
+		map[string]interface{}{
+			"frame_type": "new_token",
+			"token":      map[string]interface{}{"data": "deadbeef"},
+		},
+	)
+}
 
-	It("marshals STREAM frames with FIN", func() {
-		check(
-			&logging.StreamFrame{
+func TestStreamFrame(t *testing.T) {
+	tests := []struct {
+		name     string
+		frame    *logging.StreamFrame
+		expected map[string]interface{}
+	}{
+		{
+			name: "with FIN",
+			frame: &logging.StreamFrame{
 				StreamID: 42,
 				Offset:   1337,
 				Fin:      true,
 				Length:   9876,
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type": "stream",
 				"stream_id":  42,
 				"offset":     1337,
 				"fin":        true,
 				"length":     9876,
 			},
-		)
-	})
-
-	It("marshals STREAM frames without FIN", func() {
-		check(
-			&logging.StreamFrame{
+		},
+		{
+			name: "without FIN",
+			frame: &logging.StreamFrame{
 				StreamID: 42,
 				Offset:   1337,
 				Length:   3,
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type": "stream",
 				"stream_id":  42,
 				"offset":     1337,
 				"length":     3,
 			},
-		)
-	})
+		},
+	}
 
-	It("marshals MAX_DATA frames", func() {
-		check(
-			&logging.MaxDataFrame{
-				MaximumData: 1337,
-			},
-			map[string]interface{}{
-				"frame_type": "max_data",
-				"maximum":    1337,
-			},
-		)
-	})
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			check(t, tt.frame, tt.expected)
+		})
+	}
+}
 
-	It("marshals MAX_STREAM_DATA frames", func() {
-		check(
-			&logging.MaxStreamDataFrame{
-				StreamID:          1234,
-				MaximumStreamData: 1337,
-			},
-			map[string]interface{}{
-				"frame_type": "max_stream_data",
-				"stream_id":  1234,
-				"maximum":    1337,
-			},
-		)
-	})
+func TestMaxDataFrame(t *testing.T) {
+	check(t,
+		&logging.MaxDataFrame{
+			MaximumData: 1337,
+		},
+		map[string]interface{}{
+			"frame_type": "max_data",
+			"maximum":    1337,
+		},
+	)
+}
 
-	It("marshals MAX_STREAMS frames", func() {
-		check(
-			&logging.MaxStreamsFrame{
-				Type:         protocol.StreamTypeBidi,
-				MaxStreamNum: 42,
-			},
-			map[string]interface{}{
-				"frame_type":  "max_streams",
-				"stream_type": "bidirectional",
-				"maximum":     42,
-			},
-		)
-	})
+func TestMaxStreamDataFrame(t *testing.T) {
+	check(t,
+		&logging.MaxStreamDataFrame{
+			StreamID:          1234,
+			MaximumStreamData: 1337,
+		},
+		map[string]interface{}{
+			"frame_type": "max_stream_data",
+			"stream_id":  1234,
+			"maximum":    1337,
+		},
+	)
+}
 
-	It("marshals DATA_BLOCKED frames", func() {
-		check(
-			&logging.DataBlockedFrame{
-				MaximumData: 1337,
-			},
-			map[string]interface{}{
-				"frame_type": "data_blocked",
-				"limit":      1337,
-			},
-		)
-	})
+func TestMaxStreamsFrame(t *testing.T) {
+	check(t,
+		&logging.MaxStreamsFrame{
+			Type:         protocol.StreamTypeBidi,
+			MaxStreamNum: 42,
+		},
+		map[string]interface{}{
+			"frame_type":  "max_streams",
+			"stream_type": "bidirectional",
+			"maximum":     42,
+		},
+	)
+}
 
-	It("marshals STREAM_DATA_BLOCKED frames", func() {
-		check(
-			&logging.StreamDataBlockedFrame{
-				StreamID:          42,
-				MaximumStreamData: 1337,
-			},
-			map[string]interface{}{
-				"frame_type": "stream_data_blocked",
-				"stream_id":  42,
-				"limit":      1337,
-			},
-		)
-	})
+func TestDataBlockedFrame(t *testing.T) {
+	check(t,
+		&logging.DataBlockedFrame{
+			MaximumData: 1337,
+		},
+		map[string]interface{}{
+			"frame_type": "data_blocked",
+			"limit":      1337,
+		},
+	)
+}
 
-	It("marshals STREAMS_BLOCKED frames", func() {
-		check(
-			&logging.StreamsBlockedFrame{
-				Type:        protocol.StreamTypeUni,
-				StreamLimit: 123,
-			},
-			map[string]interface{}{
-				"frame_type":  "streams_blocked",
-				"stream_type": "unidirectional",
-				"limit":       123,
-			},
-		)
-	})
+func TestStreamDataBlockedFrame(t *testing.T) {
+	check(t,
+		&logging.StreamDataBlockedFrame{
+			StreamID:          42,
+			MaximumStreamData: 1337,
+		},
+		map[string]interface{}{
+			"frame_type": "stream_data_blocked",
+			"stream_id":  42,
+			"limit":      1337,
+		},
+	)
+}
 
-	It("marshals NEW_CONNECTION_ID frames", func() {
-		check(
-			&logging.NewConnectionIDFrame{
-				SequenceNumber:      42,
-				RetirePriorTo:       24,
-				ConnectionID:        protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
-				StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf},
-			},
-			map[string]interface{}{
-				"frame_type":            "new_connection_id",
-				"sequence_number":       42,
-				"retire_prior_to":       24,
-				"length":                4,
-				"connection_id":         "deadbeef",
-				"stateless_reset_token": "000102030405060708090a0b0c0d0e0f",
-			},
-		)
-	})
+func TestStreamsBlockedFrame(t *testing.T) {
+	check(t,
+		&logging.StreamsBlockedFrame{
+			Type:        protocol.StreamTypeUni,
+			StreamLimit: 123,
+		},
+		map[string]interface{}{
+			"frame_type":  "streams_blocked",
+			"stream_type": "unidirectional",
+			"limit":       123,
+		},
+	)
+}
 
-	It("marshals RETIRE_CONNECTION_ID frames", func() {
-		check(
-			&logging.RetireConnectionIDFrame{
-				SequenceNumber: 1337,
-			},
-			map[string]interface{}{
-				"frame_type":      "retire_connection_id",
-				"sequence_number": 1337,
-			},
-		)
-	})
+func TestNewConnectionIDFrame(t *testing.T) {
+	check(t,
+		&logging.NewConnectionIDFrame{
+			SequenceNumber:      42,
+			RetirePriorTo:       24,
+			ConnectionID:        protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
+			StatelessResetToken: protocol.StatelessResetToken{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf},
+		},
+		map[string]interface{}{
+			"frame_type":            "new_connection_id",
+			"sequence_number":       42,
+			"retire_prior_to":       24,
+			"length":                4,
+			"connection_id":         "deadbeef",
+			"stateless_reset_token": "000102030405060708090a0b0c0d0e0f",
+		},
+	)
+}
 
-	It("marshals PATH_CHALLENGE frames", func() {
-		check(
-			&logging.PathChallengeFrame{
-				Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01},
-			},
-			map[string]interface{}{
-				"frame_type": "path_challenge",
-				"data":       "deadbeefcafec001",
-			},
-		)
-	})
+func TestRetireConnectionIDFrame(t *testing.T) {
+	check(t,
+		&logging.RetireConnectionIDFrame{
+			SequenceNumber: 1337,
+		},
+		map[string]interface{}{
+			"frame_type":      "retire_connection_id",
+			"sequence_number": 1337,
+		},
+	)
+}
 
-	It("marshals PATH_RESPONSE frames", func() {
-		check(
-			&logging.PathResponseFrame{
-				Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01},
-			},
-			map[string]interface{}{
-				"frame_type": "path_response",
-				"data":       "deadbeefcafec001",
-			},
-		)
-	})
+func TestPathChallengeFrame(t *testing.T) {
+	check(t,
+		&logging.PathChallengeFrame{
+			Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01},
+		},
+		map[string]interface{}{
+			"frame_type": "path_challenge",
+			"data":       "deadbeefcafec001",
+		},
+	)
+}
+
+func TestPathResponseFrame(t *testing.T) {
+	check(t,
+		&logging.PathResponseFrame{
+			Data: [8]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xc0, 0x01},
+		},
+		map[string]interface{}{
+			"frame_type": "path_response",
+			"data":       "deadbeefcafec001",
+		},
+	)
+}
 
-	It("marshals CONNECTION_CLOSE frames, for application error codes", func() {
-		check(
-			&logging.ConnectionCloseFrame{
+func TestConnectionCloseFrame(t *testing.T) {
+	tests := []struct {
+		name     string
+		frame    *logging.ConnectionCloseFrame
+		expected map[string]interface{}
+	}{
+		{
+			name: "application error code",
+			frame: &logging.ConnectionCloseFrame{
 				IsApplicationError: true,
 				ErrorCode:          1337,
 				ReasonPhrase:       "lorem ipsum",
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type":     "connection_close",
 				"error_space":    "application",
 				"error_code":     1337,
 				"raw_error_code": 1337,
 				"reason":         "lorem ipsum",
 			},
-		)
-	})
-
-	It("marshals CONNECTION_CLOSE frames, for transport error codes", func() {
-		check(
-			&logging.ConnectionCloseFrame{
+		},
+		{
+			name: "transport error code",
+			frame: &logging.ConnectionCloseFrame{
 				ErrorCode:    uint64(qerr.FlowControlError),
 				ReasonPhrase: "lorem ipsum",
 			},
-			map[string]interface{}{
+			expected: map[string]interface{}{
 				"frame_type":     "connection_close",
 				"error_space":    "transport",
 				"error_code":     "flow_control_error",
 				"raw_error_code": int(qerr.FlowControlError),
 				"reason":         "lorem ipsum",
 			},
-		)
-	})
+		},
+	}
 
-	It("marshals HANDSHAKE_DONE frames", func() {
-		check(
-			&logging.HandshakeDoneFrame{},
-			map[string]interface{}{
-				"frame_type": "handshake_done",
-			},
-		)
-	})
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			check(t, tt.frame, tt.expected)
+		})
+	}
+}
 
-	It("marshals DATAGRAM frames", func() {
-		check(
-			&logging.DatagramFrame{Length: 1337},
-			map[string]interface{}{
-				"frame_type": "datagram",
-				"length":     1337,
-			},
-		)
-	})
-})
+func TestHandshakeDoneFrame(t *testing.T) {
+	check(t,
+		&logging.HandshakeDoneFrame{},
+		map[string]interface{}{
+			"frame_type": "handshake_done",
+		},
+	)
+}
+
+func TestDatagramFrame(t *testing.T) {
+	check(t,
+		&logging.DatagramFrame{Length: 1337},
+		map[string]interface{}{
+			"frame_type": "datagram",
+			"length":     1337,
+		},
+	)
+}
diff --git a/qlog/json_helper_test.go b/qlog/json_helper_test.go
new file mode 100644
index 00000000..d63cb41c
--- /dev/null
+++ b/qlog/json_helper_test.go
@@ -0,0 +1,107 @@
+package qlog
+
+import (
+	"bytes"
+	"encoding/json"
+	"os"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+//nolint:unparam
+func scaleDuration(t time.Duration) time.Duration {
+	scaleFactor := 1
+	if f, err := strconv.Atoi(os.Getenv("TIMESCALE_FACTOR")); err == nil { // parsing "" errors, so this works fine if the env is not set
+		scaleFactor = f
+	}
+	if scaleFactor == 0 {
+		panic("TIMESCALE_FACTOR must not be 0")
+	}
+	return time.Duration(scaleFactor) * t
+}
+
+func unmarshal(data []byte, v interface{}) error {
+	if data[0] == recordSeparator {
+		data = data[1:]
+	}
+	return json.Unmarshal(data, v)
+}
+
+func checkEncoding(t *testing.T, data []byte, expected map[string]interface{}) {
+	m := make(map[string]interface{})
+	require.NoError(t, json.Unmarshal(data, &m))
+	require.Len(t, m, len(expected))
+
+	for key, value := range expected {
+		switch v := value.(type) {
+		case bool, string, map[string]interface{}:
+			require.Equal(t, v, m[key])
+		case int:
+			require.Equal(t, float64(v), m[key])
+		case [][]float64: // used in the ACK frame
+			require.Contains(t, m, key)
+			outerSlice, ok := m[key].([]interface{})
+			require.True(t, ok)
+			require.Len(t, outerSlice, len(v))
+			for i, innerExpected := range v {
+				innerSlice, ok := outerSlice[i].([]interface{})
+				require.True(t, ok)
+				require.Len(t, innerSlice, len(innerExpected))
+				for j, expectedValue := range innerExpected {
+					v, ok := innerSlice[j].(float64)
+					require.True(t, ok)
+					require.Equal(t, expectedValue, v)
+				}
+			}
+		default:
+			t.Fatalf("unexpected type: %T", v)
+		}
+	}
+}
+
+type entry struct {
+	Time  time.Time
+	Name  string
+	Event map[string]interface{}
+}
+
+func exportAndParse(t *testing.T, buf *bytes.Buffer) []entry {
+	m := make(map[string]interface{})
+	line, err := buf.ReadBytes('\n')
+	require.NoError(t, err)
+	require.NoError(t, unmarshal(line, &m))
+	require.Contains(t, m, "trace")
+	var entries []entry
+	trace := m["trace"].(map[string]interface{})
+	require.Contains(t, trace, "common_fields")
+	commonFields := trace["common_fields"].(map[string]interface{})
+	require.Contains(t, commonFields, "reference_time")
+	referenceTime := time.Unix(0, int64(commonFields["reference_time"].(float64)*1e6))
+	require.NotContains(t, trace, "events")
+
+	for buf.Len() > 0 {
+		line, err := buf.ReadBytes('\n')
+		require.NoError(t, err)
+		ev := make(map[string]interface{})
+		require.NoError(t, unmarshal(line, &ev))
+		require.Len(t, ev, 3)
+		require.Contains(t, ev, "time")
+		require.Contains(t, ev, "name")
+		require.Contains(t, ev, "data")
+		entries = append(entries, entry{
+			Time:  referenceTime.Add(time.Duration(ev["time"].(float64)*1e6) * time.Nanosecond),
+			Name:  ev["name"].(string),
+			Event: ev["data"].(map[string]interface{}),
+		})
+	}
+	return entries
+}
+
+func exportAndParseSingle(t *testing.T, buf *bytes.Buffer) entry {
+	entries := exportAndParse(t, buf)
+	require.Len(t, entries, 1)
+	return entries[0]
+}
diff --git a/qlog/packet_header_test.go b/qlog/packet_header_test.go
index e4ed264a..19387db8 100644
--- a/qlog/packet_header_test.go
+++ b/qlog/packet_header_test.go
@@ -3,136 +3,143 @@ package qlog
 import (
 	"bytes"
 	"encoding/json"
+	"testing"
 
 	"github.com/francoispqt/gojay"
-
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
 	"github.com/quic-go/quic-go/logging"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Packet Header", func() {
-	It("determines the packet type from the encryption level", func() {
-		Expect(getPacketTypeFromEncryptionLevel(protocol.EncryptionInitial)).To(BeEquivalentTo(logging.PacketTypeInitial))
-		Expect(getPacketTypeFromEncryptionLevel(protocol.EncryptionHandshake)).To(BeEquivalentTo(logging.PacketTypeHandshake))
-		Expect(getPacketTypeFromEncryptionLevel(protocol.Encryption0RTT)).To(BeEquivalentTo(logging.PacketType0RTT))
-		Expect(getPacketTypeFromEncryptionLevel(protocol.Encryption1RTT)).To(BeEquivalentTo(logging.PacketType1RTT))
-	})
-
-	Context("marshalling", func() {
-		check := func(hdr *wire.ExtendedHeader, expected map[string]interface{}) {
-			buf := &bytes.Buffer{}
-			enc := gojay.NewEncoder(buf)
-			ExpectWithOffset(1, enc.Encode(transformLongHeader(hdr))).To(Succeed())
-			data := buf.Bytes()
-			ExpectWithOffset(1, json.Valid(data)).To(BeTrue())
-			checkEncoding(data, expected)
-		}
+func TestPacketTypeFromEncryptionLevel(t *testing.T) {
+	tests := []struct {
+		name  string
+		level protocol.EncryptionLevel
+		want  logging.PacketType
+	}{
+		{"Initial", protocol.EncryptionInitial, logging.PacketTypeInitial},
+		{"Handshake", protocol.EncryptionHandshake, logging.PacketTypeHandshake},
+		{"0-RTT", protocol.Encryption0RTT, logging.PacketType0RTT},
+		{"1-RTT", protocol.Encryption1RTT, logging.PacketType1RTT},
+	}
 
-		It("marshals a header with a payload length", func() {
-			check(
-				&wire.ExtendedHeader{
-					PacketNumber: 42,
-					Header: wire.Header{
-						Type:    protocol.PacketTypeInitial,
-						Length:  123,
-						Version: protocol.Version(0xdecafbad),
-					},
-				},
-				map[string]interface{}{
-					"packet_type":   "initial",
-					"packet_number": 42,
-					"dcil":          0,
-					"scil":          0,
-					"version":       "decafbad",
-				},
-			)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := getPacketTypeFromEncryptionLevel(tt.level)
+			require.Equal(t, tt.want, got)
 		})
+	}
+}
 
-		It("marshals an Initial with a token", func() {
-			check(
-				&wire.ExtendedHeader{
-					PacketNumber: 4242,
-					Header: wire.Header{
-						Type:    protocol.PacketTypeInitial,
-						Length:  123,
-						Version: protocol.Version(0xdecafbad),
-						Token:   []byte{0xde, 0xad, 0xbe, 0xef},
-					},
-				},
-				map[string]interface{}{
-					"packet_type":   "initial",
-					"packet_number": 4242,
-					"dcil":          0,
-					"scil":          0,
-					"version":       "decafbad",
-					"token":         map[string]interface{}{"data": "deadbeef"},
-				},
-			)
-		})
+func checkHeader(t *testing.T, hdr *wire.ExtendedHeader, expected map[string]interface{}) {
+	buf := &bytes.Buffer{}
+	enc := gojay.NewEncoder(buf)
+	require.NoError(t, enc.Encode(transformLongHeader(hdr)))
+	data := buf.Bytes()
+	require.True(t, json.Valid(data))
+	checkEncoding(t, data, expected)
+}
 
-		It("marshals a Retry packet", func() {
-			check(
-				&wire.ExtendedHeader{
-					Header: wire.Header{
-						Type:            protocol.PacketTypeRetry,
-						SrcConnectionID: protocol.ParseConnectionID([]byte{0x11, 0x22, 0x33, 0x44}),
-						Version:         protocol.Version(0xdecafbad),
-						Token:           []byte{0xde, 0xad, 0xbe, 0xef},
-					},
-				},
-				map[string]interface{}{
-					"packet_type": "retry",
-					"dcil":        0,
-					"scil":        4,
-					"scid":        "11223344",
-					"token":       map[string]interface{}{"data": "deadbeef"},
-					"version":     "decafbad",
-				},
-			)
-		})
+func TestMarshalHeaderWithPayloadLength(t *testing.T) {
+	checkHeader(t,
+		&wire.ExtendedHeader{
+			PacketNumber: 42,
+			Header: wire.Header{
+				Type:    protocol.PacketTypeInitial,
+				Length:  123,
+				Version: protocol.Version(0xdecafbad),
+			},
+		},
+		map[string]interface{}{
+			"packet_type":   "initial",
+			"packet_number": 42,
+			"dcil":          0,
+			"scil":          0,
+			"version":       "decafbad",
+		},
+	)
+}
 
-		It("marshals a packet with packet number 0", func() {
-			check(
-				&wire.ExtendedHeader{
-					PacketNumber: 0,
-					Header: wire.Header{
-						Type:    protocol.PacketTypeHandshake,
-						Version: protocol.Version(0xdecafbad),
-					},
-				},
-				map[string]interface{}{
-					"packet_type":   "handshake",
-					"packet_number": 0,
-					"dcil":          0,
-					"scil":          0,
-					"version":       "decafbad",
-				},
-			)
-		})
+func TestMarshalInitialWithToken(t *testing.T) {
+	checkHeader(t,
+		&wire.ExtendedHeader{
+			PacketNumber: 4242,
+			Header: wire.Header{
+				Type:    protocol.PacketTypeInitial,
+				Length:  123,
+				Version: protocol.Version(0xdecafbad),
+				Token:   []byte{0xde, 0xad, 0xbe, 0xef},
+			},
+		},
+		map[string]interface{}{
+			"packet_type":   "initial",
+			"packet_number": 4242,
+			"dcil":          0,
+			"scil":          0,
+			"version":       "decafbad",
+			"token":         map[string]interface{}{"data": "deadbeef"},
+		},
+	)
+}
 
-		It("marshals a header with a source connection ID", func() {
-			check(
-				&wire.ExtendedHeader{
-					PacketNumber: 42,
-					Header: wire.Header{
-						Type:            protocol.PacketTypeHandshake,
-						SrcConnectionID: protocol.ParseConnectionID([]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}),
-						Version:         protocol.Version(0xdecafbad),
-					},
-				},
-				map[string]interface{}{
-					"packet_type":   "handshake",
-					"packet_number": 42,
-					"dcil":          0,
-					"scil":          16,
-					"scid":          "00112233445566778899aabbccddeeff",
-					"version":       "decafbad",
-				},
-			)
-		})
-	})
-})
+func TestMarshalRetryPacket(t *testing.T) {
+	checkHeader(t,
+		&wire.ExtendedHeader{
+			Header: wire.Header{
+				Type:            protocol.PacketTypeRetry,
+				SrcConnectionID: protocol.ParseConnectionID([]byte{0x11, 0x22, 0x33, 0x44}),
+				Version:         protocol.Version(0xdecafbad),
+				Token:           []byte{0xde, 0xad, 0xbe, 0xef},
+			},
+		},
+		map[string]interface{}{
+			"packet_type": "retry",
+			"dcil":        0,
+			"scil":        4,
+			"scid":        "11223344",
+			"token":       map[string]interface{}{"data": "deadbeef"},
+			"version":     "decafbad",
+		},
+	)
+}
+
+func TestMarshalPacketWithPacketNumber0(t *testing.T) {
+	checkHeader(t,
+		&wire.ExtendedHeader{
+			PacketNumber: 0,
+			Header: wire.Header{
+				Type:    protocol.PacketTypeHandshake,
+				Version: protocol.Version(0xdecafbad),
+			},
+		},
+		map[string]interface{}{
+			"packet_type":   "handshake",
+			"packet_number": 0,
+			"dcil":          0,
+			"scil":          0,
+			"version":       "decafbad",
+		},
+	)
+}
+
+func TestMarshalHeaderWithSourceConnectionID(t *testing.T) {
+	checkHeader(t,
+		&wire.ExtendedHeader{
+			PacketNumber: 42,
+			Header: wire.Header{
+				Type:            protocol.PacketTypeHandshake,
+				SrcConnectionID: protocol.ParseConnectionID([]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}),
+				Version:         protocol.Version(0xdecafbad),
+			},
+		},
+		map[string]interface{}{
+			"packet_type":   "handshake",
+			"packet_number": 42,
+			"dcil":          0,
+			"scil":          16,
+			"scid":          "00112233445566778899aabbccddeeff",
+			"version":       "decafbad",
+		},
+	)
+}
diff --git a/qlog/qlog_dir_test.go b/qlog/qlog_dir_test.go
index 837465a5..ad8ff557 100644
--- a/qlog/qlog_dir_test.go
+++ b/qlog/qlog_dir_test.go
@@ -4,53 +4,62 @@ import (
 	"context"
 	"os"
 	"path"
+	"testing"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/logging"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("qlog dir tests", Serial, func() {
-	var originalQlogDirValue string
-	var tempTestDirPath string
-	ctx := context.Background()
-	perspective := logging.PerspectiveClient
+var (
+	originalQlogDirValue string
+	tempTestDirPath      string
+	ctx                  = context.Background()
+	perspective          = logging.PerspectiveClient
+)
+
+func setup(t *testing.T) {
+	originalQlogDirValue = os.Getenv("QLOGDIR")
+	var err error
+	tempTestDirPath, err = os.MkdirTemp("", "temp_test_dir")
+	require.NoError(t, err)
+}
+
+func cleanup(t *testing.T) {
+	require.NoError(t, os.Setenv("QLOGDIR", originalQlogDirValue))
+	require.NoError(t, os.RemoveAll(tempTestDirPath))
+}
+
+func TestEnvironmentVariableIsSet(t *testing.T) {
+	setup(t)
+	defer cleanup(t)
+
+	connID, _ := protocol.GenerateConnectionIDForInitial()
+	qlogDir := path.Join(tempTestDirPath, "qlogs")
+	err := os.Setenv("QLOGDIR", qlogDir)
+	require.NoError(t, err)
+
+	tracer := DefaultConnectionTracer(ctx, perspective, connID)
+	require.NotNil(t, tracer)
+	tracer.Close()
+
+	_, err = os.Stat(qlogDir)
+	qlogDirCreated := !os.IsNotExist(err)
+	require.True(t, qlogDirCreated)
+
+	childs, err := os.ReadDir(qlogDir)
+	require.NoError(t, err)
+	require.Len(t, childs, 1)
+}
+
+func TestEnvironmentVariableIsNotSet(t *testing.T) {
+	setup(t)
+	defer cleanup(t)
+
 	connID, _ := protocol.GenerateConnectionIDForInitial()
+	err := os.Setenv("QLOGDIR", "")
+	require.NoError(t, err)
 
-	BeforeEach(func() {
-		originalQlogDirValue = os.Getenv("QLOGDIR")
-		var err error
-		tempTestDirPath, err = os.MkdirTemp("", "temp_test_dir")
-		Expect(err).ToNot(HaveOccurred())
-	})
-
-	AfterEach(func() {
-		err := os.Setenv("QLOGDIR", originalQlogDirValue)
-		Expect(err).ToNot(HaveOccurred())
-		err = os.RemoveAll(tempTestDirPath)
-		Expect(err).ToNot(HaveOccurred())
-	})
-
-	It("environment variable is set", func() {
-		qlogDir := path.Join(tempTestDirPath, "qlogs")
-		err := os.Setenv("QLOGDIR", qlogDir)
-		Expect(err).ToNot(HaveOccurred())
-		tracer := DefaultConnectionTracer(ctx, perspective, connID)
-		Expect(tracer).ToNot(BeNil())
-		tracer.Close()
-		_, err = os.Stat(qlogDir)
-		qlogDirCreated := !os.IsNotExist(err)
-		Expect(qlogDirCreated).To(BeTrue())
-		childs, err := os.ReadDir(qlogDir)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(len(childs)).To(Equal(1))
-	})
-
-	It("environment variable is not set", func() {
-		err := os.Setenv("QLOGDIR", "")
-		Expect(err).ToNot(HaveOccurred())
-		tracer := DefaultConnectionTracer(ctx, perspective, connID)
-		Expect(tracer).To(BeNil())
-	})
-})
+	tracer := DefaultConnectionTracer(ctx, perspective, connID)
+	require.Nil(t, tracer)
+}
diff --git a/qlog/qlog_suite_test.go b/qlog/qlog_suite_test.go
deleted file mode 100644
index 984211da..00000000
--- a/qlog/qlog_suite_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package qlog
-
-import (
-	"encoding/json"
-	"os"
-	"strconv"
-	"testing"
-	"time"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-)
-
-func TestQlog(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "qlog Suite")
-}
-
-//nolint:unparam
-func scaleDuration(t time.Duration) time.Duration {
-	scaleFactor := 1
-	if f, err := strconv.Atoi(os.Getenv("TIMESCALE_FACTOR")); err == nil { // parsing "" errors, so this works fine if the env is not set
-		scaleFactor = f
-	}
-	Expect(scaleFactor).ToNot(BeZero())
-	return time.Duration(scaleFactor) * t
-}
-
-func unmarshal(data []byte, v interface{}) error {
-	if data[0] == recordSeparator {
-		data = data[1:]
-	}
-	return json.Unmarshal(data, v)
-}
-
-func checkEncoding(data []byte, expected map[string]interface{}) {
-	// unmarshal the data
-	m := make(map[string]interface{})
-	ExpectWithOffset(2, json.Unmarshal(data, &m)).To(Succeed())
-	ExpectWithOffset(2, m).To(HaveLen(len(expected)))
-	for key, value := range expected {
-		switch v := value.(type) {
-		case bool, string, map[string]interface{}:
-			ExpectWithOffset(1, m).To(HaveKeyWithValue(key, v))
-		case int:
-			ExpectWithOffset(1, m).To(HaveKeyWithValue(key, float64(v)))
-		case [][]float64: // used in the ACK frame
-			ExpectWithOffset(1, m).To(HaveKey(key))
-			for i, l := range v {
-				for j, s := range l {
-					ExpectWithOffset(1, m[key].([]interface{})[i].([]interface{})[j].(float64)).To(Equal(s))
-				}
-			}
-		default:
-			Fail("unexpected type")
-		}
-	}
-}
diff --git a/qlog/tracer_test.go b/qlog/tracer_test.go
index 04e79b65..825760f9 100644
--- a/qlog/tracer_test.go
+++ b/qlog/tracer_test.go
@@ -3,134 +3,131 @@ package qlog
 import (
 	"bytes"
 	"net"
+	"testing"
 	"time"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/logging"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Tracing", func() {
-	var (
-		tracer *logging.Tracer
-		buf    *bytes.Buffer
-	)
-
-	BeforeEach(func() {
-		buf = &bytes.Buffer{}
-		tracer = NewTracer(nopWriteCloser(buf))
-	})
+func newTracer() (*logging.Tracer, *bytes.Buffer) {
+	buf := &bytes.Buffer{}
+	tracer := NewTracer(nopWriteCloser(buf))
+	return tracer, buf
+}
 
-	It("exports a trace that has the right metadata", func() {
-		tracer.Close()
+func TestTraceMetadata(t *testing.T) {
+	tracer, buf := newTracer()
+	tracer.Close()
 
-		m := make(map[string]interface{})
-		Expect(unmarshal(buf.Bytes(), &m)).To(Succeed())
-		Expect(m).To(HaveKeyWithValue("qlog_version", "0.3"))
-		Expect(m).To(HaveKey("title"))
-		Expect(m).To(HaveKey("trace"))
-		trace := m["trace"].(map[string]interface{})
-		Expect(trace).To(HaveKey("common_fields"))
-		commonFields := trace["common_fields"].(map[string]interface{})
-		Expect(commonFields).ToNot(HaveKey("ODCID"))
-		Expect(commonFields).ToNot(HaveKey("group_id"))
-		Expect(commonFields).To(HaveKey("reference_time"))
-		referenceTime := time.Unix(0, int64(commonFields["reference_time"].(float64)*1e6))
-		Expect(referenceTime).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-		Expect(commonFields).To(HaveKeyWithValue("time_format", "relative"))
-		Expect(trace).To(HaveKey("vantage_point"))
-		vantagePoint := trace["vantage_point"].(map[string]interface{})
-		Expect(vantagePoint).To(HaveKeyWithValue("type", "transport"))
-	})
+	var m map[string]interface{}
+	err := unmarshal(buf.Bytes(), &m)
+	require.NoError(t, err)
+	require.Equal(t, "0.3", m["qlog_version"])
+	require.Contains(t, m, "title")
+	require.Contains(t, m, "trace")
+	trace := m["trace"].(map[string]interface{})
+	require.Contains(t, trace, "common_fields")
+	commonFields := trace["common_fields"].(map[string]interface{})
+	require.NotContains(t, commonFields, "ODCID")
+	require.NotContains(t, commonFields, "group_id")
+	require.Contains(t, commonFields, "reference_time")
+	referenceTime := time.Unix(0, int64(commonFields["reference_time"].(float64)*1e6))
+	require.WithinDuration(t, time.Now(), referenceTime, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "relative", commonFields["time_format"])
+	require.Contains(t, trace, "vantage_point")
+	vantagePoint := trace["vantage_point"].(map[string]interface{})
+	require.Equal(t, "transport", vantagePoint["type"])
+}
 
-	Context("Events", func() {
-		It("records a sent long header packet, without an ACK", func() {
-			tracer.SentPacket(
-				nil,
-				&logging.Header{
-					Type:             protocol.PacketTypeHandshake,
-					DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
-					SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
-					Length:           1337,
-					Version:          protocol.Version1,
-				},
-				1234,
-				[]logging.Frame{
-					&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
-					&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
-				},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_sent"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			raw := ev["raw"].(map[string]interface{})
-			Expect(raw).To(HaveKeyWithValue("length", float64(1234)))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "handshake"))
-			Expect(hdr).To(HaveKeyWithValue("scid", "04030201"))
-			Expect(ev).To(HaveKey("frames"))
-			frames := ev["frames"].([]interface{})
-			Expect(frames).To(HaveLen(2))
-			Expect(frames[0].(map[string]interface{})).To(HaveKeyWithValue("frame_type", "max_stream_data"))
-			Expect(frames[1].(map[string]interface{})).To(HaveKeyWithValue("frame_type", "stream"))
-		})
+func TestTracerSentLongHeaderPacket(t *testing.T) {
+	tracer, buf := newTracer()
+	tracer.SentPacket(
+		nil,
+		&logging.Header{
+			Type:             protocol.PacketTypeHandshake,
+			DestConnectionID: protocol.ParseConnectionID([]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+			SrcConnectionID:  protocol.ParseConnectionID([]byte{4, 3, 2, 1}),
+			Length:           1337,
+			Version:          protocol.Version1,
+		},
+		1234,
+		[]logging.Frame{
+			&logging.MaxStreamDataFrame{StreamID: 42, MaximumStreamData: 987},
+			&logging.StreamFrame{StreamID: 123, Offset: 1234, Length: 6, Fin: true},
+		},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_sent", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	raw := ev["raw"].(map[string]interface{})
+	require.Equal(t, float64(1234), raw["length"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Equal(t, "handshake", hdr["packet_type"])
+	require.Equal(t, "04030201", hdr["scid"])
+	require.Contains(t, ev, "frames")
+	frames := ev["frames"].([]interface{})
+	require.Len(t, frames, 2)
+	require.Equal(t, "max_stream_data", frames[0].(map[string]interface{})["frame_type"])
+	require.Equal(t, "stream", frames[1].(map[string]interface{})["frame_type"])
+}
 
-		It("records sending of a Version Negotiation packet", func() {
-			tracer.SentVersionNegotiationPacket(
-				nil,
-				protocol.ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8},
-				protocol.ArbitraryLenConnectionID{4, 3, 2, 1},
-				[]protocol.Version{0xdeadbeef, 0xdecafbad},
-			)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_sent"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("header"))
-			Expect(ev).ToNot(HaveKey("frames"))
-			Expect(ev).To(HaveKey("supported_versions"))
-			Expect(ev["supported_versions"].([]interface{})).To(Equal([]interface{}{"deadbeef", "decafbad"}))
-			header := ev["header"]
-			Expect(header).To(HaveKeyWithValue("packet_type", "version_negotiation"))
-			Expect(header).ToNot(HaveKey("packet_number"))
-			Expect(header).ToNot(HaveKey("version"))
-			Expect(header).To(HaveKeyWithValue("dcid", "0102030405060708"))
-			Expect(header).To(HaveKeyWithValue("scid", "04030201"))
-		})
+func TestSendingVersionNegotiationPacket(t *testing.T) {
+	tracer, buf := newTracer()
+	tracer.SentVersionNegotiationPacket(
+		nil,
+		protocol.ArbitraryLenConnectionID{1, 2, 3, 4, 5, 6, 7, 8},
+		protocol.ArbitraryLenConnectionID{4, 3, 2, 1},
+		[]protocol.Version{0xdeadbeef, 0xdecafbad},
+	)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_sent", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "header")
+	require.NotContains(t, ev, "frames")
+	require.Contains(t, ev, "supported_versions")
+	require.Equal(t, []interface{}{"deadbeef", "decafbad"}, ev["supported_versions"].([]interface{}))
+	header := ev["header"]
+	require.Equal(t, "version_negotiation", header.(map[string]interface{})["packet_type"])
+	require.NotContains(t, header, "packet_number")
+	require.NotContains(t, header, "version")
+	require.Equal(t, "0102030405060708", header.(map[string]interface{})["dcid"])
+	require.Equal(t, "04030201", header.(map[string]interface{})["scid"])
+}
 
-		It("records dropped packets", func() {
-			addr := net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 1234}
-			tracer.DroppedPacket(&addr, logging.PacketTypeInitial, 1337, logging.PacketDropPayloadDecryptError)
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:packet_dropped"))
-			ev := entry.Event
-			Expect(ev).To(HaveKey("raw"))
-			Expect(ev["raw"].(map[string]interface{})).To(HaveKeyWithValue("length", float64(1337)))
-			Expect(ev).To(HaveKey("header"))
-			hdr := ev["header"].(map[string]interface{})
-			Expect(hdr).To(HaveLen(1))
-			Expect(hdr).To(HaveKeyWithValue("packet_type", "initial"))
-			Expect(ev).To(HaveKeyWithValue("trigger", "payload_decrypt_error"))
-		})
+func TestDroppedPackets(t *testing.T) {
+	tracer, buf := newTracer()
+	addr := net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 1234}
+	tracer.DroppedPacket(&addr, logging.PacketTypeInitial, 1337, logging.PacketDropPayloadDecryptError)
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:packet_dropped", entry.Name)
+	ev := entry.Event
+	require.Contains(t, ev, "raw")
+	require.Equal(t, float64(1337), ev["raw"].(map[string]interface{})["length"])
+	require.Contains(t, ev, "header")
+	hdr := ev["header"].(map[string]interface{})
+	require.Len(t, hdr, 1)
+	require.Equal(t, "initial", hdr["packet_type"])
+	require.Equal(t, "payload_decrypt_error", ev["trigger"])
+}
 
-		It("records a generic event", func() {
-			tracer.Debug("foo", "bar")
-			tracer.Close()
-			entry := exportAndParseSingle(buf)
-			Expect(entry.Time).To(BeTemporally("~", time.Now(), scaleDuration(10*time.Millisecond)))
-			Expect(entry.Name).To(Equal("transport:foo"))
-			ev := entry.Event
-			Expect(ev).To(HaveLen(1))
-			Expect(ev).To(HaveKeyWithValue("details", "bar"))
-		})
-	})
-})
+func TestGenericTracerEvent(t *testing.T) {
+	tracer, buf := newTracer()
+	tracer.Debug("foo", "bar")
+	tracer.Close()
+	entry := exportAndParseSingle(t, buf)
+	require.WithinDuration(t, time.Now(), entry.Time, scaleDuration(10*time.Millisecond))
+	require.Equal(t, "transport:foo", entry.Name)
+	ev := entry.Event
+	require.Len(t, ev, 1)
+	require.Equal(t, "bar", ev["details"])
+}
diff --git a/qlog/types_test.go b/qlog/types_test.go
index d6be3e68..b31c3e1a 100644
--- a/qlog/types_test.go
+++ b/qlog/types_test.go
@@ -7,151 +7,260 @@ import (
 	"path"
 	"runtime"
 	"strconv"
+	"testing"
 
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/qerr"
 	"github.com/quic-go/quic-go/logging"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("Types", func() {
-	It("has a string representation for the owner", func() {
-		Expect(ownerLocal.String()).To(Equal("local"))
-		Expect(ownerRemote.String()).To(Equal("remote"))
-	})
-
-	It("has a string representation for the category", func() {
-		Expect(categoryConnectivity.String()).To(Equal("connectivity"))
-		Expect(categoryTransport.String()).To(Equal("transport"))
-		Expect(categoryRecovery.String()).To(Equal("recovery"))
-		Expect(categorySecurity.String()).To(Equal("security"))
-	})
-
-	It("has a string representation for the packet type", func() {
-		Expect(packetType(logging.PacketTypeInitial).String()).To(Equal("initial"))
-		Expect(packetType(logging.PacketTypeHandshake).String()).To(Equal("handshake"))
-		Expect(packetType(logging.PacketType0RTT).String()).To(Equal("0RTT"))
-		Expect(packetType(logging.PacketType1RTT).String()).To(Equal("1RTT"))
-		Expect(packetType(logging.PacketTypeStatelessReset).String()).To(Equal("stateless_reset"))
-		Expect(packetType(logging.PacketTypeRetry).String()).To(Equal("retry"))
-		Expect(packetType(logging.PacketTypeVersionNegotiation).String()).To(Equal("version_negotiation"))
-		Expect(packetType(logging.PacketTypeNotDetermined).String()).To(BeEmpty())
-	})
-
-	It("has a string representation for the packet drop reason", func() {
-		Expect(packetDropReason(logging.PacketDropKeyUnavailable).String()).To(Equal("key_unavailable"))
-		Expect(packetDropReason(logging.PacketDropUnknownConnectionID).String()).To(Equal("unknown_connection_id"))
-		Expect(packetDropReason(logging.PacketDropHeaderParseError).String()).To(Equal("header_parse_error"))
-		Expect(packetDropReason(logging.PacketDropPayloadDecryptError).String()).To(Equal("payload_decrypt_error"))
-		Expect(packetDropReason(logging.PacketDropProtocolViolation).String()).To(Equal("protocol_violation"))
-		Expect(packetDropReason(logging.PacketDropDOSPrevention).String()).To(Equal("dos_prevention"))
-		Expect(packetDropReason(logging.PacketDropUnsupportedVersion).String()).To(Equal("unsupported_version"))
-		Expect(packetDropReason(logging.PacketDropUnexpectedPacket).String()).To(Equal("unexpected_packet"))
-		Expect(packetDropReason(logging.PacketDropUnexpectedSourceConnectionID).String()).To(Equal("unexpected_source_connection_id"))
-		Expect(packetDropReason(logging.PacketDropUnexpectedVersion).String()).To(Equal("unexpected_version"))
-	})
-
-	It("has a string representation for the timer type", func() {
-		Expect(timerType(logging.TimerTypeACK).String()).To(Equal("ack"))
-		Expect(timerType(logging.TimerTypePTO).String()).To(Equal("pto"))
-	})
-
-	It("has a string representation for the key type", func() {
-		Expect(encLevelToKeyType(protocol.EncryptionInitial, protocol.PerspectiveClient).String()).To(Equal("client_initial_secret"))
-		Expect(encLevelToKeyType(protocol.EncryptionInitial, protocol.PerspectiveServer).String()).To(Equal("server_initial_secret"))
-		Expect(encLevelToKeyType(protocol.EncryptionHandshake, protocol.PerspectiveClient).String()).To(Equal("client_handshake_secret"))
-		Expect(encLevelToKeyType(protocol.EncryptionHandshake, protocol.PerspectiveServer).String()).To(Equal("server_handshake_secret"))
-		Expect(encLevelToKeyType(protocol.Encryption0RTT, protocol.PerspectiveClient).String()).To(Equal("client_0rtt_secret"))
-		Expect(encLevelToKeyType(protocol.Encryption0RTT, protocol.PerspectiveServer).String()).To(Equal("server_0rtt_secret"))
-		Expect(encLevelToKeyType(protocol.Encryption1RTT, protocol.PerspectiveClient).String()).To(Equal("client_1rtt_secret"))
-		Expect(encLevelToKeyType(protocol.Encryption1RTT, protocol.PerspectiveServer).String()).To(Equal("server_1rtt_secret"))
-	})
-
-	It("has a string representation for the key update trigger", func() {
-		Expect(keyUpdateTLS.String()).To(Equal("tls"))
-		Expect(keyUpdateRemote.String()).To(Equal("remote_update"))
-		Expect(keyUpdateLocal.String()).To(Equal("local_update"))
-	})
-
-	It("tells the packet number space from the encryption level", func() {
-		Expect(encLevelToPacketNumberSpace(protocol.EncryptionInitial)).To(Equal("initial"))
-		Expect(encLevelToPacketNumberSpace(protocol.EncryptionHandshake)).To(Equal("handshake"))
-		Expect(encLevelToPacketNumberSpace(protocol.Encryption0RTT)).To(Equal("application_data"))
-		Expect(encLevelToPacketNumberSpace(protocol.Encryption1RTT)).To(Equal("application_data"))
-	})
-
-	Context("transport errors", func() {
-		It("has a string representation for every error code", func() {
-			// We parse the error code file, extract all constants, and verify that
-			// each of them has a string version. Go FTW!
-			_, thisfile, _, ok := runtime.Caller(0)
-			if !ok {
-				panic("Failed to get current frame")
-			}
-			filename := path.Join(path.Dir(thisfile), "../internal/qerr/error_codes.go")
-			fileAst, err := parser.ParseFile(gotoken.NewFileSet(), filename, nil, 0)
-			Expect(err).NotTo(HaveOccurred())
-			constSpecs := fileAst.Decls[2].(*ast.GenDecl).Specs
-			Expect(len(constSpecs)).To(BeNumerically(">", 4)) // at time of writing
-			for _, c := range constSpecs {
-				valString := c.(*ast.ValueSpec).Values[0].(*ast.BasicLit).Value
-				val, err := strconv.ParseInt(valString, 0, 64)
-				Expect(err).NotTo(HaveOccurred())
-				Expect(transportError(val).String()).ToNot(BeEmpty())
-			}
-		})
-
-		It("has a string representation for transport errors", func() {
-			Expect(transportError(qerr.NoError).String()).To(Equal("no_error"))
-			Expect(transportError(qerr.InternalError).String()).To(Equal("internal_error"))
-			Expect(transportError(qerr.ConnectionRefused).String()).To(Equal("connection_refused"))
-			Expect(transportError(qerr.FlowControlError).String()).To(Equal("flow_control_error"))
-			Expect(transportError(qerr.StreamLimitError).String()).To(Equal("stream_limit_error"))
-			Expect(transportError(qerr.StreamStateError).String()).To(Equal("stream_state_error"))
-			Expect(transportError(qerr.FrameEncodingError).String()).To(Equal("frame_encoding_error"))
-			Expect(transportError(qerr.ConnectionIDLimitError).String()).To(Equal("connection_id_limit_error"))
-			Expect(transportError(qerr.ProtocolViolation).String()).To(Equal("protocol_violation"))
-			Expect(transportError(qerr.InvalidToken).String()).To(Equal("invalid_token"))
-			Expect(transportError(qerr.ApplicationErrorErrorCode).String()).To(Equal("application_error"))
-			Expect(transportError(qerr.CryptoBufferExceeded).String()).To(Equal("crypto_buffer_exceeded"))
-			Expect(transportError(qerr.NoViablePathError).String()).To(Equal("no_viable_path"))
-			Expect(transportError(1337).String()).To(BeEmpty())
-		})
-	})
-
-	It("has a string representation for congestion state updates", func() {
-		Expect(congestionState(logging.CongestionStateSlowStart).String()).To(Equal("slow_start"))
-		Expect(congestionState(logging.CongestionStateCongestionAvoidance).String()).To(Equal("congestion_avoidance"))
-		Expect(congestionState(logging.CongestionStateApplicationLimited).String()).To(Equal("application_limited"))
-		Expect(congestionState(logging.CongestionStateRecovery).String()).To(Equal("recovery"))
-	})
-
-	It("has a string representation for the ECN bits", func() {
-		Expect(ecn(logging.ECT0).String()).To(Equal("ECT(0)"))
-		Expect(ecn(logging.ECT1).String()).To(Equal("ECT(1)"))
-		Expect(ecn(logging.ECNCE).String()).To(Equal("CE"))
-		Expect(ecn(logging.ECTNot).String()).To(Equal("Not-ECT"))
-		Expect(ecn(42).String()).To(Equal("unknown ECN"))
-	})
-
-	It("has a string representation for the ECN state", func() {
-		Expect(ecnState(logging.ECNStateTesting).String()).To(Equal("testing"))
-		Expect(ecnState(logging.ECNStateUnknown).String()).To(Equal("unknown"))
-		Expect(ecnState(logging.ECNStateFailed).String()).To(Equal("failed"))
-		Expect(ecnState(logging.ECNStateCapable).String()).To(Equal("capable"))
-		Expect(ecnState(42).String()).To(Equal("unknown ECN state"))
-	})
-
-	It("has a string representation for the ECN state trigger", func() {
-		Expect(ecnStateTrigger(logging.ECNTriggerNoTrigger).String()).To(Equal(""))
-		Expect(ecnStateTrigger(logging.ECNFailedNoECNCounts).String()).To(Equal("ACK doesn't contain ECN marks"))
-		Expect(ecnStateTrigger(logging.ECNFailedDecreasedECNCounts).String()).To(Equal("ACK decreases ECN counts"))
-		Expect(ecnStateTrigger(logging.ECNFailedLostAllTestingPackets).String()).To(Equal("all ECN testing packets declared lost"))
-		Expect(ecnStateTrigger(logging.ECNFailedMoreECNCountsThanSent).String()).To(Equal("ACK contains more ECN counts than ECN-marked packets sent"))
-		Expect(ecnStateTrigger(logging.ECNFailedTooFewECNCounts).String()).To(Equal("ACK contains fewer new ECN counts than acknowledged ECN-marked packets"))
-		Expect(ecnStateTrigger(logging.ECNFailedManglingDetected).String()).To(Equal("ECN mangling detected"))
-		Expect(ecnStateTrigger(42).String()).To(Equal("unknown ECN state trigger"))
-	})
-})
+func TestOwnerStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		owner    owner
+		expected string
+	}{
+		{ownerLocal, "local"},
+		{ownerRemote, "remote"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, tc.owner.String())
+	}
+}
+
+func TestCategoryStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		category category
+		expected string
+	}{
+		{categoryConnectivity, "connectivity"},
+		{categoryTransport, "transport"},
+		{categoryRecovery, "recovery"},
+		{categorySecurity, "security"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, tc.category.String())
+	}
+}
+
+func TestPacketTypeStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		packetType logging.PacketType
+		expected   string
+	}{
+		{logging.PacketTypeInitial, "initial"},
+		{logging.PacketTypeHandshake, "handshake"},
+		{logging.PacketType0RTT, "0RTT"},
+		{logging.PacketType1RTT, "1RTT"},
+		{logging.PacketTypeStatelessReset, "stateless_reset"},
+		{logging.PacketTypeRetry, "retry"},
+		{logging.PacketTypeVersionNegotiation, "version_negotiation"},
+		{logging.PacketTypeNotDetermined, ""},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, packetType(tc.packetType).String())
+	}
+}
+
+func TestPacketDropReasonStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		reason   logging.PacketDropReason
+		expected string
+	}{
+		{logging.PacketDropKeyUnavailable, "key_unavailable"},
+		{logging.PacketDropUnknownConnectionID, "unknown_connection_id"},
+		{logging.PacketDropHeaderParseError, "header_parse_error"},
+		{logging.PacketDropPayloadDecryptError, "payload_decrypt_error"},
+		{logging.PacketDropProtocolViolation, "protocol_violation"},
+		{logging.PacketDropDOSPrevention, "dos_prevention"},
+		{logging.PacketDropUnsupportedVersion, "unsupported_version"},
+		{logging.PacketDropUnexpectedPacket, "unexpected_packet"},
+		{logging.PacketDropUnexpectedSourceConnectionID, "unexpected_source_connection_id"},
+		{logging.PacketDropUnexpectedVersion, "unexpected_version"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, packetDropReason(tc.reason).String())
+	}
+}
+
+func TestTimerTypeStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		timerType logging.TimerType
+		expected  string
+	}{
+		{logging.TimerTypeACK, "ack"},
+		{logging.TimerTypePTO, "pto"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, timerType(tc.timerType).String())
+	}
+}
+
+func TestKeyTypeStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		encLevel    protocol.EncryptionLevel
+		perspective protocol.Perspective
+		expected    string
+	}{
+		{protocol.EncryptionInitial, protocol.PerspectiveClient, "client_initial_secret"},
+		{protocol.EncryptionInitial, protocol.PerspectiveServer, "server_initial_secret"},
+		{protocol.EncryptionHandshake, protocol.PerspectiveClient, "client_handshake_secret"},
+		{protocol.EncryptionHandshake, protocol.PerspectiveServer, "server_handshake_secret"},
+		{protocol.Encryption0RTT, protocol.PerspectiveClient, "client_0rtt_secret"},
+		{protocol.Encryption0RTT, protocol.PerspectiveServer, "server_0rtt_secret"},
+		{protocol.Encryption1RTT, protocol.PerspectiveClient, "client_1rtt_secret"},
+		{protocol.Encryption1RTT, protocol.PerspectiveServer, "server_1rtt_secret"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, encLevelToKeyType(tc.encLevel, tc.perspective).String())
+	}
+}
+
+func TestKeyUpdateTriggerStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		trigger  keyUpdateTrigger
+		expected string
+	}{
+		{keyUpdateTLS, "tls"},
+		{keyUpdateRemote, "remote_update"},
+		{keyUpdateLocal, "local_update"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, tc.trigger.String())
+	}
+}
+
+func TestPacketNumberSpaceFromEncryptionLevel(t *testing.T) {
+	testCases := []struct {
+		encLevel protocol.EncryptionLevel
+		expected string
+	}{
+		{protocol.EncryptionInitial, "initial"},
+		{protocol.EncryptionHandshake, "handshake"},
+		{protocol.Encryption0RTT, "application_data"},
+		{protocol.Encryption1RTT, "application_data"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, encLevelToPacketNumberSpace(tc.encLevel))
+	}
+}
+
+func TestTransportErrorStringRepresentationForEveryErrorCode(t *testing.T) {
+	_, thisfile, _, ok := runtime.Caller(0)
+	require.True(t, ok, "Failed to get current frame")
+	filename := path.Join(path.Dir(thisfile), "../internal/qerr/error_codes.go")
+	fileAst, err := parser.ParseFile(gotoken.NewFileSet(), filename, nil, 0)
+	require.NoError(t, err)
+	constSpecs := fileAst.Decls[2].(*ast.GenDecl).Specs
+	require.Greater(t, len(constSpecs), 4)
+	for _, c := range constSpecs {
+		valString := c.(*ast.ValueSpec).Values[0].(*ast.BasicLit).Value
+		val, err := strconv.ParseInt(valString, 0, 64)
+		require.NoError(t, err)
+		require.NotEmpty(t, transportError(val).String())
+	}
+}
+
+func TestTransportErrorStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		err      qerr.TransportErrorCode
+		expected string
+	}{
+		{qerr.NoError, "no_error"},
+		{qerr.InternalError, "internal_error"},
+		{qerr.ConnectionRefused, "connection_refused"},
+		{qerr.FlowControlError, "flow_control_error"},
+		{qerr.StreamLimitError, "stream_limit_error"},
+		{qerr.StreamStateError, "stream_state_error"},
+		{qerr.FrameEncodingError, "frame_encoding_error"},
+		{qerr.ConnectionIDLimitError, "connection_id_limit_error"},
+		{qerr.ProtocolViolation, "protocol_violation"},
+		{qerr.InvalidToken, "invalid_token"},
+		{qerr.ApplicationErrorErrorCode, "application_error"},
+		{qerr.CryptoBufferExceeded, "crypto_buffer_exceeded"},
+		{qerr.NoViablePathError, "no_viable_path"},
+		{1337, ""},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, transportError(tc.err).String())
+	}
+}
+
+func TestCongestionStateUpdatesStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		state    logging.CongestionState
+		expected string
+	}{
+		{logging.CongestionStateSlowStart, "slow_start"},
+		{logging.CongestionStateCongestionAvoidance, "congestion_avoidance"},
+		{logging.CongestionStateApplicationLimited, "application_limited"},
+		{logging.CongestionStateRecovery, "recovery"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, congestionState(tc.state).String())
+	}
+}
+
+func TestECNBitsStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		ecn      logging.ECN
+		expected string
+	}{
+		{logging.ECT0, "ECT(0)"},
+		{logging.ECT1, "ECT(1)"},
+		{logging.ECNCE, "CE"},
+		{logging.ECTNot, "Not-ECT"},
+		{42, "unknown ECN"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, ecn(tc.ecn).String())
+	}
+}
+
+func TestECNStateStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		state    logging.ECNState
+		expected string
+	}{
+		{logging.ECNStateTesting, "testing"},
+		{logging.ECNStateUnknown, "unknown"},
+		{logging.ECNStateFailed, "failed"},
+		{logging.ECNStateCapable, "capable"},
+		{42, "unknown ECN state"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, ecnState(tc.state).String())
+	}
+}
+
+func TestECNStateTriggerStringRepresentation(t *testing.T) {
+	testCases := []struct {
+		trigger  logging.ECNStateTrigger
+		expected string
+	}{
+		{logging.ECNTriggerNoTrigger, ""},
+		{logging.ECNFailedNoECNCounts, "ACK doesn't contain ECN marks"},
+		{logging.ECNFailedDecreasedECNCounts, "ACK decreases ECN counts"},
+		{logging.ECNFailedLostAllTestingPackets, "all ECN testing packets declared lost"},
+		{logging.ECNFailedMoreECNCountsThanSent, "ACK contains more ECN counts than ECN-marked packets sent"},
+		{logging.ECNFailedTooFewECNCounts, "ACK contains fewer new ECN counts than acknowledged ECN-marked packets"},
+		{logging.ECNFailedManglingDetected, "ECN mangling detected"},
+		{42, "unknown ECN state trigger"},
+	}
+
+	for _, tc := range testCases {
+		require.Equal(t, tc.expected, ecnStateTrigger(tc.trigger).String())
+	}
+}
diff --git a/qlog/writer_test.go b/qlog/writer_test.go
index 8b4437ed..27696bc7 100644
--- a/qlog/writer_test.go
+++ b/qlog/writer_test.go
@@ -6,11 +6,10 @@ import (
 	"io"
 	"log"
 	"os"
+	"testing"
 
 	"github.com/quic-go/quic-go/internal/protocol"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
 type limitedWriter struct {
@@ -28,22 +27,26 @@ func (w *limitedWriter) Write(p []byte) (int, error) {
 	return n, err
 }
 
-var _ = Describe("Writing", func() {
-	It("stops writing when encountering an error", func() {
-		buf := &bytes.Buffer{}
-		t := NewConnectionTracer(
+func TestWritingStopping(t *testing.T) {
+	buf := &bytes.Buffer{}
+	t.Run("stops writing when encountering an error", func(t *testing.T) {
+		tracer := NewConnectionTracer(
 			&limitedWriter{WriteCloser: nopWriteCloser(buf), N: 250},
 			protocol.PerspectiveServer,
 			protocol.ParseConnectionID([]byte{0xde, 0xad, 0xbe, 0xef}),
 		)
+
 		for i := uint32(0); i < 1000; i++ {
-			t.UpdatedPTOCount(i)
+			tracer.UpdatedPTOCount(i)
 		}
 
-		b := &bytes.Buffer{}
-		log.SetOutput(b)
+		// Capture log output
+		var logBuf bytes.Buffer
+		log.SetOutput(&logBuf)
 		defer log.SetOutput(os.Stdout)
-		t.Close()
-		Expect(b.String()).To(ContainSubstring("writer full"))
+
+		tracer.Close()
+
+		require.Contains(t, logBuf.String(), "writer full")
 	})
-})
+}