golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/quic/qlog/qlog.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.21
     6  
     7  // Package qlog serializes qlog events.
     8  package qlog
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"errors"
    14  	"io"
    15  	"log/slog"
    16  	"os"
    17  	"path/filepath"
    18  	"sync"
    19  	"time"
    20  )
    21  
    22  // Vantage is the vantage point of a trace.
    23  type Vantage string
    24  
    25  const (
    26  	// VantageEndpoint traces contain events not specific to a single connection.
    27  	VantageEndpoint = Vantage("endpoint")
    28  
    29  	// VantageClient traces follow a connection from the client's perspective.
    30  	VantageClient = Vantage("client")
    31  
    32  	// VantageServer traces follow a connection from the server's perspective.
    33  	VantageServer = Vantage("server")
    34  )
    35  
    36  // TraceInfo contains information about a trace.
    37  type TraceInfo struct {
    38  	// Vantage is the vantage point of the trace.
    39  	Vantage Vantage
    40  
    41  	// GroupID identifies the logical group the trace belongs to.
    42  	// For a connection trace, the group will be the same for
    43  	// both the client and server vantage points.
    44  	GroupID string
    45  }
    46  
    47  // HandlerOptions are options for a JSONHandler.
    48  type HandlerOptions struct {
    49  	// Level reports the minimum record level that will be logged.
    50  	// If Level is nil, the handler assumes QLogLevelEndpoint.
    51  	Level slog.Leveler
    52  
    53  	// Dir is the directory in which to create trace files.
    54  	// The handler will create one file per connection.
    55  	// If NewTrace is non-nil or Dir is "", the handler will not create files.
    56  	Dir string
    57  
    58  	// NewTrace is called to create a new trace.
    59  	// If NewTrace is nil and Dir is set,
    60  	// the handler will create a new file in Dir for each trace.
    61  	NewTrace func(TraceInfo) (io.WriteCloser, error)
    62  }
    63  
    64  type endpointHandler struct {
    65  	opts HandlerOptions
    66  
    67  	traceOnce sync.Once
    68  	trace     *jsonTraceHandler
    69  }
    70  
    71  // NewJSONHandler returns a handler which serializes qlog events to JSON.
    72  //
    73  // The handler will write an endpoint-wide trace,
    74  // and a separate trace for each connection.
    75  // The HandlerOptions control the location traces are written.
    76  //
    77  // It uses the streamable JSON Text Sequences mapping (JSON-SEQ)
    78  // defined in draft-ietf-quic-qlog-main-schema-04, Section 6.2.
    79  //
    80  // A JSONHandler may be used as the handler for a quic.Config.QLogLogger.
    81  // It is not a general-purpose slog handler,
    82  // and may not properly handle events from other sources.
    83  func NewJSONHandler(opts HandlerOptions) slog.Handler {
    84  	if opts.Dir == "" && opts.NewTrace == nil {
    85  		return slogDiscard{}
    86  	}
    87  	return &endpointHandler{
    88  		opts: opts,
    89  	}
    90  }
    91  
    92  func (h *endpointHandler) Enabled(ctx context.Context, level slog.Level) bool {
    93  	return enabled(h.opts.Level, level)
    94  }
    95  
    96  func (h *endpointHandler) Handle(ctx context.Context, r slog.Record) error {
    97  	h.traceOnce.Do(func() {
    98  		h.trace, _ = newJSONTraceHandler(h.opts, nil)
    99  	})
   100  	if h.trace != nil {
   101  		h.trace.Handle(ctx, r)
   102  	}
   103  	return nil
   104  }
   105  
   106  func (h *endpointHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
   107  	// Create a new trace output file for each top-level WithAttrs.
   108  	tr, err := newJSONTraceHandler(h.opts, attrs)
   109  	if err != nil {
   110  		return withAttrs(h, attrs)
   111  	}
   112  	return tr
   113  }
   114  
   115  func (h *endpointHandler) WithGroup(name string) slog.Handler {
   116  	return withGroup(h, name)
   117  }
   118  
   119  type jsonTraceHandler struct {
   120  	level slog.Leveler
   121  	w     jsonWriter
   122  	start time.Time
   123  	buf   bytes.Buffer
   124  }
   125  
   126  func newJSONTraceHandler(opts HandlerOptions, attrs []slog.Attr) (*jsonTraceHandler, error) {
   127  	w, err := newTraceWriter(opts, traceInfoFromAttrs(attrs))
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	// For testing, it might be nice to set the start time used for relative timestamps
   133  	// to the time of the first event.
   134  	//
   135  	// At the expense of some additional complexity here, we could defer writing
   136  	// the reference_time header field until the first event is processed.
   137  	//
   138  	// Just use the current time for now.
   139  	start := time.Now()
   140  
   141  	h := &jsonTraceHandler{
   142  		w:     jsonWriter{w: w},
   143  		level: opts.Level,
   144  		start: start,
   145  	}
   146  	h.writeHeader(attrs)
   147  	return h, nil
   148  }
   149  
   150  func traceInfoFromAttrs(attrs []slog.Attr) TraceInfo {
   151  	info := TraceInfo{
   152  		Vantage: VantageEndpoint, // default if not specified
   153  	}
   154  	for _, a := range attrs {
   155  		if a.Key == "group_id" && a.Value.Kind() == slog.KindString {
   156  			info.GroupID = a.Value.String()
   157  		}
   158  		if a.Key == "vantage_point" && a.Value.Kind() == slog.KindGroup {
   159  			for _, aa := range a.Value.Group() {
   160  				if aa.Key == "type" && aa.Value.Kind() == slog.KindString {
   161  					info.Vantage = Vantage(aa.Value.String())
   162  				}
   163  			}
   164  		}
   165  	}
   166  	return info
   167  }
   168  
   169  func newTraceWriter(opts HandlerOptions, info TraceInfo) (io.WriteCloser, error) {
   170  	var w io.WriteCloser
   171  	var err error
   172  	if opts.NewTrace != nil {
   173  		w, err = opts.NewTrace(info)
   174  	} else if opts.Dir != "" {
   175  		var filename string
   176  		if info.GroupID != "" {
   177  			filename = info.GroupID + "_"
   178  		}
   179  		filename += string(info.Vantage) + ".sqlog"
   180  		if !filepath.IsLocal(filename) {
   181  			return nil, errors.New("invalid trace filename")
   182  		}
   183  		w, err = os.OpenFile(filepath.Join(opts.Dir, filename), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0666)
   184  	} else {
   185  		err = errors.New("no log destination")
   186  	}
   187  	return w, err
   188  }
   189  
   190  func (h *jsonTraceHandler) writeHeader(attrs []slog.Attr) {
   191  	h.w.writeRecordStart()
   192  	defer h.w.writeRecordEnd()
   193  
   194  	// At the time of writing this comment the most recent version is 0.4,
   195  	// but qvis only supports up to 0.3.
   196  	h.w.writeStringField("qlog_version", "0.3")
   197  	h.w.writeStringField("qlog_format", "JSON-SEQ")
   198  
   199  	// The attrs flatten both common trace event fields and Trace fields.
   200  	// This identifies the fields that belong to the Trace.
   201  	isTraceSeqField := func(s string) bool {
   202  		switch s {
   203  		case "title", "description", "configuration", "vantage_point":
   204  			return true
   205  		}
   206  		return false
   207  	}
   208  
   209  	h.w.writeObjectField("trace", func() {
   210  		h.w.writeObjectField("common_fields", func() {
   211  			h.w.writeRawField("protocol_type", `["QUIC"]`)
   212  			h.w.writeStringField("time_format", "relative")
   213  			h.w.writeTimeField("reference_time", h.start)
   214  			for _, a := range attrs {
   215  				if !isTraceSeqField(a.Key) {
   216  					h.w.writeAttr(a)
   217  				}
   218  			}
   219  		})
   220  		for _, a := range attrs {
   221  			if isTraceSeqField(a.Key) {
   222  				h.w.writeAttr(a)
   223  			}
   224  		}
   225  	})
   226  }
   227  
   228  func (h *jsonTraceHandler) Enabled(ctx context.Context, level slog.Level) bool {
   229  	return enabled(h.level, level)
   230  }
   231  
   232  func (h *jsonTraceHandler) Handle(ctx context.Context, r slog.Record) error {
   233  	h.w.writeRecordStart()
   234  	defer h.w.writeRecordEnd()
   235  	h.w.writeDurationField("time", r.Time.Sub(h.start))
   236  	h.w.writeStringField("name", r.Message)
   237  	h.w.writeObjectField("data", func() {
   238  		r.Attrs(func(a slog.Attr) bool {
   239  			h.w.writeAttr(a)
   240  			return true
   241  		})
   242  	})
   243  	return nil
   244  }
   245  
   246  func (h *jsonTraceHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
   247  	return withAttrs(h, attrs)
   248  }
   249  
   250  func (h *jsonTraceHandler) WithGroup(name string) slog.Handler {
   251  	return withGroup(h, name)
   252  }
   253  
   254  func enabled(leveler slog.Leveler, level slog.Level) bool {
   255  	var minLevel slog.Level
   256  	if leveler != nil {
   257  		minLevel = leveler.Level()
   258  	}
   259  	return level >= minLevel
   260  }
   261  
   262  type slogDiscard struct{}
   263  
   264  func (slogDiscard) Enabled(context.Context, slog.Level) bool        { return false }
   265  func (slogDiscard) Handle(ctx context.Context, r slog.Record) error { return nil }
   266  func (slogDiscard) WithAttrs(attrs []slog.Attr) slog.Handler        { return slogDiscard{} }
   267  func (slogDiscard) WithGroup(name string) slog.Handler              { return slogDiscard{} }