diff --git a/qlog/qlog.go b/qlog/qlog.go new file mode 100644 index 00000000..82554772 --- /dev/null +++ b/qlog/qlog.go @@ -0,0 +1,50 @@ +package qlog + +import ( + "io" + + "github.com/francoispqt/gojay" + "github.com/lucas-clemente/quic-go/internal/protocol" +) + +// A Tracer records events to be exported to a qlog. +type Tracer interface { + Export() error +} + +type tracer struct { + w io.WriteCloser + odcid protocol.ConnectionID + perspective protocol.Perspective + + events []event +} + +var _ Tracer = &tracer{} + +// NewTracer creates a new tracer to record a qlog. +func NewTracer(w io.WriteCloser, p protocol.Perspective, odcid protocol.ConnectionID) Tracer { + return &tracer{ + w: w, + perspective: p, + odcid: odcid, + } +} + +// Export writes a qlog. +func (t *tracer) Export() error { + enc := gojay.NewEncoder(t.w) + tl := &topLevel{ + traces: traces{ + { + VantagePoint: vantagePoint{Type: t.perspective}, + CommonFields: commonFields{ODCID: connectionID(t.odcid), GroupID: connectionID(t.odcid)}, + EventFields: eventFields[:], + Events: t.events, + }, + }} + if err := enc.Encode(tl); err != nil { + return err + } + return t.w.Close() +} diff --git a/qlog/qlog_test.go b/qlog/qlog_test.go new file mode 100644 index 00000000..44eb4a00 --- /dev/null +++ b/qlog/qlog_test.go @@ -0,0 +1,60 @@ +package qlog + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/lucas-clemente/quic-go/internal/protocol" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type nopWriteCloserImpl struct{ io.Writer } + +func (nopWriteCloserImpl) Close() error { return nil } + +func nopWriteCloser(w io.Writer) io.WriteCloser { + return &nopWriteCloserImpl{Writer: w} +} + +var _ = Describe("Tracer", func() { + var ( + tracer Tracer + buf *bytes.Buffer + ) + + BeforeEach(func() { + buf = &bytes.Buffer{} + tracer = NewTracer( + nopWriteCloser(buf), + protocol.PerspectiveServer, + protocol.ConnectionID{0xde, 0xad, 0xbe, 0xef}, + ) + }) + + It("exports a trace that has the right metadata", func() { + Expect(tracer.Export()).To(Succeed()) + + m := make(map[string]interface{}) + Expect(json.Unmarshal(buf.Bytes(), &m)).To(Succeed()) + Expect(m).To(HaveKeyWithValue("qlog_version", "draft-02-wip")) + Expect(m).To(HaveKey("title")) + Expect(m).To(HaveKey("traces")) + traces := m["traces"].([]interface{}) + Expect(traces).To(HaveLen(1)) + trace := traces[0].(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(trace).To(HaveKey("event_fields")) + for i, ef := range trace["event_fields"].([]interface{}) { + Expect(ef.(string)).To(Equal(eventFields[i])) + } + Expect(trace).To(HaveKey("vantage_point")) + vantagePoint := trace["vantage_point"].(map[string]interface{}) + Expect(vantagePoint).To(HaveKeyWithValue("type", "server")) + }) +}) diff --git a/qlog/trace.go b/qlog/trace.go new file mode 100644 index 00000000..2d3628d0 --- /dev/null +++ b/qlog/trace.go @@ -0,0 +1,72 @@ +package qlog + +import ( + "github.com/francoispqt/gojay" + + "github.com/lucas-clemente/quic-go/internal/protocol" +) + +type topLevel struct { + traces traces +} + +func (topLevel) IsNil() bool { return false } +func (l topLevel) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("qlog_version", "draft-02-wip") + enc.StringKeyOmitEmpty("title", "quic-go qlog") + enc.ArrayKey("traces", l.traces) +} + +type vantagePoint struct { + Name string + Type protocol.Perspective +} + +func (p vantagePoint) IsNil() bool { return false } +func (p vantagePoint) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKeyOmitEmpty("name", p.Name) + switch p.Type { + case protocol.PerspectiveClient: + enc.StringKey("type", "client") + case protocol.PerspectiveServer: + enc.StringKey("type", "server") + } +} + +type commonFields struct { + ODCID connectionID + GroupID connectionID + ProtocolType string +} + +func (f commonFields) MarshalJSONObject(enc *gojay.Encoder) { + enc.StringKey("ODCID", f.ODCID.String()) + enc.StringKey("group_id", f.ODCID.String()) + enc.StringKeyOmitEmpty("protocol_type", f.ProtocolType) +} + +func (f commonFields) IsNil() bool { return false } + +type traces []trace + +func (t traces) IsNil() bool { return t == nil } +func (t traces) MarshalJSONArray(enc *gojay.Encoder) { + for _, tr := range t { + enc.Object(tr) + } +} + +type trace struct { + VantagePoint vantagePoint + CommonFields commonFields + EventFields []string + Events events +} + +func (trace) IsNil() bool { return false } +func (t trace) MarshalJSONObject(enc *gojay.Encoder) { + enc.ObjectKey("vantage_point", t.VantagePoint) + enc.ObjectKey("common_fields", t.CommonFields) + enc.SliceStringKey("event_fields", t.EventFields) + enc.ArrayKey("events", t.Events) +}