pomerium/internal/controlplane/grpc_accesslog.go
Caleb Doxsey 638d9f3d6c
proxy: add support for logging http request headers (#4388)
* config: add customization options for logging

* config: validate log fields

* proxy: add support for logging http request headers

* log subset of headers

* fix test name

* dont use log.HTTPHeaders for access logs

* canonicalize http/2 headers
2023-07-25 09:46:42 -06:00

90 lines
3.1 KiB
Go

package controlplane
import (
"strings"
envoy_data_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3"
envoy_service_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3"
"github.com/rs/zerolog"
"github.com/pomerium/pomerium/internal/log"
)
func (srv *Server) registerAccessLogHandlers() {
envoy_service_accesslog_v3.RegisterAccessLogServiceServer(srv.GRPCServer, srv)
}
// StreamAccessLogs receives logs from envoy and prints them to stdout.
func (srv *Server) StreamAccessLogs(stream envoy_service_accesslog_v3.AccessLogService_StreamAccessLogsServer) error {
for {
msg, err := stream.Recv()
if err != nil {
log.Error(stream.Context()).Err(err).Msg("access log stream error, disconnecting")
return err
}
for _, entry := range msg.GetHttpLogs().LogEntry {
reqPath := entry.GetRequest().GetPath()
var evt *zerolog.Event
if reqPath == "/ping" || reqPath == "/healthz" {
evt = log.Debug(stream.Context())
} else {
evt = log.Info(stream.Context())
}
evt = evt.Str("service", "envoy")
fields := srv.currentConfig.Load().Config.Options.GetAccessLogFields()
for _, field := range fields {
evt = populateLogEvent(field, evt, entry)
}
// headers are selected in the envoy access logs config, so we can log all of them here
if len(entry.GetRequest().GetRequestHeaders()) > 0 {
evt = evt.Interface("headers", entry.GetRequest().GetRequestHeaders())
}
evt.Msg("http-request")
}
}
}
func populateLogEvent(
field log.AccessLogField,
evt *zerolog.Event,
entry *envoy_data_accesslog_v3.HTTPAccessLogEntry,
) *zerolog.Event {
switch field {
case log.AccessLogFieldAuthority:
return evt.Str(string(field), entry.GetRequest().GetAuthority())
case log.AccessLogFieldDuration:
dur := entry.CommonProperties.TimeToLastDownstreamTxByte.AsDuration()
return evt.Dur(string(field), dur)
case log.AccessLogFieldForwardedFor:
return evt.Str(string(field), entry.GetRequest().GetForwardedFor())
case log.AccessLogFieldMethod:
return evt.Str(string(field), entry.GetRequest().GetRequestMethod().String())
case log.AccessLogFieldPath:
return evt.Str(string(field), stripQueryString(entry.GetRequest().GetPath()))
case log.AccessLogFieldReferer:
return evt.Str(string(field), stripQueryString(entry.GetRequest().GetReferer()))
case log.AccessLogFieldRequestID:
return evt.Str(string(field), entry.GetRequest().GetRequestId())
case log.AccessLogFieldResponseCode:
return evt.Uint32(string(field), entry.GetResponse().GetResponseCode().GetValue())
case log.AccessLogFieldResponseCodeDetails:
return evt.Str(string(field), entry.GetResponse().GetResponseCodeDetails())
case log.AccessLogFieldSize:
return evt.Uint64(string(field), entry.Response.ResponseBodyBytes)
case log.AccessLogFieldUpstreamCluster:
return evt.Str(string(field), entry.GetCommonProperties().GetUpstreamCluster())
case log.AccessLogFieldUserAgent:
return evt.Str(string(field), entry.GetRequest().GetUserAgent())
default:
return evt
}
}
func stripQueryString(str string) string {
if idx := strings.Index(str, "?"); idx != -1 {
str = str[:idx]
}
return str
}