mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved Streams allow differentiating logs produced by different components of Zitadel. # How the Problems Are Solved The `backend/v3/instrumentation/logging` package now exposes convenience function for setting and getting a logger from the context. As well as high-level functions to emit log records at various levels. When constructing a new logger a "stream" needs to be specified: - **runtime**: General runtime logs, such as startup and shutdown messages. Default for logs that do not belong to the other categories. - **request**: Logs for incoming API and HTTP requests. - **event_handler**: Logs for event handling in projections. - **queue**: Logs for the job queue processing. - **event_pusher**: Logs for event pushing to the database. Disabled by default, contains sensitive information. Each line from the returned logger contains a `stream` field as well as a `version` field with the current Zitadel version. ## Runtime config Streams can be enabled by passing an array of stream names in the runtime config. Because some log streams may contain sensitive data (especially events), it is now also possible to mask values by their key. # Additional Changes - Wrap `slogctx` in the `logging` package. (Except API error converter packages, because of import cycle) - Add some docs to `logging` package so other devs understand how to add logging to Zitadel - Add `logging.OnError` and `logging.WithError` helper functions with `Panic()` and `Fatal()` methods, to preserve current calls in the `cmd` packages. - Add instance context extractor. - Only output request details in the request info log. Request ID remains propagated through context. - Moved middleware functionality into protocol specific packages. - Removed setting of URI to context in metric middleware. There were ony setters and no getters. (Unused value) - Reuse a single statusWriter in the middleware package for middlewares that need to know the response status. # Additional Context - Closes #11333 - Closes #11331 - Partly #11330 --------- Co-authored-by: Silvan <[email protected]>
222 lines
8.0 KiB
Go
222 lines
8.0 KiB
Go
// Package logging provides utilities for structured logging with context support.
|
|
// It builds on top of slog and slog-context to offer a consistent logging experience
|
|
// across different parts of the application by categorizing logs into different streams.
|
|
//
|
|
// The package uses the global [slog.Default] logger as the base logger,
|
|
// which is configured at application startup to set the desired logging level and output format.
|
|
// It provides functions to create new loggers for specific streams and to
|
|
// add logging capabilities to contexts.
|
|
// Streams are defined using the [Stream] enumeration.
|
|
// Log context can be created using [NewCtx], and loggers can be retrieved from contexts using [FromCtx].
|
|
// Streams are typically initialized at the start of different application components
|
|
// (e.g., request handling, event processing) to ensure that all logs generated within those components
|
|
// are tagged appropriately.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// // Initialize a context for request handling, typically done in middleware
|
|
// ctx := logging.NewCtx(context.Background(), logging.StreamRequest, slog.String("request_id", "12345"))
|
|
// // Somewhere deeper in the call stack
|
|
// logging.Info(ctx, "Something to log")
|
|
//
|
|
// This will produce a log entry with the stream set to "request" and include the request ID.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"os"
|
|
"runtime"
|
|
"time"
|
|
|
|
slogctx "github.com/veqryn/slog-context"
|
|
"github.com/zitadel/sloggcp"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/instrumentation"
|
|
"github.com/zitadel/zitadel/cmd/build"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
// Stream represents a logging stream for categorizing log entries.
|
|
// This is a type alias for [instrumentation.Stream] to expose it in this package.
|
|
type Stream = instrumentation.Stream
|
|
|
|
const (
|
|
StreamRuntime = instrumentation.StreamRuntime // Application runtime logs.
|
|
StreamReady = instrumentation.StreamReady // Readiness and liveness checks.
|
|
StreamRequest = instrumentation.StreamRequest // API request handling.
|
|
StreamEventPusher = instrumentation.StreamEventPusher // Event pushing to the database.
|
|
StreamEventHandler = instrumentation.StreamEventHandler // Event handling and processing.
|
|
StreamQueue = instrumentation.StreamQueue // Queue operations and job processing.
|
|
)
|
|
|
|
var noop = slog.New(slog.DiscardHandler)
|
|
|
|
// New creates a new logger with the given stream and additional arguments.
|
|
func New(stream Stream, args ...any) *slog.Logger {
|
|
if !instrumentation.IsStreamEnabled(stream) {
|
|
return noop
|
|
}
|
|
args = append(args,
|
|
slog.String("stream", stream.String()),
|
|
slog.String("version", build.Version()),
|
|
)
|
|
return slog.Default().With(args...)
|
|
}
|
|
|
|
// NewCtx creates a new context with a logger for the given stream and additional arguments.
|
|
// Use the [FromCtx] or other helpers to retrieve the logger from the context.
|
|
// An existing logger in the context will be replaced.
|
|
func NewCtx(ctx context.Context, stream Stream, args ...any) context.Context {
|
|
logger := New(stream, args...)
|
|
return ToCtx(ctx, logger)
|
|
}
|
|
|
|
// ToCtx adds the given logger to the context.
|
|
// See [slogctx.NewCtx].
|
|
func ToCtx(ctx context.Context, logger *slog.Logger) context.Context {
|
|
return slogctx.NewCtx(ctx, logger)
|
|
}
|
|
|
|
// FromCtx retrieves the logger from the context.
|
|
// See [slogctx.FromCtx].
|
|
func FromCtx(ctx context.Context) *slog.Logger {
|
|
return slogctx.FromCtx(ctx)
|
|
}
|
|
|
|
// With adds the given arguments to the logger in the context.
|
|
// See [slogctx.With].
|
|
func With(ctx context.Context, args ...any) context.Context {
|
|
return slogctx.With(ctx, args...)
|
|
}
|
|
|
|
// WithGroup adds a group to the logger in the context.
|
|
// See [slogctx.WithGroup].
|
|
func WithGroup(ctx context.Context, name string) context.Context {
|
|
return slogctx.WithGroup(ctx, name)
|
|
}
|
|
|
|
// Log logs a message with the given level and arguments using the logger from the context.
|
|
// See [slogctx.Log].
|
|
func Log(ctx context.Context, level slog.Level, msg string, args ...any) {
|
|
log(ctx, FromCtx(ctx), level, msg, 1, args...)
|
|
}
|
|
|
|
// Debug logs a debug message using the logger from the context.
|
|
// See [slogctx.Debug].
|
|
func Debug(ctx context.Context, msg string, args ...any) {
|
|
log(ctx, FromCtx(ctx), slog.LevelDebug, msg, 1, args...)
|
|
}
|
|
|
|
// Info logs an info message using the logger from the context.
|
|
// See [slogctx.Info].
|
|
func Info(ctx context.Context, msg string, args ...any) {
|
|
log(ctx, FromCtx(ctx), slog.LevelInfo, msg, 1, args...)
|
|
}
|
|
|
|
// Warn logs a warning message using the logger from the context.
|
|
// See [slogctx.Warn].
|
|
func Warn(ctx context.Context, msg string, args ...any) {
|
|
log(ctx, FromCtx(ctx), slog.LevelWarn, msg, 1, args...)
|
|
}
|
|
|
|
// Error logs an error message using the logger from the context.
|
|
// See [slogctx.Error].
|
|
func Error(ctx context.Context, msg string, args ...any) {
|
|
log(ctx, FromCtx(ctx), slog.LevelError, msg, 1, args...)
|
|
}
|
|
|
|
// WithError adds an error attribute to the logger from the context and returns the new logger.
|
|
// If the error is not a [zerrors.ZitadelError], it is wrapped in a generic ZitadelError with kind [zerrors.KindUnknown].
|
|
func WithError(ctx context.Context, err error) *ErrorContextLogger {
|
|
var target *zerrors.ZitadelError
|
|
if !errors.As(err, &target) {
|
|
target = zerrors.CreateZitadelError(zerrors.KindUnknown, err, "LOG-Ao5ch", "an unknown error occurred", 1)
|
|
}
|
|
return &ErrorContextLogger{
|
|
ctx: ctx,
|
|
logger: slogctx.FromCtx(ctx).With(slogctx.Err(target)),
|
|
canTerminate: true,
|
|
}
|
|
}
|
|
|
|
// OnError returns a logger that includes the error as an attribute when err is non-nil.
|
|
// If err is nil, it returns a no-op logger.
|
|
// If err is not a [zerrors.ZitadelError], it is wrapped as a generic ZitadelError of kind [zerrors.KindUnknown].
|
|
func OnError(ctx context.Context, err error) *ErrorContextLogger {
|
|
if err == nil {
|
|
return &ErrorContextLogger{ctx, noop, false}
|
|
}
|
|
var target *zerrors.ZitadelError
|
|
if !errors.As(err, &target) {
|
|
target = zerrors.CreateZitadelError(zerrors.KindUnknown, err, "LOG-ii6Pi", "an unknown error occurred", 1)
|
|
}
|
|
return &ErrorContextLogger{
|
|
ctx: ctx,
|
|
logger: slogctx.FromCtx(ctx).With(slogctx.Err(target)),
|
|
canTerminate: true,
|
|
}
|
|
}
|
|
|
|
type ErrorContextLogger struct {
|
|
ctx context.Context
|
|
logger *slog.Logger
|
|
// canTerminate sets whether Panic/Fatal should actually call panic or os.Exit.
|
|
// False when OnError returned a no-op logger, true in all other cases.
|
|
canTerminate bool
|
|
}
|
|
|
|
func (l *ErrorContextLogger) Debug(msg string, args ...any) {
|
|
log(l.ctx, l.logger, slog.LevelDebug, msg, 1, args...)
|
|
}
|
|
|
|
func (l *ErrorContextLogger) Info(msg string, args ...any) {
|
|
log(l.ctx, l.logger, slog.LevelInfo, msg, 1, args...)
|
|
}
|
|
|
|
func (l *ErrorContextLogger) Warn(msg string, args ...any) {
|
|
log(l.ctx, l.logger, slog.LevelWarn, msg, 1, args...)
|
|
}
|
|
|
|
func (l *ErrorContextLogger) Error(msg string, args ...any) {
|
|
log(l.ctx, l.logger, slog.LevelError, msg, 1, args...)
|
|
}
|
|
|
|
// Panic logs a [sloggcp.LevelAlert] leveled message and panics.
|
|
// If the logger was created via [OnError] with a nil error, this method does nothing.
|
|
func (l *ErrorContextLogger) Panic(msg string, args ...any) {
|
|
log(l.ctx, l.logger, sloggcp.LevelAlert, msg, 1, args...)
|
|
if l.canTerminate {
|
|
panic(msg)
|
|
}
|
|
}
|
|
|
|
// Fatal logs a [sloggcp.LevelEmergency] leveled message and exits the application with code 1.
|
|
// If the logger was created via [OnError] with a nil error, this method does nothing.
|
|
func (l *ErrorContextLogger) Fatal(msg string, args ...any) {
|
|
log(l.ctx, l.logger, sloggcp.LevelEmergency, msg, 1, args...)
|
|
if l.canTerminate {
|
|
exit(1)
|
|
}
|
|
}
|
|
|
|
// exit is a variable to allow testing of Fatal without exiting the test process.
|
|
var exit = os.Exit
|
|
|
|
// log is a helper function that logs a message with the given level and arguments using the provided logger.
|
|
func log(ctx context.Context, logger *slog.Logger, level slog.Level, msg string, skip int, args ...any) {
|
|
handler := logger.Handler()
|
|
if !handler.Enabled(ctx, level) {
|
|
return
|
|
}
|
|
var pcs [1]uintptr
|
|
if instrumentation.IsAddSourceEnabled() {
|
|
runtime.Callers(skip+2, pcs[:])
|
|
|
|
}
|
|
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
|
|
r.Add(args...)
|
|
_ = logger.Handler().Handle(ctx, r)
|
|
}
|