feat(logging): add a new flag for preprending a prefix to each field in the logs (close #659)

This commit is contained in:
Julien Neuhart
2023-07-19 22:03:09 +02:00
parent f5ce319c24
commit 9f459867bf
4 changed files with 262 additions and 84 deletions

View File

@@ -48,6 +48,7 @@ CHROMIUM_DISABLE_ROUTES=false
LIBREOFFICE_DISABLES_ROUTES=false LIBREOFFICE_DISABLES_ROUTES=false
LOG_LEVEL=info LOG_LEVEL=info
LOG_FORMAT=auto LOG_FORMAT=auto
LOG_FIELDS_PREFIX=
PDFENGINES_ENGINES= PDFENGINES_ENGINES=
PDFENGINES_DISABLE_ROUTES=false PDFENGINES_DISABLE_ROUTES=false
PROMETHEUS_NAMESPACE=gotenberg PROMETHEUS_NAMESPACE=gotenberg
@@ -94,6 +95,7 @@ run: ## Start a Gotenberg container
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLES_ROUTES) \ --libreoffice-disable-routes=$(LIBREOFFICE_DISABLES_ROUTES) \
--log-level=$(LOG_LEVEL) \ --log-level=$(LOG_LEVEL) \
--log-format=$(LOG_FORMAT) \ --log-format=$(LOG_FORMAT) \
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
--pdfengines-engines=$(PDFENGINES_ENGINES) \ --pdfengines-engines=$(PDFENGINES_ENGINES) \
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \ --pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \ --prometheus-namespace=$(PROMETHEUS_NAMESPACE) \

View File

@@ -2,8 +2,8 @@ package gotenberg
import "go.uber.org/zap" import "go.uber.org/zap"
// LoggerProvider is a module interface which exposes a method for creating a // LoggerProvider is an interface for a module that supplies a method for
// zap.Logger for other modules. // creating a zap.Logger instance for use by other modules.
// //
// func (m *YourModule) Provision(ctx *gotenberg.Context) error { // func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(gotenberg.LoggerProvider)) // provider, _ := ctx.Module(new(gotenberg.LoggerProvider))

View File

@@ -32,8 +32,9 @@ const (
// Logging is a module which implements the gotenberg.LoggerProvider interface. // Logging is a module which implements the gotenberg.LoggerProvider interface.
type Logging struct { type Logging struct {
level string level string
format string format string
fieldsPrefix string
} }
// Descriptor returns a Logging's module descriptor. // Descriptor returns a Logging's module descriptor.
@@ -42,8 +43,9 @@ func (Logging) Descriptor() gotenberg.ModuleDescriptor {
ID: "logging", ID: "logging",
FlagSet: func() *flag.FlagSet { FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("logging", flag.ExitOnError) fs := flag.NewFlagSet("logging", flag.ExitOnError)
fs.String("log-level", infoLoggingLevel, fmt.Sprintf("Set the log level - %s, %s, %s, or %s", errorLoggingLevel, warnLoggingLevel, infoLoggingLevel, debugLoggingLevel)) fs.String("log-level", infoLoggingLevel, fmt.Sprintf("Choose the level of logging detail. Options include %s, %s, %s, or %s", errorLoggingLevel, warnLoggingLevel, infoLoggingLevel, debugLoggingLevel))
fs.String("log-format", autoLoggingFormat, fmt.Sprintf("Set log format - %s, %s, or %s", autoLoggingFormat, jsonLoggingFormat, textLoggingFormat)) fs.String("log-format", autoLoggingFormat, fmt.Sprintf("Specify the format of logging. Options include %s, %s, or %s", autoLoggingFormat, jsonLoggingFormat, textLoggingFormat))
fs.String("log-fields-prefix", "", "Prepend a specified prefix to each field in the logs")
return fs return fs
}(), }(),
@@ -57,6 +59,7 @@ func (log *Logging) Provision(ctx *gotenberg.Context) error {
log.level = flags.MustString("log-level") log.level = flags.MustString("log-level")
log.format = flags.MustString("log-format") log.format = flags.MustString("log-format")
log.fieldsPrefix = flags.MustString("log-fields-prefix")
return nil return nil
} }
@@ -101,29 +104,64 @@ func (log Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
return nil, fmt.Errorf("get log encoder: %w", err) return nil, fmt.Errorf("get log encoder: %w", err)
} }
core := zapcore.NewCore(encoder, os.Stderr, lvl) logger = zap.New(customCore{
logger = zap.New(core) Core: zapcore.NewCore(encoder, os.Stderr, lvl),
fieldsPrefix: log.fieldsPrefix,
// nolint })
defer logger.Sync()
} }
return logger.Named(mod.Descriptor().ID), nil return logger.Named(mod.Descriptor().ID), nil
} }
func newLogLevel(level string) (zapcore.Level, error) { // See https://github.com/gotenberg/gotenberg/issues/659.
switch level { type customCore struct {
case errorLoggingLevel: zapcore.Core
return zap.ErrorLevel, nil fieldsPrefix string
case warnLoggingLevel: }
return zap.WarnLevel, nil
case infoLoggingLevel: func (c customCore) With(fields []zapcore.Field) zapcore.Core {
return zap.InfoLevel, nil if c.fieldsPrefix != "" {
case debugLoggingLevel: for i := range fields {
return zap.DebugLevel, nil fields[i].Key = c.fieldsPrefix + "_" + fields[i].Key
default: }
return -2, fmt.Errorf("%s is not a recognized log level", level)
} }
return customCore{
Core: c.Core.With(fields),
fieldsPrefix: c.fieldsPrefix,
}
}
func (c customCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
// This is a copy from the zapcore.ioCore implementation. Indeed, by doing
// so, we are able to prefix the fields given to the logger methods like
// Debug, Info, Warn, Error, etc.
if c.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}
return ce
}
func (c customCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
if c.fieldsPrefix != "" {
for i := range fields {
fields[i].Key = c.fieldsPrefix + "_" + fields[i].Key
}
}
return c.Core.Write(entry, fields)
}
func newLogLevel(level string) (zapcore.Level, error) {
lvl := zapcore.InvalidLevel
err := lvl.UnmarshalText([]byte(level))
if err != nil {
return lvl, fmt.Errorf("%q is not a recognized log level: %w", level, err)
}
return lvl, nil
} }
func newLogEncoder(format string) (zapcore.Encoder, error) { func newLogEncoder(format string) (zapcore.Encoder, error) {
@@ -158,6 +196,7 @@ func newLogEncoder(format string) (zapcore.Encoder, error) {
} }
} }
// Singleton so that we instantiate our logger only once.
var logger *zap.Logger = nil var logger *zap.Logger = nil
// Interface guards. // Interface guards.
@@ -166,4 +205,5 @@ var (
_ gotenberg.Provisioner = (*Logging)(nil) _ gotenberg.Provisioner = (*Logging)(nil)
_ gotenberg.Validator = (*Logging)(nil) _ gotenberg.Validator = (*Logging)(nil)
_ gotenberg.LoggerProvider = (*Logging)(nil) _ gotenberg.LoggerProvider = (*Logging)(nil)
_ zapcore.Core = (*customCore)(nil)
) )

View File

@@ -1,24 +1,18 @@
package logging package logging
import ( import (
"fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
"go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
) )
type ProtoModule struct {
descriptor func() gotenberg.ModuleDescriptor
}
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
return mod.descriptor()
}
func TestLogging_Descriptor(t *testing.T) { func TestLogging_Descriptor(t *testing.T) {
descriptor := Logging{}.Descriptor() descriptor := Logging{}.Descriptor()
actual := reflect.TypeOf(descriptor.New()) actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Logging)) expect := reflect.TypeOf(new(Logging))
@@ -28,155 +22,302 @@ func TestLogging_Descriptor(t *testing.T) {
} }
func TestLogging_Provision(t *testing.T) { func TestLogging_Provision(t *testing.T) {
logging := new(Logging) for _, tc := range []struct {
fs := logging.Descriptor().FlagSet scenario string
level string
format string
fieldsPrefix string
expectLevel string
expectFormat string
expectFieldsPrefix string
}{
{
scenario: "default values",
expectLevel: infoLoggingLevel,
expectFormat: autoLoggingFormat,
expectFieldsPrefix: "",
},
{
scenario: "explicit values",
level: "debug",
format: "json",
fieldsPrefix: "gotenberg",
expectLevel: debugLoggingLevel,
expectFormat: jsonLoggingFormat,
expectFieldsPrefix: "gotenberg",
},
{
scenario: "wrong values", // no validation at this point.
level: "foo",
format: "foo",
expectLevel: "foo",
expectFormat: "foo",
expectFieldsPrefix: "",
},
} {
var flags []string
err := fs.Parse([]string{""}) if tc.level != "" {
if err != nil { flags = append(flags, "--log-level", tc.level)
t.Fatalf("expected no error but got: %v", err) }
}
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil) if tc.format != "" {
flags = append(flags, "--log-format", tc.format)
}
err = logging.Provision(ctx) if tc.fieldsPrefix != "" {
if err != nil { flags = append(flags, "--log-fields-prefix", tc.fieldsPrefix)
t.Errorf("expected no error but got: %v", err) }
logging := new(Logging)
fs := logging.Descriptor().FlagSet
err := fs.Parse(flags)
if err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
}
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{FlagSet: fs}, nil)
err = logging.Provision(ctx)
if err != nil {
t.Fatalf("%s: expected no error but got: %v", tc.scenario, err)
}
if logging.level != tc.expectLevel {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectLevel, logging.level)
}
if logging.format != tc.expectFormat {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFormat, logging.format)
}
if logging.fieldsPrefix != tc.expectFieldsPrefix {
t.Errorf("%s: expected '%s' but got '%s'", tc.scenario, tc.expectFieldsPrefix, logging.fieldsPrefix)
}
} }
} }
func TestLogging_Validate(t *testing.T) { func TestLogging_Validate(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
level, format string scenario string
expectErr bool level string
format string
expectErr bool
}{ }{
{ {
scenario: "invalid level",
level: "foo", level: "foo",
expectErr: true, expectErr: true,
}, },
{ {
scenario: "invalid format",
level: debugLoggingLevel, level: debugLoggingLevel,
format: "foo", format: "foo",
expectErr: true, expectErr: true,
}, },
{ {
level: debugLoggingLevel, scenario: "valid level and format",
format: autoLoggingFormat, level: debugLoggingLevel,
format: autoLoggingFormat,
}, },
} { } {
mod := new(Logging) logging := new(Logging)
mod.level = tc.level logging.level = tc.level
mod.format = tc.format logging.format = tc.format
err := mod.Validate() err := logging.Validate()
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("%s: expected error but got: %v", tc.scenario, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
} }
} }
} }
func TestLogging_Logger(t *testing.T) { func TestLogging_Logger(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
level, format string scenario string
expectErr bool level string
format string
fieldsPrefix string
expectErr bool
}{ }{
{ {
scenario: "invalid level",
level: "foo", level: "foo",
expectErr: true, expectErr: true,
}, },
{ {
scenario: "invalid format",
level: debugLoggingLevel, level: debugLoggingLevel,
format: "foo", format: "foo",
expectErr: true, expectErr: true,
}, },
{ {
level: debugLoggingLevel, scenario: "valid level and format",
format: autoLoggingFormat, level: debugLoggingLevel,
format: autoLoggingFormat,
}, },
} { } {
mod := new(Logging) logging := new(Logging)
mod.level = tc.level logging.level = tc.level
mod.format = tc.format logging.format = tc.format
logging.fieldsPrefix = tc.fieldsPrefix
_, err := mod.Logger(ProtoModule{ _, err := logging.Logger(gotenberg.ModuleMock{
descriptor: func() gotenberg.ModuleDescriptor { DescriptorMock: func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: nil} return gotenberg.ModuleDescriptor{ID: "mock", New: nil}
}, },
}) })
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("%s: expected error but got: %v", tc.scenario, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
} }
} }
} }
func TestNewLogLevel(t *testing.T) { func TestCustomCore(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
level zapcore.Level
fieldsPrefix string
expectEntry bool
}{
{
scenario: "level enabled",
level: zapcore.DebugLevel,
fieldsPrefix: "gotenberg",
expectEntry: true,
},
{
scenario: "no fields prefix",
level: zapcore.DebugLevel,
expectEntry: true,
},
{
scenario: "level disabled",
level: zapcore.ErrorLevel,
},
} {
core, obsvr := observer.New(tc.level)
lgr := zap.New(customCore{
Core: core,
fieldsPrefix: tc.fieldsPrefix,
}).With(zap.String("a_field", "a value"))
lgr.Debug("a debug message", zap.String("another_field", "another value"))
entries := obsvr.TakeAll()
if tc.expectEntry && len(entries) == 0 {
t.Fatalf("%s: expected an entry", tc.scenario)
}
if !tc.expectEntry && len(entries) != 0 {
t.Fatalf("%s: expected no entry", tc.scenario)
}
var prefix string
if tc.fieldsPrefix != "" {
prefix = tc.fieldsPrefix + "_"
}
for _, entry := range entries {
fields := entry.Context
if len(fields) != 2 {
t.Fatalf("expected 2 fields but got %d", len(fields))
}
if fields[0].Key != fmt.Sprintf("%sa_field", prefix) {
t.Errorf("expected 'gotenberg_a_field' but got '%s'", fields[0].Key)
}
if fields[1].Key != fmt.Sprintf("%sanother_field", prefix) {
t.Errorf("expected 'gotenberg_another_field' but got '%s'", fields[1].Key)
}
}
}
}
func Test_newLogLevel(t *testing.T) {
for _, tc := range []struct {
scenario string
level string level string
expectZapLevel zapcore.Level expectZapLevel zapcore.Level
expectErr bool expectErr bool
}{ }{
{ {
scenario: "error level",
level: errorLoggingLevel, level: errorLoggingLevel,
expectZapLevel: zapcore.ErrorLevel, expectZapLevel: zapcore.ErrorLevel,
}, },
{ {
scenario: "warning level",
level: warnLoggingLevel, level: warnLoggingLevel,
expectZapLevel: zapcore.WarnLevel, expectZapLevel: zapcore.WarnLevel,
}, },
{ {
scenario: "info level",
level: infoLoggingLevel, level: infoLoggingLevel,
expectZapLevel: zapcore.InfoLevel, expectZapLevel: zapcore.InfoLevel,
}, },
{ {
scenario: "debug level",
level: debugLoggingLevel, level: debugLoggingLevel,
expectZapLevel: zapcore.DebugLevel, expectZapLevel: zapcore.DebugLevel,
}, },
{ {
scenario: "invalid level",
level: "foo", level: "foo",
expectZapLevel: -2, expectZapLevel: zapcore.InvalidLevel,
expectErr: true, expectErr: true,
}, },
} { } {
actual, err := newLogLevel(tc.level) actual, err := newLogLevel(tc.level)
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("%s: expected error but got: %v", tc.scenario, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
} }
if tc.expectZapLevel != actual { if tc.expectZapLevel != actual {
t.Errorf("test %d: expected %d level but got %d", i, tc.expectZapLevel, actual) t.Errorf("%s: expected %d level but got %d", tc.scenario, tc.expectZapLevel, actual)
} }
} }
} }
func TestNewLogEncoder(t *testing.T) { func Test_newLogEncoder(t *testing.T) {
for i, tc := range []struct { for _, tc := range []struct {
scenario string
format string format string
expectErr bool expectErr bool
}{ }{
{ {
format: autoLoggingFormat, scenario: "auto format",
format: autoLoggingFormat,
}, },
{ {
format: textLoggingFormat, scenario: "text format",
format: textLoggingFormat,
}, },
{ {
format: jsonLoggingFormat, scenario: "json format",
format: jsonLoggingFormat,
}, },
{ {
scenario: "invalid format",
format: "foo", format: "foo",
expectErr: true, expectErr: true,
}, },
@@ -184,16 +325,11 @@ func TestNewLogEncoder(t *testing.T) {
_, err := newLogEncoder(tc.format) _, err := newLogEncoder(tc.format)
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err) t.Errorf("%s: expected error but got: %v", tc.scenario, err)
} }
if !tc.expectErr && err != nil { if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err) t.Errorf("%s: expected no error but got: %v", tc.scenario, err)
} }
} }
} }
// Interface guards.
var (
_ gotenberg.Module = (*ProtoModule)(nil)
)