caddy/modules/logging/encoders.go
Matt Holt b00dfd3965
v2: Logging! (#2831)
* logging: Initial implementation

* logging: More encoder formats, better defaults

* logging: Fix repetition bug with FilterEncoder; add more presets

* logging: DiscardWriter; delete or no-op logs that discard their output

* logging: Add http.handlers.log module; enhance Replacer methods

The Replacer interface has new methods to customize how to handle empty
or unrecognized placeholders. Closes #2815.

* logging: Overhaul HTTP logging, fix bugs, improve filtering, etc.

* logging: General cleanup, begin transitioning to using new loggers

* Fixes after merge conflict
2019-10-28 14:39:37 -06:00

268 lines
7.6 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 logging
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/caddyserver/caddy/v2"
zaplogfmt "github.com/jsternberg/zap-logfmt"
"go.uber.org/zap"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
func init() {
caddy.RegisterModule(ConsoleEncoder{})
caddy.RegisterModule(JSONEncoder{})
caddy.RegisterModule(LogfmtEncoder{})
caddy.RegisterModule(StringEncoder{})
}
// ConsoleEncoder encodes log entries that are mostly human-readable.
type ConsoleEncoder struct {
zapcore.Encoder
LogEncoderConfig
}
// CaddyModule returns the Caddy module information.
func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.console",
New: func() caddy.Module { return new(ConsoleEncoder) },
}
}
// Provision sets up the encoder.
func (ce *ConsoleEncoder) Provision(_ caddy.Context) error {
ce.Encoder = zapcore.NewConsoleEncoder(ce.ZapcoreEncoderConfig())
return nil
}
// JSONEncoder encodes entries as JSON.
type JSONEncoder struct {
zapcore.Encoder
*LogEncoderConfig
}
// CaddyModule returns the Caddy module information.
func (JSONEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.json",
New: func() caddy.Module { return new(JSONEncoder) },
}
}
// Provision sets up the encoder.
func (je *JSONEncoder) Provision(_ caddy.Context) error {
je.Encoder = zapcore.NewJSONEncoder(je.ZapcoreEncoderConfig())
return nil
}
// LogfmtEncoder encodes log entries as logfmt:
// https://www.brandur.org/logfmt
type LogfmtEncoder struct {
zapcore.Encoder
LogEncoderConfig
}
// CaddyModule returns the Caddy module information.
func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.logfmt",
New: func() caddy.Module { return new(LogfmtEncoder) },
}
}
// Provision sets up the encoder.
func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error {
lfe.Encoder = zaplogfmt.NewEncoder(lfe.ZapcoreEncoderConfig())
return nil
}
// StringEncoder writes a log entry that consists entirely
// of a single string field in the log entry. This is useful
// for custom, self-encoded log entries that consist of a
// single field in the structured log entry.
type StringEncoder struct {
zapcore.Encoder
FieldName string `json:"field,omitempty"`
FallbackRaw json.RawMessage `json:"fallback,omitempty"`
}
// CaddyModule returns the Caddy module information.
func (StringEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
Name: "caddy.logging.encoders.string",
New: func() caddy.Module { return new(StringEncoder) },
}
}
// Provision sets up the encoder.
func (se *StringEncoder) Provision(ctx caddy.Context) error {
if se.FallbackRaw != nil {
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", se.FallbackRaw)
if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err)
}
se.FallbackRaw = nil // allow GC to deallocate
se.Encoder = val.(zapcore.Encoder)
}
if se.Encoder == nil {
se.Encoder = nopEncoder{}
}
return nil
}
// Clone wraps the underlying encoder's Clone. This is
// necessary because we implement our own EncodeEntry,
// and if we simply let the embedded encoder's Clone
// be promoted, it would return a clone of that, and
// we'd lose our StringEncoder's EncodeEntry.
func (se StringEncoder) Clone() zapcore.Encoder {
return StringEncoder{
Encoder: se.Encoder.Clone(),
FieldName: se.FieldName,
}
}
// EncodeEntry partially implements the zapcore.Encoder interface.
func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
for _, f := range fields {
if f.Key == se.FieldName {
buf := bufferpool.Get()
buf.AppendString(f.String)
if !strings.HasSuffix(f.String, "\n") {
buf.AppendByte('\n')
}
return buf, nil
}
}
if se.Encoder == nil {
return nil, fmt.Errorf("no fallback encoder defined")
}
return se.Encoder.EncodeEntry(ent, fields)
}
// LogEncoderConfig holds configuration common to most encoders.
type LogEncoderConfig struct {
MessageKey *string `json:"message_key,omitempty"`
LevelKey *string `json:"level_key,omitempty"`
TimeKey *string `json:"time_key,omitempty"`
NameKey *string `json:"name_key,omitempty"`
CallerKey *string `json:"caller_key,omitempty"`
StacktraceKey *string `json:"stacktrace_key,omitempty"`
LineEnding *string `json:"line_ending,omitempty"`
TimeFormat string `json:"time_format,omitempty"`
DurationFormat string `json:"duration_format,omitempty"`
LevelFormat string `json:"level_format,omitempty"`
}
// ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig.
// If lec is nil, zap.NewProductionEncoderConfig() is returned.
func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
cfg := zap.NewProductionEncoderConfig()
if lec == nil {
lec = new(LogEncoderConfig)
}
if lec.MessageKey != nil {
cfg.MessageKey = *lec.MessageKey
}
if lec.TimeKey != nil {
cfg.TimeKey = *lec.TimeKey
}
if lec.NameKey != nil {
cfg.NameKey = *lec.NameKey
}
if lec.CallerKey != nil {
cfg.CallerKey = *lec.CallerKey
}
if lec.StacktraceKey != nil {
cfg.StacktraceKey = *lec.StacktraceKey
}
if lec.LineEnding != nil {
cfg.LineEnding = *lec.LineEnding
}
// time format
var timeFormatter zapcore.TimeEncoder
switch lec.TimeFormat {
case "", "unix_seconds_float":
timeFormatter = zapcore.EpochTimeEncoder
case "unix_milli_float":
timeFormatter = zapcore.EpochMillisTimeEncoder
case "unix_nano":
timeFormatter = zapcore.EpochNanosTimeEncoder
case "iso8601":
timeFormatter = zapcore.ISO8601TimeEncoder
default:
timeFormat := lec.TimeFormat
switch lec.TimeFormat {
case "rfc3339":
timeFormat = time.RFC3339
case "rfc3339_nano":
timeFormat = time.RFC3339Nano
case "wall":
timeFormat = "2006/01/02 15:04:05"
case "wall_milli":
timeFormat = "2006/01/02 15:04:05.000"
case "wall_nano":
timeFormat = "2006/01/02 15:04:05.000000000"
}
timeFormatter = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(ts.UTC().Format(timeFormat))
}
}
cfg.EncodeTime = timeFormatter
// duration format
var durFormatter zapcore.DurationEncoder
switch lec.DurationFormat {
case "", "seconds":
durFormatter = zapcore.SecondsDurationEncoder
case "nano":
durFormatter = zapcore.NanosDurationEncoder
case "string":
durFormatter = zapcore.StringDurationEncoder
}
cfg.EncodeDuration = durFormatter
// level format
var levelFormatter zapcore.LevelEncoder
switch lec.LevelFormat {
case "", "lower":
levelFormatter = zapcore.LowercaseLevelEncoder
case "upper":
levelFormatter = zapcore.CapitalLevelEncoder
case "color":
levelFormatter = zapcore.CapitalColorLevelEncoder
}
cfg.EncodeLevel = levelFormatter
return cfg
}
var bufferpool = buffer.NewPool()
// Interface guards
var (
_ zapcore.Encoder = (*ConsoleEncoder)(nil)
_ zapcore.Encoder = (*JSONEncoder)(nil)
_ zapcore.Encoder = (*LogfmtEncoder)(nil)
_ zapcore.Encoder = (*StringEncoder)(nil)
)