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.

<details>

<summary>Default text output</summary>

```
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
```

</details>

<details>

<summary>Default JSON output</summary>

```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"
    }
  }
}
```

</details>

When stack trace is enabled, using a standard logger,
it is logged under the `stackTrace` key.

<details>

<summary>Standard JSON format with stack trace</summary>

```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/[email protected].1/handler.go:51 +0x17e\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003ded5c0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].1/interceptor.go:120 +0xd9\nconnectrpc.com/otelconnect.(*Interceptor).WrapUnary.func1({0xa3c1410, 0xc003db1050}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected]/interceptor.go:145 +0x16d7\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1410, 0xc003db0ff0}, {0xa405e70, 0xc002ebf180})\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected].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/[email protected].1/handler.go:78 +0x4e2\nconnectrpc.com/connect.(*Handler).ServeHTTP(0xc0004dd420, {0xa3bb420, 0xc0006b22a0}, 0xc002e3b2c0)\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected].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/[email protected].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/[email protected]/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/[email protected].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/[email protected].0/http2/server.go:2359 +0x1df\n"
  }
}
```

</details>

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.

<details>

<summary>GCP error report with stack trace</summary>

```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/[email protected].1/handler.go:51 +0x17e\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc002cba540}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].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/[email protected].1/interceptor.go:120 +0xd9\nconnectrpc.com/otelconnect.(*Interceptor).WrapUnary.func1({0xa3c1450, 0xc0016364e0}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected]/interceptor.go:145 +0x16d7\nconnectrpc.com/connect.unaryThunk.func1({0xa3c1450, 0xc001636450}, {0xa405eb0, 0xc00216ec80})\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected].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/[email protected].1/handler.go:78 +0x4e2\nconnectrpc.com/connect.(*Handler).ServeHTTP(0xc0040768c0, {0xa3bb460, 0xc0025bf360}, 0xc0024c23c0)\n\t/home/tim/go/pkg/mod/connectrpc.com/[email protected].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/[email protected].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/[email protected]/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/[email protected].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/[email protected].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"
}
```

</details>

When the GCP error reporting handler is enabled without stack trace,
the `message` field contains the error string, also as per formatting
requirements.

<details>

<summary>GCP error report without stack trace</summary>

```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"
}
```

</details>

# 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 <[email protected]>
This commit is contained in:
Tim Möhlmann
2026-01-16 14:24:06 +00:00
committed by GitHub
co-authored by Copilot
parent 1e5e3b9b3e
commit 34799389b7
24 changed files with 175 additions and 156 deletions
@@ -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.
@@ -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
}
}
@@ -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
}
}
@@ -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
+30 -9
View File
@@ -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
}