// 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": ["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. // If the logger name is an empty, the default logger is used, i.e. // the logger "http.log.access". // // Keys must be hostnames (without ports), and may contain wildcards // to match subdomains. The value is an array of logger names. // // 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, req *http.Request) []*zap.Logger { // using the `log_name` directive or the `access_logger_names` variable, // the logger names can be overridden for the current request if names := GetVar(req.Context(), AccessLoggerNameVarKey); names != nil { if namesSlice, ok := names.([]any); ok { loggers := make([]*zap.Logger, 0, len(namesSlice)) for _, loggerName := range namesSlice { // no name, use the default logger if loggerName == "" { loggers = append(loggers, logger) continue } // make a logger with the given name loggers = append(loggers, logger.Named(loggerName.(string))) } return loggers } } // get the hostname from the request, with the port number stripped host, _, err := net.SplitHostPort(req.Host) if err != nil { host = req.Host } // get the logger names for this host from the config hosts := slc.getLoggerHosts(host) // make a list of named loggers, or the default logger loggers := make([]*zap.Logger, 0, len(hosts)) for _, loggerName := range hosts { // no name, use the default logger if loggerName == "" { loggers = append(loggers, logger) continue } // make a logger with the given name loggers = append(loggers, logger.Named(loggerName)) } return loggers } func (slc ServerLogConfig) getLoggerHosts(host string) []string { // try the exact hostname first if hosts, ok := slc.LoggerNames[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 := slc.LoggerNames[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" // Variable name used to indicate the logger to be used AccessLoggerNameVarKey string = "access_logger_names" )