mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-25 17:56:34 +08:00
70953e873a
* caddyhttp: Support multiple logger names per host * Lint * Add adapt test * Implement "string or array" parsing, keep original `logger_names` * Rewrite adapter test to be more representative of the usecase
215 lines
6.4 KiB
Go
215 lines
6.4 KiB
Go
// 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
)
|
|
|
|
// 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 logger_names map.
|
|
DefaultLoggerName string `json:"default_logger_name,omitempty"`
|
|
|
|
// LoggerNames maps request hostnames to one or more custom logger
|
|
// names. 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". If there are
|
|
// multiple logger names, then the log will be emitted to all of them.
|
|
// For backwards compatibility, if the value is a string, it is treated
|
|
// as a single-element array.
|
|
LoggerNames map[string]StringArray `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
|
|
// 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 one or more logger named
|
|
// according to user preferences for the given host.
|
|
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, host string) []*zap.Logger {
|
|
hosts := slc.getLoggerHosts(host)
|
|
loggers := make([]*zap.Logger, 0, len(hosts))
|
|
for _, loggerName := range hosts {
|
|
if loggerName == "" {
|
|
continue
|
|
}
|
|
loggers = append(loggers, logger.Named(loggerName))
|
|
}
|
|
return loggers
|
|
}
|
|
|
|
func (slc ServerLogConfig) getLoggerHosts(host string) []string {
|
|
tryHost := func(key string) ([]string, bool) {
|
|
// first try exact match
|
|
if hosts, ok := slc.LoggerNames[key]; ok {
|
|
return hosts, 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 []string{}, false
|
|
}
|
|
hosts, ok := slc.LoggerNames[hostOnly]
|
|
return hosts, ok
|
|
}
|
|
|
|
// try the exact hostname first
|
|
if hosts, ok := tryHost(host); ok {
|
|
return hosts
|
|
}
|
|
|
|
// 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 hosts, ok := tryHost(wildcardHost); ok {
|
|
return hosts
|
|
}
|
|
}
|
|
|
|
return []string{slc.DefaultLoggerName}
|
|
}
|
|
|
|
func (slc *ServerLogConfig) clone() *ServerLogConfig {
|
|
clone := &ServerLogConfig{
|
|
DefaultLoggerName: slc.DefaultLoggerName,
|
|
LoggerNames: make(map[string]StringArray),
|
|
SkipHosts: append([]string{}, slc.SkipHosts...),
|
|
SkipUnmappedHosts: slc.SkipUnmappedHosts,
|
|
ShouldLogCredentials: slc.ShouldLogCredentials,
|
|
}
|
|
for k, v := range slc.LoggerNames {
|
|
clone.LoggerNames[k] = append([]string{}, v...)
|
|
}
|
|
return clone
|
|
}
|
|
|
|
// StringArray is a slices of strings, but also accepts
|
|
// a single string as a value when JSON unmarshaling,
|
|
// converting it to a slice of one string.
|
|
type StringArray []string
|
|
|
|
// UnmarshalJSON satisfies json.Unmarshaler.
|
|
func (sa *StringArray) UnmarshalJSON(b []byte) error {
|
|
var jsonObj any
|
|
err := json.Unmarshal(b, &jsonObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch obj := jsonObj.(type) {
|
|
case string:
|
|
*sa = StringArray([]string{obj})
|
|
return nil
|
|
case []any:
|
|
s := make([]string, 0, len(obj))
|
|
for _, v := range obj {
|
|
value, ok := v.(string)
|
|
if !ok {
|
|
return errors.New("unsupported type")
|
|
}
|
|
s = append(s, value)
|
|
}
|
|
*sa = StringArray(s)
|
|
return nil
|
|
}
|
|
return errors.New("unsupported type")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// ExtraLogFields is a list of extra fields to log with every request.
|
|
type ExtraLogFields struct {
|
|
fields []zapcore.Field
|
|
}
|
|
|
|
// Add adds a field to the list of extra fields to log.
|
|
func (e *ExtraLogFields) Add(field zap.Field) {
|
|
e.fields = append(e.fields, field)
|
|
}
|
|
|
|
// Set sets a field in the list of extra fields to log.
|
|
// If the field already exists, it is replaced.
|
|
func (e *ExtraLogFields) Set(field zap.Field) {
|
|
for i := range e.fields {
|
|
if e.fields[i].Key == field.Key {
|
|
e.fields[i] = field
|
|
return
|
|
}
|
|
}
|
|
e.fields = append(e.fields, field)
|
|
}
|
|
|
|
const (
|
|
// Variable name used to indicate that this request
|
|
// should be omitted from the access logs
|
|
LogSkipVar string = "log_skip"
|
|
|
|
// For adding additional fields to the access logs
|
|
ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields"
|
|
)
|