Files
releases/runtime/logging.go
Ashutosh Narkar a8ac7b38bb plugins/logs: Include http request context in decision logs
It would be useful if users had the ability to enhance the
decision log with info from the incoming HTTP request such as
headers. This change allows users to configure headers whose
values if present in the incoming HTTP request would be
surfaced via the decision log. This can be extended in the
future to include more context from the request.

Fixes: #6693

Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
2024-05-20 11:08:42 -07:00

261 lines
6.4 KiB
Go

// Copyright 2016 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package runtime
import (
"bytes"
"compress/gzip"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/open-policy-agent/opa/logging"
"github.com/open-policy-agent/opa/topdown/print"
)
type loggingPrintHook struct {
logger logging.Logger
}
func (h loggingPrintHook) Print(pctx print.Context, msg string) error {
// NOTE(tsandall): if the request context is not present then do not panic,
// just log the print message without the additional context.
var fields map[string]any
rctx, ok := logging.FromContext(pctx.Context)
if ok {
fields = rctx.Fields()
} else {
fields = make(map[string]any, 1)
}
fields["line"] = pctx.Location.String()
h.logger.WithFields(fields).Info(msg)
return nil
}
// LoggingHandler returns an http.Handler that will print log messages
// containing the request information as well as response status and latency.
type LoggingHandler struct {
logger logging.Logger
inner http.Handler
requestID uint64
}
// NewLoggingHandler returns a new http.Handler.
func NewLoggingHandler(logger logging.Logger, inner http.Handler) http.Handler {
return &LoggingHandler{
logger: logger,
inner: inner,
requestID: uint64(0),
}
}
func (h *LoggingHandler) loggingEnabled(level logging.Level) bool {
return level <= h.logger.GetLevel()
}
func (h *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var rctx logging.RequestContext
rctx.ReqID = atomic.AddUint64(&h.requestID, uint64(1))
rctx.HTTPRequestContext = logging.HTTPRequestContext{Header: r.Header.Clone()}
recorder := newRecorder(h.logger, w, r, rctx.ReqID, h.loggingEnabled(logging.Debug))
t0 := time.Now()
if h.loggingEnabled(logging.Info) {
rctx.ClientAddr = r.RemoteAddr
rctx.ReqMethod = r.Method
rctx.ReqPath = r.URL.EscapedPath()
r = r.WithContext(logging.NewContext(r.Context(), &rctx))
var err error
fields := rctx.Fields()
if h.loggingEnabled(logging.Debug) {
var bs []byte
if r.Body != nil {
bs, r.Body, err = readBody(r.Body)
}
if err == nil {
if gzipReceived(r.Header) {
// the request is compressed
var gzReader *gzip.Reader
var plainOutput []byte
reader := bytes.NewReader(bs)
gzReader, err = gzip.NewReader(reader)
if err == nil {
plainOutput, err = io.ReadAll(gzReader)
if err == nil {
defer gzReader.Close()
fields["req_body"] = string(plainOutput)
}
}
} else {
fields["req_body"] = string(bs)
}
}
// err can be thrown on different statements
if err != nil {
fields["err"] = err
}
fields["req_params"] = r.URL.Query()
}
if err == nil {
h.logger.WithFields(fields).Info("Received request.")
} else {
h.logger.WithFields(fields).Error("Failed to read body.")
}
}
h.inner.ServeHTTP(recorder, r)
dt := time.Since(t0)
statusCode := 200
if recorder.statusCode != 0 {
statusCode = recorder.statusCode
}
if h.loggingEnabled(logging.Info) {
fields := map[string]interface{}{
"client_addr": rctx.ClientAddr,
"req_id": rctx.ReqID,
"req_method": rctx.ReqMethod,
"req_path": rctx.ReqPath,
"resp_status": statusCode,
"resp_bytes": recorder.bytesWritten,
"resp_duration": float64(dt.Nanoseconds()) / 1e6,
}
if h.loggingEnabled(logging.Debug) {
switch {
case isPprofEndpoint(r):
// pprof always sends binary data (protobuf)
fields["resp_body"] = "[binary payload]"
case gzipAccepted(r.Header) && isMetricsEndpoint(r):
// metrics endpoint does so when the client accepts it (e.g. prometheus)
fields["resp_body"] = "[compressed payload]"
case gzipAccepted(r.Header) && gzipReceived(w.Header()) && (isDataEndpoint(r) || isCompileEndpoint(r)):
// data and compile endpoints might compress the response
gzReader, gzErr := gzip.NewReader(recorder.buf)
if gzErr == nil {
plainOutput, readErr := io.ReadAll(gzReader)
if readErr == nil {
defer gzReader.Close()
fields["resp_body"] = string(plainOutput)
} else {
h.logger.Error("Failed to decompressed the payload: %v", readErr.Error())
}
} else {
h.logger.Error("Failed to read the compressed payload: %v", gzErr.Error())
}
default:
fields["resp_body"] = recorder.buf.String()
}
}
h.logger.WithFields(fields).Info("Sent response.")
}
}
func gzipAccepted(header http.Header) bool {
a := header.Get("Accept-Encoding")
parts := strings.Split(a, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "gzip" || strings.HasPrefix(part, "gzip;") {
return true
}
}
return false
}
func gzipReceived(header http.Header) bool {
a := header.Get("Content-Encoding")
parts := strings.Split(a, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "gzip" || strings.HasPrefix(part, "gzip;") {
return true
}
}
return false
}
func isPprofEndpoint(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/debug/pprof/")
}
func isMetricsEndpoint(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/metrics")
}
func isDataEndpoint(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/v1/data") || strings.HasPrefix(req.URL.Path, "/v0/data")
}
func isCompileEndpoint(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/v1/compile")
}
type recorder struct {
logger logging.Logger
inner http.ResponseWriter
req *http.Request
id uint64
buf *bytes.Buffer
bytesWritten int
statusCode int
}
func newRecorder(logger logging.Logger, w http.ResponseWriter, r *http.Request, id uint64, buffer bool) *recorder {
var buf *bytes.Buffer
if buffer {
buf = new(bytes.Buffer)
}
return &recorder{
logger: logger,
buf: buf,
inner: w,
req: r,
id: id,
}
}
func (r *recorder) Header() http.Header {
return r.inner.Header()
}
func (r *recorder) Write(bs []byte) (int, error) {
r.bytesWritten += len(bs)
if r.buf != nil {
r.buf.Write(bs)
}
return r.inner.Write(bs)
}
func (r *recorder) WriteHeader(s int) {
r.statusCode = s
r.inner.WriteHeader(s)
}
func readBody(r io.ReadCloser) ([]byte, io.ReadCloser, error) {
if r == http.NoBody {
return nil, r, nil
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(r); err != nil {
return nil, r, err
}
return buf.Bytes(), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
}