diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f67d499d3..c3915e55d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,7 +33,7 @@ }, "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.24" + "version": "1.25" }, "ghcr.io/devcontainers/features/docker-outside-of-docker": {}, "ghcr.io/schlich/devcontainer-features/cypress:1": { diff --git a/backend/v3/instrumentation/logformat_enumer.go b/backend/v3/instrumentation/logformat_enumer.go index 0e6081534..5ee05de16 100644 --- a/backend/v3/instrumentation/logformat_enumer.go +++ b/backend/v3/instrumentation/logformat_enumer.go @@ -1,4 +1,4 @@ -// Code generated by "enumer -type=LogFormat -trimprefix=LogFormat -text -linecomment"; DO NOT EDIT. +// Code generated by "enumer -type=LogFormat -trimprefix=LogFormat -text -linecomment -transform=snake"; DO NOT EDIT. package instrumentation @@ -7,11 +7,11 @@ import ( "strings" ) -const _LogFormatName = "DisabledTextJSONGCP" +const _LogFormatName = "disabledtextjsongcpgcp_error_reporting" -var _LogFormatIndex = [...]uint8{0, 0, 8, 12, 16, 19} +var _LogFormatIndex = [...]uint8{0, 0, 8, 12, 16, 19, 38} -const _LogFormatLowerName = "disabledtextjsongcp" +const _LogFormatLowerName = "disabledtextjsongcpgcp_error_reporting" func (i LogFormat) String() string { if i < 0 || i >= LogFormat(len(_LogFormatIndex)-1) { @@ -29,9 +29,10 @@ func _LogFormatNoOp() { _ = x[LogFormatText-(2)] _ = x[LogFormatJSON-(3)] _ = x[LogFormatGCP-(4)] + _ = x[LogFormatGCPErrorReporting-(5)] } -var _LogFormatValues = []LogFormat{LogFormatUndefined, LogFormatDisabled, LogFormatText, LogFormatJSON, LogFormatGCP} +var _LogFormatValues = []LogFormat{LogFormatUndefined, LogFormatDisabled, LogFormatText, LogFormatJSON, LogFormatGCP, LogFormatGCPErrorReporting} var _LogFormatNameToValueMap = map[string]LogFormat{ _LogFormatName[0:0]: LogFormatUndefined, @@ -44,6 +45,8 @@ var _LogFormatNameToValueMap = map[string]LogFormat{ _LogFormatLowerName[12:16]: LogFormatJSON, _LogFormatName[16:19]: LogFormatGCP, _LogFormatLowerName[16:19]: LogFormatGCP, + _LogFormatName[19:38]: LogFormatGCPErrorReporting, + _LogFormatLowerName[19:38]: LogFormatGCPErrorReporting, } var _LogFormatNames = []string{ @@ -52,6 +55,7 @@ var _LogFormatNames = []string{ _LogFormatName[8:12], _LogFormatName[12:16], _LogFormatName[16:19], + _LogFormatName[19:38], } // LogFormatString retrieves an enum value from the enum constants string name. diff --git a/backend/v3/instrumentation/logging/connect_handler.go b/backend/v3/instrumentation/logging/connect_handler.go index 14a9b9f18..0d3e3e475 100644 --- a/backend/v3/instrumentation/logging/connect_handler.go +++ b/backend/v3/instrumentation/logging/connect_handler.go @@ -2,7 +2,6 @@ package logging import ( "context" - "log/slog" "slices" "strings" @@ -29,43 +28,7 @@ func NewConnectInterceptor(next connect.UnaryFunc, ignoredMethodSuffixes ...stri if err != nil { code = connect.CodeOf(err) } - logger.Log(ctx, - assertConnectLevel(code), - "connect RPC served", - "code", code, - ) - + logger.InfoContext(ctx, "connect RPC request", "code", code) return resp, err } } - -func assertConnectLevel(code connect.Code) slog.Level { - switch code { - case connect.CodeUnimplemented: - return slog.LevelDebug - - // client errors - case connect.CodeCanceled, - connect.CodeInvalidArgument, - connect.CodeNotFound, - connect.CodeAlreadyExists, - connect.CodePermissionDenied, - connect.CodeResourceExhausted, - connect.CodeFailedPrecondition, - connect.CodeOutOfRange, - connect.CodeUnauthenticated: - return slog.LevelWarn - - // server errors - case connect.CodeUnknown, - connect.CodeAborted, - connect.CodeDeadlineExceeded, - connect.CodeInternal, - connect.CodeUnavailable, - connect.CodeDataLoss: - return slog.LevelError - - default: // includes 0 code when no error - return slog.LevelInfo - } -} diff --git a/backend/v3/instrumentation/logging/grpc_handler.go b/backend/v3/instrumentation/logging/grpc_handler.go index c5315f63c..64e88e6ba 100644 --- a/backend/v3/instrumentation/logging/grpc_handler.go +++ b/backend/v3/instrumentation/logging/grpc_handler.go @@ -2,7 +2,6 @@ package logging import ( "context" - "log/slog" "slices" "strings" @@ -31,44 +30,7 @@ func NewGrpcInterceptor(ignoredMethodSuffixes ...string) grpc.UnaryServerInterce if err != nil { code = status.Code(err) } - logger.Log(ctx, - assertGrpcLevel(code), - "gRPC served", - "code", code, - ) - + logger.InfoContext(ctx, "gRPC request", "code", code) return resp, err } } - -func assertGrpcLevel(code codes.Code) slog.Level { - switch code { - case codes.Unimplemented: - return slog.LevelDebug - - // client errors - case codes.Canceled, - codes.InvalidArgument, - codes.NotFound, - codes.AlreadyExists, - codes.PermissionDenied, - codes.ResourceExhausted, - codes.FailedPrecondition, - codes.OutOfRange, - codes.Unauthenticated: - return slog.LevelWarn - - // server errors - case codes.Unknown, - codes.Aborted, - codes.DeadlineExceeded, - codes.Internal, - codes.Unavailable, - codes.DataLoss: - return slog.LevelError - case codes.OK: - fallthrough - default: // includes 0 code when no error - return slog.LevelInfo - } -} diff --git a/backend/v3/instrumentation/logging/http_handler.go b/backend/v3/instrumentation/logging/http_handler.go index 31efeb59c..898003abf 100644 --- a/backend/v3/instrumentation/logging/http_handler.go +++ b/backend/v3/instrumentation/logging/http_handler.go @@ -1,7 +1,6 @@ package logging import ( - "log/slog" "net/http" slogctx "github.com/veqryn/slog-context" @@ -23,27 +22,10 @@ func NewHandler(next http.Handler, service string, ignoredPrefix ...string) http next.ServeHTTP(sw, r.WithContext(ctx)) - logger.Log(ctx, - LogLevelFromStatus(sw.status), - "http request served", - "status", sw.status, - ) + logger.InfoContext(ctx, "http request", "status", sw.status) }) } -func LogLevelFromStatus(status int) slog.Level { - if status < 200 { - return slog.LevelDebug - } - if status < 400 { - return slog.LevelInfo - } - if status < 500 { - return slog.LevelWarn - } - return slog.LevelError -} - // statusWriter is a [http.ResponseWriter] that captures the status code for logging. type statusWriter struct { http.ResponseWriter diff --git a/backend/v3/instrumentation/slog.go b/backend/v3/instrumentation/slog.go index b5a30a861..211298580 100644 --- a/backend/v3/instrumentation/slog.go +++ b/backend/v3/instrumentation/slog.go @@ -9,21 +9,22 @@ import ( "time" "connectrpc.com/connect" - "github.com/muhlemmer/sloggcp" "github.com/rs/xid" slogmulti "github.com/samber/slog-multi" slogctx "github.com/veqryn/slog-context" slogotel "github.com/veqryn/slog-context/otel" + "github.com/zitadel/sloggcp" "go.opentelemetry.io/contrib/bridges/otelslog" "go.opentelemetry.io/otel/sdk/log" "google.golang.org/grpc" http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/zerrors" ) type LogFormat int -//go:generate enumer -type=LogFormat -trimprefix=LogFormat -text -linecomment +//go:generate enumer -type=LogFormat -trimprefix=LogFormat -text -linecomment -transform=snake const ( // Empty line comment sets empty string of unspecified value LogFormatUndefined LogFormat = iota // @@ -32,19 +33,24 @@ const ( LogFormatJSON // JSON formatted logs compatible with Google Cloud Platform LogFormatGCP + LogFormatGCPErrorReporting ) type LogConfig struct { - Level slog.Level - Format LogFormat - AddSource bool - GoogleProjectID string - GoogleLogName string - Exporter ExporterConfig + Level slog.Level + Format LogFormat + AddSource bool + Errors ErrorConfig + Exporter ExporterConfig +} + +type ErrorConfig struct { + ReportLocation bool + StackTrace bool } // setLogger configures the global slog logger. -// Logs are sent to StdErr and/or the [log.LoggerProvider]. +// Logs are sent to [os.Stderr] and/or the [log.LoggerProvider]. // // When present in the context, each line emitted contains: // - Trace ID @@ -70,8 +76,15 @@ func setLogger(provider *log.LoggerProvider, cfg LogConfig) { case LogFormatGCP: options.ReplaceAttr = sloggcp.ReplaceAttr stdErrHandler = slog.NewJSONHandler(os.Stderr, options) + case LogFormatGCPErrorReporting: + zerrors.GCPErrorReportingEnabled(true) + options.ReplaceAttr = replaceErrAttr + stdErrHandler = sloggcp.NewErrorReportingHandler(os.Stderr, options) } + zerrors.EnableReportLocation(cfg.Errors.ReportLocation) + zerrors.EnableStackTrace(cfg.Errors.StackTrace) + stdErrHandler = slogctx.NewHandler( stdErrHandler, &slogctx.HandlerOptions{ @@ -195,3 +208,11 @@ func requestExtractor(ctx context.Context, _ time.Time, _ slog.Level, _ string) } return nil } + +// replaceErrAttr renames the "err" attribute to the Google Cloud Platform compatible error key. +func replaceErrAttr(groups []string, a slog.Attr) slog.Attr { + if len(groups) == 0 && a.Key == "err" { + a.Key = sloggcp.ErrorKey + } + return a +} diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 5762b5323..5bcb80f4e 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -49,10 +49,17 @@ Instrumentation: # - "text": Logs are printed on StdErr as human-readable text # - "json": Logs are printed on StdErr as JSON objects # - "gcp": JSON formatted logs compatible with Google Cloud Platform logging. Need to configure GoogleProjectID below! + # - "gcp_error_reporting": JSON formatted logs compatible with Google Cloud Platform Error Reporting. Need to configure GoogleProjectID below! Format: "text" # ZITADEL_INSTRUMENTATION_LOG_STDERR # Adds the source file and line number where the log line was emitted. # Not to be confused with the source of an error. AddSource: true # ZITADEL_INSTRUMENTATION_LOG_ADDSOURCE + # Configure how errors are logged. + Errors: + # Adds the source file, line number and function where the error was created. + ReportLocation: true # ZITADEL_INSTRUMENTATION_LOG_ERRORS_REPORTLOCATION + # Adds stack traces to logged errors. + StackTrace: false # ZITADEL_INSTRUMENTATION_LOG_ERRORS_STACKTRACE Exporter: # The following exporter types are supported: # - "none": Disables OTEL log exporter diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index c02e05fa9..3b55ecb8a 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -49,10 +49,17 @@ Instrumentation: # - "text": Logs are printed on StdErr as human-readable text # - "json": Logs are printed on StdErr as JSON objects # - "gcp": JSON formatted logs compatible with Google Cloud Platform logging. Need to configure GoogleProjectID below! + # - "gcp_error_reporting": JSON formatted logs compatible with Google Cloud Platform Error Reporting. Need to configure GoogleProjectID below! Format: "text" # ZITADEL_INSTRUMENTATION_LOG_STDERR # Adds the source file and line number where the log line was emitted. # Not to be confused with the source of an error. AddSource: true # ZITADEL_INSTRUMENTATION_LOG_ADDSOURCE + # Configure how errors are logged. + Errors: + # Adds the source file, line number and function where the error was created. + ReportLocation: true # ZITADEL_INSTRUMENTATION_LOG_ERRORS_REPORTLOCATION + # Adds stack traces to logged errors. + StackTrace: false # ZITADEL_INSTRUMENTATION_LOG_ERRORS_STACKTRACE Exporter: # The following exporter types are supported: # - "none": Disables OTEL log exporter diff --git a/go.mod b/go.mod index 6bc160b2b..35a6d0f9d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/zitadel/zitadel -go 1.24.0 - -toolchain go1.24.7 +go 1.25 require ( cloud.google.com/go/profiler v0.4.3 @@ -67,7 +65,6 @@ require ( github.com/muesli/gamut v0.3.1 github.com/muhlemmer/gu v0.3.1 github.com/muhlemmer/httpforwarded v0.1.0 - github.com/muhlemmer/sloggcp v0.1.0 github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/pashagolub/pgxmock/v4 v4.9.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 @@ -104,6 +101,7 @@ require ( github.com/zitadel/passwap v0.10.0 github.com/zitadel/saml v0.4.1 github.com/zitadel/schema v1.3.1 + github.com/zitadel/sloggcp v0.5.1 go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 diff --git a/go.sum b/go.sum index c51e5eefb..a24517371 100644 --- a/go.sum +++ b/go.sum @@ -589,8 +589,6 @@ github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= -github.com/muhlemmer/sloggcp v0.1.0 h1:iWaksY9nzkqlEecQPkfd7hbASqaIQPSoM7kldC2kAmc= -github.com/muhlemmer/sloggcp v0.1.0/go.mod h1:2JKYrbP9Xc/azEDCIhvMrKP4/bxm6CqjdnMPS3OfpT0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -836,6 +834,8 @@ github.com/zitadel/saml v0.4.1 h1:4PPOAvnUFcfwOFIAFZ3oGTMD+tKVeF96ySfGBf5j3IU= github.com/zitadel/saml v0.4.1/go.mod h1:ybs3e4tIWdYgSYBpuCsvf3T4FNDfbXYM+GPv5vIpHYk= github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= +github.com/zitadel/sloggcp v0.5.1 h1:AoUN9+6iRZCAy0P+iR7TOpvCO+lOpUzGslvzKSoAKKc= +github.com/zitadel/sloggcp v0.5.1/go.mod h1:QNiYB+IkVRsx/NT/Z2rnBevp9D4F/5o8DAGrUO0BBoo= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= diff --git a/internal/zerrors/already_exists.go b/internal/zerrors/already_exists.go index f2ed7352c..11fdef01f 100644 --- a/internal/zerrors/already_exists.go +++ b/internal/zerrors/already_exists.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowAlreadyExists(parent error, id, message string) error { - return CreateZitadelError(KindAlreadyExists, parent, id, message) + return newZitadelError(KindAlreadyExists, parent, id, message) } func ThrowAlreadyExistsf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindAlreadyExists, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindAlreadyExists, parent, id, fmt.Sprintf(format, a...)) } func IsErrorAlreadyExists(err error) bool { diff --git a/internal/zerrors/deadline_exceeded.go b/internal/zerrors/deadline_exceeded.go index 312dcba52..00fe0fa15 100644 --- a/internal/zerrors/deadline_exceeded.go +++ b/internal/zerrors/deadline_exceeded.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowDeadlineExceeded(parent error, id, message string) error { - return CreateZitadelError(KindDeadlineExceeded, parent, id, message) + return newZitadelError(KindDeadlineExceeded, parent, id, message) } func ThrowDeadlineExceededf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindDeadlineExceeded, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindDeadlineExceeded, parent, id, fmt.Sprintf(format, a...)) } func IsDeadlineExceeded(err error) bool { diff --git a/internal/zerrors/internal.go b/internal/zerrors/internal.go index 3edc8165d..950633566 100644 --- a/internal/zerrors/internal.go +++ b/internal/zerrors/internal.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowInternal(parent error, id, message string) error { - return CreateZitadelError(KindInternal, parent, id, message) + return newZitadelError(KindInternal, parent, id, message) } func ThrowInternalf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindInternal, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindInternal, parent, id, fmt.Sprintf(format, a...)) } func IsInternal(err error) bool { diff --git a/internal/zerrors/invalid_argument.go b/internal/zerrors/invalid_argument.go index 6ee9a7da5..fd0df961a 100644 --- a/internal/zerrors/invalid_argument.go +++ b/internal/zerrors/invalid_argument.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowInvalidArgument(parent error, id, message string) error { - return CreateZitadelError(KindInvalidArgument, parent, id, message) + return newZitadelError(KindInvalidArgument, parent, id, message) } func ThrowInvalidArgumentf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindInvalidArgument, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindInvalidArgument, parent, id, fmt.Sprintf(format, a...)) } func IsErrorInvalidArgument(err error) bool { diff --git a/internal/zerrors/not_found.go b/internal/zerrors/not_found.go index 0701efe11..c64d4b415 100644 --- a/internal/zerrors/not_found.go +++ b/internal/zerrors/not_found.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowNotFound(parent error, id, message string) error { - return CreateZitadelError(KindNotFound, parent, id, message) + return newZitadelError(KindNotFound, parent, id, message) } func ThrowNotFoundf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindNotFound, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindNotFound, parent, id, fmt.Sprintf(format, a...)) } func IsNotFound(err error) bool { diff --git a/internal/zerrors/permission_denied.go b/internal/zerrors/permission_denied.go index 46066d842..076791f68 100644 --- a/internal/zerrors/permission_denied.go +++ b/internal/zerrors/permission_denied.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowPermissionDenied(parent error, id, message string) error { - return CreateZitadelError(KindPermissionDenied, parent, id, message) + return newZitadelError(KindPermissionDenied, parent, id, message) } func ThrowPermissionDeniedf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindPermissionDenied, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindPermissionDenied, parent, id, fmt.Sprintf(format, a...)) } func IsPermissionDenied(err error) bool { diff --git a/internal/zerrors/precondition_failed.go b/internal/zerrors/precondition_failed.go index c8840eaf8..c92b1b56f 100644 --- a/internal/zerrors/precondition_failed.go +++ b/internal/zerrors/precondition_failed.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowPreconditionFailed(parent error, id, message string) error { - return CreateZitadelError(KindPreconditionFailed, parent, id, message) + return newZitadelError(KindPreconditionFailed, parent, id, message) } func ThrowPreconditionFailedf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindPreconditionFailed, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindPreconditionFailed, parent, id, fmt.Sprintf(format, a...)) } func IsPreconditionFailed(err error) bool { diff --git a/internal/zerrors/resource_exhausted.go b/internal/zerrors/resource_exhausted.go index e35bef6ca..63cb000db 100644 --- a/internal/zerrors/resource_exhausted.go +++ b/internal/zerrors/resource_exhausted.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowResourceExhausted(parent error, id, message string) error { - return CreateZitadelError(KindResourceExhausted, parent, id, message) + return newZitadelError(KindResourceExhausted, parent, id, message) } func ThrowResourceExhaustedf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindResourceExhausted, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindResourceExhausted, parent, id, fmt.Sprintf(format, a...)) } func IsResourceExhausted(err error) bool { diff --git a/internal/zerrors/unauthenticated.go b/internal/zerrors/unauthenticated.go index be4b78e33..13f1ec317 100644 --- a/internal/zerrors/unauthenticated.go +++ b/internal/zerrors/unauthenticated.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowUnauthenticated(parent error, id, message string) error { - return CreateZitadelError(KindUnauthenticated, parent, id, message) + return newZitadelError(KindUnauthenticated, parent, id, message) } func ThrowUnauthenticatedf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindUnauthenticated, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindUnauthenticated, parent, id, fmt.Sprintf(format, a...)) } func IsUnauthenticated(err error) bool { diff --git a/internal/zerrors/unavailable.go b/internal/zerrors/unavailable.go index bcffdb637..2b5c1cff0 100644 --- a/internal/zerrors/unavailable.go +++ b/internal/zerrors/unavailable.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowUnavailable(parent error, id, message string) error { - return CreateZitadelError(KindUnavailable, parent, id, message) + return newZitadelError(KindUnavailable, parent, id, message) } func ThrowUnavailablef(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindUnavailable, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindUnavailable, parent, id, fmt.Sprintf(format, a...)) } func IsUnavailable(err error) bool { diff --git a/internal/zerrors/unimplemented.go b/internal/zerrors/unimplemented.go index 4a6cc5ab6..790a86391 100644 --- a/internal/zerrors/unimplemented.go +++ b/internal/zerrors/unimplemented.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowUnimplemented(parent error, id, message string) error { - return CreateZitadelError(KindUnimplemented, parent, id, message) + return newZitadelError(KindUnimplemented, parent, id, message) } func ThrowUnimplementedf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindUnimplemented, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindUnimplemented, parent, id, fmt.Sprintf(format, a...)) } func IsUnimplemented(err error) bool { diff --git a/internal/zerrors/unknown.go b/internal/zerrors/unknown.go index cbe63462b..63d114153 100644 --- a/internal/zerrors/unknown.go +++ b/internal/zerrors/unknown.go @@ -3,11 +3,11 @@ package zerrors import "fmt" func ThrowUnknown(parent error, id, message string) error { - return CreateZitadelError(KindUnknown, parent, id, message) + return newZitadelError(KindUnknown, parent, id, message) } func ThrowUnknownf(parent error, id, format string, a ...any) error { - return CreateZitadelError(KindUnknown, parent, id, fmt.Sprintf(format, a...)) + return newZitadelError(KindUnknown, parent, id, fmt.Sprintf(format, a...)) } func IsUnknown(err error) bool { diff --git a/internal/zerrors/zerror.go b/internal/zerrors/zerror.go index 5a5de563e..19b9cba3e 100644 --- a/internal/zerrors/zerror.go +++ b/internal/zerrors/zerror.go @@ -4,6 +4,10 @@ import ( "errors" "fmt" "log/slog" + "runtime/debug" + "sync/atomic" + + "github.com/zitadel/sloggcp" ) //go:generate enumer -type=Kind -trimprefix=Kind -json @@ -76,24 +80,66 @@ const ( KindUnauthenticated Kind = 16 ) +// Because errors are created through singletons, config is global. +var ( + enableReportLocation atomic.Bool + enableStackTrace atomic.Bool + gcpErrorReporting atomic.Bool +) + +// EnableReportLocation enables or disables report locations for created errors. +func EnableReportLocation(enable bool) { + enableReportLocation.Store(enable) +} + +// EnableStackTrace enables or disables stack traces for created errors. +func EnableStackTrace(enable bool) { + enableStackTrace.Store(enable) +} + +// GCPErrorReportingEnabled enables or disables special handling for GCP Error Reporting. +// It must be enabled when using a sloggcp handler to avoid duplicate report locations and stack traces. +func GCPErrorReportingEnabled(enable bool) { + gcpErrorReporting.Store(enable) +} + type ZitadelError struct { Kind Kind Parent error Message string ID string + + // location where the error was created + reportLocation *sloggcp.ReportLocation + // stack trace at the point the error was created + stackTrace []byte + hasStackTrace bool } func ThrowError(parent error, id, message string) error { - return CreateZitadelError(KindUnknown, parent, id, message) + return newZitadelError(KindUnknown, parent, id, message) } func CreateZitadelError(kind Kind, parent error, id, message string) *ZitadelError { - return &ZitadelError{ + return newZitadelError(kind, parent, id, message) +} + +func newZitadelError(kind Kind, parent error, id, message string) *ZitadelError { + err := &ZitadelError{ Kind: kind, Parent: parent, ID: id, Message: message, } + if enableReportLocation.Load() { + // skip 2: newZitadelError + Create / Throw function. + err.reportLocation = sloggcp.NewReportLocation(2) + } + if enableStackTrace.Load() { + err.stackTrace = debug.Stack() + err.hasStackTrace = true + } + return err } func (err *ZitadelError) Error() string { @@ -158,10 +204,41 @@ func AsZitadelError(err error) (*ZitadelError, bool) { var _ slog.LogValuer = (*ZitadelError)(nil) func (err *ZitadelError) LogValue() slog.Value { - return slog.GroupValue( + attributes := make([]slog.Attr, 0, 6) + attributes = append(attributes, slog.String("kind", err.Kind.String()), slog.String("message", err.Message), slog.String("id", err.ID), - slog.Any("parent", err.Parent), ) + if err.Parent != nil { + attributes = append(attributes, slog.Any("parent", err.Parent)) + } + // if gcp error reporting is enabled, log the error as a group without + // report location and stack trace, as those are handled by the handler. + if gcpErrorReporting.Load() { + return slog.GroupValue(attributes...) + } + + if err.reportLocation != nil { + attributes = append(attributes, slog.Any("reportLocation", err.reportLocation)) + } + if err.hasStackTrace { + attributes = append(attributes, slog.String("stackTrace", string(err.stackTrace))) + } + return slog.GroupValue(attributes...) } + +// ReportLocation implements [sloggcp.ReportLocationError]. +func (err *ZitadelError) ReportLocation() *sloggcp.ReportLocation { + return err.reportLocation +} + +// StackTrace implements [sloggcp.StackTraceError]. +func (err *ZitadelError) StackTrace() (trace []byte, ok bool) { + return err.stackTrace, err.hasStackTrace +} + +var ( + _ sloggcp.ReportLocationError = (*ZitadelError)(nil) + _ sloggcp.StackTraceError = (*ZitadelError)(nil) +) diff --git a/internal/zerrors/zerror_test.go b/internal/zerrors/zerror_test.go index e03f66c29..8ea50cd64 100644 --- a/internal/zerrors/zerror_test.go +++ b/internal/zerrors/zerror_test.go @@ -1,27 +1,25 @@ -package zerrors_test +package zerrors import ( "errors" "testing" "github.com/stretchr/testify/assert" - - "github.com/zitadel/zitadel/internal/zerrors" ) func TestErrorMethod(t *testing.T) { - err := zerrors.ThrowError(nil, "id", "msg") + err := ThrowError(nil, "id", "msg") expected := "ID=id Message=msg" assert.Equal(t, expected, err.Error()) - err = zerrors.ThrowError(err, "subID", "subMsg") + err = ThrowError(err, "subID", "subMsg") subExptected := "ID=subID Message=subMsg Parent=(ID=id Message=msg)" assert.Equal(t, subExptected, err.Error()) } func TestZitadelError_Is(t *testing.T) { parent := errors.New("parent error") - target := zerrors.CreateZitadelError(zerrors.KindAborted, parent, "id", "message") + target := newZitadelError(KindAborted, parent, "id", "message") tests := []struct { name string // description of this test case err error @@ -34,27 +32,27 @@ func TestZitadelError_Is(t *testing.T) { }, { name: "different kind", - err: zerrors.CreateZitadelError(zerrors.KindNotFound, parent, "id", "message"), + err: newZitadelError(KindNotFound, parent, "id", "message"), want: false, }, { name: "different id", - err: zerrors.CreateZitadelError(zerrors.KindAborted, parent, "otherID", "message"), + err: newZitadelError(KindAborted, parent, "otherID", "message"), want: false, }, { name: "different message", - err: zerrors.CreateZitadelError(zerrors.KindAborted, parent, "id", "other message"), + err: newZitadelError(KindAborted, parent, "id", "other message"), want: false, }, { name: "different parent", - err: zerrors.CreateZitadelError(zerrors.KindAborted, errors.New("other parent"), "id", "message"), + err: newZitadelError(KindAborted, errors.New("other parent"), "id", "message"), want: false, }, { name: "same error", - err: zerrors.CreateZitadelError(zerrors.KindAborted, parent, "id", "message"), + err: newZitadelError(KindAborted, parent, "id", "message"), want: true, }, }