debian-mirror-gitlab/workhorse-vendor/gocloud.dev/server/requestlog/requestlog.go
2023-01-13 15:02:22 +05:30

235 lines
6 KiB
Go

// Copyright 2018 The Go Cloud Development Kit Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package requestlog provides an http.Handler that logs information
// about requests.
package requestlog // import "gocloud.dev/server/requestlog"
import (
"bufio"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"time"
"go.opencensus.io/trace"
)
// Logger wraps the Log method. Log must be safe to call from multiple
// goroutines. Log must not hold onto an Entry after it returns.
type Logger interface {
Log(*Entry)
}
// A Handler emits request information to a Logger.
type Handler struct {
log Logger
h http.Handler
}
// NewHandler returns a handler that emits information to log and calls
// h.ServeHTTP.
func NewHandler(log Logger, h http.Handler) *Handler {
return &Handler{
log: log,
h: h,
}
}
// ServeHTTP calls its underlying handler's ServeHTTP method, then calls
// Log after the handler returns.
//
// ServeHTTP will always consume the request body up to the first error,
// even if the underlying handler does not.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sc := trace.FromContext(r.Context()).SpanContext()
ent := &Entry{
Request: cloneRequestWithoutBody(r),
ReceivedTime: start,
RequestMethod: r.Method,
RequestURL: r.URL.String(),
RequestHeaderSize: headerSize(r.Header),
UserAgent: r.UserAgent(),
Referer: r.Referer(),
Proto: r.Proto,
RemoteIP: ipFromHostPort(r.RemoteAddr),
TraceID: sc.TraceID,
SpanID: sc.SpanID,
}
if addr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
ent.ServerIP = ipFromHostPort(addr.String())
}
r2 := new(http.Request)
*r2 = *r
rcc := &readCounterCloser{r: r.Body}
r2.Body = rcc
w2 := &responseStats{w: w}
h.h.ServeHTTP(w2, r2)
ent.Latency = time.Since(start)
if rcc.err == nil && rcc.r != nil && !w2.hijacked {
// If the handler hasn't encountered an error in the Body (like EOF),
// then consume the rest of the Body to provide an accurate rcc.n.
io.Copy(ioutil.Discard, rcc)
}
ent.RequestBodySize = rcc.n
ent.Status = w2.code
if ent.Status == 0 {
ent.Status = http.StatusOK
}
ent.ResponseHeaderSize, ent.ResponseBodySize = w2.size()
h.log.Log(ent)
}
func cloneRequestWithoutBody(r *http.Request) *http.Request {
r = r.Clone(r.Context())
r.Body = nil
return r
}
// Entry records information about a completed HTTP request.
type Entry struct {
// Request is the http request that has been completed.
//
// This request's Body is always nil, regardless of the actual request body.
Request *http.Request
ReceivedTime time.Time
RequestBodySize int64
Status int
ResponseHeaderSize int64
ResponseBodySize int64
Latency time.Duration
TraceID trace.TraceID
SpanID trace.SpanID
// Deprecated. This value is available by evaluating Request.Referer().
Referer string
// Deprecated. This value is available directing in Request.Proto.
Proto string
// Deprecated. This value is available directly in Request.Method.
RequestMethod string
// Deprecated. This value is available directly in Request.URL.
RequestURL string
// Deprecated. This value is available by evaluating Request.Header.
RequestHeaderSize int64
// Deprecated. This value is available by evaluating Request.Header.
UserAgent string
// Deprecated. This value is available by evaluating Request.RemoteAddr..
RemoteIP string
// Deprecated. This value is available by evaluating reading the
// http.LocalAddrContextKey value from the context returned by Request.Context().
ServerIP string
}
func ipFromHostPort(hp string) string {
h, _, err := net.SplitHostPort(hp)
if err != nil {
return ""
}
if len(h) > 0 && h[0] == '[' {
return h[1 : len(h)-1]
}
return h
}
type readCounterCloser struct {
r io.ReadCloser
n int64
err error
}
func (rcc *readCounterCloser) Read(p []byte) (n int, err error) {
if rcc.err != nil {
return 0, rcc.err
}
n, rcc.err = rcc.r.Read(p)
rcc.n += int64(n)
return n, rcc.err
}
func (rcc *readCounterCloser) Close() error {
rcc.err = errors.New("read from closed reader")
return rcc.r.Close()
}
type writeCounter int64
func (wc *writeCounter) Write(p []byte) (n int, err error) {
*wc += writeCounter(len(p))
return len(p), nil
}
func headerSize(h http.Header) int64 {
var wc writeCounter
h.Write(&wc)
return int64(wc) + 2 // for CRLF
}
type responseStats struct {
w http.ResponseWriter
hsize int64
wc writeCounter
code int
hijacked bool
}
func (r *responseStats) Header() http.Header {
return r.w.Header()
}
func (r *responseStats) WriteHeader(statusCode int) {
if r.code != 0 {
return
}
r.hsize = headerSize(r.w.Header())
r.w.WriteHeader(statusCode)
r.code = statusCode
}
func (r *responseStats) Write(p []byte) (n int, err error) {
if r.code == 0 {
r.WriteHeader(http.StatusOK)
}
n, err = r.w.Write(p)
r.wc.Write(p[:n])
return
}
func (r *responseStats) size() (hdr, body int64) {
if r.code == 0 {
return headerSize(r.w.Header()), 0
}
// Use the header size from the time WriteHeader was called.
// The Header map can be mutated after the call to add HTTP Trailers,
// which we don't want to count.
return r.hsize, int64(r.wc)
}
func (r *responseStats) Hijack() (_ net.Conn, _ *bufio.ReadWriter, err error) {
defer func() {
if err == nil {
r.hijacked = true
}
}()
if hj, ok := r.w.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("underlying ResponseWriter does not support hijacking")
}