mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 22:23:48 +03:00
caddyhttp: Add 'skip_log' var to omit request from logs (#4691)
* caddyhttp: Implement `skip_log` handler * Refactor to use vars middleware Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This commit is contained in:
parent
a1ad20e472
commit
9ad0ebc956
5 changed files with 195 additions and 116 deletions
|
@ -48,6 +48,7 @@ func init() {
|
||||||
RegisterHandlerDirective("handle", parseHandle)
|
RegisterHandlerDirective("handle", parseHandle)
|
||||||
RegisterDirective("handle_errors", parseHandleErrors)
|
RegisterDirective("handle_errors", parseHandleErrors)
|
||||||
RegisterDirective("log", parseLog)
|
RegisterDirective("log", parseLog)
|
||||||
|
RegisterHandlerDirective("skip_log", parseSkipLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseBind parses the bind directive. Syntax:
|
// parseBind parses the bind directive. Syntax:
|
||||||
|
@ -858,3 +859,15 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||||
}
|
}
|
||||||
return configValues, nil
|
return configValues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSkipLog parses the skip_log directive. Syntax:
|
||||||
|
//
|
||||||
|
// skip_log [<matcher>]
|
||||||
|
func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
for h.Next() {
|
||||||
|
if h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caddyhttp.VarsMiddleware{"skip_log": true}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ var directiveOrder = []string{
|
||||||
"map",
|
"map",
|
||||||
"vars",
|
"vars",
|
||||||
"root",
|
"root",
|
||||||
|
"skip_log",
|
||||||
|
|
||||||
"header",
|
"header",
|
||||||
"copy_response_headers", // only in reverse_proxy's handle_response
|
"copy_response_headers", // only in reverse_proxy's handle_response
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
http://localhost:2020 {
|
http://localhost:2020 {
|
||||||
log
|
log
|
||||||
|
skip_log /first-hidden*
|
||||||
|
skip_log /second-hidden*
|
||||||
respond 200
|
respond 200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +30,36 @@ http://localhost:2020 {
|
||||||
{
|
{
|
||||||
"handler": "subroute",
|
"handler": "subroute",
|
||||||
"routes": [
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"skip_log": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/second-hidden*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars",
|
||||||
|
"skip_log": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/first-hidden*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
|
|
144
modules/caddyhttp/logging.go
Normal file
144
modules/caddyhttp/logging.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// Copyright 2015 Matthew Holt and The Caddy 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
|
||||||
|
//
|
||||||
|
// http://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 caddyhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerLogConfig describes a server's logging configuration. If
|
||||||
|
// enabled without customization, all requests to this server are
|
||||||
|
// logged to the default logger; logger destinations may be
|
||||||
|
// customized per-request-host.
|
||||||
|
type ServerLogConfig struct {
|
||||||
|
// The default logger name for all logs emitted by this server for
|
||||||
|
// hostnames that are not in the LoggerNames (logger_names) map.
|
||||||
|
DefaultLoggerName string `json:"default_logger_name,omitempty"`
|
||||||
|
|
||||||
|
// LoggerNames maps request hostnames to a custom logger name.
|
||||||
|
// For example, a mapping of "example.com" to "example" would
|
||||||
|
// cause access logs from requests with a Host of example.com
|
||||||
|
// to be emitted by a logger named "http.log.access.example".
|
||||||
|
LoggerNames map[string]string `json:"logger_names,omitempty"`
|
||||||
|
|
||||||
|
// By default, all requests to this server will be logged if
|
||||||
|
// access logging is enabled. This field lists the request
|
||||||
|
// hosts for which access logging should be disabled.
|
||||||
|
SkipHosts []string `json:"skip_hosts,omitempty"`
|
||||||
|
|
||||||
|
// If true, requests to any host not appearing in the
|
||||||
|
// LoggerNames (logger_names) map will not be logged.
|
||||||
|
SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`
|
||||||
|
|
||||||
|
// If true, credentials that are otherwise omitted, will be logged.
|
||||||
|
// The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials,
|
||||||
|
// and this includes some request and response headers, i.e `Cookie`,
|
||||||
|
// `Set-Cookie`, `Authorization`, and `Proxy-Authorization`.
|
||||||
|
ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapLogger wraps logger in a logger named according to user preferences for the given host.
|
||||||
|
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger {
|
||||||
|
if loggerName := slc.getLoggerName(host); loggerName != "" {
|
||||||
|
return logger.Named(loggerName)
|
||||||
|
}
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slc ServerLogConfig) getLoggerName(host string) string {
|
||||||
|
tryHost := func(key string) (string, bool) {
|
||||||
|
// first try exact match
|
||||||
|
if loggerName, ok := slc.LoggerNames[key]; ok {
|
||||||
|
return loggerName, ok
|
||||||
|
}
|
||||||
|
// strip port and try again (i.e. Host header of "example.com:1234" should
|
||||||
|
// match "example.com" if there is no "example.com:1234" in the map)
|
||||||
|
hostOnly, _, err := net.SplitHostPort(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
loggerName, ok := slc.LoggerNames[hostOnly]
|
||||||
|
return loggerName, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// try the exact hostname first
|
||||||
|
if loggerName, ok := tryHost(host); ok {
|
||||||
|
return loggerName
|
||||||
|
}
|
||||||
|
|
||||||
|
// try matching wildcard domains if other non-specific loggers exist
|
||||||
|
labels := strings.Split(host, ".")
|
||||||
|
for i := range labels {
|
||||||
|
if labels[i] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
labels[i] = "*"
|
||||||
|
wildcardHost := strings.Join(labels, ".")
|
||||||
|
if loggerName, ok := tryHost(wildcardHost); ok {
|
||||||
|
return loggerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slc.DefaultLoggerName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slc *ServerLogConfig) clone() *ServerLogConfig {
|
||||||
|
clone := &ServerLogConfig{
|
||||||
|
DefaultLoggerName: slc.DefaultLoggerName,
|
||||||
|
LoggerNames: make(map[string]string),
|
||||||
|
SkipHosts: append([]string{}, slc.SkipHosts...),
|
||||||
|
SkipUnmappedHosts: slc.SkipUnmappedHosts,
|
||||||
|
ShouldLogCredentials: slc.ShouldLogCredentials,
|
||||||
|
}
|
||||||
|
for k, v := range slc.LoggerNames {
|
||||||
|
clone.LoggerNames[k] = v
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// errLogValues inspects err and returns the status code
|
||||||
|
// to use, the error log message, and any extra fields.
|
||||||
|
// If err is a HandlerError, the returned values will
|
||||||
|
// have richer information.
|
||||||
|
func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
|
||||||
|
var handlerErr HandlerError
|
||||||
|
if errors.As(err, &handlerErr) {
|
||||||
|
status = handlerErr.StatusCode
|
||||||
|
if handlerErr.Err == nil {
|
||||||
|
msg = err.Error()
|
||||||
|
} else {
|
||||||
|
msg = handlerErr.Err.Error()
|
||||||
|
}
|
||||||
|
fields = []zapcore.Field{
|
||||||
|
zap.Int("status", handlerErr.StatusCode),
|
||||||
|
zap.String("err_id", handlerErr.ID),
|
||||||
|
zap.String("err_trace", handlerErr.Trace),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status = http.StatusInternalServerError
|
||||||
|
msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable name used to indicate that this request
|
||||||
|
// should be omitted from the access logs
|
||||||
|
const SkipLogVar = "skip_log"
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -226,6 +225,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
accLog := s.accessLogger.With(loggableReq)
|
accLog := s.accessLogger.With(loggableReq)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
// this request may be flagged as omitted from the logs
|
||||||
|
if skipLog, ok := GetVar(r.Context(), SkipLogVar).(bool); ok && skipLog {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
repl.Set("http.response.status", wrec.Status())
|
repl.Set("http.response.status", wrec.Status())
|
||||||
repl.Set("http.response.size", wrec.Size())
|
repl.Set("http.response.size", wrec.Size())
|
||||||
repl.Set("http.response.duration", duration)
|
repl.Set("http.response.duration", duration)
|
||||||
|
@ -592,96 +596,6 @@ func (s *Server) protocol(proto string) bool {
|
||||||
// EXPERIMENTAL: Subject to change or removal.
|
// EXPERIMENTAL: Subject to change or removal.
|
||||||
func (s *Server) Listeners() []net.Listener { return s.listeners }
|
func (s *Server) Listeners() []net.Listener { return s.listeners }
|
||||||
|
|
||||||
// ServerLogConfig describes a server's logging configuration. If
|
|
||||||
// enabled without customization, all requests to this server are
|
|
||||||
// logged to the default logger; logger destinations may be
|
|
||||||
// customized per-request-host.
|
|
||||||
type ServerLogConfig struct {
|
|
||||||
// The default logger name for all logs emitted by this server for
|
|
||||||
// hostnames that are not in the LoggerNames (logger_names) map.
|
|
||||||
DefaultLoggerName string `json:"default_logger_name,omitempty"`
|
|
||||||
|
|
||||||
// LoggerNames maps request hostnames to a custom logger name.
|
|
||||||
// For example, a mapping of "example.com" to "example" would
|
|
||||||
// cause access logs from requests with a Host of example.com
|
|
||||||
// to be emitted by a logger named "http.log.access.example".
|
|
||||||
LoggerNames map[string]string `json:"logger_names,omitempty"`
|
|
||||||
|
|
||||||
// By default, all requests to this server will be logged if
|
|
||||||
// access logging is enabled. This field lists the request
|
|
||||||
// hosts for which access logging should be disabled.
|
|
||||||
SkipHosts []string `json:"skip_hosts,omitempty"`
|
|
||||||
|
|
||||||
// If true, requests to any host not appearing in the
|
|
||||||
// LoggerNames (logger_names) map will not be logged.
|
|
||||||
SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`
|
|
||||||
|
|
||||||
// If true, credentials that are otherwise omitted, will be logged.
|
|
||||||
// The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials,
|
|
||||||
// and this includes some request and response headers, i.e `Cookie`,
|
|
||||||
// `Set-Cookie`, `Authorization`, and `Proxy-Authorization`.
|
|
||||||
ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapLogger wraps logger in a logger named according to user preferences for the given host.
|
|
||||||
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) *zap.Logger {
|
|
||||||
if loggerName := slc.getLoggerName(host); loggerName != "" {
|
|
||||||
return logger.Named(loggerName)
|
|
||||||
}
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (slc ServerLogConfig) getLoggerName(host string) string {
|
|
||||||
tryHost := func(key string) (string, bool) {
|
|
||||||
// first try exact match
|
|
||||||
if loggerName, ok := slc.LoggerNames[key]; ok {
|
|
||||||
return loggerName, ok
|
|
||||||
}
|
|
||||||
// strip port and try again (i.e. Host header of "example.com:1234" should
|
|
||||||
// match "example.com" if there is no "example.com:1234" in the map)
|
|
||||||
hostOnly, _, err := net.SplitHostPort(key)
|
|
||||||
if err != nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
loggerName, ok := slc.LoggerNames[hostOnly]
|
|
||||||
return loggerName, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// try the exact hostname first
|
|
||||||
if loggerName, ok := tryHost(host); ok {
|
|
||||||
return loggerName
|
|
||||||
}
|
|
||||||
|
|
||||||
// try matching wildcard domains if other non-specific loggers exist
|
|
||||||
labels := strings.Split(host, ".")
|
|
||||||
for i := range labels {
|
|
||||||
if labels[i] == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
labels[i] = "*"
|
|
||||||
wildcardHost := strings.Join(labels, ".")
|
|
||||||
if loggerName, ok := tryHost(wildcardHost); ok {
|
|
||||||
return loggerName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return slc.DefaultLoggerName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (slc *ServerLogConfig) clone() *ServerLogConfig {
|
|
||||||
clone := &ServerLogConfig{
|
|
||||||
DefaultLoggerName: slc.DefaultLoggerName,
|
|
||||||
LoggerNames: make(map[string]string),
|
|
||||||
SkipHosts: append([]string{}, slc.SkipHosts...),
|
|
||||||
SkipUnmappedHosts: slc.SkipUnmappedHosts,
|
|
||||||
ShouldLogCredentials: slc.ShouldLogCredentials,
|
|
||||||
}
|
|
||||||
for k, v := range slc.LoggerNames {
|
|
||||||
clone.LoggerNames[k] = v
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareRequest fills the request r for use in a Caddy HTTP handler chain. w and s can
|
// PrepareRequest fills the request r for use in a Caddy HTTP handler chain. w and s can
|
||||||
// be nil, but the handlers will lose response placeholders and access to the server.
|
// be nil, but the handlers will lose response placeholders and access to the server.
|
||||||
func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter, s *Server) *http.Request {
|
func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter, s *Server) *http.Request {
|
||||||
|
@ -701,31 +615,6 @@ func PrepareRequest(r *http.Request, repl *caddy.Replacer, w http.ResponseWriter
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// errLogValues inspects err and returns the status code
|
|
||||||
// to use, the error log message, and any extra fields.
|
|
||||||
// If err is a HandlerError, the returned values will
|
|
||||||
// have richer information.
|
|
||||||
func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
|
|
||||||
var handlerErr HandlerError
|
|
||||||
if errors.As(err, &handlerErr) {
|
|
||||||
status = handlerErr.StatusCode
|
|
||||||
if handlerErr.Err == nil {
|
|
||||||
msg = err.Error()
|
|
||||||
} else {
|
|
||||||
msg = handlerErr.Err.Error()
|
|
||||||
}
|
|
||||||
fields = []zapcore.Field{
|
|
||||||
zap.Int("status", handlerErr.StatusCode),
|
|
||||||
zap.String("err_id", handlerErr.ID),
|
|
||||||
zap.String("err_trace", handlerErr.Trace),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
status = http.StatusInternalServerError
|
|
||||||
msg = err.Error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// originalRequest returns a partial, shallow copy of
|
// originalRequest returns a partial, shallow copy of
|
||||||
// req, including: req.Method, deep copy of req.URL
|
// req, including: req.Method, deep copy of req.URL
|
||||||
// (into the urlCopy parameter, which should be on the
|
// (into the urlCopy parameter, which should be on the
|
||||||
|
|
Loading…
Reference in a new issue