diff --git a/qlog/event.go b/qlog/event.go index 9d49f87d0..2d7a54477 100644 --- a/qlog/event.go +++ b/qlog/event.go @@ -39,3 +39,25 @@ func (e event) MarshalJSONArray(enc *gojay.Encoder) { enc.String(e.Name()) enc.Object(e.eventDetails) } + +type eventPacketSent struct { + PacketType packetType + Header packetHeader + Frames frames + IsCoalesced bool + Trigger string +} + +var _ eventDetails = eventPacketSent{} + +func (e eventPacketSent) Category() category { return categoryTransport } +func (e eventPacketSent) Name() string { return "packet_sent" } +func (e eventPacketSent) IsNil() bool { return false } + +func (e eventPacketSent) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("packet_type", e.PacketType.String()) + enc.ObjectKey("header", e.Header) + enc.ArrayKeyOmitEmpty("frames", e.Frames) + enc.BoolKeyOmitEmpty("is_coalesced", e.IsCoalesced) + enc.StringKeyOmitEmpty("trigger", e.Trigger) +} diff --git a/qlog/frame.go b/qlog/frame.go index 5e5bb7086..b76a48f53 100644 --- a/qlog/frame.go +++ b/qlog/frame.go @@ -15,6 +15,17 @@ type frame struct { var _ gojay.MarshalerJSONObject = frame{} +type frames []frame + +func (fs frames) IsNil() bool { return fs == nil } +func (fs frames) MarshalJSONArray(enc *gojay.Encoder) { + for _, f := range fs { + enc.Object(f) + } +} + +var _ gojay.MarshalerJSONArray = frames{} + type cryptoFrame struct { Offset protocol.ByteCount Length protocol.ByteCount diff --git a/qlog/qlog.go b/qlog/qlog.go index 82554772d..4da78fa7a 100644 --- a/qlog/qlog.go +++ b/qlog/qlog.go @@ -2,14 +2,17 @@ package qlog import ( "io" + "time" "github.com/francoispqt/gojay" "github.com/lucas-clemente/quic-go/internal/protocol" + "github.com/lucas-clemente/quic-go/internal/wire" ) // A Tracer records events to be exported to a qlog. type Tracer interface { Export() error + SentPacket(time.Time, *wire.ExtendedHeader, *wire.AckFrame, []wire.Frame) } type tracer struct { @@ -48,3 +51,25 @@ func (t *tracer) Export() error { } return t.w.Close() } + +func (t *tracer) SentPacket(time time.Time, hdr *wire.ExtendedHeader, ack *wire.AckFrame, frames []wire.Frame) { + numFrames := len(frames) + if ack != nil { + numFrames++ + } + fs := make([]frame, 0, numFrames) + if ack != nil { + fs = append(fs, *transformFrame(ack)) + } + for _, f := range frames { + fs = append(fs, *transformFrame(f)) + } + t.events = append(t.events, event{ + Time: time, + eventDetails: eventPacketSent{ + PacketType: getPacketType(hdr), + Header: *transformHeader(hdr), + Frames: fs, + }, + }) +} diff --git a/qlog/qlog_test.go b/qlog/qlog_test.go index 44eb4a00d..15107b42a 100644 --- a/qlog/qlog_test.go +++ b/qlog/qlog_test.go @@ -4,8 +4,10 @@ import ( "bytes" "encoding/json" "io" + "time" "github.com/lucas-clemente/quic-go/internal/protocol" + "github.com/lucas-clemente/quic-go/internal/wire" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -57,4 +59,76 @@ var _ = Describe("Tracer", func() { vantagePoint := trace["vantage_point"].(map[string]interface{}) Expect(vantagePoint).To(HaveKeyWithValue("type", "server")) }) + + Context("Events", func() { + exportAndParse := func() (time.Time, string /* category */, string /* event */, map[string]interface{}) { + Expect(tracer.Export()).To(Succeed()) + + m := make(map[string]interface{}) + Expect(json.Unmarshal(buf.Bytes(), &m)).To(Succeed()) + Expect(m).To(HaveKey("traces")) + traces := m["traces"].([]interface{}) + Expect(traces).To(HaveLen(1)) + trace := traces[0].(map[string]interface{}) + Expect(trace).To(HaveKey("events")) + events := trace["events"].([]interface{}) + Expect(events).To(HaveLen(1)) + ev := events[0].([]interface{}) + Expect(ev).To(HaveLen(4)) + return time.Unix(0, int64(1e6*ev[0].(float64))), ev[1].(string), ev[2].(string), ev[3].(map[string]interface{}) + } + + It("records a sent packet, without an ACK", func() { + now := time.Now() + tracer.SentPacket( + now, + &wire.ExtendedHeader{ + Header: wire.Header{ + IsLongHeader: true, + Type: protocol.PacketTypeHandshake, + DestConnectionID: protocol.ConnectionID{1, 2, 3, 4, 5, 6, 7, 8}, + SrcConnectionID: protocol.ConnectionID{4, 3, 2, 1}, + Version: protocol.VersionTLS, + }, + PacketNumber: 1337, + }, + nil, + []wire.Frame{ + &wire.MaxStreamDataFrame{StreamID: 42, ByteOffset: 987}, + &wire.StreamFrame{StreamID: 123, Offset: 1234, Data: []byte("foobar"), FinBit: true}, + }, + ) + t, category, eventName, ev := exportAndParse() + Expect(t).To(BeTemporally("~", now, time.Millisecond)) + Expect(category).To(Equal("transport")) + Expect(eventName).To(Equal("packet_sent")) + Expect(ev).To(HaveKeyWithValue("packet_type", "handshake")) + Expect(ev).To(HaveKey("header")) + 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")) + }) + + It("records a sent packet, without an ACK", func() { + tracer.SentPacket( + time.Now(), + &wire.ExtendedHeader{ + Header: wire.Header{DestConnectionID: protocol.ConnectionID{1, 2, 3, 4}}, + PacketNumber: 1337, + }, + &wire.AckFrame{AckRanges: []wire.AckRange{{Smallest: 1, Largest: 10}}}, + []wire.Frame{&wire.MaxDataFrame{ByteOffset: 987}}, + ) + _, _, _, ev := exportAndParse() + Expect(ev).To(HaveKeyWithValue("packet_type", "1RTT")) + Expect(ev).To(HaveKey("header")) + 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")) + }) + }) })