snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
{
"$id": "ory://logging-config",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Log",
"description": "Configure logging using the following options. Logs will always be sent to stdout and stderr.",
"type": "object",
"properties": {
"level": {
"title": "Level",
"description": "The level of log entries to show. Debug enables stack traces on errors.",
"type": "string",
"default": "info",
"enum": ["panic", "fatal", "error", "warn", "info", "debug", "trace"]
},
"format": {
"title": "Log Format",
"description": "The output format of log messages.",
"type": "string",
"default": "text",
"enum": ["json", "json_pretty", "gelf", "text"]
},
"leak_sensitive_values": {
"type": "boolean",
"title": "Leak Sensitive Log Values",
"description": "If set will leak sensitive values (e.g. emails) in the logs.",
"default": false
},
"redaction_text": {
"type": "string",
"title": "Sensitive log value redaction text",
"description": "Text to use, when redacting sensitive log value."
},
"additional_redacted_headers": {
"type": "array",
"title": "Additional redacted headers",
"description": "List of HTTP headers which will be redacted.",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}

293
oryx/logrusx/helper.go Normal file
View File

@@ -0,0 +1,293 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package logrusx
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"reflect"
"strings"
"sync"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
"github.com/ory/x/errorsx"
)
// loggerConfig holds the mutable state that can change after construction via
// UseConfig. It is accessed via a pointer so all shallow copies of Logger
// (WithField, WithFields, WithContext, NewEntry) share and see the same config.
type loggerConfig struct {
mu sync.RWMutex
leakSensitive bool
redactionText string
additionalRedactedHeaders map[string]struct{}
}
type (
Logger struct {
*logrus.Entry
cfg *loggerConfig
opts []Option
}
Provider interface {
Logger() *Logger
}
)
var opts = otelhttptrace.WithPropagators(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
func (l *Logger) Logrus() *logrus.Logger {
return l.Entry.Logger
}
func (l *Logger) NewEntry() *Logger {
ll := *l
ll.Entry = logrus.NewEntry(l.Logger)
return &ll
}
func (l *Logger) WithContext(ctx context.Context) *Logger {
ll := *l
ll.Entry = l.Logger.WithContext(ctx)
return &ll
}
func (l *Logger) HTTPHeadersRedacted(h http.Header) map[string]any {
// Snapshot the mutable config fields once under the read lock so we neither
// hold the lock during iteration nor risk a concurrent map write via UseConfig.
l.cfg.mu.RLock()
leakSensitive := l.cfg.leakSensitive
redactionText := l.cfg.redactionText
additionalHeaders := l.cfg.additionalRedactedHeaders
l.cfg.mu.RUnlock()
headers := map[string]any{}
for key, value := range h {
switch keyLower := strings.ToLower(key); keyLower {
case "authorization", "cookie", "set-cookie", "x-session-token":
headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText)
case "location":
locationURL, err := url.Parse(h.Get("Location"))
if err != nil {
headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText)
continue
}
if leakSensitive {
headers[keyLower] = locationURL.String()
} else {
locationURL.RawQuery = ""
locationURL.Fragment = ""
headers[keyLower] = locationURL.Redacted()
}
default:
if _, ok := additionalHeaders[keyLower]; ok {
headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText)
continue
}
headers[keyLower] = h.Get(key)
}
}
return headers
}
func (l *Logger) WithRequest(r *http.Request) *Logger {
headers := l.HTTPHeadersRedacted(r.Header)
if ua := r.UserAgent(); len(ua) > 0 {
headers["user-agent"] = ua
}
scheme := "https"
if r.TLS == nil {
scheme = "http"
}
ll := l.WithField("http_request", map[string]any{
"remote": r.RemoteAddr,
"method": r.Method,
"path": r.URL.EscapedPath(),
"query": l.maybeRedact(r.URL.RawQuery),
"scheme": scheme,
"host": r.Host,
"headers": headers,
})
spanCtx := trace.SpanContextFromContext(r.Context())
if !spanCtx.IsValid() {
_, _, spanCtx = otelhttptrace.Extract(r.Context(), r, opts)
}
if spanCtx.IsValid() {
traces := make(map[string]string, 2)
if spanCtx.HasTraceID() {
traces["trace_id"] = spanCtx.TraceID().String()
}
if spanCtx.HasSpanID() {
traces["span_id"] = spanCtx.SpanID().String()
}
ll = ll.WithField("otel", traces)
}
return ll
}
func (l *Logger) WithSpanFromContext(ctx context.Context) *Logger {
spanCtx := trace.SpanContextFromContext(ctx)
if !spanCtx.IsValid() {
return l
}
traces := make(map[string]string, 2)
if spanCtx.HasTraceID() {
traces["trace_id"] = spanCtx.TraceID().String()
}
if spanCtx.HasSpanID() {
traces["span_id"] = spanCtx.SpanID().String()
}
return l.WithField("otel", traces)
}
func (l *Logger) Logf(level logrus.Level, format string, args ...any) {
l.cfg.mu.RLock()
leakSensitive := l.cfg.leakSensitive
l.cfg.mu.RUnlock()
if !leakSensitive {
for i, arg := range args {
switch urlArg := arg.(type) {
case url.URL:
urlCopy := url.URL{Scheme: urlArg.Scheme, Host: urlArg.Host, Path: urlArg.Path}
args[i] = urlCopy
case *url.URL:
urlCopy := url.URL{Scheme: urlArg.Scheme, Host: urlArg.Host, Path: urlArg.Path}
args[i] = &urlCopy
default:
continue
}
}
}
l.Entry.Logf(level, format, args...)
}
func (l *Logger) Tracef(format string, args ...any) {
l.Logf(logrus.TraceLevel, format, args...)
}
func (l *Logger) Debugf(format string, args ...any) {
l.Logf(logrus.DebugLevel, format, args...)
}
func (l *Logger) Infof(format string, args ...any) {
l.Logf(logrus.InfoLevel, format, args...)
}
func (l *Logger) Warnf(format string, args ...any) {
l.Logf(logrus.WarnLevel, format, args...)
}
func (l *Logger) Errorf(format string, args ...any) {
l.Logf(logrus.ErrorLevel, format, args...)
}
func (l *Logger) Fatalf(format string, args ...any) {
l.Logf(logrus.FatalLevel, format, args...)
l.Entry.Logger.Exit(1)
}
func (l *Logger) Panicf(format string, args ...any) {
l.Logf(logrus.PanicLevel, format, args...)
}
func (l *Logger) WithFields(f logrus.Fields) *Logger {
ll := *l
ll.Entry = l.Entry.WithFields(f)
return &ll
}
func (l *Logger) WithField(key string, value any) *Logger {
ll := *l
ll.Entry = l.Entry.WithField(key, value)
return &ll
}
// maybeRedactWith is the lock-free redaction helper; callers must supply
// pre-snapshotted values of leakSensitive and redactionText.
func maybeRedactWith(value any, leakSensitive bool, redactionText string) any {
if fmt.Sprintf("%v", value) == "" || value == nil {
return nil
}
if !leakSensitive {
return redactionText
}
return value
}
func (l *Logger) maybeRedact(value any) any {
l.cfg.mu.RLock()
leak := l.cfg.leakSensitive
text := l.cfg.redactionText
l.cfg.mu.RUnlock()
return maybeRedactWith(value, leak, text)
}
func (l *Logger) WithSensitiveField(key string, value any) *Logger {
return l.WithField(key, l.maybeRedact(value))
}
func (l *Logger) WithError(err error) *Logger {
if err == nil {
return l
}
ctx := map[string]any{"message": err.Error()}
if l.Entry.Logger.IsLevelEnabled(logrus.DebugLevel) {
if e, ok := err.(errorsx.StackTracer); ok {
ctx["stack_trace"] = fmt.Sprintf("%+v", e.StackTrace())
} else {
ctx["stack_trace"] = fmt.Sprintf("stack trace could not be recovered from error type %s", reflect.TypeOf(err))
}
}
if c := errorsx.ReasonCarrier(nil); errors.As(err, &c) {
ctx["reason"] = c.Reason()
}
if c := errorsx.RequestIDCarrier(nil); errors.As(err, &c) && c.RequestID() != "" {
ctx["request_id"] = c.RequestID()
}
if c := errorsx.DetailsCarrier(nil); errors.As(err, &c) && c.Details() != nil {
ctx["details"] = c.Details()
}
if c := errorsx.StatusCarrier(nil); errors.As(err, &c) && c.Status() != "" {
ctx["status"] = c.Status()
}
if c := errorsx.StatusCodeCarrier(nil); errors.As(err, &c) && c.StatusCode() != 0 {
ctx["status_code"] = c.StatusCode()
}
if c := errorsx.DebugCarrier(nil); errors.As(err, &c) {
ctx["debug"] = c.Debug()
}
return l.WithField("error", ctx)
}
func (l *Logger) StdLogger(lvl logrus.Level) *log.Logger {
return log.New(writer{l.Logger, lvl}, "", 0)
}
type writer struct {
l *logrus.Logger
lvl logrus.Level
}
func (w writer) Write(p []byte) (n int, err error) {
w.l.Log(w.lvl, strings.TrimSuffix(string(p), "\n"))
return len(p), nil
}

278
oryx/logrusx/logrus.go Normal file
View File

@@ -0,0 +1,278 @@
// 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...)
}
}