Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
279 lines
6.9 KiB
Go
279 lines
6.9 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package logrusx
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
_ "embed"
|
|
"io"
|
|
"maps"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
gelf "github.com/seatgeek/logrus-gelf-formatter"
|
|
|
|
"github.com/ory/x/stringsx"
|
|
)
|
|
|
|
type (
|
|
options struct {
|
|
l *logrus.Logger
|
|
level *logrus.Level
|
|
formatter logrus.Formatter
|
|
format string
|
|
reportCaller bool
|
|
exitFunc func(int)
|
|
leakSensitive bool
|
|
redactionText string
|
|
additionalRedactedHeaders []string
|
|
hooks []logrus.Hook
|
|
c configurator
|
|
}
|
|
Option func(*options)
|
|
nullConfigurator struct{}
|
|
configurator interface {
|
|
Bool(key string) bool
|
|
String(key string) string
|
|
Strings(path string) []string
|
|
}
|
|
)
|
|
|
|
//go:embed config.schema.json
|
|
var ConfigSchema string
|
|
|
|
const ConfigSchemaID = "ory://logging-config"
|
|
|
|
// AddConfigSchema adds the logging schema to the compiler.
|
|
// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version.
|
|
func AddConfigSchema(c interface {
|
|
AddResource(url string, r io.Reader) error
|
|
},
|
|
) error {
|
|
return c.AddResource(ConfigSchemaID, bytes.NewBufferString(ConfigSchema))
|
|
}
|
|
|
|
func newLogger(parent *logrus.Logger, o *options) *logrus.Logger {
|
|
l := parent
|
|
if l == nil {
|
|
l = logrus.New()
|
|
}
|
|
|
|
if o.exitFunc != nil {
|
|
l.ExitFunc = o.exitFunc
|
|
}
|
|
|
|
for _, hook := range o.hooks {
|
|
l.AddHook(hook)
|
|
}
|
|
|
|
setLevel(l, o)
|
|
setFormatter(l, o)
|
|
|
|
l.ReportCaller = o.reportCaller || l.IsLevelEnabled(logrus.TraceLevel)
|
|
return l
|
|
}
|
|
|
|
func setLevel(l *logrus.Logger, o *options) {
|
|
if o.level != nil {
|
|
l.SetLevel(*o.level)
|
|
} else {
|
|
level, err := logrus.ParseLevel(cmp.Or(
|
|
o.c.String("log.level"),
|
|
os.Getenv("LOG_LEVEL")))
|
|
if err != nil {
|
|
level = logrus.InfoLevel
|
|
}
|
|
l.SetLevel(level)
|
|
}
|
|
}
|
|
|
|
func setFormatter(l *logrus.Logger, o *options) {
|
|
if o.formatter != nil {
|
|
l.SetFormatter(o.formatter)
|
|
} else {
|
|
var unknownFormat bool // we first have to set the formatter before we can complain about the unknown format
|
|
var formatter logrus.Formatter
|
|
|
|
format := stringsx.SwitchExact(cmp.Or(o.format, o.c.String("log.format"), os.Getenv("LOG_FORMAT")))
|
|
switch {
|
|
case format.AddCase("json"):
|
|
formatter = &logrus.JSONFormatter{PrettyPrint: false, TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true}
|
|
case format.AddCase("json_pretty"):
|
|
formatter = &logrus.JSONFormatter{PrettyPrint: true, TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true}
|
|
case format.AddCase("gelf"):
|
|
formatter = new(gelf.GelfFormatter)
|
|
default:
|
|
unknownFormat = true
|
|
fallthrough
|
|
case format.AddCase("text", ""):
|
|
formatter = &logrus.TextFormatter{
|
|
DisableQuote: true,
|
|
DisableTimestamp: false,
|
|
FullTimestamp: true,
|
|
}
|
|
}
|
|
|
|
l.SetFormatter(formatter)
|
|
|
|
if unknownFormat {
|
|
l.WithError(format.ToUnknownCaseErr()).Warn("got unknown \"log.format\", falling back to \"text\"")
|
|
}
|
|
}
|
|
}
|
|
|
|
func ForceLevel(level logrus.Level) Option {
|
|
return func(o *options) {
|
|
o.level = &level
|
|
}
|
|
}
|
|
|
|
func ForceFormatter(formatter logrus.Formatter) Option {
|
|
return func(o *options) {
|
|
o.formatter = formatter
|
|
}
|
|
}
|
|
|
|
func WithConfigurator(c configurator) Option {
|
|
return func(o *options) {
|
|
o.c = c
|
|
}
|
|
}
|
|
|
|
func ForceFormat(format string) Option {
|
|
return func(o *options) {
|
|
o.format = format
|
|
}
|
|
}
|
|
|
|
func WithHook(hook logrus.Hook) Option {
|
|
return func(o *options) {
|
|
o.hooks = append(o.hooks, hook)
|
|
}
|
|
}
|
|
|
|
func WithExitFunc(exitFunc func(int)) Option {
|
|
return func(o *options) {
|
|
o.exitFunc = exitFunc
|
|
}
|
|
}
|
|
|
|
func ReportCaller(reportCaller bool) Option {
|
|
return func(o *options) {
|
|
o.reportCaller = reportCaller
|
|
}
|
|
}
|
|
|
|
func UseLogger(l *logrus.Logger) Option {
|
|
return func(o *options) {
|
|
o.l = l
|
|
}
|
|
}
|
|
|
|
func LeakSensitive() Option {
|
|
return func(o *options) {
|
|
o.leakSensitive = true
|
|
}
|
|
}
|
|
|
|
func RedactionText(text string) Option {
|
|
return func(o *options) {
|
|
o.redactionText = text
|
|
}
|
|
}
|
|
|
|
func WithAdditionalRedactedHeaders(headers []string) Option {
|
|
return func(o *options) {
|
|
o.additionalRedactedHeaders = headers
|
|
}
|
|
}
|
|
|
|
func toHeaderMap(headers []string) map[string]struct{} {
|
|
m := make(map[string]struct{}, len(headers))
|
|
for _, h := range headers {
|
|
m[strings.ToLower(h)] = struct{}{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (c *nullConfigurator) Bool(_ string) bool { return false }
|
|
func (c *nullConfigurator) String(_ string) string { return "" }
|
|
func (c *nullConfigurator) Strings(_ string) []string { return []string{} }
|
|
|
|
func newOptions(opts []Option) *options {
|
|
o := new(options)
|
|
o.c = new(nullConfigurator)
|
|
for _, f := range opts {
|
|
f(o)
|
|
}
|
|
return o
|
|
}
|
|
|
|
// New creates a new logger with all the important fields set.
|
|
func New(name string, version string, opts ...Option) *Logger {
|
|
o := newOptions(opts)
|
|
return &Logger{
|
|
opts: opts,
|
|
cfg: &loggerConfig{
|
|
leakSensitive: o.leakSensitive || o.c.Bool("log.leak_sensitive_values"),
|
|
redactionText: cmp.Or(o.redactionText, `Value is sensitive and has been redacted. To see the value set config key "log.leak_sensitive_values = true" or environment variable "LOG_LEAK_SENSITIVE_VALUES=true".`),
|
|
additionalRedactedHeaders: toHeaderMap(func() []string {
|
|
if len(o.additionalRedactedHeaders) > 0 {
|
|
return o.additionalRedactedHeaders
|
|
}
|
|
return o.c.Strings("log.additional_redacted_headers")
|
|
}()),
|
|
},
|
|
Entry: newLogger(o.l, o).WithFields(logrus.Fields{
|
|
"audience": "application", "service_name": name, "service_version": version,
|
|
}),
|
|
}
|
|
}
|
|
|
|
func NewT(t testing.TB, opts ...Option) *Logger {
|
|
opts = append(opts, LeakSensitive(), WithExitFunc(func(code int) {
|
|
t.Fatalf("Logger exited with code %d", code)
|
|
}))
|
|
l := New(t.Name(), "test", opts...)
|
|
l.Logger.SetOutput(t.Output())
|
|
return l
|
|
}
|
|
|
|
func (l *Logger) UseConfig(c configurator) {
|
|
// Update logrus level and formatter first, using their mutex-safe methods,
|
|
// before acquiring our own lock to avoid any lock ordering issues.
|
|
o := newOptions(append(l.opts, WithConfigurator(c)))
|
|
setLevel(l.Entry.Logger, o)
|
|
setFormatter(l.Entry.Logger, o)
|
|
|
|
l.cfg.mu.Lock()
|
|
defer l.cfg.mu.Unlock()
|
|
l.cfg.leakSensitive = l.cfg.leakSensitive || c.Bool("log.leak_sensitive_values")
|
|
l.cfg.redactionText = cmp.Or(c.String("log.redaction_text"), l.cfg.redactionText)
|
|
// Replace the map rather than mutating it in-place so that readers holding
|
|
// a snapshot of the old pointer can iterate it safely without a lock.
|
|
newHeaders := maps.Clone(l.cfg.additionalRedactedHeaders)
|
|
for k := range toHeaderMap(c.Strings("log.additional_redacted_headers")) {
|
|
newHeaders[k] = struct{}{}
|
|
}
|
|
l.cfg.additionalRedactedHeaders = newHeaders
|
|
}
|
|
|
|
func (l *Logger) ReportError(r *http.Request, code int, err error, args ...interface{}) {
|
|
logger := l.WithError(err).WithRequest(r).WithField("http_response", map[string]interface{}{
|
|
"status_code": code,
|
|
})
|
|
switch {
|
|
case code < 500:
|
|
logger.Info(args...)
|
|
default:
|
|
logger.Error(args...)
|
|
}
|
|
}
|