From 34799389b7a76643e8ac05e2a820fcc1cabd673d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 16 Jan 2026 15:24:06 +0100 Subject: [PATCH] feat(logging): gcp error reporting (#11355) # Which Problems Are Solved Zitadel Errors (`zerrors` package) are logged by default. This change add more verbose error reporting through log output, including compatibility with GCP error reporting if enabled. # How the Problems Are Solved Errors can now carry their "report location" and a stack trace. "Report location" is enabled by default, while stack-traces are disabled. Our new [`zitadel/sloggcp`](https://github.com/zitadel/sloggcp) package provides the GCP error reporting handler for `slog`. The following section displays the different formats for the same error (User not found). Newlines and formatting of output is applied for readability and is not part of the actual log output.
Default text output ``` time=2026-01-15T14:27:20.625+01:00 level=WARN source=/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/gerrors/zitadel_errors.go:57 msg="User could not be found (COMMAND-bd4ir1mblj)" domain.instance_host=localhost:8080 domain.protocol=http protocol=connect service=zitadel.user.v2.UserService http_method=POST path=/zitadel.user.v2.UserService/DeleteUser request_id=d5keme0ednc50mm74bfg duration=66.761569ms TraceID=319374baa56acc2dbd1d0179f1bbec9f SpanID=541621b0abea7ff1 err.kind=NotFound err.message="User could not be found" err.id=COMMAND-bd4ir1mblj err.reportLocation.filePath=/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go err.reportLocation.lineNumber=140 err.reportLocation.functionName=github.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2 ```
Default JSON output ```json { "time": "2026-01-15T14:30:59.280497754+01:00", "level": "WARN", "source": { "function": "github.com/zitadel/zitadel/internal/api/grpc/gerrors.ZITADELToConnectError", "file": "/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/gerrors/zitadel_errors.go", "line": 57 }, "msg": "User could not be found (COMMAND-bd4ir1mblj)", "domain": { "instance_host": "localhost:8080", "protocol": "http" }, "protocol": "connect", "service": "zitadel.user.v2.UserService", "http_method": "POST", "path": "/zitadel.user.v2.UserService/DeleteUser", "request_id": "d5keo4oednc6klmo3vbg", "duration": 54229055, "TraceID": "daa21183bd8da5da56969b0ea45f4388", "SpanID": "70da728e3086c104", "err": { "kind": "NotFound", "message": "User could not be found", "id": "COMMAND-bd4ir1mblj", "reportLocation": { "filePath": "/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go", "lineNumber": 140, "functionName": "github.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2" } } } ```
When stack trace is enabled, using a standard logger, it is logged under the `stackTrace` key.
Standard JSON format with stack trace ```json { "time": "2026-01-15T14:36:56.522692368+01:00", "level": "WARN", "source": { "function": "github.com/zitadel/zitadel/internal/api/grpc/gerrors.ZITADELToConnectError", "file": "/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/gerrors/zitadel_errors.go", "line": 57 }, "msg": "User could not be found (COMMAND-bd4ir1mblj)", "domain": { "instance_host": "localhost:8080", "protocol": "http" }, "protocol": "connect", "service": "zitadel.user.v2.UserService", "http_method": "POST", "path": "/zitadel.user.v2.UserService/DeleteUser", "request_id": "d5kequ0ednc7lphvgfn0", "duration": 57820793, "TraceID": "1b05d2b01306f2a64feb1a24b65a89c5", "SpanID": "c5ff35a27a917764", "err": { "kind": "NotFound", "message": "User could not be found", "id": "COMMAND-bd4ir1mblj", "reportLocation": { "filePath": "/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go", "lineNumber": 140, "functionName": "github.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2" }, "stackTrace": "goroutine 673 [running]:\nruntime/debug.Stack()\n\t/usr/lib/go/src/runtime/debug/stack.go:26 +0x8e\ngithub.com/zitadel/zitadel/internal/zerrors.newZitadelError(0x5, {0x0, 0x0}, {0x6af7719, 0x12}, {0x6aff994, 0x14})\n\t/home/tim/Repositories/zitadel/zitadel/internal/zerrors/zerror.go:139 +0x1df\ngithub.com/zitadel/zitadel/internal/zerrors.ThrowNotFound({0x0, 0x0}, {0x6af7719, 0x12}, {0x6aff994, 0x14})\n\t/home/tim/Repositories/zitadel/zitadel/internal/zerrors/not_found.go:6 +0x70\ngithub.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2(0xc0035a4008, {0xa3c1410, 0xc003ded5c0}, {0xc0029693a0, 0x3}, {0x0, 0x0}, {0xd550a60, 0x0, 0x0}, ...)\n\t/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go:140 +0x711\ngithub.com/zitadel/zitadel/internal/api/grpc/user/v2.(*Server).DeleteUser(0xc00272d080, {0xa3c1410, 0xc003ded5c0}, 0xc002ebf180)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/user/v2/user.go:191 +0x3f1\nconnectrpc.com/connect.NewUnaryHandler[...].func1({0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/handler.go:51 +0x17e\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded5c0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ActivityInterceptor.func1.1({0xa3c1410, 0xc003ded5c0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/activity_interceptor.go:20 +0x1b1\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ServiceHandler.func1.1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/service_interceptor.go:22 +0x283\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.validate({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180}, 0xc0031bff60)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/validation_interceptor.go:35 +0x1bd\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ValidationHandler.func1.1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/validation_interceptor.go:15 +0x85\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ExecutionHandler.func1.1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/execution_interceptor.go:39 +0x376\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.QuotaExhaustedInterceptor.func1.1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/quota_interceptor.go:25 +0x42d\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.TranslationHandler.func1.1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/translation_interceptor.go:18 +0x94\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded4d0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.authorize({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180}, 0xc00321c040, {0xa3f0a70, 0xc0004ebc70}, {{0xc000d2fc80, 0x9, 0x9}}, ...)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/auth_interceptor.go:42 +0x894\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.AuthorizationInterceptor.func1.1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/auth_interceptor.go:17 +0x151\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.LimitsInterceptor.func1.1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/limits_interceptor.go:31 +0x3b7\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.toConnectError({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180}, 0xc00321c060)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/error_interceptor.go:21 +0x8a\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ErrorHandler.func1.1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/error_interceptor.go:15 +0x85\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.AccessStorageInterceptor.func1.1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/access_interceptor.go:21 +0x814\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003dec000}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.addInstanceByRequestedHost({0xa3c1410, 0xc003db1110}, {0xa405e70, 0xc002ebf180}, 0xc00321c090, {0xa3a63e8, 0xc0002f4a00}, 0xc002dee9b0, {0xc0012106a0, 0x9})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/instance_interceptor.go:107 +0xbc3\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.setInstance({0xa3c1410, 0xc003db10e0}, {0xa405e70, 0xc002ebf180}, 0xc00321c090, {0xa3a63e8, 0xc0002f4a00}, {0xc0012106a0, 0x9}, 0xc002dee9b0, ...)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/instance_interceptor.go:61 +0xb05\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.InstanceInterceptor.func1.1({0xa3c1410, 0xc003db10e0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/instance_interceptor.go:23 +0x147\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003db10e0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.NoCacheInterceptor.func1.1({0xa3c1410, 0xc003db10e0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/cache_interceptor.go:21 +0x351\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003db10e0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/backend/v3/instrumentation/logging.NewConnectInterceptor.func1({0xa3c1410, 0xc003db10e0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/backend/v3/instrumentation/logging/connect_handler.go:26 +0x27b\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003db1050}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.RegisterMetrics({0xa3c1410, 0xc003db1050}, {0xa405e70, 0xc002ebf180}, 0xc00321c0d0, {0xcf754f8, 0x3, 0x3}, {0xc0008b8840, 0x4, ...})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/metrics_interceptor.go:47 +0x3f3\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.MetricsHandler.func1.1({0xa3c1410, 0xc003db1050}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/metrics_interceptor.go:31 +0x111\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003db1050}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\nconnectrpc.com/otelconnect.(*Interceptor).WrapUnary.func1({0xa3c1410, 0xc003db1050}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/otelconnect@v0.8.0/interceptor.go:145 +0x16d7\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003db0ff0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.CallDurationHandler.func1.1({0xa3c1410, 0xc003db0ff0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/call_interceptor.go:15 +0xab\nconnectrpc.com/connect.NewUnaryHandler[...].func2({0x7f11083fe700, 0xc000503a40})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/handler.go:78 +0x4e2\nconnectrpc.com/connect.(*Handler).ServeHTTP(0xc0004dd420, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/handler.go:333 +0x870\ngithub.com/zitadel/zitadel/pkg/grpc/user/v2/userconnect.NewUserServiceHandler.func1({0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/home/tim/Repositories/zitadel/zitadel/pkg/grpc/user/v2/userconnect/user_service.connect.go:1951 +0x8e5\nnet/http.HandlerFunc.ServeHTTP(0xc002d7a000, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/rs/cors.(*Cors).Handler.func1({0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/home/tim/go/pkg/mod/github.com/rs/cors@v1.11.1/cors.go:289 +0x2fd\nnet/http.HandlerFunc.ServeHTTP(0xc000191aa0, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/zitadel/zitadel/internal/api/http/middleware.RobotsTagHandler.func1({0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/http/middleware/robots_tag_interceptor.go:12 +0x88\nnet/http.HandlerFunc.ServeHTTP(0xc002e14798, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/zitadel/zitadel/internal/api/http/middleware.WithOrigin.func1.1({0xa3bb420, 0xc0006b22a0}, 0xc002e3b180)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/http/middleware/origin_interceptor.go:27 +0x42a\nnet/http.HandlerFunc.ServeHTTP(0xc003d89a40, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b180)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/gorilla/mux.(*Router).ServeHTTP(0xc002dfa840, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b180)\n\t/home/tim/go/pkg/mod/github.com/gorilla/mux@v1.8.1/mux.go:212 +0x2bb\ngolang.org/x/net/http2.(*serverConn).runHandler(0xc003e143c0, 0xc0006b22a0, 0xc0002f4000, 0xc002d9ae88)\n\t/home/tim/go/pkg/mod/golang.org/x/net@v0.47.0/http2/server.go:2424 +0x1b9\ncreated by golang.org/x/net/http2.(*serverConn).scheduleHandler in goroutine 607\n\t/home/tim/go/pkg/mod/golang.org/x/net@v0.47.0/http2/server.go:2359 +0x1df\n" } } ```
When the GCP error reporting handler is enabled with stack trace, as per [formatting requirements](https://docs.cloud.google.com/error-reporting/docs/formatting-error-messages): - A `@type` field is provided - Some fields are renamed according to Google Cloud Logging specs. (severity, message etc) - The stack strace is appended to the `message` field. - The report location is moved into the top-level `reportLocation` field. The original error is still logged in the `error` field. This is not part of the error reporting API but may still be useful for logging.
GCP error report with stack trace ```json { "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent", "SpanID": "4fdeae9817ade4f5", "TraceID": "78e31366d4c36e0718a2dcb6ca73d2f3", "domain": { "instance_host": "localhost:8080", "protocol": "http" }, "duration": "82.464896ms", "error": { "id": "COMMAND-bd4ir1mblj", "kind": "NotFound", "message": "User could not be found" }, "http_method": "POST", "logging.googleapis.com/sourceLocation": { "function": "github.com/zitadel/zitadel/internal/api/grpc/gerrors.ZITADELToConnectError", "file": "/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/gerrors/zitadel_errors.go", "line": 57 }, "message": "ID=COMMAND-bd4ir1mblj Message=User could not be found\ngoroutine 1165 [running]:\nruntime/debug.Stack()\n\t/usr/lib/go/src/runtime/debug/stack.go:26 +0x8e\ngithub.com/zitadel/zitadel/internal/zerrors.newZitadelError(0x5, {0x0, 0x0}, {0x6af7719, 0x12}, {0x6aff994, 0x14})\n\t/home/tim/Repositories/zitadel/zitadel/internal/zerrors/zerror.go:139 +0x1df\ngithub.com/zitadel/zitadel/internal/zerrors.ThrowNotFound({0x0, 0x0}, {0x6af7719, 0x12}, {0x6aff994, 0x14})\n\t/home/tim/Repositories/zitadel/zitadel/internal/zerrors/not_found.go:6 +0x70\ngithub.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2(0xc000498588, {0xa3c1450, 0xc002cba540}, {0xc003030910, 0xb}, {0x0, 0x0}, {0xd550a80, 0x0, 0x0}, ...)\n\t/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go:140 +0x711\ngithub.com/zitadel/zitadel/internal/api/grpc/user/v2.(*Server).DeleteUser(0xc003101500, {0xa3c1450, 0xc002cba540}, 0xc00216ec80)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/user/v2/user.go:191 +0x3f1\nconnectrpc.com/connect.NewUnaryHandler[...].func1({0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/handler.go:51 +0x17e\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba540}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ActivityInterceptor.func1.1({0xa3c1450, 0xc002cba540}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/activity_interceptor.go:20 +0x1b1\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ServiceHandler.func1.1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/service_interceptor.go:22 +0x283\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.validate({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80}, 0xc003033180)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/validation_interceptor.go:35 +0x1bd\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ValidationHandler.func1.1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/validation_interceptor.go:15 +0x85\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ExecutionHandler.func1.1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/execution_interceptor.go:39 +0x376\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.QuotaExhaustedInterceptor.func1.1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/quota_interceptor.go:25 +0x42d\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.TranslationHandler.func1.1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/translation_interceptor.go:18 +0x94\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba330}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.authorize({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80}, 0xc0030331e0, {0xa3f0ab0, 0xc000203f10}, {{0xc000865980, 0x9, 0x9}}, ...)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/auth_interceptor.go:42 +0x894\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.AuthorizationInterceptor.func1.1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/auth_interceptor.go:17 +0x151\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.LimitsInterceptor.func1.1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/limits_interceptor.go:31 +0x3b7\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.toConnectError({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80}, 0xc003033200)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/error_interceptor.go:21 +0x8a\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.ErrorHandler.func1.1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/error_interceptor.go:15 +0x85\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.AccessStorageInterceptor.func1.1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/access_interceptor.go:21 +0x814\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0015827b0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.addInstanceByRequestedHost({0xa3c1450, 0xc001636630}, {0xa405eb0, 0xc00216ec80}, 0xc003033230, {0xa3a6428, 0xc00069f040}, 0xc002076910, {0xc002cef510, 0x9})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/instance_interceptor.go:107 +0xbc3\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.setInstance({0xa3c1450, 0xc0016365a0}, {0xa405eb0, 0xc00216ec80}, 0xc003033230, {0xa3a6428, 0xc00069f040}, {0xc002cef510, 0x9}, 0xc002076910, ...)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/instance_interceptor.go:61 +0xb05\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.InstanceInterceptor.func1.1({0xa3c1450, 0xc0016365a0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/instance_interceptor.go:23 +0x147\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0016365a0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.NoCacheInterceptor.func1.1({0xa3c1450, 0xc0016365a0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/cache_interceptor.go:21 +0x351\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0016365a0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/backend/v3/instrumentation/logging.NewConnectInterceptor.func1({0xa3c1450, 0xc0016365a0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/backend/v3/instrumentation/logging/connect_handler.go:26 +0x27b\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0016364e0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.RegisterMetrics({0xa3c1450, 0xc0016364e0}, {0xa405eb0, 0xc00216ec80}, 0xc003033270, {0xcf754f8, 0x3, 0x3}, {0xc000530f60, 0x4, ...})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/metrics_interceptor.go:47 +0x3f3\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.MetricsHandler.func1.1({0xa3c1450, 0xc0016364e0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/metrics_interceptor.go:31 +0x111\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc0016364e0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\nconnectrpc.com/otelconnect.(*Interceptor).WrapUnary.func1({0xa3c1450, 0xc0016364e0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/otelconnect@v0.8.0/interceptor.go:145 +0x16d7\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc001636450}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/interceptor.go:120 +0xd9\ngithub.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware.CallDurationHandler.func1.1({0xa3c1450, 0xc001636450}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/server/connect_middleware/call_interceptor.go:15 +0xab\nconnectrpc.com/connect.NewUnaryHandler[...].func2({0x7fb6100a8e60, 0xc002fa5680})\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/handler.go:78 +0x4e2\nconnectrpc.com/connect.(*Handler).ServeHTTP(0xc0040768c0, {0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/home/tim/go/pkg/mod/connectrpc.com/connect@v1.19.1/handler.go:333 +0x870\ngithub.com/zitadel/zitadel/pkg/grpc/user/v2/userconnect.NewUserServiceHandler.func1({0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/home/tim/Repositories/zitadel/zitadel/pkg/grpc/user/v2/userconnect/user_service.connect.go:1951 +0x8e5\nnet/http.HandlerFunc.ServeHTTP(0xc00314a200, {0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/rs/cors.(*Cors).Handler.func1({0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/home/tim/go/pkg/mod/github.com/rs/cors@v1.11.1/cors.go:289 +0x2fd\nnet/http.HandlerFunc.ServeHTTP(0xc0022cbdc0, {0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/zitadel/zitadel/internal/api/http/middleware.RobotsTagHandler.func1({0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/http/middleware/robots_tag_interceptor.go:12 +0x88\nnet/http.HandlerFunc.ServeHTTP(0xc001c8fae8, {0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/zitadel/zitadel/internal/api/http/middleware.WithOrigin.func1.1({0xa3bb460, 0xc0025bf360}, 0xc0024c2140)\n\t/home/tim/Repositories/zitadel/zitadel/internal/api/http/middleware/origin_interceptor.go:27 +0x42a\nnet/http.HandlerFunc.ServeHTTP(0xc00048e540, {0xa3bb460, 0xc0025bf360}, 0xc0024c2140)\n\t/usr/lib/go/src/net/http/server.go:2322 +0x33\ngithub.com/gorilla/mux.(*Router).ServeHTTP(0xc00207c540, {0xa3bb460, 0xc0025bf360}, 0xc0024c2140)\n\t/home/tim/go/pkg/mod/github.com/gorilla/mux@v1.8.1/mux.go:212 +0x2bb\ngolang.org/x/net/http2.(*serverConn).runHandler(0xc0025545a0, 0xc0025bf360, 0xc0023fd180, 0xc001703488)\n\t/home/tim/go/pkg/mod/golang.org/x/net@v0.47.0/http2/server.go:2424 +0x1b9\ncreated by golang.org/x/net/http2.(*serverConn).scheduleHandler in goroutine 620\n\t/home/tim/go/pkg/mod/golang.org/x/net@v0.47.0/http2/server.go:2359 +0x1df\n", "path": "/zitadel.user.v2.UserService/DeleteUser", "protocol": "connect", "reportLocation": { "filePath": "/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go", "lineNumber": 140, "functionName": "github.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2" }, "request_id": "d5kgsu8edncaon6puvag", "service": "zitadel.user.v2.UserService", "severity": "WARNING", "time": "2026-01-15T16:57:45.341000592+01:00" } ```
When the GCP error reporting handler is enabled without stack trace, the `message` field contains the error string, also as per formatting requirements.
GCP error report without stack trace ```json { "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent", "SpanID": "2b4157f874eddbf4", "TraceID": "c34f7404fc0d021e1868ccc687bf2995", "domain": { "instance_host": "localhost:8080", "protocol": "http" }, "duration": "52.061597ms", "error": { "id": "COMMAND-bd4ir1mblj", "kind": "NotFound", "message": "User could not be found" }, "http_method": "POST", "logging.googleapis.com/sourceLocation": { "function": "github.com/zitadel/zitadel/internal/api/grpc/gerrors.ZITADELToConnectError", "file": "/home/tim/Repositories/zitadel/zitadel/internal/api/grpc/gerrors/zitadel_errors.go", "line": 57 }, "message": "ID=COMMAND-bd4ir1mblj Message=User could not be found", "path": "/zitadel.user.v2.UserService/DeleteUser", "protocol": "connect", "reportLocation": { "filePath": "/home/tim/Repositories/zitadel/zitadel/internal/command/user_v2.go", "lineNumber": 140, "functionName": "github.com/zitadel/zitadel/internal/command.(*Commands).RemoveUserV2" }, "request_id": "d5kf5tgedncaiajvls9g", "service": "zitadel.user.v2.UserService", "severity": "WARNING", "time": "2026-01-15T15:00:22.910899273+01:00" } ```
# Additional Changes - Private constructor for zitadel errors so that the stack-depth is consistent for different ways of error creation. (Throw and Create variants) - Request logs are now always at info level. - Bump Go version 1.25, required for `sloggcp` # Additional Context - closes #11329 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .devcontainer/devcontainer.json | 2 +- .../v3/instrumentation/logformat_enumer.go | 14 +-- .../logging/connect_handler.go | 39 +-------- .../instrumentation/logging/grpc_handler.go | 40 +-------- .../instrumentation/logging/http_handler.go | 20 +---- backend/v3/instrumentation/slog.go | 39 +++++++-- cmd/defaults.yaml | 7 ++ cmd/mirror/defaults.yaml | 7 ++ go.mod | 6 +- go.sum | 4 +- internal/zerrors/already_exists.go | 4 +- internal/zerrors/deadline_exceeded.go | 4 +- internal/zerrors/internal.go | 4 +- internal/zerrors/invalid_argument.go | 4 +- internal/zerrors/not_found.go | 4 +- internal/zerrors/permission_denied.go | 4 +- internal/zerrors/precondition_failed.go | 4 +- internal/zerrors/resource_exhausted.go | 4 +- internal/zerrors/unauthenticated.go | 4 +- internal/zerrors/unavailable.go | 4 +- internal/zerrors/unimplemented.go | 4 +- internal/zerrors/unknown.go | 4 +- internal/zerrors/zerror.go | 85 ++++++++++++++++++- internal/zerrors/zerror_test.go | 20 ++--- 24 files changed, 175 insertions(+), 156 deletions(-) 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, }, }