Files

203 lines
5.7 KiB
Go

// Package trace implement structured logging of requests
package trace
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/vulcand/oxy/v2/internal/holsterv4/clock"
"github.com/vulcand/oxy/v2/utils"
)
// Tracer records request and response emitting JSON structured data to the output.
type Tracer struct {
errHandler utils.ErrorHandler
next http.Handler
reqHeaders []string
respHeaders []string
writer io.Writer
log utils.Logger
}
// New creates a new Tracer middleware that emits all the request/response information in structured format
// to writer and passes the request to the next handler. It can optionally capture request and response headers,
// see RequestHeaders and ResponseHeaders options for details.
func New(next http.Handler, writer io.Writer, opts ...Option) (*Tracer, error) {
t := &Tracer{
writer: writer,
next: next,
log: &utils.NoopLogger{},
}
for _, o := range opts {
if err := o(t); err != nil {
return nil, err
}
}
if t.errHandler == nil {
t.errHandler = utils.DefaultHandler
}
return t, nil
}
func (t *Tracer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
start := clock.Now()
pw := utils.NewProxyWriterWithLogger(w, t.log)
t.next.ServeHTTP(pw, req)
l := t.newRecord(req, pw, clock.Since(start))
if err := json.NewEncoder(t.writer).Encode(l); err != nil {
t.log.Error("Failed to marshal request: %v", err)
}
}
func (t *Tracer) newRecord(req *http.Request, pw *utils.ProxyWriter, diff time.Duration) *Record {
return &Record{
Request: Request{
Method: req.Method,
URL: req.URL.String(),
TLS: newTLS(req),
BodyBytes: bodyBytes(req.Header),
Headers: captureHeaders(req.Header, t.reqHeaders),
},
Response: Response{
Code: pw.StatusCode(),
BodyBytes: bodyBytes(pw.Header()),
Roundtrip: float64(diff) / float64(clock.Millisecond),
Headers: captureHeaders(pw.Header(), t.respHeaders),
},
}
}
func captureHeaders(in http.Header, headers []string) http.Header {
if len(headers) == 0 || in == nil {
return nil
}
out := make(http.Header, len(headers))
for _, h := range headers {
vals, ok := in[h]
if !ok || len(out[h]) != 0 {
continue
}
for i := range vals {
out.Add(h, vals[i])
}
}
return out
}
// Record represents a structured request and response record.
type Record struct {
Request Request `json:"request"`
Response Response `json:"response"`
}
// Request contains information about an HTTP request.
type Request struct {
Method string `json:"method"` // Method - request method
BodyBytes int64 `json:"body_bytes"` // BodyBytes - size of request body in bytes
URL string `json:"url"` // URL - Request URL
Headers http.Header `json:"headers,omitempty"` // Headers - optional request headers, will be recorded if configured
TLS *TLS `json:"tls,omitempty"` // TLS - optional TLS record, will be recorded if it's a TLS connection
}
// Response contains information about HTTP response.
type Response struct {
Code int `json:"code"` // Code - response status code
Roundtrip float64 `json:"roundtrip"` // Roundtrip - round trip time in milliseconds
Headers http.Header `json:"headers,omitempty"` // Headers - optional headers, will be recorded if configured
BodyBytes int64 `json:"body_bytes"` // BodyBytes - size of response body in bytes
}
// TLS contains information about this TLS connection.
type TLS struct {
Version string `json:"version"` // Version - TLS version
Resume bool `json:"resume"` // Resume tells if the session has been re-used (session tickets)
CipherSuite string `json:"cipher_suite"` // CipherSuite contains cipher suite used for this connection
Server string `json:"server"` // Server contains server name used in SNI
}
func newTLS(req *http.Request) *TLS {
if req.TLS == nil {
return nil
}
return &TLS{
Version: versionToString(req.TLS.Version),
Resume: req.TLS.DidResume,
CipherSuite: csToString(req.TLS.CipherSuite),
Server: req.TLS.ServerName,
}
}
func versionToString(v uint16) string {
switch v {
case tls.VersionTLS10:
return "TLS10"
case tls.VersionTLS11:
return "TLS11"
case tls.VersionTLS12:
return "TLS12"
}
return fmt.Sprintf("unknown: %x", v)
}
func csToString(cs uint16) string {
switch cs {
case tls.TLS_RSA_WITH_RC4_128_SHA:
return "TLS_RSA_WITH_RC4_128_SHA"
case tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA:
return "TLS_RSA_WITH_3DES_EDE_CBC_SHA"
case tls.TLS_RSA_WITH_AES_128_CBC_SHA:
return "TLS_RSA_WITH_AES_128_CBC_SHA"
case tls.TLS_RSA_WITH_AES_256_CBC_SHA:
return "TLS_RSA_WITH_AES_256_CBC_SHA"
case tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA:
return "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA"
case tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:
return "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA"
case tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
return "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"
case tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA:
return "TLS_ECDHE_RSA_WITH_RC4_128_SHA"
case tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:
return "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA"
case tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:
return "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
case tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
return "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"
case tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
case tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
}
return fmt.Sprintf("unknown: %x", cs)
}
func bodyBytes(h http.Header) int64 {
length := h.Get("Content-Length")
if length == "" {
return 0
}
bytes, err := strconv.ParseInt(length, 10, 0)
if err == nil {
return bytes
}
return 0
}