From eb22b58756dc9cf7bda0cbdf6356d08a436ec4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 12 Jan 2026 06:51:39 +0100 Subject: [PATCH] feat(telemetry): improved instrumentation for observability (#11159) # Which Problems Are Solved Zitadel did not provide easy correlation between errors, logs, traces and metrics. The configuration for those instrumentations was also not consistent, with some supporting different exporters then others. Implementation and parsing of config was also spaghettified over multiple packages, with awkward parsing and inconsistent naming of options. # How the Problems Are Solved All telemetry is now merged under the name "instrumentation". Why? 1. We thought it was a good idea in the past to call the milestone exporter `Telemtry` in the runtime config. Calling this `TelemetryV2` looks weird. 2. Not everything is a meter and not everything is sent (tele...). 3. It's also [defined](https://opentelemetry.io/docs/concepts/instrumentation/) as such by the OTEL documentation. ## New features - Adds structured, context based logging with trace-ID awareness - Static log fields are added to the context, such as service and request path - Static log fields are injected in each logline emitted by the application - Structured logs can also be send to an otel exporter - Structured logs can be printed to StdErr in text and JSON format - Error sinks make sure every error is logged at the correct level: - Warnings for client side errors (HTTP 400 range, Invalid request etc) - Error for server side errors (Internal server errors) - Metrics can now also be send to a OTEL collector. (previously they could only be scraped from `/debug/metrics` with prometheus) ## Exporters This change adds all the exporters supported by OTEL upstream and some google specific exporters for our cloud deployment. - StdOut / StdErr: all instrumentations - OTEL gRPC / HTTP: all instrumentations - Google: all instrumentations except logging - Prometheus (pull-based): only metrics The exception is profiling, which only supports the google exporting due to lack of support by OTEL upstream. ## Configuration and structure - All instrumentation is moved into the new `backend/v3/instrumentation` package. It reuses configuration types, so both code and runtime configuration are easier to understand. - The `internal/telemetry` packages are removed. - Instrumentation is started with a single function and a proper shutdown function is now provided. - Legacy configuration is still parsed from the runtime config, as long as the new configuration is disabled. This allows backporting this feature to v4 without breaking existing configurations. # Additional Changes - Devcontainer: set `$PATH` variable so installed go binaries can be run individually, without NX. - NX: install GCI tool to fix imports # Additional Context - Closes https://github.com/zitadel/zitadel/issues/8408 - Closes https://github.com/zitadel/zitadel/issues/6664 - Backport to v4 --- .devcontainer/devcontainer.json | 4 + apps/api/project.json | 3 +- backend/v3/domain/defaults.go | 14 - backend/v3/domain/invoker_logging.go | 3 + backend/v3/domain/invoker_tracing.go | 9 +- backend/v3/instrumentation/caller.go | 46 +++ backend/v3/instrumentation/caller_test.go | 48 +++ .../v3/instrumentation/exportertype_enumer.go | 114 +++++++ backend/v3/instrumentation/instrumentation.go | 86 ++++++ .../instrumentation/instrumentation_test.go | 40 +++ .../v3/instrumentation/logformat_enumer.go | 102 +++++++ backend/v3/instrumentation/logger.go | 93 ++++++ backend/v3/instrumentation/logger_test.go | 81 +++++ .../logging/connect_handler.go | 71 +++++ .../instrumentation/logging/grpc_handler.go | 74 +++++ .../instrumentation/logging/http_handler.go | 63 ++++ backend/v3/instrumentation/metric.go | 286 ++++++++++++++++++ backend/v3/instrumentation/metric_test.go | 186 ++++++++++++ .../instrumentation}/metrics/http_handler.go | 32 +- .../v3/instrumentation/metrics/metric.go | 56 ++-- .../v3/instrumentation}/metrics/mock.go | 0 backend/v3/instrumentation/otel.go | 138 +++++++++ backend/v3/instrumentation/otel_test.go | 37 +++ backend/v3/instrumentation/profiler.go | 44 +++ backend/v3/instrumentation/profiler_test.go | 137 +++++++++ .../v3/instrumentation}/sampler_test.go | 4 +- backend/v3/instrumentation/slog.go | 197 ++++++++++++ backend/v3/instrumentation/tracing.go | 269 ++++++++++++++++ .../instrumentation/tracing/http_handler.go | 23 ++ backend/v3/instrumentation/tracing/tracing.go | 65 ++++ backend/v3/instrumentation/tracing_test.go | 191 ++++++++++++ backend/v3/telemetry/logging/logger.go | 12 - backend/v3/telemetry/metric/doc.go | 2 - backend/v3/telemetry/tracing/tracer.go | 24 -- cmd/admin/admin.go | 8 +- cmd/defaults.yaml | 83 ++++- cmd/initialise/init.go | 22 +- cmd/key/key.go | 9 +- cmd/mirror/auth.go | 12 +- cmd/mirror/config.go | 28 +- cmd/mirror/defaults.yaml | 78 +++++ cmd/mirror/event_store.go | 11 +- cmd/mirror/mirror.go | 25 +- cmd/mirror/system.go | 12 +- cmd/mirror/verify.go | 12 +- cmd/setup/cleanup.go | 12 +- cmd/setup/config.go | 30 +- cmd/setup/config_test.go | 3 +- cmd/setup/setup.go | 37 ++- cmd/setup/steps.yaml | 79 +++++ cmd/start/config.go | 51 ++-- cmd/start/start.go | 21 +- cmd/start/start_from_init.go | 43 ++- cmd/start/start_from_setup.go | 42 ++- cmd/zitadel.go | 3 +- go.mod | 53 ++-- go.sum | 102 ++++--- internal/actions/http_module_test.go | 5 +- internal/api/api.go | 11 +- internal/api/assets/asset.go | 2 +- internal/api/grpc/gerrors/zitadel_errors.go | 101 ++++--- .../api/grpc/gerrors/zitadel_errors_test.go | 36 ++- .../activity_interceptor.go | 2 +- .../connect_middleware/error_interceptor.go | 2 +- .../instance_interceptor.go | 4 +- .../connect_middleware/log_interceptor.go | 13 + .../connect_middleware/metrics_interceptor.go | 2 +- internal/api/grpc/server/gateway.go | 5 +- .../server/middleware/activity_interceptor.go | 2 +- .../server/middleware/error_interceptor.go | 2 +- .../server/middleware/instance_interceptor.go | 4 +- .../grpc/server/middleware/log_interceptor.go | 11 + .../server/middleware/metrics_interceptor.go | 2 +- internal/api/grpc/server/server.go | 3 +- .../user/v2/integration_test/intent_test.go | 8 +- internal/api/http/error.go | 94 ++++-- internal/api/http/error_test.go | 9 +- .../api/http/middleware/log_interceptor.go | 13 + .../http/middleware/metrics_interceptor.go | 4 +- .../http/middleware/telemetry_interceptor.go | 18 -- .../api/http/middleware/trace_interceptor.go | 18 ++ internal/api/http/request_context.go | 17 ++ internal/api/oidc/auth_request.go | 12 +- internal/api/oidc/client.go | 6 +- internal/api/oidc/error.go | 5 +- internal/api/oidc/error_test.go | 2 +- internal/api/oidc/introspect.go | 2 +- internal/api/oidc/key.go | 2 +- internal/api/oidc/op.go | 5 +- internal/api/oidc/server.go | 2 +- internal/api/oidc/token_client_credentials.go | 2 +- internal/api/oidc/token_code.go | 2 +- internal/api/oidc/token_device.go | 2 +- internal/api/oidc/token_exchange.go | 2 +- internal/api/oidc/token_jwt_profile.go | 2 +- internal/api/oidc/token_refresh.go | 2 +- internal/api/oidc/userinfo.go | 2 +- internal/api/saml/provider.go | 5 +- internal/api/scim/serrors/errors.go | 5 +- internal/api/ui/console/console.go | 7 +- .../api/ui/login/external_provider_handler.go | 2 +- internal/api/ui/login/login.go | 13 +- internal/domain/refresh_token_test.go | 3 +- internal/eventstore/handler/v2/metrics.go | 8 +- .../eventstore/handler/v2/metrics_test.go | 22 +- internal/notification/channels.go | 2 +- .../channels/instrumenting/metrics.go | 2 +- internal/queue/queue.go | 4 +- internal/telemetry/http_handler.go | 34 --- internal/telemetry/metrics/config/config.go | 37 --- internal/telemetry/metrics/noop.go | 45 --- internal/telemetry/metrics/otel/config.go | 20 -- .../telemetry/metrics/otel/open_telemetry.go | 163 ---------- internal/telemetry/otel/resource.go | 22 -- internal/telemetry/profiler/config/config.go | 30 -- .../telemetry/profiler/google/profiler.go | 26 -- internal/telemetry/tracing/caller.go | 22 -- internal/telemetry/tracing/config/config.go | 34 --- .../telemetry/tracing/google/google_tracer.go | 41 --- internal/telemetry/tracing/log/config.go | 39 --- internal/telemetry/tracing/otel/config.go | 77 ----- .../telemetry/tracing/otel/open_telemetry.go | 77 ----- internal/telemetry/tracing/sampler.go | 46 --- internal/telemetry/tracing/span.go | 46 --- internal/telemetry/tracing/tracing.go | 76 ++--- internal/zerrors/already_exists.go | 38 +-- internal/zerrors/already_exists_test.go | 69 +++-- internal/zerrors/deadline_exceeded.go | 42 +-- internal/zerrors/deadline_exceeded_test.go | 65 ++-- internal/zerrors/error.go | 19 -- internal/zerrors/error_test.go | 19 -- internal/zerrors/generate/error.go.tmpl | 34 --- internal/zerrors/generate/error_creator.go | 75 ----- .../zerrors/generate/error_interface.go.tmpl | 5 - internal/zerrors/generate/error_test.go.tmpl | 33 -- internal/zerrors/internal.go | 42 +-- internal/zerrors/internal_test.go | 65 ++-- internal/zerrors/invalid_argument.go | 51 +--- internal/zerrors/invalid_argument_test.go | 65 ++-- internal/zerrors/kind_enumer.go | 153 ++++++++++ internal/zerrors/not_found.go | 33 +- internal/zerrors/not_found_test.go | 65 ++-- internal/zerrors/permission_denied.go | 42 +-- internal/zerrors/permission_denied_test.go | 65 ++-- internal/zerrors/precondition_failed.go | 42 +-- internal/zerrors/precondition_failed_test.go | 65 ++-- internal/zerrors/resource_exhausted.go | 44 +-- internal/zerrors/resource_exhausted_test.go | 65 ++-- internal/zerrors/unauthenticated.go | 42 +-- internal/zerrors/unauthenticated_test.go | 65 ++-- internal/zerrors/unavailable.go | 42 +-- internal/zerrors/unavailable_test.go | 65 ++-- internal/zerrors/unimplemented.go | 42 +-- internal/zerrors/unimplemented_test.go | 65 ++-- internal/zerrors/unknown.go | 42 +-- internal/zerrors/unknown_test.go | 65 ++-- internal/zerrors/zerror.go | 107 ++++++- internal/zerrors/zerror_test.go | 39 ++- main.go | 7 +- pkg/grpc/admin/event.go | 2 +- 160 files changed, 4320 insertions(+), 2135 deletions(-) create mode 100644 backend/v3/instrumentation/caller.go create mode 100644 backend/v3/instrumentation/caller_test.go create mode 100644 backend/v3/instrumentation/exportertype_enumer.go create mode 100644 backend/v3/instrumentation/instrumentation.go create mode 100644 backend/v3/instrumentation/instrumentation_test.go create mode 100644 backend/v3/instrumentation/logformat_enumer.go create mode 100644 backend/v3/instrumentation/logger.go create mode 100644 backend/v3/instrumentation/logger_test.go create mode 100644 backend/v3/instrumentation/logging/connect_handler.go create mode 100644 backend/v3/instrumentation/logging/grpc_handler.go create mode 100644 backend/v3/instrumentation/logging/http_handler.go create mode 100644 backend/v3/instrumentation/metric.go create mode 100644 backend/v3/instrumentation/metric_test.go rename {internal/telemetry => backend/v3/instrumentation}/metrics/http_handler.go (82%) rename internal/telemetry/metrics/metrics.go => backend/v3/instrumentation/metrics/metric.go (68%) rename {internal/telemetry => backend/v3/instrumentation}/metrics/mock.go (100%) create mode 100644 backend/v3/instrumentation/otel.go create mode 100644 backend/v3/instrumentation/otel_test.go create mode 100644 backend/v3/instrumentation/profiler.go create mode 100644 backend/v3/instrumentation/profiler_test.go rename {internal/telemetry/tracing => backend/v3/instrumentation}/sampler_test.go (95%) create mode 100644 backend/v3/instrumentation/slog.go create mode 100644 backend/v3/instrumentation/tracing.go create mode 100644 backend/v3/instrumentation/tracing/http_handler.go create mode 100644 backend/v3/instrumentation/tracing/tracing.go create mode 100644 backend/v3/instrumentation/tracing_test.go delete mode 100644 backend/v3/telemetry/logging/logger.go delete mode 100644 backend/v3/telemetry/metric/doc.go delete mode 100644 backend/v3/telemetry/tracing/tracer.go create mode 100644 internal/api/grpc/server/connect_middleware/log_interceptor.go create mode 100644 internal/api/grpc/server/middleware/log_interceptor.go create mode 100644 internal/api/http/middleware/log_interceptor.go delete mode 100644 internal/api/http/middleware/telemetry_interceptor.go create mode 100644 internal/api/http/middleware/trace_interceptor.go delete mode 100644 internal/telemetry/http_handler.go delete mode 100644 internal/telemetry/metrics/config/config.go delete mode 100644 internal/telemetry/metrics/noop.go delete mode 100644 internal/telemetry/metrics/otel/config.go delete mode 100644 internal/telemetry/metrics/otel/open_telemetry.go delete mode 100644 internal/telemetry/otel/resource.go delete mode 100644 internal/telemetry/profiler/config/config.go delete mode 100644 internal/telemetry/profiler/google/profiler.go delete mode 100644 internal/telemetry/tracing/caller.go delete mode 100644 internal/telemetry/tracing/config/config.go delete mode 100644 internal/telemetry/tracing/google/google_tracer.go delete mode 100644 internal/telemetry/tracing/log/config.go delete mode 100644 internal/telemetry/tracing/otel/config.go delete mode 100644 internal/telemetry/tracing/otel/open_telemetry.go delete mode 100644 internal/telemetry/tracing/sampler.go delete mode 100644 internal/telemetry/tracing/span.go delete mode 100644 internal/zerrors/error.go delete mode 100644 internal/zerrors/error_test.go delete mode 100644 internal/zerrors/generate/error.go.tmpl delete mode 100644 internal/zerrors/generate/error_creator.go delete mode 100644 internal/zerrors/generate/error_interface.go.tmpl delete mode 100644 internal/zerrors/generate/error_test.go.tmpl create mode 100644 internal/zerrors/kind_enumer.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9ac80d39d..f67d499d3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -43,6 +43,10 @@ "containerEnv": { "DEVCONTAINER_LOCAL_WORKSPACE_ROOT": "${localWorkspaceFolder}" }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/workspaces/zitadel/.artifacts/bin/linux/amd64" + }, + "postStartCommand": "echo 'export PATH=\"${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH):$PATH\"' >> ~/.bashrc", "customizations": { "vscode": { "extensions": [ diff --git a/apps/api/project.json b/apps/api/project.json index a620aada0..775689f70 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -118,7 +118,7 @@ "dependsOn": [ "generate" ], - "command": "go test -race -coverprofile=profile.api.test-unit.cov -coverpkg=./internal/... ./...", + "command": "go test -race -coverprofile=profile.api.test-unit.cov -coverpkg=./internal/...,./backend/... ./...", "inputs": [ "sources", "{workspaceRoot}/go*" @@ -224,6 +224,7 @@ "executor": "nx:run-commands", "options": { "commands": [ + "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install github.com/daixiang0/gci@v0.11.2", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install github.com/dmarkham/enumer@v1.5.11", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install go.uber.org/mock/mockgen@v0.4.0", "GOBIN=${PWD}/.artifacts/bin/$(go env GOOS)/$(go env GOARCH) go install golang.org/x/tools/cmd/stringer@v0.36.0", diff --git a/backend/v3/domain/defaults.go b/backend/v3/domain/defaults.go index 62204c372..627d4583b 100644 --- a/backend/v3/domain/defaults.go +++ b/backend/v3/domain/defaults.go @@ -1,18 +1,12 @@ package domain import ( - "log/slog" - "github.com/zitadel/zitadel/backend/v3/storage/database" "github.com/zitadel/zitadel/backend/v3/storage/eventstore" - "github.com/zitadel/zitadel/backend/v3/telemetry/logging" - "github.com/zitadel/zitadel/backend/v3/telemetry/tracing" ) var ( pool database.Pool - tracer tracing.Tracer - logger logging.Logger = *logging.NewLogger(slog.Default()) legacyEventstore eventstore.LegacyEventstore ) @@ -20,14 +14,6 @@ func SetPool(p database.Pool) { pool = p } -func SetTracer(t tracing.Tracer) { - tracer = t -} - -func SetLogger(l logging.Logger) { - logger = l -} - func SetLegacyEventstore(es eventstore.LegacyEventstore) { legacyEventstore = es } diff --git a/backend/v3/domain/invoker_logging.go b/backend/v3/domain/invoker_logging.go index 1dc5311c7..6b987d4bc 100644 --- a/backend/v3/domain/invoker_logging.go +++ b/backend/v3/domain/invoker_logging.go @@ -3,6 +3,8 @@ package domain import ( "context" "time" + + slogctx "github.com/veqryn/slog-context" ) // loggingInvoker decorates each command with logging. @@ -19,6 +21,7 @@ func NewLoggingInvoker(next Invoker) *loggingInvoker { func (i *loggingInvoker) Invoke(ctx context.Context, executor Executor, opts *InvokeOpts) (err error) { start := time.Now() + logger := slogctx.FromCtx(ctx) logger.InfoContext(ctx, "invoking", "name", executor.String()) err = i.execute(ctx, executor, opts) diff --git a/backend/v3/domain/invoker_tracing.go b/backend/v3/domain/invoker_tracing.go index 2ee757445..c7ce71b3b 100644 --- a/backend/v3/domain/invoker_tracing.go +++ b/backend/v3/domain/invoker_tracing.go @@ -3,6 +3,8 @@ package domain import ( "context" "fmt" + + "github.com/zitadel/zitadel/backend/v3/instrumentation/tracing" ) // traceInvoker decorates each command with tracing. @@ -17,12 +19,9 @@ func NewTraceInvoker(next Invoker) *traceInvoker { } func (i *traceInvoker) Invoke(ctx context.Context, executor Executor, opts *InvokeOpts) (err error) { - ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", executor)) + ctx, span := tracing.NewNamedSpan(ctx, fmt.Sprintf("%T", executor)) defer func() { - if err != nil { - span.RecordError(err) - } - span.End() + span.EndWithError(err) }() return i.execute(ctx, executor, opts) diff --git a/backend/v3/instrumentation/caller.go b/backend/v3/instrumentation/caller.go new file mode 100644 index 000000000..5d46b8240 --- /dev/null +++ b/backend/v3/instrumentation/caller.go @@ -0,0 +1,46 @@ +package instrumentation + +import ( + "log/slog" + "runtime" +) + +type Caller struct { + Function string + File string + Line int +} + +// GetCaller returns the caller information +// skipping the given number of stack frames relative to the caller of this function. +func GetCaller(skip int) (Caller, bool) { + pc, file, line, ok := runtime.Caller(skip + 1) + if !ok { + slog.Default().Debug("no caller info", "id", "TRACE-3r8bE") + return Caller{}, false + } + + f := runtime.FuncForPC(pc) + if f == nil { + slog.Default().Debug("caller was nil", "id", "TRACE-25POw") + return Caller{}, false + } + return Caller{ + Function: f.Name(), + File: file, + Line: line, + }, true +} + +const CallerUnknown = "unknown" + +// GetCallingFunc returns the caller function name, +// skipping the given number of stack frames relative to the caller of this function. +// If the caller cannot be determined, [CallerUnknown] is returned. +func GetCallingFunc(skip int) string { + caller, ok := GetCaller(skip + 1) + if !ok { + return CallerUnknown + } + return caller.Function +} diff --git a/backend/v3/instrumentation/caller_test.go b/backend/v3/instrumentation/caller_test.go new file mode 100644 index 000000000..dc20a1543 --- /dev/null +++ b/backend/v3/instrumentation/caller_test.go @@ -0,0 +1,48 @@ +package instrumentation + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetCaller(t *testing.T) { + _, _, prevLine, _ := runtime.Caller(0) + caller, ok := GetCaller(0) + require.True(t, ok) + want := Caller{ + Function: "github.com/zitadel/zitadel/backend/v3/instrumentation.TestGetCaller", + File: "backend/v3/instrumentation/caller_test.go", + Line: prevLine + 1, + } + assert.Equal(t, want.Function, caller.Function, "function") + assert.Contains(t, caller.File, want.File, "file") + assert.Equal(t, want.Line, caller.Line, "line") +} + +func TestGetCallingFunc(t *testing.T) { + tests := []struct { + name string + skip int + want string + }{ + { + name: "test function", + skip: 0, + want: "github.com/zitadel/zitadel/backend/v3/instrumentation.TestGetCallingFunc.func1", + }, + { + name: "unknown caller", + skip: 100, + want: CallerUnknown, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + callerFunc := GetCallingFunc(tt.skip) + assert.Equal(t, tt.want, callerFunc, "function") + }) + } +} diff --git a/backend/v3/instrumentation/exportertype_enumer.go b/backend/v3/instrumentation/exportertype_enumer.go new file mode 100644 index 000000000..3f9d15b7b --- /dev/null +++ b/backend/v3/instrumentation/exportertype_enumer.go @@ -0,0 +1,114 @@ +// Code generated by "enumer -type=ExporterType -trimprefix=ExporterType -text -linecomment"; DO NOT EDIT. + +package instrumentation + +import ( + "fmt" + "strings" +) + +const _ExporterTypeName = "NoneStdOutStdErrGRPCHTTPGooglePrometheus" + +var _ExporterTypeIndex = [...]uint8{0, 0, 4, 10, 16, 20, 24, 30, 40} + +const _ExporterTypeLowerName = "nonestdoutstderrgrpchttpgoogleprometheus" + +func (i ExporterType) String() string { + if i < 0 || i >= ExporterType(len(_ExporterTypeIndex)-1) { + return fmt.Sprintf("ExporterType(%d)", i) + } + return _ExporterTypeName[_ExporterTypeIndex[i]:_ExporterTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _ExporterTypeNoOp() { + var x [1]struct{} + _ = x[ExporterTypeUnspecified-(0)] + _ = x[ExporterTypeNone-(1)] + _ = x[ExporterTypeStdOut-(2)] + _ = x[ExporterTypeStdErr-(3)] + _ = x[ExporterTypeGRPC-(4)] + _ = x[ExporterTypeHTTP-(5)] + _ = x[ExporterTypeGoogle-(6)] + _ = x[ExporterTypePrometheus-(7)] +} + +var _ExporterTypeValues = []ExporterType{ExporterTypeUnspecified, ExporterTypeNone, ExporterTypeStdOut, ExporterTypeStdErr, ExporterTypeGRPC, ExporterTypeHTTP, ExporterTypeGoogle, ExporterTypePrometheus} + +var _ExporterTypeNameToValueMap = map[string]ExporterType{ + _ExporterTypeName[0:0]: ExporterTypeUnspecified, + _ExporterTypeLowerName[0:0]: ExporterTypeUnspecified, + _ExporterTypeName[0:4]: ExporterTypeNone, + _ExporterTypeLowerName[0:4]: ExporterTypeNone, + _ExporterTypeName[4:10]: ExporterTypeStdOut, + _ExporterTypeLowerName[4:10]: ExporterTypeStdOut, + _ExporterTypeName[10:16]: ExporterTypeStdErr, + _ExporterTypeLowerName[10:16]: ExporterTypeStdErr, + _ExporterTypeName[16:20]: ExporterTypeGRPC, + _ExporterTypeLowerName[16:20]: ExporterTypeGRPC, + _ExporterTypeName[20:24]: ExporterTypeHTTP, + _ExporterTypeLowerName[20:24]: ExporterTypeHTTP, + _ExporterTypeName[24:30]: ExporterTypeGoogle, + _ExporterTypeLowerName[24:30]: ExporterTypeGoogle, + _ExporterTypeName[30:40]: ExporterTypePrometheus, + _ExporterTypeLowerName[30:40]: ExporterTypePrometheus, +} + +var _ExporterTypeNames = []string{ + _ExporterTypeName[0:0], + _ExporterTypeName[0:4], + _ExporterTypeName[4:10], + _ExporterTypeName[10:16], + _ExporterTypeName[16:20], + _ExporterTypeName[20:24], + _ExporterTypeName[24:30], + _ExporterTypeName[30:40], +} + +// ExporterTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ExporterTypeString(s string) (ExporterType, error) { + if val, ok := _ExporterTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _ExporterTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to ExporterType values", s) +} + +// ExporterTypeValues returns all values of the enum +func ExporterTypeValues() []ExporterType { + return _ExporterTypeValues +} + +// ExporterTypeStrings returns a slice of all String values of the enum +func ExporterTypeStrings() []string { + strs := make([]string, len(_ExporterTypeNames)) + copy(strs, _ExporterTypeNames) + return strs +} + +// IsAExporterType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i ExporterType) IsAExporterType() bool { + for _, v := range _ExporterTypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for ExporterType +func (i ExporterType) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for ExporterType +func (i *ExporterType) UnmarshalText(text []byte) error { + var err error + *i, err = ExporterTypeString(string(text)) + return err +} diff --git a/backend/v3/instrumentation/instrumentation.go b/backend/v3/instrumentation/instrumentation.go new file mode 100644 index 000000000..d7bc7df05 --- /dev/null +++ b/backend/v3/instrumentation/instrumentation.go @@ -0,0 +1,86 @@ +// Package instrumentation initializes and configures telemetry and logging instrumentation. +// After [Start] is called, the instrumentation providers can be accessed using global getters. +package instrumentation + +import ( + "context" + "log/slog" + "net/http" + "slices" + "strings" + "sync" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/trace" +) + +const Name = "zitadel/backend/v3/instrumentation/tracing" + +type Config struct { + ServiceName string + Trace TraceConfig + Metric MetricConfig + Log LogConfig + Profile ProfileConfig +} + +type ShutdownFunc func(context.Context) error + +var ( + startOnce sync.Once + shutdown ShutdownFunc + errStart error +) + +// Start initializes global instrumentation once, based on the provided configuration. +// It is safe to call multiple times; subsequent calls will return the same shutdown function and error as the first call. +func Start(ctx context.Context, cfg Config) (ShutdownFunc, error) { + startOnce.Do(func() { + errStart = startProfiler(cfg.Profile, cfg.ServiceName) + if errStart != nil { + return + } + var logProvider *log.LoggerProvider + logProvider, shutdown, errStart = setupOTelSDK(ctx, cfg) + if errStart != nil { + return + } + setLogger(logProvider, cfg.Log) + }) + return shutdown, errStart +} + +// NewTracer creates a new tracer from the global provider. +// See [trace.TracerProvider.Tracer] for details. +func NewTracer(name string, options ...trace.TracerOption) *Tracer { + provider := otel.GetTracerProvider() + return &Tracer{provider.Tracer(name, options...)} +} + +// NewMeter creates a new meter from the global provider. +// See [metric.MeterProvider.Meter] for details. +func NewMeter(name string, options ...metric.MeterOption) *Meter { + provider := otel.GetMeterProvider() + return &Meter{ + Meter: provider.Meter(name, options...), + } +} + +// Logger returns the globally configured logger. +func Logger() *slog.Logger { + return slog.Default() +} + +func RequestFilter(ignoredPrefix ...string) otelhttp.Filter { + return func(r *http.Request) bool { + return !slices.ContainsFunc( + ignoredPrefix, + func(endpoint string) bool { + return strings.HasPrefix(r.URL.RequestURI(), endpoint) + }, + ) + } +} diff --git a/backend/v3/instrumentation/instrumentation_test.go b/backend/v3/instrumentation/instrumentation_test.go new file mode 100644 index 000000000..77f3b5454 --- /dev/null +++ b/backend/v3/instrumentation/instrumentation_test.go @@ -0,0 +1,40 @@ +package instrumentation + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRequestFilter(t *testing.T) { + filter := RequestFilter("/foo", "/bar") + var tests = []struct { + name string + r *http.Request + want bool + }{ + { + name: "foo subpath false", + r: httptest.NewRequest("POST", "/foo/some", nil), + want: false, + }, + { + name: "foo exact false", + r: httptest.NewRequest("POST", "/foo", nil), + want: false, + }, + { + name: "other path true", + r: httptest.NewRequest("POST", "/other", nil), + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := filter(tt.r) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/backend/v3/instrumentation/logformat_enumer.go b/backend/v3/instrumentation/logformat_enumer.go new file mode 100644 index 000000000..0e6081534 --- /dev/null +++ b/backend/v3/instrumentation/logformat_enumer.go @@ -0,0 +1,102 @@ +// Code generated by "enumer -type=LogFormat -trimprefix=LogFormat -text -linecomment"; DO NOT EDIT. + +package instrumentation + +import ( + "fmt" + "strings" +) + +const _LogFormatName = "DisabledTextJSONGCP" + +var _LogFormatIndex = [...]uint8{0, 0, 8, 12, 16, 19} + +const _LogFormatLowerName = "disabledtextjsongcp" + +func (i LogFormat) String() string { + if i < 0 || i >= LogFormat(len(_LogFormatIndex)-1) { + return fmt.Sprintf("LogFormat(%d)", i) + } + return _LogFormatName[_LogFormatIndex[i]:_LogFormatIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _LogFormatNoOp() { + var x [1]struct{} + _ = x[LogFormatUndefined-(0)] + _ = x[LogFormatDisabled-(1)] + _ = x[LogFormatText-(2)] + _ = x[LogFormatJSON-(3)] + _ = x[LogFormatGCP-(4)] +} + +var _LogFormatValues = []LogFormat{LogFormatUndefined, LogFormatDisabled, LogFormatText, LogFormatJSON, LogFormatGCP} + +var _LogFormatNameToValueMap = map[string]LogFormat{ + _LogFormatName[0:0]: LogFormatUndefined, + _LogFormatLowerName[0:0]: LogFormatUndefined, + _LogFormatName[0:8]: LogFormatDisabled, + _LogFormatLowerName[0:8]: LogFormatDisabled, + _LogFormatName[8:12]: LogFormatText, + _LogFormatLowerName[8:12]: LogFormatText, + _LogFormatName[12:16]: LogFormatJSON, + _LogFormatLowerName[12:16]: LogFormatJSON, + _LogFormatName[16:19]: LogFormatGCP, + _LogFormatLowerName[16:19]: LogFormatGCP, +} + +var _LogFormatNames = []string{ + _LogFormatName[0:0], + _LogFormatName[0:8], + _LogFormatName[8:12], + _LogFormatName[12:16], + _LogFormatName[16:19], +} + +// LogFormatString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func LogFormatString(s string) (LogFormat, error) { + if val, ok := _LogFormatNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _LogFormatNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to LogFormat values", s) +} + +// LogFormatValues returns all values of the enum +func LogFormatValues() []LogFormat { + return _LogFormatValues +} + +// LogFormatStrings returns a slice of all String values of the enum +func LogFormatStrings() []string { + strs := make([]string, len(_LogFormatNames)) + copy(strs, _LogFormatNames) + return strs +} + +// IsALogFormat returns "true" if the value is listed in the enum definition. "false" otherwise +func (i LogFormat) IsALogFormat() bool { + for _, v := range _LogFormatValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for LogFormat +func (i LogFormat) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for LogFormat +func (i *LogFormat) UnmarshalText(text []byte) error { + var err error + *i, err = LogFormatString(string(text)) + return err +} diff --git a/backend/v3/instrumentation/logger.go b/backend/v3/instrumentation/logger.go new file mode 100644 index 000000000..ac0b5b3b7 --- /dev/null +++ b/backend/v3/instrumentation/logger.go @@ -0,0 +1,93 @@ +package instrumentation + +import ( + "context" + "fmt" + "os" + + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + sdk_log "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/resource" +) + +func newLoggerProvider(ctx context.Context, cfg ExporterConfig, resource *resource.Resource) (_ *sdk_log.LoggerProvider, err error) { + var exporter sdk_log.Exporter + switch cfg.Type { + case ExporterTypeUnspecified, ExporterTypeNone: + // no exporter + case ExporterTypeStdOut, ExporterTypeStdErr: + exporter, err = logStdOutExporter(cfg) + case ExporterTypeGRPC: + exporter, err = logGrpcExporter(ctx, cfg) + case ExporterTypeHTTP: + exporter, err = logHttpExporter(ctx, cfg) + case ExporterTypeGoogle, ExporterTypePrometheus: + fallthrough // google, prometheus are not supported for logs + default: + err = errExporterType(cfg.Type, "logger") + } + if err != nil { + return nil, fmt.Errorf("log exporter: %w", err) + } + opts := []sdk_log.LoggerProviderOption{ + sdk_log.WithResource(resource), + } + if exporter != nil { + opts = append(opts, sdk_log.WithProcessor( + sdk_log.NewBatchProcessor( + exporter, + sdk_log.WithExportInterval(cfg.BatchDuration), + ), + )) + } + loggerProvider := sdk_log.NewLoggerProvider(opts...) + return loggerProvider, nil +} + +func logStdOutExporter(cfg ExporterConfig) (sdk_log.Exporter, error) { + options := []stdoutlog.Option{ + stdoutlog.WithPrettyPrint(), + } + if cfg.Type == ExporterTypeStdErr { + options = append(options, stdoutlog.WithWriter(os.Stderr)) + } + exporter, err := stdoutlog.New(options...) + if err != nil { + return nil, err + } + return exporter, nil +} + +func logGrpcExporter(ctx context.Context, cfg ExporterConfig) (sdk_log.Exporter, error) { + var grpcOpts []otlploggrpc.Option + if cfg.Endpoint != "" { + grpcOpts = append(grpcOpts, otlploggrpc.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + grpcOpts = append(grpcOpts, otlploggrpc.WithInsecure()) + } + + exporter, err := otlploggrpc.New(ctx, grpcOpts...) + if err != nil { + return nil, err + } + return exporter, nil +} + +func logHttpExporter(ctx context.Context, cfg ExporterConfig) (sdk_log.Exporter, error) { + var httpOpts []otlploghttp.Option + if cfg.Endpoint != "" { + httpOpts = append(httpOpts, otlploghttp.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + httpOpts = append(httpOpts, otlploghttp.WithInsecure()) + } + + exporter, err := otlploghttp.New(ctx, httpOpts...) + if err != nil { + return nil, err + } + return exporter, nil +} diff --git a/backend/v3/instrumentation/logger_test.go b/backend/v3/instrumentation/logger_test.go new file mode 100644 index 000000000..dfa6d9464 --- /dev/null +++ b/backend/v3/instrumentation/logger_test.go @@ -0,0 +1,81 @@ +package instrumentation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/resource" +) + +func Test_newLoggerProvider(t *testing.T) { + resource, err := resource.New(t.Context()) + require.NoError(t, err) + + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + cfg ExporterConfig + wantErr bool + }{ + { + name: "none exporter returns no error", + cfg: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + { + name: "stdout exporter returns no error", + cfg: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + { + name: "stderr exporter returns no error", + cfg: ExporterConfig{ + Type: ExporterTypeStdErr, + }, + }, + { + name: "grpc exporter returns no error", + cfg: ExporterConfig{ + Type: ExporterTypeGRPC, + Endpoint: "localhost:4317", + }, + }, + { + name: "http exporter returns no error", + cfg: ExporterConfig{ + Type: ExporterTypeHTTP, + Endpoint: "localhost:4318", + }, + }, + { + name: "google exporter returns error", + cfg: ExporterConfig{ + Type: ExporterTypeGoogle, + GoogleProjectID: "test-project", + }, + wantErr: true, + }, + { + name: "prometheus exporter returns error", + cfg: ExporterConfig{ + Type: ExporterTypePrometheus, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := newLoggerProvider(t.Context(), tt.cfg, resource) + if tt.wantErr { + assert.Error(t, gotErr) + assert.Nil(t, got) + } else { + assert.NoError(t, gotErr) + assert.NotNil(t, got) + } + }) + } +} diff --git a/backend/v3/instrumentation/logging/connect_handler.go b/backend/v3/instrumentation/logging/connect_handler.go new file mode 100644 index 000000000..14a9b9f18 --- /dev/null +++ b/backend/v3/instrumentation/logging/connect_handler.go @@ -0,0 +1,71 @@ +package logging + +import ( + "context" + "log/slog" + "slices" + "strings" + + "connectrpc.com/connect" + slogctx "github.com/veqryn/slog-context" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" +) + +func NewConnectInterceptor(next connect.UnaryFunc, ignoredMethodSuffixes ...string) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if slices.ContainsFunc(ignoredMethodSuffixes, func(s string) bool { + return strings.HasSuffix(req.Spec().Procedure, s) + }) { + return next(ctx, req) + } + + logger := instrumentation.Logger() + ctx = instrumentation.SetConnectRequestDetails(ctx, req) + ctx = slogctx.NewCtx(ctx, logger) + + resp, err := next(ctx, req) + var code connect.Code + if err != nil { + code = connect.CodeOf(err) + } + logger.Log(ctx, + assertConnectLevel(code), + "connect RPC served", + "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 new file mode 100644 index 000000000..c5315f63c --- /dev/null +++ b/backend/v3/instrumentation/logging/grpc_handler.go @@ -0,0 +1,74 @@ +package logging + +import ( + "context" + "log/slog" + "slices" + "strings" + + slogctx "github.com/veqryn/slog-context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" +) + +func NewGrpcInterceptor(ignoredMethodSuffixes ...string) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (any, error) { + if slices.ContainsFunc(ignoredMethodSuffixes, func(s string) bool { + return strings.HasSuffix(info.FullMethod, s) + }) { + return next(ctx, req) + } + + logger := instrumentation.Logger() + ctx = instrumentation.SetGrpcRequestDetails(ctx, info) + ctx = slogctx.NewCtx(ctx, logger) + + resp, err := next(ctx, req) + var code codes.Code + if err != nil { + code = status.Code(err) + } + logger.Log(ctx, + assertGrpcLevel(code), + "gRPC served", + "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 new file mode 100644 index 000000000..31efeb59c --- /dev/null +++ b/backend/v3/instrumentation/logging/http_handler.go @@ -0,0 +1,63 @@ +package logging + +import ( + "log/slog" + "net/http" + + slogctx "github.com/veqryn/slog-context" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" +) + +func NewHandler(next http.Handler, service string, ignoredPrefix ...string) http.Handler { + filter := instrumentation.RequestFilter(ignoredPrefix...) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !filter(r) { + next.ServeHTTP(w, r) + return + } + logger := instrumentation.Logger() + ctx := instrumentation.SetHttpRequestDetails(r.Context(), service, r) + ctx = slogctx.NewCtx(ctx, logger) + sw := &statusWriter{ResponseWriter: w} + + next.ServeHTTP(sw, r.WithContext(ctx)) + + logger.Log(ctx, + LogLevelFromStatus(sw.status), + "http request served", + "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 + status int +} + +func (w *statusWriter) Write(p []byte) (int, error) { + if w.status == 0 { + w.status = http.StatusOK + } + return w.ResponseWriter.Write(p) +} + +func (w *statusWriter) WriteHeader(statusCode int) { + w.status = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/backend/v3/instrumentation/metric.go b/backend/v3/instrumentation/metric.go new file mode 100644 index 000000000..01c0d05bb --- /dev/null +++ b/backend/v3/instrumentation/metric.go @@ -0,0 +1,286 @@ +package instrumentation + +import ( + "context" + "fmt" + "net/http" + "os" + "sync" + + google_metric "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/instrumentation" + sdk_metric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + + "github.com/zitadel/zitadel/internal/zerrors" +) + +type MetricConfig struct { + Exporter ExporterConfig +} + +type Meter struct { + Meter metric.Meter + Counters sync.Map + UpDownSumObserver sync.Map + ValueObservers sync.Map + Histograms sync.Map +} + +// TODO: remove for v5 release +type LegacyMetricConfig struct { + Type string +} + +func (c *MetricConfig) SetLegacyConfig(lc *LegacyMetricConfig) { + typ := c.Exporter.Type + if lc == nil || !typ.isNone() { + return + } + if lc.Type == "otel" { + c.Exporter.Type = ExporterTypePrometheus + } +} + +var hasPrometheusExporter bool + +func (m *Meter) GetExporter() http.Handler { + if hasPrometheusExporter { + return promhttp.Handler() + } + return http.NotFoundHandler() +} + +func (m *Meter) RegisterCounter(name, description string) error { + if _, exists := m.Counters.Load(name); exists { + return nil + } + counter, err := m.Meter.Int64Counter(name, metric.WithDescription(description)) + if err != nil { + return err + } + m.Counters.Store(name, counter) + return nil +} + +func (m *Meter) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { + counter, exists := m.Counters.Load(name) + if !exists { + return zerrors.ThrowNotFound(nil, "METER-4u8fs", "Errors.Metrics.Counter.NotFound") + } + counter.(metric.Int64Counter).Add(ctx, value, MapToAddOption(labels)...) + return nil +} + +func (m *Meter) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { + histogram, exists := m.Histograms.Load(name) + if !exists { + return zerrors.ThrowNotFound(nil, "METER-5wwb1", "Errors.Metrics.Histogram.NotFound") + } + histogram.(metric.Float64Histogram).Record(ctx, value, MapToRecordOption(labels)...) + return nil +} + +func (m *Meter) RegisterHistogram(name, description, unit string, buckets []float64) error { + if _, exists := m.Histograms.Load(name); exists { + return nil + } + + histogram, err := m.Meter.Float64Histogram(name, + metric.WithDescription(description), + metric.WithUnit(unit), + metric.WithExplicitBucketBoundaries(buckets...), + ) + if err != nil { + return err + } + + m.Histograms.Store(name, histogram) + return nil +} + +func (m *Meter) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { + if _, exists := m.UpDownSumObserver.Load(name); exists { + return nil + } + + counter, err := m.Meter.Int64ObservableUpDownCounter(name, metric.WithInt64Callback(callbackFunc), metric.WithDescription(description)) + if err != nil { + return err + } + + m.UpDownSumObserver.Store(name, counter) + return nil +} + +func (m *Meter) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { + if _, exists := m.UpDownSumObserver.Load(name); exists { + return nil + } + + gauge, err := m.Meter.Int64ObservableGauge(name, metric.WithInt64Callback(callbackFunc), metric.WithDescription(description)) + if err != nil { + return err + } + + m.UpDownSumObserver.Store(name, gauge) + return nil +} + +func MapToAddOption(labels map[string]attribute.Value) []metric.AddOption { + return []metric.AddOption{metric.WithAttributes(labelsToAttributes(labels)...)} +} + +func MapToRecordOption(labels map[string]attribute.Value) []metric.RecordOption { + return []metric.RecordOption{metric.WithAttributes(labelsToAttributes(labels)...)} +} + +func labelsToAttributes(labels map[string]attribute.Value) []attribute.KeyValue { + if labels == nil { + return nil + } + attributes := make([]attribute.KeyValue, 0, len(labels)) + for key, value := range labels { + attributes = append(attributes, attribute.KeyValue{ + Key: attribute.Key(key), + Value: value, + }) + } + return attributes +} + +func newMeterProvider(ctx context.Context, cfg MetricConfig, resource *resource.Resource) (_ *sdk_metric.MeterProvider, err error) { + var readerOption sdk_metric.Option + switch cfg.Exporter.Type { + case ExporterTypeUnspecified, ExporterTypeNone: + // no reader option + case ExporterTypeStdOut, ExporterTypeStdErr: + readerOption, err = metricStdOutOption(cfg.Exporter) + case ExporterTypeGRPC: + readerOption, err = metricGrpcOption(ctx, cfg.Exporter) + case ExporterTypeHTTP: + readerOption, err = metricHttpOption(ctx, cfg.Exporter) + case ExporterTypeGoogle: + readerOption, err = metricGoogleOption(cfg.Exporter) + case ExporterTypePrometheus: + readerOption, err = metricPrometheusOption() + default: + err = errExporterType(cfg.Exporter.Type, "metrics") + } + if err != nil { + return nil, fmt.Errorf("meter reader: %w", err) + } + + // create a view to filter out unwanted attributes + view := sdk_metric.NewView( + sdk_metric.Instrument{ + Scope: instrumentation.Scope{Name: otelhttp.ScopeName}, + }, + sdk_metric.Stream{ + AttributeFilter: attribute.NewAllowKeysFilter("http.method", "http.status_code", "http.target"), + }, + ) + + opts := []sdk_metric.Option{ + sdk_metric.WithResource(resource), + sdk_metric.WithView(view), + } + if readerOption != nil { + opts = append(opts, readerOption) + } + meterProvider := sdk_metric.NewMeterProvider(opts...) + return meterProvider, nil +} + +func metricStdOutOption(cfg ExporterConfig) (sdk_metric.Option, error) { + options := []stdoutmetric.Option{ + stdoutmetric.WithPrettyPrint(), + } + if cfg.Type == ExporterTypeStdErr { + options = append(options, stdoutmetric.WithWriter(os.Stderr)) + } + exporter, err := stdoutmetric.New(options...) + if err != nil { + return nil, err + } + return sdk_metric.WithReader( + sdk_metric.NewPeriodicReader( + exporter, + sdk_metric.WithInterval(cfg.BatchDuration), + ), + ), nil +} + +func metricGrpcOption(ctx context.Context, cfg ExporterConfig) (sdk_metric.Option, error) { + var grpcOpts []otlpmetricgrpc.Option + if cfg.Endpoint != "" { + grpcOpts = append(grpcOpts, otlpmetricgrpc.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + grpcOpts = append(grpcOpts, otlpmetricgrpc.WithInsecure()) + } + + exporter, err := otlpmetricgrpc.New(ctx, grpcOpts...) + if err != nil { + return nil, err + } + return sdk_metric.WithReader( + sdk_metric.NewPeriodicReader( + exporter, + sdk_metric.WithInterval(cfg.BatchDuration), + ), + ), nil +} + +func metricHttpOption(ctx context.Context, cfg ExporterConfig) (sdk_metric.Option, error) { + var httpOpts []otlpmetrichttp.Option + if cfg.Endpoint != "" { + httpOpts = append(httpOpts, otlpmetrichttp.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + httpOpts = append(httpOpts, otlpmetrichttp.WithInsecure()) + } + + exporter, err := otlpmetrichttp.New(ctx, httpOpts...) + if err != nil { + return nil, err + } + return sdk_metric.WithReader( + sdk_metric.NewPeriodicReader( + exporter, + sdk_metric.WithInterval(cfg.BatchDuration), + ), + ), nil +} + +func metricGoogleOption(cfg ExporterConfig) (sdk_metric.Option, error) { + exporter, err := google_metric.New( + google_metric.WithProjectID(cfg.GoogleProjectID), + ) + if err != nil { + return nil, err + } + return sdk_metric.WithReader( + sdk_metric.NewPeriodicReader( + exporter, + sdk_metric.WithInterval(cfg.BatchDuration), + ), + ), nil +} + +func metricPrometheusOption() (sdk_metric.Option, error) { + prom, err := prometheus.New(prometheus.WithoutScopeInfo()) + if err != nil { + return nil, err + } + hasPrometheusExporter = true + return sdk_metric.WithReader(prom), nil +} diff --git a/backend/v3/instrumentation/metric_test.go b/backend/v3/instrumentation/metric_test.go new file mode 100644 index 000000000..c2ffef1a8 --- /dev/null +++ b/backend/v3/instrumentation/metric_test.go @@ -0,0 +1,186 @@ +package instrumentation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/resource" +) + +func TestMetricConfig_SetLegacyConfig(t *testing.T) { + tests := []struct { + name string // description of this test case + target MetricConfig + lc *LegacyMetricConfig + want MetricConfig + }{ + { + name: "nil legacy config does not change target", + target: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + lc: nil, + want: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + }, + { + name: "non-none target type does not change target", + target: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + lc: &LegacyMetricConfig{ + Type: "otel", + }, + want: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + }, + { + name: "sets fields from legacy config (type none)", + target: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + lc: &LegacyMetricConfig{ + Type: "otel", + }, + want: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypePrometheus, + }, + }, + }, + { + name: "sets fields from legacy config (type unspecified)", + target: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeUnspecified, + }, + }, + lc: &LegacyMetricConfig{ + Type: "otel", + }, + want: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypePrometheus, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.target.SetLegacyConfig(tt.lc) + assert.Equal(t, tt.want, tt.target) + }) + } +} + +func Test_newMeterProvider(t *testing.T) { + resource, err := resource.New(t.Context()) + require.NoError(t, err) + + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + cfg MetricConfig + wantErr bool + }{ + { + name: "none exporter returns no error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + wantErr: false, + }, + { + name: "unsupported exporter returns error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterType(-1), + }, + }, + wantErr: true, + }, + { + name: "prometheus exporter returns no error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypePrometheus, + }, + }, + wantErr: false, + }, + { + name: "stdout exporter returns no error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + wantErr: false, + }, + { + name: "stderr exporter returns no error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdErr, + }, + }, + wantErr: false, + }, + { + name: "http exporter returns no error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeHTTP, + Endpoint: "localhost:4318", + }, + }, + wantErr: false, + }, + { + name: "google exporter return error (no credentials)", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + }, + }, + wantErr: true, + }, + { + name: "grpc exporter returns no error", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGRPC, + Endpoint: "localhost:4317", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := newMeterProvider(t.Context(), tt.cfg, resource) + if tt.wantErr { + assert.Error(t, gotErr) + assert.Nil(t, got) + } else { + assert.NoError(t, gotErr) + assert.NotNil(t, got) + } + }) + } +} diff --git a/internal/telemetry/metrics/http_handler.go b/backend/v3/instrumentation/metrics/http_handler.go similarity index 82% rename from internal/telemetry/metrics/http_handler.go rename to backend/v3/instrumentation/metrics/http_handler.go index cdfe21ef4..f03d3b5ac 100644 --- a/internal/telemetry/metrics/http_handler.go +++ b/backend/v3/instrumentation/metrics/http_handler.go @@ -5,7 +5,10 @@ import ( "net/http" "strings" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" ) const ( @@ -23,7 +26,7 @@ const ( type Handler struct { handler http.Handler methods []MetricType - filters []Filter + filter otelhttp.Filter } type MetricType int32 @@ -47,13 +50,11 @@ func (r *StatusRecorder) WriteHeader(status int) { type Filter func(*http.Request) bool -func NewMetricsHandler(handler http.Handler, metricMethods []MetricType, ignoredEndpoints ...string) http.Handler { +func NewHandler(handler http.Handler, metricMethods []MetricType, ignoredEndpoints ...string) http.Handler { h := Handler{ handler: handler, methods: metricMethods, - } - if len(ignoredEndpoints) > 0 { - h.filters = append(h.filters, shouldNotIgnore(ignoredEndpoints...)) + filter: instrumentation.RequestFilter(ignoredEndpoints...), } return &h } @@ -76,12 +77,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handler.ServeHTTP(w, r) return } - for _, f := range h.filters { - if !f(r) { - // Simply pass through to the handler if a filter rejects the request - h.handler.ServeHTTP(w, r) - return - } + if !h.filter(r) { + // Simply pass through to the handler if a filter rejects the request + h.handler.ServeHTTP(w, r) + return } uri := strings.Split(r.RequestURI, "?")[0] recorder := &StatusRecorder{ @@ -134,14 +133,3 @@ func RegisterRequestCodeCounter(recorder *StatusRecorder, r *http.Request) { RegisterCounter(ReturnCodeCounter, ReturnCodeCounterDescription) AddCount(r.Context(), ReturnCodeCounter, 1, labels) } - -func shouldNotIgnore(endpoints ...string) func(r *http.Request) bool { - return func(r *http.Request) bool { - for _, endpoint := range endpoints { - if strings.HasPrefix(r.URL.RequestURI(), endpoint) { - return false - } - } - return true - } -} diff --git a/internal/telemetry/metrics/metrics.go b/backend/v3/instrumentation/metrics/metric.go similarity index 68% rename from internal/telemetry/metrics/metrics.go rename to backend/v3/instrumentation/metrics/metric.go index b25dc619c..3fe5aa7fb 100644 --- a/internal/telemetry/metrics/metrics.go +++ b/backend/v3/instrumentation/metrics/metric.go @@ -3,9 +3,13 @@ package metrics import ( "context" "net/http" + "sync" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" + "github.com/zitadel/zitadel/cmd/build" ) const ( @@ -19,7 +23,6 @@ const ( type Metrics interface { GetExporter() http.Handler - GetMetricsProvider() metric.MeterProvider RegisterCounter(name, description string) error AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error @@ -28,60 +31,41 @@ type Metrics interface { RegisterHistogram(name, description, unit string, buckets []float64) error } -var M Metrics +var globalMeter = sync.OnceValue(func() Metrics { + return instrumentation.NewMeter( + instrumentation.Name, + metric.WithInstrumentationVersion(build.Version()), + ) +}) -func GetExporter() http.Handler { - if M == nil { - return nil - } - return M.GetExporter() +func GlobalMeter() Metrics { + return globalMeter() } -func GetMetricsProvider() metric.MeterProvider { - if M == nil { - return nil - } - return M.GetMetricsProvider() +func GetExporter() http.Handler { + return globalMeter().GetExporter() } func RegisterCounter(name, description string) error { - if M == nil { - return nil - } - return M.RegisterCounter(name, description) + return globalMeter().RegisterCounter(name, description) } func AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { - if M == nil { - return nil - } - return M.AddCount(ctx, name, value, labels) + return globalMeter().AddCount(ctx, name, value, labels) } func AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { - if M == nil { - return nil - } - return M.AddHistogramMeasurement(ctx, name, value, labels) + return globalMeter().AddHistogramMeasurement(ctx, name, value, labels) } func RegisterHistogram(name, description, unit string, buckets []float64) error { - if M == nil { - return nil - } - return M.RegisterHistogram(name, description, unit, buckets) + return globalMeter().RegisterHistogram(name, description, unit, buckets) } func RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { - if M == nil { - return nil - } - return M.RegisterUpDownSumObserver(name, description, callbackFunc) + return globalMeter().RegisterUpDownSumObserver(name, description, callbackFunc) } func RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { - if M == nil { - return nil - } - return M.RegisterValueObserver(name, description, callbackFunc) + return globalMeter().RegisterValueObserver(name, description, callbackFunc) } diff --git a/internal/telemetry/metrics/mock.go b/backend/v3/instrumentation/metrics/mock.go similarity index 100% rename from internal/telemetry/metrics/mock.go rename to backend/v3/instrumentation/metrics/mock.go diff --git a/backend/v3/instrumentation/otel.go b/backend/v3/instrumentation/otel.go new file mode 100644 index 000000000..7cc5a2fff --- /dev/null +++ b/backend/v3/instrumentation/otel.go @@ -0,0 +1,138 @@ +package instrumentation + +import ( + "context" + "errors" + "fmt" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + + "github.com/zitadel/zitadel/cmd/build" +) + +//go:generate enumer -type=ExporterType -trimprefix=ExporterType -text -linecomment +type ExporterType int + +// ExporterType defines the type of exporter to use. +// - metrics supports all types +// - tracing supports all types except Prometheus +// - logging supports all types except Google and Prometheus +// - profiling supports only Google and None +const ( + // Empty line comment sets empty string of unspecified value + ExporterTypeUnspecified ExporterType = iota // + ExporterTypeNone + ExporterTypeStdOut + ExporterTypeStdErr + ExporterTypeGRPC + ExporterTypeHTTP + ExporterTypeGoogle + ExporterTypePrometheus +) + +// isNone returns true if the ExporterType is either Unspecified or None. +func (e ExporterType) isNone() bool { + return e == ExporterTypeUnspecified || e == ExporterTypeNone +} + +type ExporterConfig struct { + Type ExporterType + Endpoint string + Insecure bool + BatchDuration time.Duration + GoogleProjectID string // only for tracing, metrics and profiler +} + +// setupOTelSDK bootstraps the OpenTelemetry pipeline. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func setupOTelSDK(ctx context.Context, cfg Config) (*log.LoggerProvider, ShutdownFunc, error) { + var ( + shutdownFuncs []ShutdownFunc + err error + ) + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown := func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + // Set up propagator. + prop := newPropagator() + otel.SetTextMapPropagator(prop) + + resource, err := resourceWithService("ZITADEL") + if err != nil { + return nil, shutdown, err + } + + // Set up trace provider. + tracerProvider, err := newTracerProvider(ctx, cfg.Trace, resource) + if err != nil { + handleErr(err) + return nil, shutdown, err + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + // Set up meter provider. + meterProvider, err := newMeterProvider(ctx, cfg.Metric, resource) + if err != nil { + handleErr(err) + return nil, shutdown, err + } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + otel.SetMeterProvider(meterProvider) + // Set up logger provider. + loggerProvider, err := newLoggerProvider(ctx, cfg.Log.Exporter, resource) + if err != nil { + handleErr(err) + return nil, shutdown, err + } + shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) + global.SetLoggerProvider(loggerProvider) + + return loggerProvider, shutdown, err +} + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func resourceWithService(serviceName string) (*resource.Resource, error) { + attributes := []attribute.KeyValue{ + semconv.ServiceNameKey.String(serviceName), + } + if build.Version() != "" { + attributes = append(attributes, semconv.ServiceVersionKey.String(build.Version())) + } + return resource.Merge( + resource.Default(), + resource.NewWithAttributes("", attributes...), + ) +} + +func errExporterType(typ ExporterType, instrument string) error { + return fmt.Errorf("exporter type \"%v\" unsupported for %s", typ, instrument) +} diff --git a/backend/v3/instrumentation/otel_test.go b/backend/v3/instrumentation/otel_test.go new file mode 100644 index 000000000..e433fd416 --- /dev/null +++ b/backend/v3/instrumentation/otel_test.go @@ -0,0 +1,37 @@ +package instrumentation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExporterType_isNone(t *testing.T) { + tests := []struct { + name string + e ExporterType + want bool + }{ + { + name: "unspecified is none", + e: ExporterTypeUnspecified, + want: true, + }, + { + name: "none is none", + e: ExporterTypeNone, + want: true, + }, + { + name: "stdout is not none", + e: ExporterTypeStdOut, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.e.isNone() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/backend/v3/instrumentation/profiler.go b/backend/v3/instrumentation/profiler.go new file mode 100644 index 000000000..527a835a5 --- /dev/null +++ b/backend/v3/instrumentation/profiler.go @@ -0,0 +1,44 @@ +package instrumentation + +import ( + "cloud.google.com/go/profiler" + + "github.com/zitadel/zitadel/cmd/build" +) + +type ProfileConfig struct { + Exporter ExporterConfig +} + +// TODO: remove for v5 release +type LegacyProfileConfig struct { + Type string + ProjectID string +} + +func (c *ProfileConfig) SetLegacyConfig(lc *LegacyProfileConfig) { + typ := c.Exporter.Type + if lc == nil || !typ.isNone() { + return + } + if lc.Type == "google" { + c.Exporter.Type = ExporterTypeGoogle + c.Exporter.GoogleProjectID = lc.ProjectID + } +} + +func startProfiler(cfg ProfileConfig, serviceName string) error { + typ := cfg.Exporter.Type + if typ.isNone() { + return nil + } + if typ != ExporterTypeGoogle { + return errExporterType(typ, "profiler") + } + pc := profiler.Config{ + Service: serviceName, + ServiceVersion: build.Version(), + ProjectID: cfg.Exporter.GoogleProjectID, + } + return profiler.Start(pc) +} diff --git a/backend/v3/instrumentation/profiler_test.go b/backend/v3/instrumentation/profiler_test.go new file mode 100644 index 000000000..e96b335f1 --- /dev/null +++ b/backend/v3/instrumentation/profiler_test.go @@ -0,0 +1,137 @@ +package instrumentation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProfileConfig_SetLegacyConfig(t *testing.T) { + tests := []struct { + name string + target ProfileConfig + lc *LegacyProfileConfig + want ProfileConfig + }{ + { + name: "nil legacy config does not change target", + target: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + lc: nil, + want: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + }, + { + name: "non-none target type does not change target", + target: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + lc: &LegacyProfileConfig{ + Type: "google", + ProjectID: "project-id", + }, + want: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + }, + { + name: "sets fields from legacy config (type none)", + target: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + lc: &LegacyProfileConfig{ + Type: "google", + ProjectID: "project-id", + }, + want: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + GoogleProjectID: "project-id", + }, + }, + }, + { + name: "sets fields from legacy config (type unspecified)", + target: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeUnspecified, + }, + }, + lc: &LegacyProfileConfig{ + Type: "google", + ProjectID: "project-id", + }, + want: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + GoogleProjectID: "project-id", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.target.SetLegacyConfig(tt.lc) + assert.Equal(t, tt.want, tt.target) + }) + } +} + +func Test_startProfiler(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + cfg ProfileConfig + wantErr bool + }{ + { + name: "none exporter returns no error", + cfg: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + }, + { + name: "google exporter returns error (no credentials)", + cfg: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + GoogleProjectID: "test-project", + }, + }, + wantErr: true, + }, + { + name: "stdout exporter returns error", + cfg: ProfileConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotErr := startProfiler(tt.cfg, "service-name") + if tt.wantErr { + assert.Error(t, gotErr) + } else { + assert.NoError(t, gotErr) + } + }) + } +} diff --git a/internal/telemetry/tracing/sampler_test.go b/backend/v3/instrumentation/sampler_test.go similarity index 95% rename from internal/telemetry/tracing/sampler_test.go rename to backend/v3/instrumentation/sampler_test.go index 1f2070a0f..d494416d7 100644 --- a/internal/telemetry/tracing/sampler_test.go +++ b/backend/v3/instrumentation/sampler_test.go @@ -1,4 +1,4 @@ -package tracing +package instrumentation import ( "context" @@ -59,7 +59,7 @@ func TestSpanKindBased(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sampler := SpanKindBased(tt.args.sampler, tt.args.kinds...) + sampler := spanKindBased(tt.args.sampler, tt.args.kinds...) assert.Equal(t, tt.want.description, sampler.Description()) p := sdk_trace.NewTracerProvider(sdk_trace.WithSampler(sampler)) diff --git a/backend/v3/instrumentation/slog.go b/backend/v3/instrumentation/slog.go new file mode 100644 index 000000000..b5a30a861 --- /dev/null +++ b/backend/v3/instrumentation/slog.go @@ -0,0 +1,197 @@ +package instrumentation + +import ( + "context" + "log/slog" + "net/http" + "os" + "strings" + "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" + "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" +) + +type LogFormat int + +//go:generate enumer -type=LogFormat -trimprefix=LogFormat -text -linecomment +const ( + // Empty line comment sets empty string of unspecified value + LogFormatUndefined LogFormat = iota // + LogFormatDisabled + LogFormatText + LogFormatJSON + // JSON formatted logs compatible with Google Cloud Platform + LogFormatGCP +) + +type LogConfig struct { + Level slog.Level + Format LogFormat + AddSource bool + GoogleProjectID string + GoogleLogName string + Exporter ExporterConfig +} + +// setLogger configures the global slog logger. +// Logs are sent to StdErr and/or the [log.LoggerProvider]. +// +// When present in the context, each line emitted contains: +// - Trace ID +// - Span ID +// - Service name +// - Sanitized request hosts from [http_util.DomainCtx] +// - Request details, such as method and path. +func setLogger(provider *log.LoggerProvider, cfg LogConfig) { + options := &slog.HandlerOptions{ + AddSource: cfg.AddSource, + Level: cfg.Level, + } + var stdErrHandler slog.Handler + switch cfg.Format { + case LogFormatUndefined: + return // uses default slog logger + case LogFormatDisabled: + stdErrHandler = slog.DiscardHandler + case LogFormatText: + stdErrHandler = slog.NewTextHandler(os.Stderr, options) + case LogFormatJSON: + stdErrHandler = slog.NewJSONHandler(os.Stderr, options) + case LogFormatGCP: + options.ReplaceAttr = sloggcp.ReplaceAttr + stdErrHandler = slog.NewJSONHandler(os.Stderr, options) + } + + stdErrHandler = slogctx.NewHandler( + stdErrHandler, + &slogctx.HandlerOptions{ + Prependers: []slogctx.AttrExtractor{ + domainExtractor, + requestExtractor, + slogotel.ExtractTraceSpanID, + slogctx.ExtractPrepended, + }, + }, + ) + + otelHandler := otelslog.NewHandler( + Name, + otelslog.WithLoggerProvider(provider), + ) + + logger := slog.New(slogmulti.Fanout(stdErrHandler, otelHandler)) + logger.Info("structured logger configured", "config_level", cfg.Level, "format", cfg.Format) + slog.SetDefault(logger) +} + +const ( + ProtocolHttp = "http" + ProtocolConnect = "connect" + ProtocolGrpc = "grpc" +) + +// SetHttpRequestDetails adds static details to each context aware log entry. +func SetHttpRequestDetails(ctx context.Context, service string, request *http.Request) context.Context { + now := time.Now() + return context.WithValue(ctx, ctxKey{}, &requestDetails{ + protocol: ProtocolHttp, + service: service, + http_method: request.Method, + path: request.URL.Path, + requestID: xid.NewWithTime(now), + start: now, + }) +} + +// SetConnectRequestDetails adds static details to each context aware log entry. +func SetConnectRequestDetails(ctx context.Context, request connect.AnyRequest) context.Context { + now := time.Now() + spec := request.Spec() + return context.WithValue(ctx, ctxKey{}, &requestDetails{ + protocol: ProtocolConnect, + service: serviceFromRPCMethod(spec.Procedure), + http_method: request.HTTPMethod(), + path: spec.Procedure, + requestID: xid.NewWithTime(now), + start: now, + }) +} + +func SetGrpcRequestDetails(ctx context.Context, info *grpc.UnaryServerInfo) context.Context { + now := time.Now() + return context.WithValue(ctx, ctxKey{}, &requestDetails{ + protocol: ProtocolGrpc, + service: serviceFromRPCMethod(info.FullMethod), + http_method: http.MethodPost, // gRPC always uses POST + path: info.FullMethod, + requestID: xid.NewWithTime(now), + start: now, + }) +} + +type ctxKey struct{} + +type requestDetails struct { + protocol string + service string + http_method string + path string + requestID xid.ID + start time.Time +} + +func (r *requestDetails) attrs() []slog.Attr { + attrs := make([]slog.Attr, 0, 6) + if r.protocol != "" { + attrs = append(attrs, slog.String("protocol", r.protocol)) + } + if r.service != "" { + attrs = append(attrs, slog.String("service", r.service)) + } + if r.http_method != "" { + attrs = append(attrs, slog.String("http_method", r.http_method)) + } + if r.path != "" { + attrs = append(attrs, slog.String("path", r.path)) + } + if !r.requestID.IsZero() { + attrs = append(attrs, slog.String("request_id", r.requestID.String())) + } + if !r.start.IsZero() { + attrs = append(attrs, slog.Duration("duration", time.Since(r.start))) + } + return attrs +} + +func serviceFromRPCMethod(fullMethod string) string { + parts := strings.Split(fullMethod, "/") + if len(parts) >= 2 { + return parts[1] + } + return "unknown" +} + +// domainExtractor sets the sanitized request hosts from [http_util.DomainCtx] to a log entry. +func domainExtractor(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr { + return []slog.Attr{ + slog.Any("domain", http_util.DomainContext(ctx)), + } +} + +// requestExtractor sets the request details from [requestDetails] to a log entry. +func requestExtractor(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr { + if r, ok := ctx.Value(ctxKey{}).(*requestDetails); ok { + return r.attrs() + } + return nil +} diff --git a/backend/v3/instrumentation/tracing.go b/backend/v3/instrumentation/tracing.go new file mode 100644 index 000000000..2cb80f7d4 --- /dev/null +++ b/backend/v3/instrumentation/tracing.go @@ -0,0 +1,269 @@ +package instrumentation + +import ( + "context" + "fmt" + "net/http" + "os" + "slices" + + google_trace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" + sdk_trace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "github.com/zitadel/zitadel/internal/api/grpc/gerrors" +) + +type TraceConfig struct { + Fraction float64 + Exporter ExporterConfig +} + +// TODO: remove for v5 release +type LegacyTraceConfig struct { + Type string + Fraction float64 + Endpoint string + ProjectID string +} + +// SetLegacyConfig sets the fields of the TraceConfig based on the provided LegacyTraceConfig. +// If the Type field is already set to a value other than [ExporterTypeNone], the legacy config is ignored. +func (c *TraceConfig) SetLegacyConfig(lc *LegacyTraceConfig) { + typ := c.Exporter.Type + if lc == nil || !typ.isNone() { + return + } + typeMap := map[string]ExporterType{ + "otel": ExporterTypeGRPC, + "google": ExporterTypeGoogle, + "log": ExporterTypeStdOut, + } + + c.Fraction = lc.Fraction + c.Exporter.Type = typeMap[lc.Type] + c.Exporter.Endpoint = lc.Endpoint + c.Exporter.GoogleProjectID = lc.ProjectID +} + +type Tracer struct { + trace.Tracer +} + +func (t *Tracer) NewServerInterceptorSpan(ctx context.Context, name string) (context.Context, *Span) { + return t.newSpanFromName(ctx, name, trace.WithSpanKind(trace.SpanKindServer)) +} + +func (t *Tracer) NewServerSpan(ctx context.Context, caller string) (context.Context, *Span) { + return t.newSpan(ctx, caller, trace.WithSpanKind(trace.SpanKindServer)) +} + +func (t *Tracer) NewClientInterceptorSpan(ctx context.Context, name string) (context.Context, *Span) { + return t.newSpanFromName(ctx, name, trace.WithSpanKind(trace.SpanKindClient)) +} + +func (t *Tracer) NewClientSpan(ctx context.Context, caller string) (context.Context, *Span) { + return t.newSpan(ctx, caller, trace.WithSpanKind(trace.SpanKindClient)) +} + +func (t *Tracer) NewSpan(ctx context.Context, caller string) (context.Context, *Span) { + return t.newSpan(ctx, caller) +} + +func (t *Tracer) newSpan(ctx context.Context, caller string, options ...trace.SpanStartOption) (context.Context, *Span) { + return t.newSpanFromName(ctx, caller, options...) +} + +func (t *Tracer) newSpanFromName(ctx context.Context, name string, options ...trace.SpanStartOption) (context.Context, *Span) { + ctx, span := t.Start(ctx, name, options...) + return ctx, CreateSpan(span) +} + +func (t *Tracer) NewSpanHTTP(r *http.Request, caller string) (*http.Request, *Span) { + ctx, span := t.NewSpan(r.Context(), caller) + r = r.WithContext(ctx) + return r, span +} + +type Span struct { + span trace.Span + opts []trace.SpanEndOption +} + +func CreateSpan(span trace.Span) *Span { + return &Span{span: span, opts: []trace.SpanEndOption{}} +} + +func (s *Span) End() { + if s.span == nil { + return + } + + s.span.End(s.opts...) +} + +func (s *Span) EndWithError(err error) { + s.SetStatusByError(err) + s.End() +} + +func (s *Span) SetStatusByError(err error) { + if s.span == nil { + return + } + if err != nil { + s.span.RecordError(err) + } + + code, msg, id := gerrors.ExtractZITADELError(err) + s.span.SetAttributes(attribute.Int("grpc_code", int(code)), attribute.String("grpc_msg", msg), attribute.String("error_id", id)) +} + +func newTracerProvider(ctx context.Context, cfg TraceConfig, resource *resource.Resource) (_ *sdk_trace.TracerProvider, err error) { + var exporter sdk_trace.SpanExporter + switch cfg.Exporter.Type { + case ExporterTypeUnspecified, ExporterTypeNone: + // no exporter + case ExporterTypeStdOut, ExporterTypeStdErr: + exporter, err = traceStdOutExporter(cfg.Exporter) + case ExporterTypeGRPC: + exporter, err = traceGrpcExporter(ctx, cfg.Exporter) + case ExporterTypeHTTP: + exporter, err = traceHttpExporter(ctx, cfg.Exporter) + case ExporterTypeGoogle: + exporter, err = traceGoogleExporter(ctx, cfg.Exporter) + case ExporterTypePrometheus: + fallthrough // prometheus is not supported for logs + default: + err = errExporterType(cfg.Exporter.Type, "tracer") + } + if err != nil { + return nil, fmt.Errorf("trace exporter: %w", err) + } + sampler := newSampler(cfg) + opts := []sdk_trace.TracerProviderOption{ + sdk_trace.WithResource(resource), + sdk_trace.WithSampler(sampler), + } + if exporter != nil { + opts = append(opts, sdk_trace.WithBatcher(exporter)) + } + tracerProvider := sdk_trace.NewTracerProvider(opts...) + return tracerProvider, nil +} + +func traceStdOutExporter(cfg ExporterConfig) (sdk_trace.SpanExporter, error) { + options := []stdouttrace.Option{ + stdouttrace.WithPrettyPrint(), + } + if cfg.Type == ExporterTypeStdErr { + options = append(options, stdouttrace.WithWriter(os.Stderr)) + } + exporter, err := stdouttrace.New(options...) + if err != nil { + return nil, err + } + return exporter, nil +} + +func traceGrpcExporter(ctx context.Context, cfg ExporterConfig) (sdk_trace.SpanExporter, error) { + var grpcOpts []otlptracegrpc.Option + if cfg.Endpoint != "" { + grpcOpts = append(grpcOpts, otlptracegrpc.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + grpcOpts = append(grpcOpts, otlptracegrpc.WithInsecure()) + } + + exporter, err := otlptracegrpc.New(ctx, grpcOpts...) + if err != nil { + return nil, err + } + return exporter, nil +} + +func traceHttpExporter(ctx context.Context, cfg ExporterConfig) (sdk_trace.SpanExporter, error) { + var httpOpts []otlptracehttp.Option + if cfg.Endpoint != "" { + httpOpts = append(httpOpts, otlptracehttp.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + httpOpts = append(httpOpts, otlptracehttp.WithInsecure()) + } + + exporter, err := otlptracehttp.New(ctx, httpOpts...) + if err != nil { + return nil, err + } + return exporter, nil +} + +func traceGoogleExporter(ctx context.Context, cfg ExporterConfig) (sdk_trace.SpanExporter, error) { + exporter, err := google_trace.New( + google_trace.WithContext(ctx), + google_trace.WithProjectID(cfg.GoogleProjectID), + ) + if err != nil { + return nil, err + } + return exporter, nil +} + +// newSampler returns a sampler decorator which behaves differently, +// based on the parent of the span. If the span has no parent and is of kind server, +// the decorated sampler is used to make sampling decision. +// If the span has a parent, depending on whether the parent is remote and whether it +// is sampled, one of the following samplers will apply: +// - remote parent sampled -> always sample +// - remote parent not sampled -> sample based on the decorated sampler (fraction based) +// - local parent sampled -> always sample +// - local parent not sampled -> never sample +func newSampler(cfg TraceConfig) sdk_trace.Sampler { + fraction := sdk_trace.TraceIDRatioBased(cfg.Fraction) + return sdk_trace.ParentBased( + spanKindBased(fraction, trace.SpanKindServer), + sdk_trace.WithRemoteParentNotSampled(fraction), + ) +} + +type spanKindSampler struct { + sampler sdk_trace.Sampler + kinds []trace.SpanKind +} + +// ShouldSample implements the [sdk_trace.Sampler] interface. +// It will not sample any spans which do not match the configured span kinds. +// For spans which do match, the decorated sampler is used to make the sampling decision. +func (sk spanKindSampler) ShouldSample(p sdk_trace.SamplingParameters) sdk_trace.SamplingResult { + psc := trace.SpanContextFromContext(p.ParentContext) + if !slices.Contains(sk.kinds, p.Kind) { + return sdk_trace.SamplingResult{ + Decision: sdk_trace.Drop, + Tracestate: psc.TraceState(), + } + } + s := sk.sampler.ShouldSample(p) + return s +} + +func (sk spanKindSampler) Description() string { + return fmt.Sprintf("SpanKindBased{sampler:%s,kinds:%v}", + sk.sampler.Description(), + sk.kinds, + ) +} + +// spanKindBased returns a sampler decorator which behaves differently, based on the kind of the span. +// If the span kind does not match one of the configured kinds, it will not be sampled. +// If the span kind matches, the decorated sampler is used to make sampling decision. +func spanKindBased(sampler sdk_trace.Sampler, kinds ...trace.SpanKind) sdk_trace.Sampler { + return spanKindSampler{ + sampler: sampler, + kinds: kinds, + } +} diff --git a/backend/v3/instrumentation/tracing/http_handler.go b/backend/v3/instrumentation/tracing/http_handler.go new file mode 100644 index 000000000..408e3a101 --- /dev/null +++ b/backend/v3/instrumentation/tracing/http_handler.go @@ -0,0 +1,23 @@ +package tracing + +import ( + "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" +) + +func NewHandler(handler http.Handler, ignoredPrefix ...string) http.Handler { + return otelhttp.NewHandler(handler, + "zitadel", + otelhttp.WithFilter(instrumentation.RequestFilter(ignoredPrefix...)), + otelhttp.WithPublicEndpoint(), + otelhttp.WithSpanNameFormatter(spanNameFormatter), + otelhttp.WithMeterProvider(otel.GetMeterProvider())) +} + +func spanNameFormatter(_ string, r *http.Request) string { + return r.URL.Path +} diff --git a/backend/v3/instrumentation/tracing/tracing.go b/backend/v3/instrumentation/tracing/tracing.go new file mode 100644 index 000000000..1faba2e5b --- /dev/null +++ b/backend/v3/instrumentation/tracing/tracing.go @@ -0,0 +1,65 @@ +// Package tracing provides helper functions to create spans for telemetry tracing. +package tracing + +import ( + "context" + "net/http" + "sync" + + api_trace "go.opentelemetry.io/otel/trace" + + "github.com/zitadel/zitadel/backend/v3/instrumentation" + "github.com/zitadel/zitadel/cmd/build" +) + +type Tracer interface { + NewSpan(ctx context.Context, caller string) (context.Context, *instrumentation.Span) + NewClientSpan(ctx context.Context, caller string) (context.Context, *instrumentation.Span) + NewServerSpan(ctx context.Context, caller string) (context.Context, *instrumentation.Span) + NewClientInterceptorSpan(ctx context.Context, name string) (context.Context, *instrumentation.Span) + NewServerInterceptorSpan(ctx context.Context, name string) (context.Context, *instrumentation.Span) + NewSpanHTTP(r *http.Request, caller string) (*http.Request, *instrumentation.Span) +} + +var globalTracer = sync.OnceValue(func() Tracer { + return instrumentation.NewTracer( + instrumentation.Name, + api_trace.WithInstrumentationVersion(build.Version()), + ) +}) + +func GlobalTracer() Tracer { + return globalTracer() +} + +func NewSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return globalTracer().NewSpan(ctx, instrumentation.GetCallingFunc(1)) +} + +func NewNamedSpan(ctx context.Context, name string) (context.Context, *instrumentation.Span) { + return globalTracer().NewSpan(ctx, name) +} + +func NewClientSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return globalTracer().NewClientSpan(ctx, instrumentation.GetCallingFunc(1)) +} + +func NewServerSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return globalTracer().NewServerSpan(ctx, instrumentation.GetCallingFunc(1)) +} + +func NewClientInterceptorSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return globalTracer().NewClientInterceptorSpan(ctx, instrumentation.GetCallingFunc(1)) +} + +func NewServerInterceptorSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return globalTracer().NewServerInterceptorSpan(ctx, instrumentation.GetCallingFunc(1)) +} + +func NewSpanHTTP(r *http.Request) (*http.Request, *instrumentation.Span) { + return globalTracer().NewSpanHTTP(r, instrumentation.GetCallingFunc(1)) +} + +func TraceIDFromCtx(ctx context.Context) string { + return api_trace.SpanFromContext(ctx).SpanContext().TraceID().String() +} diff --git a/backend/v3/instrumentation/tracing_test.go b/backend/v3/instrumentation/tracing_test.go new file mode 100644 index 000000000..54c5cfbd6 --- /dev/null +++ b/backend/v3/instrumentation/tracing_test.go @@ -0,0 +1,191 @@ +package instrumentation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/resource" +) + +func TestTraceConfig_SetLegacyConfig(t *testing.T) { + tests := []struct { + name string + target TraceConfig + lc *LegacyTraceConfig + want TraceConfig + }{ + { + name: "nil legacy config does not change target", + target: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + lc: nil, + want: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + }, + { + name: "non-none target type does not change target", + target: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + lc: &LegacyTraceConfig{ + Type: "google", + Fraction: 0.5, + Endpoint: "endpoint", + }, + want: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + }, + { + name: "sets fields from legacy config (type none)", + target: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + lc: &LegacyTraceConfig{ + Type: "google", + Fraction: 0.5, + Endpoint: "endpoint", + }, + want: TraceConfig{ + Fraction: 0.5, + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + Endpoint: "endpoint", + }, + }, + }, + { + name: "sets fields from legacy config (type unspecified)", + target: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeUnspecified, + }, + }, + lc: &LegacyTraceConfig{ + Type: "google", + Fraction: 0.5, + Endpoint: "endpoint", + }, + want: TraceConfig{ + Fraction: 0.5, + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + Endpoint: "endpoint", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.target.SetLegacyConfig(tt.lc) + assert.Equal(t, tt.want, tt.target) + }) + } +} + +func Test_newTracerProvider(t *testing.T) { + resource, err := resource.New(t.Context()) + require.NoError(t, err) + + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + cfg TraceConfig + wantErr bool + }{ + { + name: "none exporter returns no error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + }, + { + name: "stdout exporter returns no error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + }, + { + name: "stderr exporter returns no error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdErr, + }, + }, + }, + { + name: "grpc exporter returns no error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGRPC, + Endpoint: "localhost:4317", + }, + }, + }, + { + name: "http exporter returns no error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeHTTP, + Endpoint: "http://localhost:4318/v1/traces", + }, + }, + }, + { + name: "google exporter return error (no credentials)", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGoogle, + }, + }, + wantErr: true, + }, + { + name: "prometheus exporter returns error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypePrometheus, + }, + }, + wantErr: true, + }, + { + name: "invalid exporter type returns error", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterType(-1), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := newTracerProvider(t.Context(), tt.cfg, resource) + if tt.wantErr { + assert.Error(t, gotErr) + assert.Nil(t, got) + } else { + assert.NoError(t, gotErr) + assert.NotNil(t, got) + } + }) + } +} diff --git a/backend/v3/telemetry/logging/logger.go b/backend/v3/telemetry/logging/logger.go deleted file mode 100644 index a7d724daf..000000000 --- a/backend/v3/telemetry/logging/logger.go +++ /dev/null @@ -1,12 +0,0 @@ -package logging - -import "log/slog" - -// Logger abstracts [slog.Logger] not sure if thats needed -type Logger struct { - *slog.Logger -} - -func NewLogger(logger *slog.Logger) *Logger { - return &Logger{Logger: logger} -} diff --git a/backend/v3/telemetry/metric/doc.go b/backend/v3/telemetry/metric/doc.go deleted file mode 100644 index e4a8b3bd6..000000000 --- a/backend/v3/telemetry/metric/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// implementation of otel metrics -package metric diff --git a/backend/v3/telemetry/tracing/tracer.go b/backend/v3/telemetry/tracing/tracer.go deleted file mode 100644 index 025a0c111..000000000 --- a/backend/v3/telemetry/tracing/tracer.go +++ /dev/null @@ -1,24 +0,0 @@ -package tracing - -import ( - "context" - - "go.opentelemetry.io/otel/trace" - "go.opentelemetry.io/otel/trace/noop" -) - -// Tracer is a wrapper around the OpenTelemetry Tracer interface. -type Tracer struct { - trace.Tracer -} - -var noopTracer = Tracer{ - Tracer: noop.NewTracerProvider().Tracer(""), -} - -func (t *Tracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - if t.Tracer == nil { - return noopTracer.Start(ctx, spanName, opts...) - } - return t.Tracer.Start(ctx, spanName, opts...) -} diff --git a/cmd/admin/admin.go b/cmd/admin/admin.go index 04741a4a8..e978dba03 100644 --- a/cmd/admin/admin.go +++ b/cmd/admin/admin.go @@ -3,6 +3,7 @@ package admin import ( _ "embed" "errors" + "log/slog" "github.com/spf13/cobra" @@ -18,7 +19,12 @@ func New() *cobra.Command { Short: "The ZITADEL admin CLI lets you interact with your instance", Long: `The ZITADEL admin CLI lets you interact with your instance`, Deprecated: "please use subcommands directly, e.g. `zitadel start`", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel admin command failed", "err", err) + } + }() return errors.New("no additional command provided") }, } diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 75754aa29..5762b5323 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1,13 +1,94 @@ +Instrumentation: + ServiceName: "zitadel" # ZITADEL_INSTRUMENTATION_SERVICENAME + Trace: + Fraction: 1.0 # ZITADEL_INSTRUMENTATION_TRACE_FRACTION + Exporter: + # The following exporter types are supported: + # "none": Disables trace exporting + # "stdOut": Exports traces to standard output + # "stdErr": Exports traces to standard error + # "grpc": Exports traces using the OTEL gRPC exporter (recommended) + # "http": Exports traces using the OTEL HTTP exporter + # "google": Exports traces to Google Cloud. Need to configure GoogleProjectID below! + Type: "none" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_ENDPOINT + # Disable TLS for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_INSECURE + # Interval for batching traces before export + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_GOOGLEPROJECTID + Metric: + Exporter: + # The following exporter types are supported: + # "none": Disables metric exporting + # "stdOut": Exports metrics to standard output + # "stdErr": Exports metrics to standard error + # "grpc": Exports metrics using the OTEL gRPC exporter (recommended) + # "http": Exports metrics using the OTEL HTTP exporter + # "google": Exports metrics to Google Cloud. Need to configure GoogleProjectID below! + # "prometheus": Exposes metrics via an HTTP endpoint for Prometheus to scrape + Type: "none" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_ENDPOINT + # Disable TLS for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_INSECURE + # Interval at which metrics are exported + BatchDuration: 1m # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_GOOGLEPROJECTID + + # Structured logging can print logs on standard error and/or export them using OTEL. + Log: + # Log lines lower than this level are not emitted. + Level: "INFO" # ZITADEL_INSTRUMENTATION_LOG_LEVEL + # Enable printing structured logs to standard error in the specified format. + # The following formats are supported: + # - "disabled": Disables logging + # - "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! + 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 + Exporter: + # The following exporter types are supported: + # - "none": Disables OTEL log exporter + # - "stdOut": Exports OTEL logs to standard output + # - "stdErr": Exports OTEL logs to standard error + # - "grpc": Exports OTEL logs using the OTEL gRPC exporter (recommended) + # - "http": Exports OTEL logs using the OTEL HTTP exporter + Type: "none" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_ENDPOINT + # Disable TLS for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_INSECURE + # Interval at which metrics are exported + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_BATCHDURATION + Profile: + Exporter: + # The following profiler types are supported: + # "none": Disables profiling + # "google": Exports profiling data to Google Cloud Profiler + Type: "none" # ZITADEL_INSTRUMENTATION_PROFILE_TYPE + # Project ID for Google Cloud Profiler + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_PROFILE_GOOGLEPROJECTID + +# Legacy logging (Deprecated, see Instrumentation.Slog) Log: Level: info # ZITADEL_LOG_LEVEL Formatter: Format: text # ZITADEL_LOG_FORMATTER_FORMAT -# Exposes metrics on /debug/metrics +# Exposes metrics on /debug/metrics (Deprecated, see Instrumentation.Metric.Prometheus) Metrics: # Select type otel (OpenTelemetry) or none (disables collection and endpoint) Type: otel # ZITADEL_METRICS_TYPE + +# Collects and exports tracing information (Deprecated, see Instrumentation.Trace) Tracing: # Choose one in "otel", "google", "log" and "none" # Depending on the type there are different configuration options diff --git a/cmd/initialise/init.go b/cmd/initialise/init.go index cc505325a..1d51f465f 100644 --- a/cmd/initialise/init.go +++ b/cmd/initialise/init.go @@ -3,6 +3,8 @@ package initialise import ( "context" "embed" + "fmt" + "log/slog" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -43,10 +45,15 @@ The user provided by flags needs privileges to - see other users and create a new one if the user does not exist - grant all rights of the ZITADEL database to the user created if not yet set `, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel init command failed", "err", err) + } + }() config := MustNewConfig(viper.GetViper()) - InitAll(cmd.Context(), config) + return InitAll(cmd.Context(), config) }, } @@ -54,16 +61,21 @@ The user provided by flags needs privileges to return cmd } -func InitAll(ctx context.Context, config *Config) { +func InitAll(ctx context.Context, config *Config) error { err := initialise(ctx, config.Database, VerifyUser(config.Database.Username(), config.Database.Password()), VerifyDatabase(config.Database.DatabaseName()), VerifyGrant(config.Database.DatabaseName(), config.Database.Username()), ) - logging.OnError(err).Fatal("unable to initialize the database") + if err != nil { + return fmt.Errorf("initialize database failed: %w", err) + } err = verifyZitadel(ctx, config.Database) - logging.OnError(err).Fatal("unable to initialize ZITADEL") + if err != nil { + return fmt.Errorf("initialize ZITADEL failed: %w", err) + } + return nil } func initialise(ctx context.Context, config database.Config, steps ...func(context.Context, *database.DB) error) error { diff --git a/cmd/key/key.go b/cmd/key/key.go index a1cf15b34..c4957e213 100644 --- a/cmd/key/key.go +++ b/cmd/key/key.go @@ -2,6 +2,7 @@ package key import ( "io" + "log/slog" "os" "strings" @@ -44,7 +45,13 @@ Requirements: Example: `new -f keys.yaml new key1=somekey key2=anotherkey new -f keys.yaml key2=anotherkey`, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel keys new command failed", "err", err) + } + }() + keys, err := keysFromArgs(args) if err != nil { return err diff --git a/cmd/mirror/auth.go b/cmd/mirror/auth.go index 3d7ae45bc..8131eec77 100644 --- a/cmd/mirror/auth.go +++ b/cmd/mirror/auth.go @@ -3,6 +3,7 @@ package mirror import ( "context" _ "embed" + "errors" "io" "strconv" "time" @@ -22,9 +23,16 @@ func authCmd() *cobra.Command { Long: `mirrors the auth requests table from one database to another ZITADEL needs to be initialized and set up with the --for-mirror flag Only auth requests are mirrored`, - Run: func(cmd *cobra.Command, args []string) { - config := mustNewMigrationConfig(viper.GetViper()) + RunE: func(cmd *cobra.Command, args []string) (err error) { + config, shutdown, err := mustNewMigrationConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() copyAuth(cmd.Context(), config) + return nil }, } diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go index 5bb19f12d..648ea6ef5 100644 --- a/cmd/mirror/config.go +++ b/cmd/mirror/config.go @@ -1,13 +1,16 @@ package mirror import ( + "context" _ "embed" + "fmt" "time" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/backend/v3/instrumentation" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" internal_authz "github.com/zitadel/zitadel/internal/api/authz" @@ -16,7 +19,6 @@ import ( "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/id" - metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" ) type Migration struct { @@ -26,9 +28,10 @@ type Migration struct { EventBulkSize uint32 MaxAuthRequestAge time.Duration - Log *logging.Config - Machine *id.Config - Metrics metrics.Config + Log *logging.Config + Machine *id.Config + Instrumentation instrumentation.Config + Metrics instrumentation.LegacyMetricConfig } var ( @@ -36,19 +39,24 @@ var ( defaultConfig []byte ) -func mustNewMigrationConfig(v *viper.Viper) *Migration { +func mustNewMigrationConfig(ctx context.Context, v *viper.Viper) (*Migration, instrumentation.ShutdownFunc, error) { config := new(Migration) mustNewConfig(v, config) - err := config.Log.SetLogger() - logging.OnError(err).Fatal("unable to set logger") + shutdown, err := instrumentation.Start(ctx, config.Instrumentation) + if err != nil { + return nil, nil, fmt.Errorf("unable to start instrumentation: %w", err) + } - err = config.Metrics.NewMeter() - logging.OnError(err).Fatal("unable to set meter") + // Legacy logger + err = config.Log.SetLogger() + if err != nil { + return nil, nil, fmt.Errorf("unable to set logger: %w", err) + } id.Configure(config.Machine) - return config + return config, shutdown, nil } func mustNewProjectionsConfig(v *viper.Viper) *ProjectionsConfig { diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml index 4d8a0a4ea..c02e05fa9 100644 --- a/cmd/mirror/defaults.yaml +++ b/cmd/mirror/defaults.yaml @@ -1,3 +1,81 @@ +Instrumentation: + ServiceName: "zitadel-mirror" # ZITADEL_INSTRUMENTATION_SERVICENAME + Trace: + Fraction: 1.0 # ZITADEL_INSTRUMENTATION_TRACE_FRACTION + Exporter: + # The following exporter types are supported: + # "none": Disables trace exporting + # "stdOut": Exports traces to standard output + # "stdErr": Exports traces to standard error + # "grpc": Exports traces using the OTEL gRPC exporter (recommended) + # "http": Exports traces using the OTEL HTTP exporter + # "google": Exports traces to Google Cloud. Need to configure GoogleProjectID below! + Type: "none" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_ENDPOINT + # Disable TLS for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_INSECURE + # Interval for batching traces before export + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_GOOGLEPROJECTID + Metric: + Exporter: + # The following exporter types are supported: + # "none": Disables metric exporting + # "stdOut": Exports metrics to standard output + # "stdErr": Exports metrics to standard error + # "grpc": Exports metrics using the OTEL gRPC exporter (recommended) + # "http": Exports metrics using the OTEL HTTP exporter + # "google": Exports metrics to Google Cloud. Need to configure GoogleProjectID below! + # "prometheus": Exposes metrics via an HTTP endpoint for Prometheus to scrape + Type: "none" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_ENDPOINT + # Disable TLS for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_INSECURE + # Interval at which metrics are exported + BatchDuration: 1m # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_GOOGLEPROJECTID + + # Structured logging can print logs on standard error and/or export them using OTEL. + Log: + # Log lines lower than this level are not emitted. + Level: "INFO" # ZITADEL_INSTRUMENTATION_LOG_LEVEL + # Enable printing structured logs to standard error in the specified format. + # The following formats are supported: + # - "disabled": Disables logging + # - "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! + 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 + Exporter: + # The following exporter types are supported: + # - "none": Disables OTEL log exporter + # - "stdOut": Exports OTEL logs to standard output + # - "stdErr": Exports OTEL logs to standard error + # - "grpc": Exports OTEL logs using the OTEL gRPC exporter (recommended) + # - "http": Exports OTEL logs using the OTEL HTTP exporter + Type: "none" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_ENDPOINT + # Disable TLS for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_INSECURE + # Interval at which metrics are exported + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_BATCHDURATION + Profile: + Exporter: + # The following profiler types are supported: + # "none": Disables profiling + # "google": Exports profiling data to Google Cloud Profiler + Type: "none" # ZITADEL_INSTRUMENTATION_PROFILE_TYPE + # Project ID for Google Cloud Profiler + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_PROFILE_GOOGLEPROJECTID + Source: cockroach: Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go index be14abe34..f6b9bccb6 100644 --- a/cmd/mirror/event_store.go +++ b/cmd/mirror/event_store.go @@ -33,9 +33,16 @@ func eventstoreCmd() *cobra.Command { Long: `mirrors the eventstore of an instance from one database to another ZITADEL needs to be initialized and set up with the --for-mirror flag Migrate only copies events2 and unique constraints`, - Run: func(cmd *cobra.Command, args []string) { - config := mustNewMigrationConfig(viper.GetViper()) + RunE: func(cmd *cobra.Command, args []string) (err error) { + config, shutdown, err := mustNewMigrationConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() copyEventstore(cmd.Context(), config) + return nil }, } diff --git a/cmd/mirror/mirror.go b/cmd/mirror/mirror.go index 866238e56..1d7cc297f 100644 --- a/cmd/mirror/mirror.go +++ b/cmd/mirror/mirror.go @@ -3,6 +3,9 @@ package mirror import ( "bytes" _ "embed" + "errors" + "fmt" + "log/slog" "strings" "github.com/spf13/cobra" @@ -44,18 +47,34 @@ Order of execution: logging.WithFields("file", file).OnError(err).Warn("unable to read config file") } }, - Run: func(cmd *cobra.Command, args []string) { - config := mustNewMigrationConfig(viper.GetViper()) + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel mirror command failed", "err", err) + } + }() + + config, shutdown, err := mustNewMigrationConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return fmt.Errorf("unable to create migration config: %w", err) + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() + projectionConfig := mustNewProjectionsConfig(viper.GetViper()) masterKey, err := key.MasterKey(cmd) - logging.OnError(err).Fatal("unable to read master key") + if err != nil { + return fmt.Errorf("unable to read master key: %w", err) + } copySystem(cmd.Context(), config) copyAuth(cmd.Context(), config) copyEventstore(cmd.Context(), config) projections(cmd.Context(), projectionConfig, masterKey) + return nil }, } diff --git a/cmd/mirror/system.go b/cmd/mirror/system.go index 57eb20543..cb11eda47 100644 --- a/cmd/mirror/system.go +++ b/cmd/mirror/system.go @@ -3,6 +3,7 @@ package mirror import ( "context" _ "embed" + "errors" "io" "time" @@ -21,9 +22,16 @@ func systemCmd() *cobra.Command { Long: `mirrors the system tables of ZITADEL from one database to another ZITADEL needs to be initialized Only keys and assets are mirrored`, - Run: func(cmd *cobra.Command, args []string) { - config := mustNewMigrationConfig(viper.GetViper()) + RunE: func(cmd *cobra.Command, args []string) (err error) { + config, shutdown, err := mustNewMigrationConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() copySystem(cmd.Context(), config) + return nil }, } diff --git a/cmd/mirror/verify.go b/cmd/mirror/verify.go index e1a507d9f..ba2e3d30d 100644 --- a/cmd/mirror/verify.go +++ b/cmd/mirror/verify.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" _ "embed" + "errors" "fmt" "slices" @@ -20,9 +21,16 @@ func verifyCmd() *cobra.Command { return &cobra.Command{ Use: "verify", Short: "counts if source and dest have the same amount of entries", - Run: func(cmd *cobra.Command, args []string) { - config := mustNewMigrationConfig(viper.GetViper()) + RunE: func(cmd *cobra.Command, args []string) (err error) { + config, shutdown, err := mustNewMigrationConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() verifyMigration(cmd.Context(), config) + return nil }, } } diff --git a/cmd/setup/cleanup.go b/cmd/setup/cleanup.go index 69f7c72e5..371d1d582 100644 --- a/cmd/setup/cleanup.go +++ b/cmd/setup/cleanup.go @@ -2,6 +2,7 @@ package setup import ( "context" + "errors" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -19,9 +20,16 @@ func NewCleanup() *cobra.Command { Use: "cleanup", Short: "cleans up migration if they got stuck", Long: `cleans up migration if they got stuck`, - Run: func(cmd *cobra.Command, args []string) { - config := MustNewConfig(viper.GetViper()) + RunE: func(cmd *cobra.Command, args []string) (err error) { + config, shutdown, err := NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() Cleanup(cmd.Context(), config) + return nil }, } } diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 36e15f695..807ec3355 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -2,6 +2,9 @@ package setup import ( "bytes" + "context" + "errors" + "fmt" "strings" "time" @@ -9,6 +12,7 @@ import ( "github.com/spf13/viper" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/backend/v3/instrumentation" "github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" @@ -27,7 +31,6 @@ import ( "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/query/projection" static_config "github.com/zitadel/zitadel/internal/static/config" - metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" ) type Config struct { @@ -40,8 +43,9 @@ type Config struct { ExternalDomain string ExternalPort uint16 ExternalSecure bool + Instrumentation instrumentation.Config Log *logging.Config - Metrics metrics.Config + Metrics *instrumentation.LegacyMetricConfig EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup Machine *id.Config @@ -66,7 +70,7 @@ type InitProjections struct { BulkLimit uint64 } -func MustNewConfig(v *viper.Viper) *Config { +func NewConfig(ctx context.Context, v *viper.Viper) (*Config, instrumentation.ShutdownFunc, error) { config := new(Config) err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( @@ -85,20 +89,28 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.TextUnmarshallerHookFunc(), )), ) - logging.OnError(err).Fatal("unable to read default config") + if err != nil { + return nil, nil, fmt.Errorf("unable to read default config: %w", err) + } + + config.Instrumentation.Metric.SetLegacyConfig(config.Metrics) + shutdown, err := instrumentation.Start(ctx, config.Instrumentation) + if err != nil { + return nil, nil, fmt.Errorf("unable to start instrumentation: %w", err) + } err = config.Log.SetLogger() - logging.OnError(err).Fatal("unable to set logger") - - err = config.Metrics.NewMeter() - logging.OnError(err).Fatal("unable to set meter") + if err != nil { + err = errors.Join(err, shutdown(ctx)) + return nil, nil, fmt.Errorf("unable to set logger: %w", err) + } id.Configure(config.Machine) // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings - return config + return config, shutdown, nil } type Steps struct { diff --git a/cmd/setup/config_test.go b/cmd/setup/config_test.go index d8bb6f627..6617cfcd2 100644 --- a/cmd/setup/config_test.go +++ b/cmd/setup/config_test.go @@ -241,7 +241,8 @@ Actions: v := viper.New() v.SetConfigType("yaml") require.NoError(t, v.ReadConfig(strings.NewReader(tt.args.yaml))) - got := MustNewConfig(v) + got, _, err := NewConfig(t.Context(), v) + require.NoError(t, err) tt.want(t, got) }) } diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 5c7d115da..9b0fe40ce 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -6,6 +6,7 @@ import ( _ "embed" "errors" "fmt" + "log/slog" "net/http" "os" "os/signal" @@ -61,23 +62,45 @@ func New() *cobra.Command { Long: `sets up data to start ZITADEL. Requirements: - postgreSQL`, - Run: func(cmd *cobra.Command, args []string) { - err := tls.ModeFromFlag(cmd) - logging.OnError(err).Fatal("invalid tlsMode") + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel setup command failed", "err", err) + } + }() + + err = tls.ModeFromFlag(cmd) + if err != nil { + return fmt.Errorf("invalid tlsMode: %w", err) + } err = BindInitProjections(cmd) - logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") + if err != nil { + return fmt.Errorf("unable to bind \"init-projections\" flag: %w", err) + } err = bindForMirror(cmd) - logging.OnError(err).Fatal("unable to bind \"for-mirror\" flag") + if err != nil { + return fmt.Errorf("unable to bind \"for-mirror\" flag: %w", err) + } + + config, shutdown, err := NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() - config := MustNewConfig(viper.GetViper()) steps := MustNewSteps(viper.New()) masterKey, err := key.MasterKey(cmd) - logging.OnError(err).Panic("No master key provided") + if err != nil { + return fmt.Errorf("no master key provided: %w", err) + } Setup(cmd.Context(), config, steps, masterKey) + return nil }, } diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml index 709becf2c..5dcb6913c 100644 --- a/cmd/setup/steps.yaml +++ b/cmd/setup/steps.yaml @@ -1,3 +1,82 @@ +Instrumentation: + ServiceName: "zitadel-setup" # ZITADEL_INSTRUMENTATION_SERVICENAME + Trace: + Fraction: 1.0 # ZITADEL_INSTRUMENTATION_TRACE_FRACTION + Exporter: + # The following exporter types are supported: + # "none": Disables trace exporting + # "stdOut": Exports traces to standard output + # "stdErr": Exports traces to standard error + # "grpc": Exports traces using the OTEL gRPC exporter (recommended) + # "http": Exports traces using the OTEL HTTP exporter + # "google": Exports traces to Google Cloud Trace + Type: "none" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_ENDPOINT + # Disable https for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_INSECURE + # Interval for batching traces before export + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_GOOGLEPROJECTID + Metric: + Exporter: + # The following exporter types are supported: + # "none": Disables metric exporting + # "stdOut": Exports metrics to standard output + # "stdErr": Exports metrics to standard error + # "grpc": Exports metrics using the OTEL gRPC exporter (recommended) + # "http": Exports metrics using the OTEL HTTP exporter + # "google": Exports metrics to Google Cloud Trace + # "prometheus": Exposes metrics via an HTTP endpoint for Prometheus to scrape + Type: "none" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_ENDPOINT + # Disable https for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_INSECURE + # Interval at which metrics are exported + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_METRIC_EXPORTER_GOOGLEPROJECTID + + # Structured logging can print logs on standard error and/or export them using OTEL. + Log: + # Log lines lower than this level are not emitted. + Level: "INFO" # ZITADEL_INSTRUMENTATION_LOG_LEVEL + # Enable printing structured logs to standard error in the specified format. + # The following formats are supported: + # - "disabled": Disables logging to standard error + # - "text": Logs are printed as human-readable text + # - "json": Logs are printed as JSON objects + # - "gcp": JSON formatted logs compatible with Google Cloud Platform logging + StdErr: "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 + Exporter: + # The following exporter types are supported: + # - "none": Disables log exporting + # - "stdOut": Exports logs to standard output + # - "stdErr": Exports logs to standard error + # - "grpc": Exports logs using the OTEL gRPC exporter (recommended) + # - "http": Exports logs using the OTEL HTTP exporter + Type: "none" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_TYPE + # Endpoint of the OTEL collector for grpc and http exporters + Endpoint: "" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_ENDPOINT + # Disable https for grpc and http exporters + Insecure: false # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_INSECURE + # Interval at which metrics are exported + BatchDuration: 1s # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_BATCHDURATION + # Project ID for Google Cloud Trace exporter + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_GOOGLEPROJECTID + Profile: + # The following profiler types are supported: + # "none": Disables profiling + # "google": Exports profiling data to Google Cloud Profiler + Type: "none" # ZITADEL_INSTRUMENTATION_PROFILE_TYPE + # Project ID for Google Cloud Profiler + GoogleProjectID: "" # ZITADEL_INSTRUMENTATION_PROFILE_GOOGLEPROJECTID + # By using the FirstInstance section, you can overwrite the DefaultInstance configuration for the first instance created by zitadel setup. FirstInstance: # If set to true zitadel is setup without initial data diff --git a/cmd/start/config.go b/cmd/start/config.go index d8a7b4089..414883e38 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -1,12 +1,16 @@ package start import ( + "context" + "errors" + "fmt" "time" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/backend/v3/instrumentation" "github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" @@ -34,12 +38,10 @@ import ( "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/serviceping" static_config "github.com/zitadel/zitadel/internal/static/config" - metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" - profiler "github.com/zitadel/zitadel/internal/telemetry/profiler/config" - tracing "github.com/zitadel/zitadel/internal/telemetry/tracing/config" ) type Config struct { + Instrumentation instrumentation.Config Log *logging.Config Port uint16 ExternalPort uint16 @@ -53,9 +55,9 @@ type Config struct { WebAuthNName string Database database.Config Caches *connector.CachesConfig - Tracing tracing.Config - Metrics metrics.Config - Profiler profiler.Config + Tracing *instrumentation.LegacyTraceConfig + Metrics *instrumentation.LegacyMetricConfig + Profiler *instrumentation.LegacyProfileConfig Projections projection.Config Notifications handlers.WorkerConfig Executions execution.WorkerConfig @@ -93,21 +95,26 @@ type QuotasConfig struct { Execution *logstore.EmitterConfig } -func MustNewConfig(v *viper.Viper) *Config { +func NewConfig(ctx context.Context, v *viper.Viper) (*Config, instrumentation.ShutdownFunc, error) { config, err := readConfig(v) - logging.OnError(err).Fatal("unable to read config") + if err != nil { + return nil, nil, fmt.Errorf("unable to read config: %w", err) + } + config.Instrumentation.Trace.SetLegacyConfig(config.Tracing) + config.Instrumentation.Metric.SetLegacyConfig(config.Metrics) + config.Instrumentation.Profile.SetLegacyConfig(config.Profiler) + shutdown, err := instrumentation.Start(ctx, config.Instrumentation) + if err != nil { + return nil, nil, fmt.Errorf("unable to start instrumentation: %w", err) + } + + // Legacy logger err = config.Log.SetLogger() - logging.OnError(err).Fatal("unable to set logger") - - err = config.Tracing.NewTracer() - logging.OnError(err).Fatal("unable to set tracer") - - err = config.Metrics.NewMeter() - logging.OnError(err).Fatal("unable to set meter") - - err = config.Profiler.NewProfiler() - logging.OnError(err).Fatal("unable to set profiler") + if err != nil { + err = errors.Join(err, shutdown(ctx)) + return nil, nil, fmt.Errorf("unable to set logger: %w", err) + } id.Configure(config.Machine) if config.Actions != nil { @@ -115,12 +122,14 @@ func MustNewConfig(v *viper.Viper) *Config { } err = config.SystemDefaults.Validate() - logging.OnError(err).Fatal("system defaults config invalid") - + if err != nil { + err = errors.Join(err, shutdown(ctx)) + return nil, nil, fmt.Errorf("system defaults config invalid: %w", err) + } // Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API. config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings - return config + return config, shutdown, nil } func readConfig(v *viper.Viper) (*Config, error) { diff --git a/cmd/start/start.go b/cmd/start/start.go index c4dc4d287..e2aceba77 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -4,7 +4,9 @@ import ( "context" "crypto/tls" _ "embed" + "errors" "fmt" + "log/slog" "math" "net/http" "os" @@ -127,12 +129,25 @@ func New(server chan<- *Server) *cobra.Command { Long: `starts ZITADEL. Requirements: - postgreSQL`, - RunE: func(cmd *cobra.Command, args []string) error { - err := cmd_tls.ModeFromFlag(cmd) + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel start command failed", "err", err) + } + }() + + err = cmd_tls.ModeFromFlag(cmd) if err != nil { return err } - config := MustNewConfig(viper.GetViper()) + config, shutdown, err := NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() + masterKey, err := key.MasterKey(cmd) if err != nil { return err diff --git a/cmd/start/start_from_init.go b/cmd/start/start_from_init.go index 41972e16a..4238141bf 100644 --- a/cmd/start/start_from_init.go +++ b/cmd/start/start_from_init.go @@ -2,10 +2,12 @@ package start import ( "context" + "errors" + "fmt" + "log/slog" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/initialise" "github.com/zitadel/zitadel/cmd/key" @@ -24,31 +26,52 @@ Last ZITADEL starts. Requirements: - postgreSQL`, - Run: func(cmd *cobra.Command, args []string) { - err := tls.ModeFromFlag(cmd) - logging.OnError(err).Fatal("invalid tlsMode") + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel start-from-init command failed", "err", err) + } + }() + + err = tls.ModeFromFlag(cmd) + if err != nil { + return fmt.Errorf("invalid tlsMode: %w", err) + } masterKey, err := key.MasterKey(cmd) - logging.OnError(err).Panic("No master key provided") + if err != nil { + return fmt.Errorf("no master key provided: %w", err) + } initCtx, cancel := context.WithCancel(cmd.Context()) initialise.InitAll(initCtx, initialise.MustNewConfig(viper.GetViper())) cancel() err = setup.BindInitProjections(cmd) - logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") + if err != nil { + return fmt.Errorf("unable to bind \"init-projections\" flag: %w", err) + } + + setupConfig, shutdown, err := setup.NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() - setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) setupCtx, cancel := context.WithCancel(cmd.Context()) setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) cancel() - startConfig := MustNewConfig(viper.GetViper()) + startConfig, _, err := NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } - err = startZitadel(cmd.Context(), startConfig, masterKey, server) - logging.OnError(err).Fatal("unable to start zitadel") + return startZitadel(cmd.Context(), startConfig, masterKey, server) }, } diff --git a/cmd/start/start_from_setup.go b/cmd/start/start_from_setup.go index 3e8a13705..8d524664b 100644 --- a/cmd/start/start_from_setup.go +++ b/cmd/start/start_from_setup.go @@ -2,10 +2,11 @@ package start import ( "context" + "errors" + "log/slog" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/zitadel/logging" "github.com/zitadel/zitadel/cmd/key" "github.com/zitadel/zitadel/cmd/setup" @@ -24,27 +25,48 @@ Requirements: - database - database is initialized `, - Run: func(cmd *cobra.Command, args []string) { - err := tls.ModeFromFlag(cmd) - logging.OnError(err).Fatal("invalid tlsMode") + RunE: func(cmd *cobra.Command, args []string) (err error) { + defer func() { + if err != nil { + slog.Error("zitadel start-from-setup command failed", "err", err) + } + }() + + err = tls.ModeFromFlag(cmd) + if err != nil { + return err + } masterKey, err := key.MasterKey(cmd) - logging.OnError(err).Panic("No master key provided") + if err != nil { + return err + } err = setup.BindInitProjections(cmd) - logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") + if err != nil { + return err + } + + setupConfig, shutdown, err := setup.NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, shutdown(cmd.Context())) + }() - setupConfig := setup.MustNewConfig(viper.GetViper()) setupSteps := setup.MustNewSteps(viper.New()) setupCtx, cancel := context.WithCancel(cmd.Context()) setup.Setup(setupCtx, setupConfig, setupSteps, masterKey) cancel() - startConfig := MustNewConfig(viper.GetViper()) + startConfig, _, err := NewConfig(cmd.Context(), viper.GetViper()) + if err != nil { + return err + } - err = startZitadel(cmd.Context(), startConfig, masterKey, server) - logging.OnError(err).Fatal("unable to start zitadel") + return startZitadel(cmd.Context(), startConfig, masterKey, server) }, } diff --git a/cmd/zitadel.go b/cmd/zitadel.go index b6d6e9c87..4cf22f9ce 100644 --- a/cmd/zitadel.go +++ b/cmd/zitadel.go @@ -36,7 +36,8 @@ func New(out io.Writer, in io.Reader, args []string, server chan<- *start.Server RunE: func(cmd *cobra.Command, args []string) error { return errors.New("no additional command provided") }, - Version: build.Version(), + Version: build.Version(), + SilenceUsage: true, } viper.AutomaticEnv() diff --git a/go.mod b/go.mod index 2a7fcadbd..6bc160b2b 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( dario.cat/mergo v1.0.2 github.com/BurntSushi/toml v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0 github.com/Masterminds/squirrel v1.5.4 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b @@ -66,6 +67,7 @@ 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 @@ -80,7 +82,9 @@ require ( github.com/riverqueue/rivercontrib/otelriver v0.5.0 github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.11.1 + github.com/rs/xid v1.6.0 github.com/russellhaering/goxmldsig v1.5.0 + github.com/samber/slog-multi v1.6.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/shopspring/decimal v1.4.0 github.com/sirupsen/logrus v1.9.3 @@ -92,33 +96,45 @@ require ( github.com/stretchr/testify v1.11.1 github.com/ttacon/libphonenumber v1.2.1 github.com/twilio/twilio-go v1.28.6 + github.com/veqryn/slog-context v0.8.0 + github.com/veqryn/slog-context/otel v0.8.0 github.com/zitadel/exifremove v0.1.0 github.com/zitadel/logging v0.6.2 github.com/zitadel/oidc/v3 v3.45.0 github.com/zitadel/passwap v0.10.0 github.com/zitadel/saml v0.4.1 github.com/zitadel/schema v1.3.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 - go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 go.opentelemetry.io/otel/exporters/prometheus v0.60.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 - go.opentelemetry.io/otel/metric v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/sdk/metric v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/otel/log v0.14.0 + go.opentelemetry.io/otel/metric v1.39.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/otel/sdk/log v0.14.0 + go.opentelemetry.io/otel/sdk/metric v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.44.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 golang.org/x/net v0.47.0 golang.org/x/oauth2 v0.33.0 golang.org/x/sync v0.18.0 - golang.org/x/sys v0.38.0 + golang.org/x/sys v0.39.0 golang.org/x/text v0.31.0 google.golang.org/api v0.256.0 - google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba - google.golang.org/grpc v1.76.0 + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 + google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 sigs.k8s.io/yaml v1.6.0 ) @@ -133,8 +149,7 @@ require ( cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/trace v1.11.6 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 // indirect @@ -145,7 +160,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -159,7 +174,7 @@ require ( github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -218,12 +233,13 @@ require ( github.com/prometheus/otlptranslator v0.0.2 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/riverqueue/river/rivershared v0.26.0 // indirect - github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/samber/slog-common v0.19.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -235,17 +251,16 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.11 // indirect diff --git a/go.sum b/go.sum index 9c30ffd9e..c51e5eefb 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0 h1:5eCqTd9rTwMlE62z0xFdzPJ+3pji75hJrwq1jrCjo5w= @@ -133,8 +133,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cockroachdb/cockroach-go/v2 v2.4.0 h1:7K5vpE3m7LylIbmpbr4eEhApDTPMgFgR+eDPy1sdJjM= github.com/cockroachdb/cockroach-go/v2 v2.4.0/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -208,10 +208,10 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= -github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -589,6 +589,8 @@ 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= @@ -717,6 +719,12 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI= +github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M= +github.com/samber/slog-multi v1.6.0 h1:i1uBY+aaln6ljwdf7Nrt4Sys8Kk6htuYuXDHWJsHtZg= +github.com/samber/slog-multi v1.6.0/go.mod h1:qTqzmKdPpT0h4PFsTN5rYRgLwom1v+fNGuIrl1Xnnts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= @@ -756,8 +764,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -797,6 +805,10 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/veqryn/slog-context v0.8.0 h1:lDhwAgjwx52K5StqqQzi5d0Y/F4SNyGZbsXGd8MtucM= +github.com/veqryn/slog-context v0.8.0/go.mod h1:8rsT72p0kzzN9lmkwtabIhxg7ZkpnKblt9x3Eix8Tc0= +github.com/veqryn/slog-context/otel v0.8.0 h1:tuMAAb/aXImUBTXz+RmNkTu6eBdzZAde8cZM/UL0HdI= +github.com/veqryn/slog-context/otel v0.8.0/go.mod h1:CHln1I9F42pK+GvLKMiH/QzIdzTf2n3R0pkHa9+vYzQ= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= @@ -812,8 +824,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8aKg= github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= @@ -833,34 +843,54 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= -go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s= +go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0 h1:EKpiGphOYq3CYnIe2eX9ftUkyU+Y8Dtte8OaWyHJ4+I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0/go.mod h1:nWFP7C+T8TygkTjJ7mAyEaFaE7wNfms3nV/vexZ6qt0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -998,8 +1028,8 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -1056,10 +1086,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4= -google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1074,8 +1104,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/internal/actions/http_module_test.go b/internal/actions/http_module_test.go index 50a007fee..4021b593a 100644 --- a/internal/actions/http_module_test.go +++ b/internal/actions/http_module_test.go @@ -3,7 +3,6 @@ package actions import ( "bytes" "context" - "errors" "io" "net" "net/http" @@ -92,13 +91,13 @@ func Test_isHostBlocked(t *testing.T) { name: "looked up failure", fields: fields{ lookup: func(host string) ([]net.IP, error) { - return nil, errors.New("some error") + return nil, io.EOF }, }, args: args{ address: mustNewURL(t, "https://test2.com/hodor"), }, - want: zerrors.ThrowInternal(nil, "ACTIO-4m9s2", "lookup failed"), + want: zerrors.ThrowInternal(io.EOF, "ACTIO-4m9s2", "lookup failed"), }, } for _, tt := range tests { diff --git a/internal/api/api.go b/internal/api/api.go index 8010a1391..4a4f1f72f 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -12,10 +12,12 @@ import ( "github.com/gorilla/mux" "github.com/improbable-eng/grpc-web/go/grpcweb" "github.com/zitadel/logging" + "go.opentelemetry.io/otel" "google.golang.org/grpc" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" "github.com/zitadel/zitadel/internal/api/authz" grpc_api "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -26,7 +28,6 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" instance_pb "github.com/zitadel/zitadel/pkg/grpc/instance/v2" @@ -131,7 +132,12 @@ func New( if err != nil { return nil, err } - api.connectOTELInterceptor, err = otelconnect.NewInterceptor() + api.connectOTELInterceptor, err = otelconnect.NewInterceptor( + otelconnect.WithTracerProvider(otel.GetTracerProvider()), + otelconnect.WithMeterProvider(otel.GetMeterProvider()), + otelconnect.WithPropagator(otel.GetTextMapPropagator()), + otelconnect.WithoutServerPeerAttributes(), + ) if err != nil { return nil, err } @@ -199,6 +205,7 @@ func (a *API) registerConnectServer(service server.ConnectServer) { connect_middleware.CallDurationHandler(), a.connectOTELInterceptor, connect_middleware.MetricsHandler(metricTypes, grpc_api.Probes...), + connect_middleware.LogHandler(grpc_api.Probes...), connect_middleware.NoCacheInterceptor(), connect_middleware.InstanceInterceptor(a.queries, a.externalDomain, a.translator, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName, instance_pb.InstanceService_ServiceDesc.ServiceName), connect_middleware.AccessStorageInterceptor(a.accessInterceptor.AccessService()), diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index cba90104c..c93d6ab62 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -79,7 +79,7 @@ type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, defaul func DefaultErrorHandler(translator *i18n.Translator) func(w http.ResponseWriter, r *http.Request, err error, defaultCode int) { return func(w http.ResponseWriter, r *http.Request, err error, defaultCode int) { logging.WithFields("uri", r.RequestURI).WithError(err).Warn("error occurred on asset api") - code, ok := http_util.ZitadelErrorToHTTPStatusCode(err) + code, ok := http_util.ZitadelErrorToHTTPStatusCode(r.Context(), err) if !ok { code = defaultCode } diff --git a/internal/api/grpc/gerrors/zitadel_errors.go b/internal/api/grpc/gerrors/zitadel_errors.go index b5d289306..967302021 100644 --- a/internal/api/grpc/gerrors/zitadel_errors.go +++ b/internal/api/grpc/gerrors/zitadel_errors.go @@ -1,10 +1,13 @@ package gerrors import ( + "context" "errors" + "log/slog" "connectrpc.com/connect" "github.com/jackc/pgx/v5/pgconn" + slogctx "github.com/veqryn/slog-context" "github.com/zitadel/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -16,16 +19,15 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/message" ) -func ZITADELToGRPCError(err error) error { +func ZITADELToGRPCError(ctx context.Context, err error) error { if err == nil { return nil } - code, key, id, ok := ExtractZITADELError(err) - if !ok { - return status.Convert(err).Err() - } + + code, key, id, lvl := extractError(err) msg := key msg += " (" + id + ")" + slogctx.FromCtx(ctx).Log(ctx, lvl, msg, "err", err, "code", code) errorInfo := getErrorInfo(id, key, err) @@ -38,20 +40,21 @@ func ZITADELToGRPCError(err error) error { return s.Err() } -func ZITADELToConnectError(err error) error { +func ZITADELToConnectError(ctx context.Context, err error) error { if err == nil { return nil } connectError := new(connect.Error) if errors.As(err, &connectError) { + // Connect error may be returned by other middlewares, + // so we assume it's a client error and log as warning. + slogctx.FromCtx(ctx).WarnContext(ctx, connectError.Message(), "err", connectError.Unwrap(), "code", connectError.Code()) return err } - code, key, id, ok := ExtractZITADELError(err) - if !ok { - return status.Convert(err).Err() - } + code, key, id, lvl := extractError(err) msg := key msg += " (" + id + ")" + slogctx.FromCtx(ctx).Log(ctx, lvl, msg, "err", err) errorInfo := getErrorInfo(id, key, err) @@ -62,44 +65,62 @@ func ZITADELToConnectError(err error) error { return cErr } -func ExtractZITADELError(err error) (c codes.Code, msg, id string, ok bool) { +func ExtractZITADELError(err error) (code codes.Code, msg, id string) { if err == nil { - return codes.OK, "", "", false + return codes.OK, "", "" } + code, msg, id, _ = extractError(err) + return code, msg, id +} + +func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) { connErr := new(pgconn.ConnectError) if ok := errors.As(err, &connErr); ok { - return codes.Internal, "db connection error", "", true + return codes.Internal, "db connection error", "", slog.LevelError } - zitadelErr := new(zerrors.ZitadelError) - if ok := errors.As(err, &zitadelErr); !ok { - return codes.Unknown, err.Error(), "", false + zitadelErr, ok := zerrors.AsZitadelError(err) + if !ok { + return codes.Unknown, err.Error(), "", slog.LevelError } - switch { - case zerrors.IsErrorAlreadyExists(err): - return codes.AlreadyExists, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsDeadlineExceeded(err): - return codes.DeadlineExceeded, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsInternal(err): - return codes.Internal, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsErrorInvalidArgument(err): - return codes.InvalidArgument, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsNotFound(err): - return codes.NotFound, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsPermissionDenied(err): - return codes.PermissionDenied, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsPreconditionFailed(err): - return codes.FailedPrecondition, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsUnauthenticated(err): - return codes.Unauthenticated, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsUnavailable(err): - return codes.Unavailable, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsUnimplemented(err): - return codes.Unimplemented, zitadelErr.GetMessage(), zitadelErr.GetID(), true - case zerrors.IsResourceExhausted(err): - return codes.ResourceExhausted, zitadelErr.GetMessage(), zitadelErr.GetID(), true + msg, id = zitadelErr.GetMessage(), zitadelErr.GetID() + + switch zitadelErr.Kind { + case zerrors.KindAlreadyExists: + c, lvl = codes.AlreadyExists, slog.LevelWarn + case zerrors.KindDeadlineExceeded: + c, lvl = codes.DeadlineExceeded, slog.LevelError + case zerrors.KindInternal: + c, lvl = codes.Internal, slog.LevelError + case zerrors.KindInvalidArgument: + c, lvl = codes.InvalidArgument, slog.LevelWarn + case zerrors.KindNotFound: + c, lvl = codes.NotFound, slog.LevelWarn + case zerrors.KindPermissionDenied: + c, lvl = codes.PermissionDenied, slog.LevelWarn + case zerrors.KindPreconditionFailed: + c, lvl = codes.FailedPrecondition, slog.LevelWarn + case zerrors.KindUnauthenticated: + c, lvl = codes.Unauthenticated, slog.LevelWarn + case zerrors.KindUnavailable: + c, lvl = codes.Unavailable, slog.LevelError + case zerrors.KindUnimplemented: + c, lvl = codes.Unimplemented, slog.LevelInfo + case zerrors.KindResourceExhausted: + c, lvl = codes.ResourceExhausted, slog.LevelError + case zerrors.KindDataLoss: + c, lvl = codes.DataLoss, slog.LevelError + case zerrors.KindCanceled: + c, lvl = codes.Canceled, slog.LevelWarn + case zerrors.KindAborted: + c, lvl = codes.Aborted, slog.LevelError + case zerrors.KindOutOfRange: + c, lvl = codes.OutOfRange, slog.LevelWarn + case zerrors.KindUnknown: + c, lvl = codes.Unknown, slog.LevelError default: - return codes.Unknown, err.Error(), "", false + c, lvl = codes.Unknown, slog.LevelError } + return c, msg, id, lvl } func getErrorInfo(id, key string, err error) protoadapt.MessageV1 { diff --git a/internal/api/grpc/gerrors/zitadel_errors_test.go b/internal/api/grpc/gerrors/zitadel_errors_test.go index 1d41825fd..8b8a2c131 100644 --- a/internal/api/grpc/gerrors/zitadel_errors_test.go +++ b/internal/api/grpc/gerrors/zitadel_errors_test.go @@ -3,6 +3,7 @@ package gerrors import ( "errors" "fmt" + "log/slog" "testing" "github.com/stretchr/testify/assert" @@ -41,7 +42,7 @@ func TestCaosToGRPCError(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := ZITADELToGRPCError(tt.args.err); (err != nil) != tt.wantErr { + if err := ZITADELToGRPCError(t.Context(), tt.args.err); (err != nil) != tt.wantErr { t.Errorf("ZITADELToGRPCError() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -96,7 +97,7 @@ func Test_getErrorInfo(t *testing.T) { } } -func Test_Extract(t *testing.T) { +func Test_extractError(t *testing.T) { type args struct { err error } @@ -106,7 +107,7 @@ func Test_Extract(t *testing.T) { wantC codes.Code wantMsg string wantID string - wantOk bool + wantLvl slog.Level }{ { "already exists", @@ -114,7 +115,7 @@ func Test_Extract(t *testing.T) { codes.AlreadyExists, "already exists", "id", - true, + slog.LevelWarn, }, { "deadline exceeded", @@ -122,7 +123,7 @@ func Test_Extract(t *testing.T) { codes.DeadlineExceeded, "deadline exceeded", "id", - true, + slog.LevelError, }, { "internal error", @@ -130,7 +131,7 @@ func Test_Extract(t *testing.T) { codes.Internal, "internal error", "id", - true, + slog.LevelError, }, { "invalid argument", @@ -138,7 +139,7 @@ func Test_Extract(t *testing.T) { codes.InvalidArgument, "invalid argument", "id", - true, + slog.LevelWarn, }, { "not found", @@ -146,7 +147,7 @@ func Test_Extract(t *testing.T) { codes.NotFound, "not found", "id", - true, + slog.LevelWarn, }, { "permission denied", @@ -154,7 +155,7 @@ func Test_Extract(t *testing.T) { codes.PermissionDenied, "permission denied", "id", - true, + slog.LevelWarn, }, { "precondition failed", @@ -162,7 +163,7 @@ func Test_Extract(t *testing.T) { codes.FailedPrecondition, "precondition failed", "id", - true, + slog.LevelWarn, }, { "unauthenticated", @@ -170,7 +171,7 @@ func Test_Extract(t *testing.T) { codes.Unauthenticated, "unauthenticated", "id", - true, + slog.LevelWarn, }, { "unavailable", @@ -178,7 +179,7 @@ func Test_Extract(t *testing.T) { codes.Unavailable, "unavailable", "id", - true, + slog.LevelError, }, { "unimplemented", @@ -186,7 +187,7 @@ func Test_Extract(t *testing.T) { codes.Unimplemented, "unimplemented", "id", - true, + slog.LevelInfo, }, { "exhausted", @@ -194,7 +195,7 @@ func Test_Extract(t *testing.T) { codes.ResourceExhausted, "exhausted", "id", - true, + slog.LevelError, }, { "unknown", @@ -202,12 +203,12 @@ func Test_Extract(t *testing.T) { codes.Unknown, "unknown", "", - false, + slog.LevelError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotC, gotMsg, gotID, gotOk := ExtractZITADELError(tt.args.err) + gotC, gotMsg, gotID := ExtractZITADELError(tt.args.err) if gotC != tt.wantC { t.Errorf("extract() gotC = %v, want %v", gotC, tt.wantC) } @@ -217,9 +218,6 @@ func Test_Extract(t *testing.T) { if gotID != tt.wantID { t.Errorf("extract() gotID = %v, want %v", gotID, tt.wantID) } - if gotOk != tt.wantOk { - t.Errorf("extract() gotOk = %v, want %v", gotOk, tt.wantOk) - } }) } } diff --git a/internal/api/grpc/server/connect_middleware/activity_interceptor.go b/internal/api/grpc/server/connect_middleware/activity_interceptor.go index 4ba604464..635d0b819 100644 --- a/internal/api/grpc/server/connect_middleware/activity_interceptor.go +++ b/internal/api/grpc/server/connect_middleware/activity_interceptor.go @@ -19,7 +19,7 @@ func ActivityInterceptor() connect.UnaryInterceptorFunc { ctx = activityInfoFromGateway(ctx, req.Header()).SetMethod(req.Spec().Procedure).IntoContext(ctx) resp, err := handler(ctx, req) if isResourceAPI(req.Spec().Procedure) { - code, _, _, _ := gerrors.ExtractZITADELError(err) + code, _, _ := gerrors.ExtractZITADELError(err) ctx = ainfo.ActivityInfoFromContext(ctx).SetGRPCStatus(code).IntoContext(ctx) activity.TriggerGRPCWithContext(ctx, activity.ResourceAPI) } diff --git a/internal/api/grpc/server/connect_middleware/error_interceptor.go b/internal/api/grpc/server/connect_middleware/error_interceptor.go index 9aef95bc6..43d61c2d1 100644 --- a/internal/api/grpc/server/connect_middleware/error_interceptor.go +++ b/internal/api/grpc/server/connect_middleware/error_interceptor.go @@ -19,5 +19,5 @@ func ErrorHandler() connect.UnaryInterceptorFunc { func toConnectError(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc) (connect.AnyResponse, error) { resp, err := handler(ctx, req) - return resp, gerrors.ZITADELToConnectError(err) // TODO ! + return resp, gerrors.ZITADELToConnectError(ctx, err) } diff --git a/internal/api/grpc/server/connect_middleware/instance_interceptor.go b/internal/api/grpc/server/connect_middleware/instance_interceptor.go index 4855dfd47..4f340df08 100644 --- a/internal/api/grpc/server/connect_middleware/instance_interceptor.go +++ b/internal/api/grpc/server/connect_middleware/instance_interceptor.go @@ -77,8 +77,8 @@ func addInstanceByID(ctx context.Context, req connect.AnyRequest, handler connec func addInstanceByDomain(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, verifier authz.InstanceVerifier, translator *i18n.Translator, domain string) (connect.AnyResponse, error) { instance, err := verifier.InstanceByHost(ctx, domain, "") if err != nil { - notFoundErr := new(zerrors.NotFoundError) - if errors.As(err, ¬FoundErr) { + notFoundErr := new(zerrors.ZitadelError) + if errors.As(err, ¬FoundErr) && notFoundErr.Kind == zerrors.KindNotFound { notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) } return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("unable to set instance using domain %s: %w", domain, notFoundErr)) diff --git a/internal/api/grpc/server/connect_middleware/log_interceptor.go b/internal/api/grpc/server/connect_middleware/log_interceptor.go new file mode 100644 index 000000000..564cf9c8f --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/log_interceptor.go @@ -0,0 +1,13 @@ +package connect_middleware + +import ( + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/backend/v3/instrumentation/logging" +) + +func LogHandler(ignoredPrefix ...string) connect.UnaryInterceptorFunc { + return func(next connect.UnaryFunc) connect.UnaryFunc { + return logging.NewConnectInterceptor(next, ignoredPrefix...) + } +} diff --git a/internal/api/grpc/server/connect_middleware/metrics_interceptor.go b/internal/api/grpc/server/connect_middleware/metrics_interceptor.go index fe9424a1b..364562c78 100644 --- a/internal/api/grpc/server/connect_middleware/metrics_interceptor.go +++ b/internal/api/grpc/server/connect_middleware/metrics_interceptor.go @@ -10,8 +10,8 @@ import ( "go.opentelemetry.io/otel/attribute" "google.golang.org/grpc/codes" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" _ "github.com/zitadel/zitadel/internal/statik" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) const ( diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go index b20819b85..a9b705e53 100644 --- a/internal/api/grpc/server/gateway.go +++ b/internal/api/grpc/server/gateway.go @@ -20,10 +20,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" client_middleware "github.com/zitadel/zitadel/internal/api/grpc/client/middleware" http_utils "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) const ( @@ -209,7 +209,7 @@ func addInterceptors( handler = http_mw.CallDurationHandler(handler) handler = http_mw.CORSInterceptor(handler) handler = http_mw.RobotsTagHandler(handler) - handler = http_mw.DefaultTelemetryHandler(handler) + handler = http_mw.DefaultTraceHandler(handler) handler = http_mw.ActivityHandler(handler) // For some non-obvious reason, the exhaustedCookieInterceptor sends the SetCookie header // only if it follows the http_mw.DefaultTelemetryHandler @@ -218,6 +218,7 @@ func addInterceptors( metrics.MetricTypeTotalCount, metrics.MetricTypeStatusCode, }, http_utils.Probes...)(handler) + handler = http_mw.LogHandler("grpc_gateway", http_utils.Probes...)(handler) return handler } diff --git a/internal/api/grpc/server/middleware/activity_interceptor.go b/internal/api/grpc/server/middleware/activity_interceptor.go index 29b7612ee..97ea70fb7 100644 --- a/internal/api/grpc/server/middleware/activity_interceptor.go +++ b/internal/api/grpc/server/middleware/activity_interceptor.go @@ -18,7 +18,7 @@ func ActivityInterceptor() grpc.UnaryServerInterceptor { ctx = activityInfoFromGateway(ctx).SetMethod(info.FullMethod).IntoContext(ctx) resp, err := handler(ctx, req) if isResourceAPI(info.FullMethod) { - code, _, _, _ := gerrors.ExtractZITADELError(err) + code, _, _ := gerrors.ExtractZITADELError(err) ctx = ainfo.ActivityInfoFromContext(ctx).SetGRPCStatus(code).IntoContext(ctx) activity.TriggerGRPCWithContext(ctx, activity.ResourceAPI) } diff --git a/internal/api/grpc/server/middleware/error_interceptor.go b/internal/api/grpc/server/middleware/error_interceptor.go index 7c5e3163b..9c4c86526 100644 --- a/internal/api/grpc/server/middleware/error_interceptor.go +++ b/internal/api/grpc/server/middleware/error_interceptor.go @@ -17,5 +17,5 @@ func ErrorHandler() grpc.UnaryServerInterceptor { func toGRPCError(ctx context.Context, req interface{}, handler grpc.UnaryHandler) (interface{}, error) { resp, err := handler(ctx, req) - return resp, gerrors.ZITADELToGRPCError(err) + return resp, gerrors.ZITADELToGRPCError(ctx, err) } diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index e9971d233..bd584709f 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -77,8 +77,8 @@ func addInstanceByID(ctx context.Context, req interface{}, handler grpc.UnaryHan func addInstanceByDomain(ctx context.Context, req interface{}, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, translator *i18n.Translator, domain string) (interface{}, error) { instance, err := verifier.InstanceByHost(ctx, domain, "") if err != nil { - notFoundErr := new(zerrors.NotFoundError) - if errors.As(err, ¬FoundErr) { + notFoundErr := new(zerrors.ZitadelError) + if errors.As(err, ¬FoundErr) && notFoundErr.Kind == zerrors.KindNotFound { notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) } return nil, status.Error(codes.NotFound, fmt.Errorf("unable to set instance using domain %s: %w", domain, notFoundErr).Error()) diff --git a/internal/api/grpc/server/middleware/log_interceptor.go b/internal/api/grpc/server/middleware/log_interceptor.go new file mode 100644 index 000000000..4b58b5291 --- /dev/null +++ b/internal/api/grpc/server/middleware/log_interceptor.go @@ -0,0 +1,11 @@ +package middleware + +import ( + "google.golang.org/grpc" + + "github.com/zitadel/zitadel/backend/v3/instrumentation/logging" +) + +func LogHandler(ignoredMethodSuffixes ...string) grpc.UnaryServerInterceptor { + return logging.NewGrpcInterceptor(ignoredMethodSuffixes...) +} diff --git a/internal/api/grpc/server/middleware/metrics_interceptor.go b/internal/api/grpc/server/middleware/metrics_interceptor.go index 805494fd3..d27902bb8 100644 --- a/internal/api/grpc/server/middleware/metrics_interceptor.go +++ b/internal/api/grpc/server/middleware/metrics_interceptor.go @@ -9,8 +9,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/status" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" _ "github.com/zitadel/zitadel/internal/statik" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) const ( diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 44ef26ca1..4134cf5fb 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -11,6 +11,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/protobuf/reflect/protoreflect" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" "github.com/zitadel/zitadel/internal/api/authz" grpc_api "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" @@ -19,7 +20,6 @@ import ( "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/logstore/record" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/telemetry/metrics" system_pb "github.com/zitadel/zitadel/pkg/grpc/system" ) @@ -71,6 +71,7 @@ func CreateServer( grpc_middleware.ChainUnaryServer( middleware.CallDurationHandler(), middleware.MetricsHandler(metricTypes, grpc_api.Probes...), + middleware.LogHandler(grpc_api.Probes...), middleware.NoCacheInterceptor(), middleware.InstanceInterceptor(queries, externalDomain, translator, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), middleware.AccessStorageInterceptor(accessSvc), diff --git a/internal/api/grpc/user/v2/integration_test/intent_test.go b/internal/api/grpc/user/v2/integration_test/intent_test.go index 0624486a4..a9d6b811c 100644 --- a/internal/api/grpc/user/v2/integration_test/intent_test.go +++ b/internal/api/grpc/user/v2/integration_test/intent_test.go @@ -12,13 +12,13 @@ import ( "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestServer_StartIdentityProviderIntent(t *testing.T) { @@ -631,7 +631,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { DisplayName: gu.Ptr("displayname"), }, Email: &user.SetHumanEmail{ - Email: "email@email.com", + Email: "email@email.com", Verification: &user.SetHumanEmail_SendCode{SendCode: &user.SendEmailVerificationCode{}}, }, }, @@ -1037,4 +1037,4 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { assert.EqualExportedValues(t, tt.want, got) }) } -} \ No newline at end of file +} diff --git a/internal/api/http/error.go b/internal/api/http/error.go index 01813d026..e34524943 100644 --- a/internal/api/http/error.go +++ b/internal/api/http/error.go @@ -1,49 +1,77 @@ package http import ( - "errors" + "context" + "log/slog" "net/http" + slogctx "github.com/veqryn/slog-context" + "github.com/zitadel/zitadel/internal/zerrors" ) -func ZitadelErrorToHTTPStatusCode(err error) (statusCode int, ok bool) { +func ZitadelErrorToHTTPStatusCode(ctx context.Context, err error) (statusCode int, ok bool) { if err == nil { return http.StatusOK, true } - //nolint:errorlint - switch err.(type) { - case *zerrors.AlreadyExistsError: - return http.StatusConflict, true - case *zerrors.DeadlineExceededError: - return http.StatusGatewayTimeout, true - case *zerrors.InternalError: - return http.StatusInternalServerError, true - case *zerrors.InvalidArgumentError: - return http.StatusBadRequest, true - case *zerrors.NotFoundError: - return http.StatusNotFound, true - case *zerrors.PermissionDeniedError: - return http.StatusForbidden, true - case *zerrors.PreconditionFailedError: - // use the same code as grpc-gateway: - // https://github.com/grpc-ecosystem/grpc-gateway/blob/9e33e38f15cb7d2f11096366e62ea391a3459ba9/runtime/errors.go#L59 - return http.StatusBadRequest, true - case *zerrors.UnauthenticatedError: - return http.StatusUnauthorized, true - case *zerrors.UnavailableError: - return http.StatusServiceUnavailable, true - case *zerrors.UnimplementedError: - return http.StatusNotImplemented, true - case *zerrors.ResourceExhaustedError: - return http.StatusTooManyRequests, true - default: - c := new(zerrors.ZitadelError) - if errors.As(err, &c) { - return ZitadelErrorToHTTPStatusCode(errors.Unwrap(err)) - } + statusCode, key, id, lvl := extractError(err) + msg := key + msg += " (" + id + ")" + slogctx.FromCtx(ctx).Log(ctx, lvl, msg, "err", err) + if statusCode == statusUnknown { return http.StatusInternalServerError, false } + return statusCode, true +} + +const statusUnknown = 0 + +func extractError(err error) (statusCode int, msg, id string, lvl slog.Level) { + zitadelErr, ok := zerrors.AsZitadelError(err) + if !ok { + return statusUnknown, err.Error(), "", slog.LevelError + } + msg, id = zitadelErr.GetMessage(), zitadelErr.GetID() + + switch zitadelErr.Kind { + case zerrors.KindAlreadyExists: + statusCode, lvl = http.StatusConflict, slog.LevelError + case zerrors.KindDeadlineExceeded: + statusCode, lvl = http.StatusGatewayTimeout, slog.LevelError + case zerrors.KindInternal: + statusCode, lvl = http.StatusInternalServerError, slog.LevelError + case zerrors.KindInvalidArgument: + statusCode, lvl = http.StatusBadRequest, slog.LevelWarn + case zerrors.KindNotFound: + statusCode, lvl = http.StatusNotFound, slog.LevelWarn + case zerrors.KindPermissionDenied: + statusCode, lvl = http.StatusForbidden, slog.LevelWarn + case zerrors.KindPreconditionFailed: + // use the same code as grpc-gateway: + // https://github.com/grpc-ecosystem/grpc-gateway/blob/9e33e38f15cb7d2f11096366e62ea391a3459ba9/runtime/errors.go#L59 + statusCode, lvl = http.StatusBadRequest, slog.LevelWarn + case zerrors.KindUnauthenticated: + statusCode, lvl = http.StatusUnauthorized, slog.LevelWarn + case zerrors.KindUnavailable: + statusCode, lvl = http.StatusServiceUnavailable, slog.LevelError + case zerrors.KindUnimplemented: + statusCode, lvl = http.StatusNotImplemented, slog.LevelInfo + case zerrors.KindResourceExhausted: + statusCode, lvl = http.StatusTooManyRequests, slog.LevelError + case zerrors.KindCanceled: + statusCode, lvl = 499, slog.LevelWarn + case zerrors.KindDataLoss: + statusCode, lvl = http.StatusInternalServerError, slog.LevelError + case zerrors.KindOutOfRange: + statusCode, lvl = http.StatusBadRequest, slog.LevelWarn + case zerrors.KindAborted: + statusCode, lvl = http.StatusConflict, slog.LevelWarn + case zerrors.KindUnknown: + fallthrough + default: + statusCode, lvl = statusUnknown, slog.LevelError + } + return statusCode, msg, id, lvl } func HTTPStatusCodeToZitadelError(parent error, statusCode int, id string, message string) error { diff --git a/internal/api/http/error_test.go b/internal/api/http/error_test.go index d912e9f34..dbd2d4556 100644 --- a/internal/api/http/error_test.go +++ b/internal/api/http/error_test.go @@ -3,6 +3,7 @@ package http import ( "errors" "fmt" + "io" "net/http" "testing" @@ -118,7 +119,7 @@ func TestZitadelErrorToHTTPStatusCode(t *testing.T) { wantOk: true, }, { - name: "no caos/zitadel error", + name: "no zitadel error", args: args{ err: errors.New("error"), }, @@ -128,7 +129,7 @@ func TestZitadelErrorToHTTPStatusCode(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotStatusCode, gotOk := ZitadelErrorToHTTPStatusCode(tt.args.err) + gotStatusCode, gotOk := ZitadelErrorToHTTPStatusCode(t.Context(), tt.args.err) if gotStatusCode != tt.wantStatusCode { t.Errorf("ZitadelErrorToHTTPStatusCode() gotStatusCode = %v, want %v", gotStatusCode, tt.wantStatusCode) } @@ -272,9 +273,9 @@ func TestHTTPStatusCodeToZitadelError(t *testing.T) { statusCode: 1000, id: "id", message: "message", - parent: errors.New("parent error"), + parent: io.EOF, }, - wantErr: zerrors.ThrowUnknown(nil, "id", "message"), + wantErr: zerrors.ThrowUnknown(io.EOF, "id", "message"), }, } for _, tt := range tests { diff --git a/internal/api/http/middleware/log_interceptor.go b/internal/api/http/middleware/log_interceptor.go new file mode 100644 index 000000000..f36f09b30 --- /dev/null +++ b/internal/api/http/middleware/log_interceptor.go @@ -0,0 +1,13 @@ +package middleware + +import ( + "net/http" + + "github.com/zitadel/zitadel/backend/v3/instrumentation/logging" +) + +func LogHandler(service string, ignoredPrefix ...string) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return logging.NewHandler(h, service, ignoredPrefix...) + } +} diff --git a/internal/api/http/middleware/metrics_interceptor.go b/internal/api/http/middleware/metrics_interceptor.go index 4b239b8c1..832f04c2d 100644 --- a/internal/api/http/middleware/metrics_interceptor.go +++ b/internal/api/http/middleware/metrics_interceptor.go @@ -3,11 +3,11 @@ package middleware import ( "net/http" - "github.com/zitadel/zitadel/internal/telemetry/metrics" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" ) func MetricsHandler(metricTypes []metrics.MetricType, ignoredMethods ...string) func(http.Handler) http.Handler { return func(handler http.Handler) http.Handler { - return metrics.NewMetricsHandler(handler, metricTypes, ignoredMethods...) + return metrics.NewHandler(handler, metricTypes, ignoredMethods...) } } diff --git a/internal/api/http/middleware/telemetry_interceptor.go b/internal/api/http/middleware/telemetry_interceptor.go deleted file mode 100644 index 3fa66e71f..000000000 --- a/internal/api/http/middleware/telemetry_interceptor.go +++ /dev/null @@ -1,18 +0,0 @@ -package middleware - -import ( - "net/http" - - http_utils "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/telemetry" -) - -func DefaultTelemetryHandler(handler http.Handler) http.Handler { - return TelemetryHandler(http_utils.Probes...)(handler) -} - -func TelemetryHandler(ignoredMethods ...string) func(http.Handler) http.Handler { - return func(handler http.Handler) http.Handler { - return telemetry.TelemetryHandler(handler, ignoredMethods...) - } -} diff --git a/internal/api/http/middleware/trace_interceptor.go b/internal/api/http/middleware/trace_interceptor.go new file mode 100644 index 000000000..44a6f7e20 --- /dev/null +++ b/internal/api/http/middleware/trace_interceptor.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "net/http" + + "github.com/zitadel/zitadel/backend/v3/instrumentation/tracing" + http_utils "github.com/zitadel/zitadel/internal/api/http" +) + +func DefaultTraceHandler(handler http.Handler) http.Handler { + return TraceHandler(http_utils.Probes...)(handler) +} + +func TraceHandler(ignoredPrefix ...string) func(http.Handler) http.Handler { + return func(handler http.Handler) http.Handler { + return tracing.NewHandler(handler, ignoredPrefix...) + } +} diff --git a/internal/api/http/request_context.go b/internal/api/http/request_context.go index 5cec32d91..1b93cce83 100644 --- a/internal/api/http/request_context.go +++ b/internal/api/http/request_context.go @@ -3,6 +3,7 @@ package http import ( "context" "fmt" + "log/slog" "net" "net/url" ) @@ -67,6 +68,22 @@ func (r *DomainCtx) Origin() string { return fmt.Sprintf("%s://%s", r.Protocol, host) } +var _ slog.LogValuer = (*DomainCtx)(nil) + +func (r *DomainCtx) LogValue() slog.Value { + values := make([]slog.Attr, 0, 3) + if r.InstanceHost != "" { + values = append(values, slog.String("instance_host", r.InstanceHost)) + } + if r.PublicHost != "" { + values = append(values, slog.String("public_host", r.PublicHost)) + } + if r.Protocol != "" { + values = append(values, slog.String("protocol", r.Protocol)) + } + return slog.GroupValue(values...) +} + func DomainContext(ctx context.Context) *DomainCtx { o, ok := ctx.Value(domainCtx).(*DomainCtx) if !ok { diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 1d949cb30..e40a5778c 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -44,7 +44,7 @@ const ( func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (_ op.AuthRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() @@ -167,7 +167,7 @@ func (o *OPStorage) audienceFromProjectID(ctx context.Context, projectID string) func (o *OPStorage) AuthRequestByID(ctx context.Context, id string) (_ op.AuthRequest, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() @@ -206,7 +206,7 @@ func (o *OPStorage) decryptGrant(grant string) (string, error) { func (o *OPStorage) SaveAuthCode(ctx context.Context, id, code string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() @@ -249,7 +249,7 @@ func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID strin func (o *OPStorage) terminateSession(ctx context.Context, userID string) (sessions []command.HumanSignOutSession, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) @@ -276,7 +276,7 @@ func (o *OPStorage) terminateSession(ctx context.Context, userID string) (sessio func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionRequest *op.EndSessionRequest) (redirectURI string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() @@ -505,7 +505,7 @@ func (o *OPStorage) revokeTokenV1(ctx context.Context, token, userID, clientID s func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 6acd1486c..c5e0851c4 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -36,8 +36,6 @@ const ( ClaimUserGroups = ScopeUserGroups ScopeCustomUserGroups = "urn:zitadel:iam:user:groups" ClaimCustomUserGroups = ScopeCustomUserGroups - - oidcCtx = "oidc" ) // GetClientByClientID implements the op.Storage interface to retrieve an OIDC client by its ID. @@ -46,7 +44,7 @@ const ( func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() client, err := o.query.ActiveOIDCClientByID(ctx, id, false) @@ -188,7 +186,7 @@ func userinfoClaims(userInfo *oidc.UserInfo) func(c *actions.FieldConfig) interf func (s *Server) VerifyClient(ctx context.Context, r *op.Request[op.ClientCredentials]) (_ op.Client, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() diff --git a/internal/api/oidc/error.go b/internal/api/oidc/error.go index 829a15d4d..5cba33044 100644 --- a/internal/api/oidc/error.go +++ b/internal/api/oidc/error.go @@ -1,6 +1,7 @@ package oidc import ( + "context" "errors" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -15,7 +16,7 @@ import ( // When err is already of the correct type is passed as-is. // If the err is a Zitadel error, it is transformed with a proper HTTP status code. // Unknown errors are treated as internal server errors. -func oidcError(err error) error { +func oidcError(ctx context.Context, err error) error { if err == nil { return nil } @@ -37,7 +38,7 @@ func oidcError(err error) error { errors.As(err, &zError) } - statusCode, _ := http_util.ZitadelErrorToHTTPStatusCode(err) + statusCode, _ := http_util.ZitadelErrorToHTTPStatusCode(ctx, err) newOidcErr := oidc.ErrServerError if statusCode < 500 { newOidcErr = oidc.ErrInvalidRequest diff --git a/internal/api/oidc/error_test.go b/internal/api/oidc/error_test.go index fbacbdc3f..95570d6d3 100644 --- a/internal/api/oidc/error_test.go +++ b/internal/api/oidc/error_test.go @@ -56,7 +56,7 @@ func Test_oidcError(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := oidcError(tt.err) + err := oidcError(t.Context(), tt.err) require.ErrorIs(t, err, tt.wantErr) }) } diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index 6ce5d72e2..d451a0aa3 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -20,7 +20,7 @@ import ( func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionRequest]) (resp *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() ctx, cancel := context.WithCancel(ctx) diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 61f874664..72671f383 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -150,7 +150,7 @@ func (k *publicKeyCache) getKey(ctx context.Context, keyID string) (_ *cachedPub func (k *publicKeyCache) verifySignature(ctx context.Context, jws *jose.JSONWebSignature, checkKeyExpiry bool) (_ []byte, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index dcfb0e3d7..504e2ba01 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -10,6 +10,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" "github.com/zitadel/zitadel/internal/api/assets" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -21,7 +22,6 @@ import ( "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -173,7 +173,8 @@ func NewServer( op.WithFallbackLogger(fallbackLogger), op.WithHTTPMiddleware( middleware.MetricsHandler(metricTypes), - middleware.TelemetryHandler(), + middleware.TraceHandler(), + middleware.LogHandler("oidc"), middleware.NoCacheInterceptor().Handler, instanceHandler, userAgentCookie, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index 4f9b3d7f7..36d1a153d 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -117,7 +117,7 @@ func (s *Server) Ready(ctx context.Context, r *op.Request[struct{}]) (_ *op.Resp func (s *Server) Discovery(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() restrictions, err := s.query.GetInstanceRestrictions(ctx) diff --git a/internal/api/oidc/token_client_credentials.go b/internal/api/oidc/token_client_credentials.go index 5871e2f13..ea2d933cc 100644 --- a/internal/api/oidc/token_client_credentials.go +++ b/internal/api/oidc/token_client_credentials.go @@ -16,7 +16,7 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) - err = oidcError(err) + err = oidcError(ctx, err) }() client, ok := r.Client.(*clientCredentialsClient) if !ok { diff --git a/internal/api/oidc/token_code.go b/internal/api/oidc/token_code.go index 033f2453b..3da071a21 100644 --- a/internal/api/oidc/token_code.go +++ b/internal/api/oidc/token_code.go @@ -19,7 +19,7 @@ func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.Acce ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) - err = oidcError(err) + err = oidcError(ctx, err) }() client, ok := r.Client.(*Client) diff --git a/internal/api/oidc/token_device.go b/internal/api/oidc/token_device.go index 8f42bb3ac..39b6686ed 100644 --- a/internal/api/oidc/token_device.go +++ b/internal/api/oidc/token_device.go @@ -18,7 +18,7 @@ func (s *Server) DeviceToken(ctx context.Context, r *op.ClientRequest[oidc.Devic ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) - err = oidcError(err) + err = oidcError(ctx, err) }() client, ok := r.Client.(*Client) diff --git a/internal/api/oidc/token_exchange.go b/internal/api/oidc/token_exchange.go index 402510d91..b762343be 100644 --- a/internal/api/oidc/token_exchange.go +++ b/internal/api/oidc/token_exchange.go @@ -32,7 +32,7 @@ func init() { func (s *Server) TokenExchange(ctx context.Context, r *op.ClientRequest[oidc.TokenExchangeRequest]) (_ *op.Response, err error) { resp, err := s.tokenExchange(ctx, r) if err != nil { - return nil, oidcError(err) + return nil, oidcError(ctx, err) } return resp, nil } diff --git a/internal/api/oidc/token_jwt_profile.go b/internal/api/oidc/token_jwt_profile.go index d60e6a283..5283d7599 100644 --- a/internal/api/oidc/token_jwt_profile.go +++ b/internal/api/oidc/token_jwt_profile.go @@ -18,7 +18,7 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) - err = oidcError(err) + err = oidcError(ctx, err) }() user, err := s.verifyJWTProfile(ctx, r.Data) diff --git a/internal/api/oidc/token_refresh.go b/internal/api/oidc/token_refresh.go index 76e85a488..4a965ae19 100644 --- a/internal/api/oidc/token_refresh.go +++ b/internal/api/oidc/token_refresh.go @@ -18,7 +18,7 @@ func (s *Server) RefreshToken(ctx context.Context, r *op.ClientRequest[oidc.Refr ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) - err = oidcError(err) + err = oidcError(ctx, err) }() client, ok := r.Client.(*Client) diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index 183b44a10..014233aa7 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -30,7 +30,7 @@ import ( func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoRequest]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { - err = oidcError(err) + err = oidcError(ctx, err) span.EndWithError(err) }() token, err := s.verifyAccessToken(ctx, r.Data.AccessToken) diff --git a/internal/api/saml/provider.go b/internal/api/saml/provider.go index 7de0a07eb..b95739451 100644 --- a/internal/api/saml/provider.go +++ b/internal/api/saml/provider.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/saml/pkg/provider" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" http_utils "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/ui/login" @@ -17,7 +18,6 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) const ( @@ -71,7 +71,8 @@ func NewProvider( options := []provider.Option{ provider.WithHttpInterceptors( middleware.MetricsHandler(metricTypes), - middleware.TelemetryHandler(), + middleware.TraceHandler(), + middleware.LogHandler("saml"), middleware.NoCacheInterceptor().Handler, instanceHandler, userAgentCookie, diff --git a/internal/api/scim/serrors/errors.go b/internal/api/scim/serrors/errors.go index cce24c6b7..2cb40acbf 100644 --- a/internal/api/scim/serrors/errors.go +++ b/internal/api/scim/serrors/errors.go @@ -127,7 +127,8 @@ func ThrowPayloadTooLarge(parent error) error { } func IsScimOrZitadelError(err error) bool { - return IsScimError(err) || zerrors.IsZitadelError(err) + _, zok := zerrors.AsZitadelError(err) + return IsScimError(err) || zok } func IsScimError(err error) bool { @@ -174,7 +175,7 @@ func MapToScimError(ctx context.Context, translator *i18n.Translator, err error) } } - statusCode, ok := http_util.ZitadelErrorToHTTPStatusCode(err) + statusCode, ok := http_util.ZitadelErrorToHTTPStatusCode(ctx, err) if !ok { statusCode = http.StatusInternalServerError } diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 1fcd3450a..047884828 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -114,7 +114,12 @@ func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, call handler.Use(security, limitingAccessInterceptor.WithoutLimiting().Handle) env := handler.NewRoute().Path(envRequestPath).Subrouter() - env.Use(callDurationInterceptor, middleware.TelemetryHandler(), instanceHandler) + env.Use( + callDurationInterceptor, + middleware.TraceHandler(), + middleware.LogHandler("console"), + instanceHandler, + ) env.HandleFunc("", func(w http.ResponseWriter, r *http.Request) { url := http_util.BuildOrigin(r.Host, externalSecure) ctx := r.Context() diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index 36018e421..7ccc0e39f 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -1611,5 +1611,5 @@ func WrapIdPError(err error) *IdPError { if errors.As(err, &zErr) { id = zErr.ID } - return &IdPError{err: zerrors.CreateZitadelError(err, id, "Errors.User.ExternalIDP.LoginFailedSwitchLocal")} + return &IdPError{err: zerrors.CreateZitadelError(zerrors.KindPreconditionFailed, err, id, "Errors.User.ExternalIDP.LoginFailedSwitchLocal")} } diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 68861baa9..c18e6fb1d 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -128,7 +128,18 @@ func CreateLogin( cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache) security := middleware.SecurityHeaders(csp(), login.cspErrorHandler) - login.router = CreateRouter(login, middleware.TelemetryHandler(IgnoreInstanceEndpoints...), oidcInstanceHandler, samlInstanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, issuerInterceptor, accessHandler) + login.router = CreateRouter(login, + middleware.TraceHandler(IgnoreInstanceEndpoints...), + middleware.LogHandler("login_v1", IgnoreInstanceEndpoints...), + oidcInstanceHandler, + samlInstanceHandler, + csrfInterceptor, + cacheInterceptor, + security, + userAgentCookie, + issuerInterceptor, + accessHandler, + ) login.renderer = CreateRenderer(HandlerPrefix, staticStorage, config.LanguageCookieName) login.parser = form.NewParser() diff --git a/internal/domain/refresh_token_test.go b/internal/domain/refresh_token_test.go index e2719bd23..c9d331601 100644 --- a/internal/domain/refresh_token_test.go +++ b/internal/domain/refresh_token_test.go @@ -122,8 +122,7 @@ func FuzzFromRefreshToken(f *testing.F) { f.Fuzz(func(t *testing.T, refreshToken string) { gotUserID, gotTokenID, gotToken, err := FromRefreshToken(refreshToken, algorithm) - target := zerrors.InvalidArgumentError{ZitadelError: new(zerrors.ZitadelError)} t.Log(gotUserID, gotTokenID, gotToken) - require.ErrorAs(t, err, &target) + require.True(t, zerrors.IsErrorInvalidArgument(err)) }) } diff --git a/internal/eventstore/handler/v2/metrics.go b/internal/eventstore/handler/v2/metrics.go index 6876bb3aa..d3ac01fbb 100644 --- a/internal/eventstore/handler/v2/metrics.go +++ b/internal/eventstore/handler/v2/metrics.go @@ -6,7 +6,7 @@ import ( "github.com/zitadel/logging" "go.opentelemetry.io/otel/attribute" - "github.com/zitadel/zitadel/internal/telemetry/metrics" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" ) const ( @@ -23,7 +23,11 @@ type ProjectionMetrics struct { } func NewProjectionMetrics() *ProjectionMetrics { - projectionMetrics := &ProjectionMetrics{provider: metrics.M} + return newProjectionMetrics(metrics.GlobalMeter()) +} + +func newProjectionMetrics(m metrics.Metrics) *ProjectionMetrics { + projectionMetrics := &ProjectionMetrics{provider: m} err := projectionMetrics.provider.RegisterCounter( ProjectionEventsProcessed, diff --git a/internal/eventstore/handler/v2/metrics_test.go b/internal/eventstore/handler/v2/metrics_test.go index 54e762346..e72b1b74b 100644 --- a/internal/eventstore/handler/v2/metrics_test.go +++ b/internal/eventstore/handler/v2/metrics_test.go @@ -8,23 +8,19 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/zitadel/internal/telemetry/metrics" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" ) func TestNewProjectionMetrics(t *testing.T) { mockMetrics := metrics.NewMockMetrics() - metrics.M = mockMetrics - - metrics := NewProjectionMetrics() + metrics := newProjectionMetrics(mockMetrics) require.NotNil(t, metrics) assert.NotNil(t, metrics.provider) } func TestProjectionMetrics_ProjectionUpdateTiming(t *testing.T) { - mockMetrics := metrics.NewMockMetrics() - metrics.M = mockMetrics - projectionMetrics := NewProjectionMetrics() + projectionMetrics := newProjectionMetrics(mockMetrics) ctx := context.Background() projection := "test_projection" @@ -42,10 +38,8 @@ func TestProjectionMetrics_ProjectionUpdateTiming(t *testing.T) { } func TestProjectionMetrics_ProjectionEventsProcessed(t *testing.T) { - mockMetrics := metrics.NewMockMetrics() - metrics.M = mockMetrics - projectionMetrics := NewProjectionMetrics() + projectionMetrics := newProjectionMetrics(mockMetrics) ctx := context.Background() projection := "test_projection" @@ -64,10 +58,8 @@ func TestProjectionMetrics_ProjectionEventsProcessed(t *testing.T) { } func TestProjectionMetrics_ProjectionStateLatency(t *testing.T) { - mockMetrics := metrics.NewMockMetrics() - metrics.M = mockMetrics - projectionMetrics := NewProjectionMetrics() + projectionMetrics := newProjectionMetrics(mockMetrics) ctx := context.Background() projection := "test_projection" @@ -85,10 +77,8 @@ func TestProjectionMetrics_ProjectionStateLatency(t *testing.T) { } func TestProjectionMetrics_Integration(t *testing.T) { - mockMetrics := metrics.NewMockMetrics() - metrics.M = mockMetrics - projectionMetrics := NewProjectionMetrics() + projectionMetrics := newProjectionMetrics(mockMetrics) ctx := context.Background() projection := "test_projection" diff --git a/internal/notification/channels.go b/internal/notification/channels.go index ba9bcb9d7..c9c7f736e 100644 --- a/internal/notification/channels.go +++ b/internal/notification/channels.go @@ -5,6 +5,7 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" "github.com/zitadel/zitadel/internal/notification/channels/email" "github.com/zitadel/zitadel/internal/notification/channels/set" "github.com/zitadel/zitadel/internal/notification/channels/sms" @@ -12,7 +13,6 @@ import ( "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/notification/senders" "github.com/zitadel/zitadel/internal/notification/types" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) var _ types.ChannelChains = (*channels)(nil) diff --git a/internal/notification/channels/instrumenting/metrics.go b/internal/notification/channels/instrumenting/metrics.go index 7033fbab5..8d6c55473 100644 --- a/internal/notification/channels/instrumenting/metrics.go +++ b/internal/notification/channels/instrumenting/metrics.go @@ -6,8 +6,8 @@ import ( "github.com/zitadel/logging" "go.opentelemetry.io/otel/attribute" + "github.com/zitadel/zitadel/backend/v3/instrumentation/metrics" "github.com/zitadel/zitadel/internal/notification/channels" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) func countMessages(ctx context.Context, channel channels.NotificationChannel, successMetricName, errorMetricName string) channels.NotificationChannel { diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 818524fd2..8ea831e6b 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -11,9 +11,9 @@ import ( "github.com/riverqueue/rivercontrib/otelriver" "github.com/robfig/cron/v3" "github.com/zitadel/logging" + "go.opentelemetry.io/otel" "github.com/zitadel/zitadel/internal/database" - "github.com/zitadel/zitadel/internal/telemetry/metrics" ) // Queue abstracts the underlying queuing library @@ -32,7 +32,7 @@ type Config struct { func NewQueue(config *Config) (_ *Queue, err error) { middleware := []rivertype.Middleware{otelriver.NewMiddleware(&otelriver.MiddlewareConfig{ - MeterProvider: metrics.GetMetricsProvider(), + MeterProvider: otel.GetMeterProvider(), DurationUnit: "ms", })} return &Queue{ diff --git a/internal/telemetry/http_handler.go b/internal/telemetry/http_handler.go deleted file mode 100644 index ff70978a5..000000000 --- a/internal/telemetry/http_handler.go +++ /dev/null @@ -1,34 +0,0 @@ -package telemetry - -import ( - "net/http" - "strings" - - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - - "github.com/zitadel/zitadel/internal/telemetry/metrics" -) - -func shouldNotIgnore(endpoints ...string) func(r *http.Request) bool { - return func(r *http.Request) bool { - for _, endpoint := range endpoints { - if strings.HasPrefix(r.URL.RequestURI(), endpoint) { - return false - } - } - return true - } -} - -func TelemetryHandler(handler http.Handler, ignoredEndpoints ...string) http.Handler { - return otelhttp.NewHandler(handler, - "zitadel", - otelhttp.WithFilter(shouldNotIgnore(ignoredEndpoints...)), - otelhttp.WithPublicEndpoint(), - otelhttp.WithSpanNameFormatter(spanNameFormatter), - otelhttp.WithMeterProvider(metrics.GetMetricsProvider())) -} - -func spanNameFormatter(_ string, r *http.Request) string { - return strings.Split(r.RequestURI, "?")[0] -} diff --git a/internal/telemetry/metrics/config/config.go b/internal/telemetry/metrics/config/config.go deleted file mode 100644 index 9e9ebec52..000000000 --- a/internal/telemetry/metrics/config/config.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "github.com/zitadel/zitadel/internal/telemetry/metrics" - "github.com/zitadel/zitadel/internal/telemetry/metrics/otel" - "github.com/zitadel/zitadel/internal/zerrors" -) - -type Config struct { - Type string - Config map[string]interface{} `mapstructure:",remain"` -} - -var meter = map[string]func(map[string]interface{}) error{ - "otel": otel.NewTracerFromConfig, - "none": registerNoopMetrics, - "": registerNoopMetrics, -} - -func (c *Config) NewMeter() error { - // When using start-from-init or start-from-setup the metric provider - // was already set in the setup phase and the start phase must not overwrite it. - if metrics.M != nil { - return nil - } - t, ok := meter[c.Type] - if !ok { - return zerrors.ThrowInternalf(nil, "METER-Dfqsx", "config type %s not supported", c.Type) - } - - return t(c.Config) -} - -func registerNoopMetrics(rawConfig map[string]interface{}) (err error) { - metrics.M = &metrics.NoopMetrics{} - return nil -} diff --git a/internal/telemetry/metrics/noop.go b/internal/telemetry/metrics/noop.go deleted file mode 100644 index 954db1d2b..000000000 --- a/internal/telemetry/metrics/noop.go +++ /dev/null @@ -1,45 +0,0 @@ -package metrics - -import ( - "context" - "net/http" - - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" -) - -type NoopMetrics struct{} - -var _ Metrics = new(NoopMetrics) - -func (n *NoopMetrics) GetExporter() http.Handler { - return nil -} - -func (n *NoopMetrics) GetMetricsProvider() metric.MeterProvider { - return nil -} - -func (n *NoopMetrics) RegisterCounter(name, description string) error { - return nil -} - -func (n *NoopMetrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { - return nil -} - -func (n *NoopMetrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { - return nil -} - -func (n *NoopMetrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { - return nil -} - -func (n *NoopMetrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { - return nil -} - -func (n *NoopMetrics) RegisterHistogram(name, description, unit string, buckets []float64) error { - return nil -} diff --git a/internal/telemetry/metrics/otel/config.go b/internal/telemetry/metrics/otel/config.go deleted file mode 100644 index b811e36ce..000000000 --- a/internal/telemetry/metrics/otel/config.go +++ /dev/null @@ -1,20 +0,0 @@ -package otel - -import ( - "github.com/zitadel/zitadel/internal/telemetry/metrics" -) - -type Config struct { - MeterName string -} - -func NewTracerFromConfig(rawConfig map[string]interface{}) (err error) { - c := new(Config) - c.MeterName, _ = rawConfig["metername"].(string) - return c.NewMetrics() -} - -func (c *Config) NewMetrics() (err error) { - metrics.M, err = NewMetrics(c.MeterName) - return err -} diff --git a/internal/telemetry/metrics/otel/open_telemetry.go b/internal/telemetry/metrics/otel/open_telemetry.go deleted file mode 100644 index ca1b3f89a..000000000 --- a/internal/telemetry/metrics/otel/open_telemetry.go +++ /dev/null @@ -1,163 +0,0 @@ -package otel - -import ( - "context" - "net/http" - "sync" - - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/prometheus" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/sdk/instrumentation" - sdk_metric "go.opentelemetry.io/otel/sdk/metric" - - "github.com/zitadel/zitadel/internal/telemetry/metrics" - otel_resource "github.com/zitadel/zitadel/internal/telemetry/otel" - "github.com/zitadel/zitadel/internal/zerrors" -) - -type Metrics struct { - Provider metric.MeterProvider - Meter metric.Meter - Counters sync.Map - UpDownSumObserver sync.Map - ValueObservers sync.Map - Histograms sync.Map -} - -func NewMetrics(meterName string) (metrics.Metrics, error) { - resource, err := otel_resource.ResourceWithService("ZITADEL") - if err != nil { - return nil, err - } - exporter, err := prometheus.New(prometheus.WithoutScopeInfo()) - if err != nil { - return &Metrics{}, err - } - // create a view to filter out unwanted attributes - view := sdk_metric.NewView( - sdk_metric.Instrument{ - Scope: instrumentation.Scope{Name: otelhttp.ScopeName}, - }, - sdk_metric.Stream{ - AttributeFilter: attribute.NewAllowKeysFilter("http.method", "http.status_code", "http.target"), - }, - ) - meterProvider := sdk_metric.NewMeterProvider( - sdk_metric.WithReader(exporter), - sdk_metric.WithResource(resource), - sdk_metric.WithView(view), - ) - return &Metrics{ - Provider: meterProvider, - Meter: meterProvider.Meter(meterName), - }, nil -} - -func (m *Metrics) GetExporter() http.Handler { - return promhttp.Handler() -} - -func (m *Metrics) GetMetricsProvider() metric.MeterProvider { - return m.Provider -} - -func (m *Metrics) RegisterCounter(name, description string) error { - if _, exists := m.Counters.Load(name); exists { - return nil - } - counter, err := m.Meter.Int64Counter(name, metric.WithDescription(description)) - if err != nil { - return err - } - m.Counters.Store(name, counter) - return nil -} - -func (m *Metrics) AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error { - counter, exists := m.Counters.Load(name) - if !exists { - return zerrors.ThrowNotFound(nil, "METER-4u8fs", "Errors.Metrics.Counter.NotFound") - } - counter.(metric.Int64Counter).Add(ctx, value, MapToAddOption(labels)...) - return nil -} - -func (m *Metrics) AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error { - histogram, exists := m.Histograms.Load(name) - if !exists { - return zerrors.ThrowNotFound(nil, "METER-5wwb1", "Errors.Metrics.Histogram.NotFound") - } - histogram.(metric.Float64Histogram).Record(ctx, value, MapToRecordOption(labels)...) - return nil -} - -func (m *Metrics) RegisterHistogram(name, description, unit string, buckets []float64) error { - if _, exists := m.Histograms.Load(name); exists { - return nil - } - - histogram, err := m.Meter.Float64Histogram(name, - metric.WithDescription(description), - metric.WithUnit(unit), - metric.WithExplicitBucketBoundaries(buckets...), - ) - if err != nil { - return err - } - - m.Histograms.Store(name, histogram) - return nil -} - -func (m *Metrics) RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error { - if _, exists := m.UpDownSumObserver.Load(name); exists { - return nil - } - - counter, err := m.Meter.Int64ObservableUpDownCounter(name, metric.WithInt64Callback(callbackFunc), metric.WithDescription(description)) - if err != nil { - return err - } - - m.UpDownSumObserver.Store(name, counter) - return nil -} - -func (m *Metrics) RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error { - if _, exists := m.UpDownSumObserver.Load(name); exists { - return nil - } - - gauge, err := m.Meter.Int64ObservableGauge(name, metric.WithInt64Callback(callbackFunc), metric.WithDescription(description)) - if err != nil { - return err - } - - m.UpDownSumObserver.Store(name, gauge) - return nil -} - -func MapToAddOption(labels map[string]attribute.Value) []metric.AddOption { - return []metric.AddOption{metric.WithAttributes(labelsToAttributes(labels)...)} -} - -func MapToRecordOption(labels map[string]attribute.Value) []metric.RecordOption { - return []metric.RecordOption{metric.WithAttributes(labelsToAttributes(labels)...)} -} - -func labelsToAttributes(labels map[string]attribute.Value) []attribute.KeyValue { - if labels == nil { - return nil - } - attributes := make([]attribute.KeyValue, 0, len(labels)) - for key, value := range labels { - attributes = append(attributes, attribute.KeyValue{ - Key: attribute.Key(key), - Value: value, - }) - } - return attributes -} diff --git a/internal/telemetry/otel/resource.go b/internal/telemetry/otel/resource.go deleted file mode 100644 index c43315e24..000000000 --- a/internal/telemetry/otel/resource.go +++ /dev/null @@ -1,22 +0,0 @@ -package otel - -import ( - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - - "github.com/zitadel/zitadel/cmd/build" -) - -func ResourceWithService(serviceName string) (*resource.Resource, error) { - attributes := []attribute.KeyValue{ - semconv.ServiceNameKey.String(serviceName), - } - if build.Version() != "" { - attributes = append(attributes, semconv.ServiceVersionKey.String(build.Version())) - } - return resource.Merge( - resource.Default(), - resource.NewWithAttributes("", attributes...), - ) -} diff --git a/internal/telemetry/profiler/config/config.go b/internal/telemetry/profiler/config/config.go deleted file mode 100644 index 5ca0023a8..000000000 --- a/internal/telemetry/profiler/config/config.go +++ /dev/null @@ -1,30 +0,0 @@ -package config - -import ( - "github.com/zitadel/zitadel/internal/telemetry/profiler/google" - "github.com/zitadel/zitadel/internal/zerrors" -) - -type Config struct { - Type string - Config map[string]interface{} `mapstructure:",remain"` -} - -var profiler = map[string]func(map[string]interface{}) error{ - "google": google.NewProfiler, - "none": NoProfiler, - "": NoProfiler, -} - -func (c *Config) NewProfiler() error { - t, ok := profiler[c.Type] - if !ok { - return zerrors.ThrowInternalf(nil, "PROFI-Dfqsx", "config type %s not supported", c.Type) - } - - return t(c.Config) -} - -func NoProfiler(_ map[string]interface{}) error { - return nil -} diff --git a/internal/telemetry/profiler/google/profiler.go b/internal/telemetry/profiler/google/profiler.go deleted file mode 100644 index a1e1855d5..000000000 --- a/internal/telemetry/profiler/google/profiler.go +++ /dev/null @@ -1,26 +0,0 @@ -package google - -import ( - "cloud.google.com/go/profiler" - - "github.com/zitadel/zitadel/cmd/build" -) - -type Config struct { - ProjectID string -} - -func NewProfiler(rawConfig map[string]interface{}) (err error) { - c := new(Config) - c.ProjectID, _ = rawConfig["projectid"].(string) - return c.NewProfiler() -} - -func (c *Config) NewProfiler() (err error) { - cfg := profiler.Config{ - Service: "zitadel", - ServiceVersion: build.Version(), - ProjectID: c.ProjectID, - } - return profiler.Start(cfg) -} diff --git a/internal/telemetry/tracing/caller.go b/internal/telemetry/tracing/caller.go deleted file mode 100644 index a7612cab8..000000000 --- a/internal/telemetry/tracing/caller.go +++ /dev/null @@ -1,22 +0,0 @@ -package tracing - -import ( - "runtime" - - "github.com/zitadel/logging" -) - -func GetCaller() string { - fpcs := make([]uintptr, 1) - n := runtime.Callers(3, fpcs) - if n == 0 { - logging.WithFields("logID", "TRACE-rWjfC").Debug("no caller") - return "" - } - caller := runtime.FuncForPC(fpcs[0] - 1) - if caller == nil { - logging.WithFields("logID", "TRACE-25POw").Debug("caller was nil") - return "" - } - return caller.Name() -} diff --git a/internal/telemetry/tracing/config/config.go b/internal/telemetry/tracing/config/config.go deleted file mode 100644 index cb44c6b65..000000000 --- a/internal/telemetry/tracing/config/config.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -import ( - "github.com/zitadel/zitadel/internal/telemetry/tracing/google" - "github.com/zitadel/zitadel/internal/telemetry/tracing/log" - "github.com/zitadel/zitadel/internal/telemetry/tracing/otel" - "github.com/zitadel/zitadel/internal/zerrors" -) - -type Config struct { - Type string - Config map[string]interface{} `mapstructure:",remain"` -} - -func (c *Config) NewTracer() error { - t, ok := tracer[c.Type] - if !ok { - return zerrors.ThrowInternalf(nil, "TRACE-dsbjh", "config type %s not supported", c.Type) - } - - return t(c.Config) -} - -var tracer = map[string]func(map[string]interface{}) error{ - "otel": otel.NewTracerFromConfig, - "google": google.NewTracer, - "log": log.NewTracer, - "none": NoTracer, - "": NoTracer, -} - -func NoTracer(_ map[string]interface{}) error { - return nil -} diff --git a/internal/telemetry/tracing/google/google_tracer.go b/internal/telemetry/tracing/google/google_tracer.go deleted file mode 100644 index 5e8008bac..000000000 --- a/internal/telemetry/tracing/google/google_tracer.go +++ /dev/null @@ -1,41 +0,0 @@ -package google - -import ( - texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace" - sdk_trace "go.opentelemetry.io/otel/sdk/trace" - - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/telemetry/tracing/otel" -) - -type Config struct { - ProjectID string - Fraction float64 - ServiceName string -} - -func NewTracer(rawConfig map[string]interface{}) (err error) { - c := new(Config) - c.ProjectID, _ = rawConfig["projectid"].(string) - c.ServiceName, _ = rawConfig["servicename"].(string) - c.Fraction, err = otel.FractionFromConfig(rawConfig["fraction"]) - if err != nil { - return err - } - return c.NewTracer() -} - -type Tracer struct { - otel.Tracer -} - -func (c *Config) NewTracer() error { - sampler := otel.NewSampler(sdk_trace.TraceIDRatioBased(c.Fraction)) - exporter, err := texporter.New(texporter.WithProjectID(c.ProjectID)) - if err != nil { - return err - } - - tracing.T, err = otel.NewTracer(sampler, exporter, c.ServiceName) - return err -} diff --git a/internal/telemetry/tracing/log/config.go b/internal/telemetry/tracing/log/config.go deleted file mode 100644 index 9713c6462..000000000 --- a/internal/telemetry/tracing/log/config.go +++ /dev/null @@ -1,39 +0,0 @@ -package log - -import ( - stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" - sdk_trace "go.opentelemetry.io/otel/sdk/trace" - - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/telemetry/tracing/otel" -) - -type Config struct { - Fraction float64 - ServiceName string -} - -func NewTracer(rawConfig map[string]interface{}) (err error) { - c := new(Config) - c.Fraction, err = otel.FractionFromConfig(rawConfig["fraction"]) - c.ServiceName, _ = rawConfig["servicename"].(string) - if err != nil { - return err - } - return c.NewTracer() -} - -type Tracer struct { - otel.Tracer -} - -func (c *Config) NewTracer() error { - sampler := otel.NewSampler(sdk_trace.TraceIDRatioBased(c.Fraction)) - exporter, err := stdout.New(stdout.WithPrettyPrint()) - if err != nil { - return err - } - - tracing.T, err = otel.NewTracer(sampler, exporter, c.ServiceName) - return err -} diff --git a/internal/telemetry/tracing/otel/config.go b/internal/telemetry/tracing/otel/config.go deleted file mode 100644 index ee9a7b4aa..000000000 --- a/internal/telemetry/tracing/otel/config.go +++ /dev/null @@ -1,77 +0,0 @@ -package otel - -import ( - "context" - "strconv" - - otlpgrpc "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - sdk_trace "go.opentelemetry.io/otel/sdk/trace" - api_trace "go.opentelemetry.io/otel/trace" - - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" -) - -type Config struct { - Fraction float64 - Endpoint string - ServiceName string -} - -func NewTracerFromConfig(rawConfig map[string]interface{}) (err error) { - c := new(Config) - c.Endpoint, _ = rawConfig["endpoint"].(string) - c.ServiceName, _ = rawConfig["servicename"].(string) - c.Fraction, err = FractionFromConfig(rawConfig["fraction"]) - if err != nil { - return err - } - return c.NewTracer() -} - -func FractionFromConfig(i interface{}) (float64, error) { - if i == nil { - return 0, nil - } - switch fraction := i.(type) { - case float64: - return fraction, nil - case int: - return float64(fraction), nil - case string: - f, err := strconv.ParseFloat(fraction, 64) - if err != nil { - return 0, zerrors.ThrowInternal(err, "OTEL-SAfe1", "could not map fraction") - } - return f, nil - default: - return 0, zerrors.ThrowInternal(nil, "OTEL-Dd2s", "could not map fraction, unknown type") - } -} - -func (c *Config) NewTracer() error { - sampler := NewSampler(sdk_trace.TraceIDRatioBased(c.Fraction)) - exporter, err := otlpgrpc.New(context.Background(), otlpgrpc.WithEndpoint(c.Endpoint), otlpgrpc.WithInsecure()) - if err != nil { - return err - } - - tracing.T, err = NewTracer(sampler, exporter, c.ServiceName) - return err -} - -// NewSampler returns a sampler decorator which behaves differently, -// based on the parent of the span. If the span has no parent and is of kind server, -// the decorated sampler is used to make sampling decision. -// If the span has a parent, depending on whether the parent is remote and whether it -// is sampled, one of the following samplers will apply: -// - remote parent sampled -> always sample -// - remote parent not sampled -> sample based on the decorated sampler (fraction based) -// - local parent sampled -> always sample -// - local parent not sampled -> never sample -func NewSampler(sampler sdk_trace.Sampler) sdk_trace.Sampler { - return sdk_trace.ParentBased( - tracing.SpanKindBased(sampler, api_trace.SpanKindServer), - sdk_trace.WithRemoteParentNotSampled(sampler), - ) -} diff --git a/internal/telemetry/tracing/otel/open_telemetry.go b/internal/telemetry/tracing/otel/open_telemetry.go deleted file mode 100644 index 1318c2efe..000000000 --- a/internal/telemetry/tracing/otel/open_telemetry.go +++ /dev/null @@ -1,77 +0,0 @@ -package otel - -import ( - "context" - "net/http" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - sdk_trace "go.opentelemetry.io/otel/sdk/trace" - api_trace "go.opentelemetry.io/otel/trace" - - otel_resource "github.com/zitadel/zitadel/internal/telemetry/otel" - "github.com/zitadel/zitadel/internal/telemetry/tracing" -) - -type Tracer struct { - Exporter api_trace.Tracer - sampler sdk_trace.Sampler -} - -func NewTracer(sampler sdk_trace.Sampler, exporter sdk_trace.SpanExporter, serviceName string) (*Tracer, error) { - resource, err := otel_resource.ResourceWithService(serviceName) - if err != nil { - return nil, err - } - - tp := sdk_trace.NewTracerProvider( - sdk_trace.WithSampler(sampler), - sdk_trace.WithBatcher(exporter), - sdk_trace.WithResource(resource), - ) - - otel.SetTracerProvider(tp) - tc := propagation.TraceContext{} - otel.SetTextMapPropagator(tc) - - return &Tracer{Exporter: tp.Tracer(""), sampler: sampler}, nil -} - -func (t *Tracer) Sampler() sdk_trace.Sampler { - return t.sampler -} - -func (t *Tracer) NewServerInterceptorSpan(ctx context.Context, name string) (context.Context, *tracing.Span) { - return t.newSpanFromName(ctx, name, api_trace.WithSpanKind(api_trace.SpanKindServer)) -} - -func (t *Tracer) NewServerSpan(ctx context.Context, caller string) (context.Context, *tracing.Span) { - return t.newSpan(ctx, caller, api_trace.WithSpanKind(api_trace.SpanKindServer)) -} - -func (t *Tracer) NewClientInterceptorSpan(ctx context.Context, name string) (context.Context, *tracing.Span) { - return t.newSpanFromName(ctx, name, api_trace.WithSpanKind(api_trace.SpanKindClient)) -} - -func (t *Tracer) NewClientSpan(ctx context.Context, caller string) (context.Context, *tracing.Span) { - return t.newSpan(ctx, caller, api_trace.WithSpanKind(api_trace.SpanKindClient)) -} - -func (t *Tracer) NewSpan(ctx context.Context, caller string) (context.Context, *tracing.Span) { - return t.newSpan(ctx, caller) -} - -func (t *Tracer) newSpan(ctx context.Context, caller string, options ...api_trace.SpanStartOption) (context.Context, *tracing.Span) { - return t.newSpanFromName(ctx, caller, options...) -} - -func (t *Tracer) newSpanFromName(ctx context.Context, name string, options ...api_trace.SpanStartOption) (context.Context, *tracing.Span) { - ctx, span := t.Exporter.Start(ctx, name, options...) - return ctx, tracing.CreateSpan(span) -} - -func (t *Tracer) NewSpanHTTP(r *http.Request, caller string) (*http.Request, *tracing.Span) { - ctx, span := t.NewSpan(r.Context(), caller) - r = r.WithContext(ctx) - return r, span -} diff --git a/internal/telemetry/tracing/sampler.go b/internal/telemetry/tracing/sampler.go deleted file mode 100644 index 4ea53980b..000000000 --- a/internal/telemetry/tracing/sampler.go +++ /dev/null @@ -1,46 +0,0 @@ -package tracing - -import ( - "fmt" - "slices" - - sdk_trace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/trace" -) - -type spanKindSampler struct { - sampler sdk_trace.Sampler - kinds []trace.SpanKind -} - -// ShouldSample implements the [sdk_trace.Sampler] interface. -// It will not sample any spans which do not match the configured span kinds. -// For spans which do match, the decorated sampler is used to make the sampling decision. -func (sk spanKindSampler) ShouldSample(p sdk_trace.SamplingParameters) sdk_trace.SamplingResult { - psc := trace.SpanContextFromContext(p.ParentContext) - if !slices.Contains(sk.kinds, p.Kind) { - return sdk_trace.SamplingResult{ - Decision: sdk_trace.Drop, - Tracestate: psc.TraceState(), - } - } - s := sk.sampler.ShouldSample(p) - return s -} - -func (sk spanKindSampler) Description() string { - return fmt.Sprintf("SpanKindBased{sampler:%s,kinds:%v}", - sk.sampler.Description(), - sk.kinds, - ) -} - -// SpanKindBased returns a sampler decorator which behaves differently, based on the kind of the span. -// If the span kind does not match one of the configured kinds, it will not be sampled. -// If the span kind matches, the decorated sampler is used to make sampling decision. -func SpanKindBased(sampler sdk_trace.Sampler, kinds ...trace.SpanKind) sdk_trace.Sampler { - return spanKindSampler{ - sampler: sampler, - kinds: kinds, - } -} diff --git a/internal/telemetry/tracing/span.go b/internal/telemetry/tracing/span.go deleted file mode 100644 index 9a6b6385e..000000000 --- a/internal/telemetry/tracing/span.go +++ /dev/null @@ -1,46 +0,0 @@ -package tracing - -import ( - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/zitadel/zitadel/internal/api/grpc/gerrors" -) - -type Span struct { - span trace.Span - opts []trace.SpanEndOption -} - -func CreateSpan(span trace.Span) *Span { - return &Span{span: span, opts: []trace.SpanEndOption{}} -} - -func (s *Span) End() { - if s.span == nil { - return - } - - s.span.End(s.opts...) -} - -func (s *Span) EndWithError(err error) { - s.SetStatusByError(err) - s.End() -} - -func (s *Span) SetStatusByError(err error) { - if s.span == nil { - return - } - if err != nil { - // trace.WithErrorStatus(codes.Error) - s.span.RecordError(err) - s.span.SetAttributes( - attribute.KeyValue{}, - ) - } - - code, msg, id, _ := gerrors.ExtractZITADELError(err) - s.span.SetAttributes(attribute.Int("grpc_code", int(code)), attribute.String("grpc_msg", msg), attribute.String("error_id", id)) -} diff --git a/internal/telemetry/tracing/tracing.go b/internal/telemetry/tracing/tracing.go index 31bbb050b..188a7cbcd 100644 --- a/internal/telemetry/tracing/tracing.go +++ b/internal/telemetry/tracing/tracing.go @@ -1,81 +1,43 @@ +// Package tracing acts as a shim to [tracing], +// so we can migrate the tracing package without changing all imports at once. package tracing import ( "context" "net/http" - sdk_trace "go.opentelemetry.io/otel/sdk/trace" - api_trace "go.opentelemetry.io/otel/trace" + "github.com/zitadel/zitadel/backend/v3/instrumentation" + "github.com/zitadel/zitadel/backend/v3/instrumentation/tracing" ) -type Tracer interface { - NewSpan(ctx context.Context, caller string) (context.Context, *Span) - NewClientSpan(ctx context.Context, caller string) (context.Context, *Span) - NewServerSpan(ctx context.Context, caller string) (context.Context, *Span) - NewClientInterceptorSpan(ctx context.Context, name string) (context.Context, *Span) - NewServerInterceptorSpan(ctx context.Context, name string) (context.Context, *Span) - NewSpanHTTP(r *http.Request, caller string) (*http.Request, *Span) - Sampler() sdk_trace.Sampler +func NewSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return tracing.GlobalTracer().NewSpan(ctx, instrumentation.GetCallingFunc(1)) } -var T Tracer - -func Sampler() sdk_trace.Sampler { - if T == nil { - return sdk_trace.NeverSample() - } - return T.Sampler() +func NewNamedSpan(ctx context.Context, name string) (context.Context, *instrumentation.Span) { + return tracing.NewNamedSpan(ctx, name) } -func NewSpan(ctx context.Context) (context.Context, *Span) { - if T == nil { - return ctx, CreateSpan(nil) - } - return T.NewSpan(ctx, GetCaller()) +func NewClientSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return tracing.GlobalTracer().NewClientSpan(ctx, instrumentation.GetCallingFunc(1)) } -func NewNamedSpan(ctx context.Context, name string) (context.Context, *Span) { - if T == nil { - return ctx, CreateSpan(nil) - } - return T.NewSpan(ctx, name) +func NewServerSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return tracing.GlobalTracer().NewServerSpan(ctx, instrumentation.GetCallingFunc(1)) } -func NewClientSpan(ctx context.Context) (context.Context, *Span) { - if T == nil { - return ctx, CreateSpan(nil) - } - return T.NewClientSpan(ctx, GetCaller()) +func NewClientInterceptorSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return tracing.GlobalTracer().NewClientInterceptorSpan(ctx, instrumentation.GetCallingFunc(1)) } -func NewServerSpan(ctx context.Context) (context.Context, *Span) { - if T == nil { - return ctx, CreateSpan(nil) - } - return T.NewServerSpan(ctx, GetCaller()) +func NewServerInterceptorSpan(ctx context.Context) (context.Context, *instrumentation.Span) { + return tracing.GlobalTracer().NewServerInterceptorSpan(ctx, instrumentation.GetCallingFunc(1)) } -func NewClientInterceptorSpan(ctx context.Context) (context.Context, *Span) { - if T == nil { - return ctx, CreateSpan(nil) - } - return T.NewClientInterceptorSpan(ctx, GetCaller()) -} - -func NewServerInterceptorSpan(ctx context.Context) (context.Context, *Span) { - if T == nil { - return ctx, CreateSpan(nil) - } - return T.NewServerInterceptorSpan(ctx, GetCaller()) -} - -func NewSpanHTTP(r *http.Request) (*http.Request, *Span) { - if T == nil { - return r, CreateSpan(nil) - } - return T.NewSpanHTTP(r, GetCaller()) +func NewSpanHTTP(r *http.Request) (*http.Request, *instrumentation.Span) { + return tracing.GlobalTracer().NewSpanHTTP(r, instrumentation.GetCallingFunc(1)) } func TraceIDFromCtx(ctx context.Context) string { - return api_trace.SpanFromContext(ctx).SpanContext().TraceID().String() + return tracing.TraceIDFromCtx(ctx) } diff --git a/internal/zerrors/already_exists.go b/internal/zerrors/already_exists.go index bdc544979..f2ed7352c 100644 --- a/internal/zerrors/already_exists.go +++ b/internal/zerrors/already_exists.go @@ -2,43 +2,15 @@ package zerrors import "fmt" -var ( - _ AlreadyExists = (*AlreadyExistsError)(nil) - _ Error = (*AlreadyExistsError)(nil) -) - -type AlreadyExists interface { - error - IsAlreadyExists() -} - -type AlreadyExistsError struct { - *ZitadelError -} - func ThrowAlreadyExists(parent error, id, message string) error { - return &AlreadyExistsError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindAlreadyExists, parent, id, message) } -func ThrowAlreadyExistsf(parent error, id, format string, a ...interface{}) error { - return &AlreadyExistsError{CreateZitadelError(parent, id, fmt.Sprintf(format, a...))} -} - -func (err *AlreadyExistsError) IsAlreadyExists() {} - -func (err *AlreadyExistsError) Is(target error) bool { - t, ok := target.(*AlreadyExistsError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) +func ThrowAlreadyExistsf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindAlreadyExists, parent, id, fmt.Sprintf(format, a...)) } func IsErrorAlreadyExists(err error) bool { - _, ok := err.(AlreadyExists) - return ok -} - -func (err *AlreadyExistsError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindAlreadyExists } diff --git a/internal/zerrors/already_exists_test.go b/internal/zerrors/already_exists_test.go index 6039c3788..6e86594d5 100644 --- a/internal/zerrors/already_exists_test.go +++ b/internal/zerrors/already_exists_test.go @@ -9,25 +9,58 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestAlreadyExistsError(t *testing.T) { - var alreadyExistsError interface{} = new(zerrors.AlreadyExistsError) - _, ok := alreadyExistsError.(zerrors.AlreadyExists) - assert.True(t, ok) -} +func TestAlreadyExists(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowAlreadyExistsf(t *testing.T) { - err := zerrors.ThrowAlreadyExistsf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.AlreadyExistsError) - assert.True(t, ok) -} + t.Run("ThrowAlreadyExists", func(t *testing.T) { + err := zerrors.ThrowAlreadyExists(parentErr, id, message) + assert.NotNil(t, err) -func TestIsErrorAlreadyExists(t *testing.T) { - err := zerrors.ThrowAlreadyExists(nil, "id", "msg") - ok := zerrors.IsErrorAlreadyExists(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindAlreadyExists, zitadelErr.Kind) - err = errors.New("Already Exists!") - ok = zerrors.IsErrorAlreadyExists(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowAlreadyExistsf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowAlreadyExistsf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindAlreadyExists, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsErrorAlreadyExists", func(t *testing.T) { + err := zerrors.ThrowAlreadyExists(parentErr, id, message) + isAlreadyExists := zerrors.IsErrorAlreadyExists(err) + assert.True(t, isAlreadyExists) + + otherErr := errors.New("some other error") + isAlreadyExists = zerrors.IsErrorAlreadyExists(otherErr) + assert.False(t, isAlreadyExists) + }) } diff --git a/internal/zerrors/deadline_exceeded.go b/internal/zerrors/deadline_exceeded.go index b19aac733..312dcba52 100644 --- a/internal/zerrors/deadline_exceeded.go +++ b/internal/zerrors/deadline_exceeded.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ DeadlineExceeded = (*DeadlineExceededError)(nil) - _ Error = (*DeadlineExceededError)(nil) -) - -type DeadlineExceeded interface { - error - IsDeadlineExceeded() -} - -type DeadlineExceededError struct { - *ZitadelError -} +import "fmt" func ThrowDeadlineExceeded(parent error, id, message string) error { - return &DeadlineExceededError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindDeadlineExceeded, parent, id, message) } -func ThrowDeadlineExceededf(parent error, id, format string, a ...interface{}) error { - return ThrowDeadlineExceeded(parent, id, fmt.Sprintf(format, a...)) +func ThrowDeadlineExceededf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindDeadlineExceeded, parent, id, fmt.Sprintf(format, a...)) } -func (err *DeadlineExceededError) IsDeadlineExceeded() {} - func IsDeadlineExceeded(err error) bool { - _, ok := err.(DeadlineExceeded) - return ok -} - -func (err *DeadlineExceededError) Is(target error) bool { - t, ok := target.(*DeadlineExceededError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *DeadlineExceededError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindDeadlineExceeded } diff --git a/internal/zerrors/deadline_exceeded_test.go b/internal/zerrors/deadline_exceeded_test.go index 743add716..3db9aa700 100644 --- a/internal/zerrors/deadline_exceeded_test.go +++ b/internal/zerrors/deadline_exceeded_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestDeadlineExceededError(t *testing.T) { - var err interface{} = new(zerrors.DeadlineExceededError) - _, ok := err.(zerrors.DeadlineExceeded) - assert.True(t, ok) -} +func TestDeadlineExceeded(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowDeadlineExceededf(t *testing.T) { - err := zerrors.ThrowDeadlineExceededf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.DeadlineExceededError) - assert.True(t, ok) -} + t.Run("ThrowDeadlineExceeded", func(t *testing.T) { + err := zerrors.ThrowDeadlineExceeded(parentErr, id, message) + assert.NotNil(t, err) -func TestIsDeadlineExceeded(t *testing.T) { - err := zerrors.ThrowDeadlineExceeded(nil, "id", "msg") - ok := zerrors.IsDeadlineExceeded(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindDeadlineExceeded, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsDeadlineExceeded(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowDeadlineExceededf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowDeadlineExceededf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindDeadlineExceeded, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsDeadlineExceeded", func(t *testing.T) { + err := zerrors.ThrowDeadlineExceeded(parentErr, id, message) + isDeadlineExceeded := zerrors.IsDeadlineExceeded(err) + assert.True(t, isDeadlineExceeded) + }) } diff --git a/internal/zerrors/error.go b/internal/zerrors/error.go deleted file mode 100644 index a2840063c..000000000 --- a/internal/zerrors/error.go +++ /dev/null @@ -1,19 +0,0 @@ -package zerrors - -import ( - "strings" -) - -// Error is a stdlib error extension. -// It contains parameters to identify errors through all application layers -type Error interface { - GetParent() error - GetMessage() string - SetMessage(string) - GetID() string -} - -// Contains compares the error message with needle -func Contains(err error, needle string) bool { - return err != nil && strings.Contains(err.Error(), needle) -} diff --git a/internal/zerrors/error_test.go b/internal/zerrors/error_test.go deleted file mode 100644 index 23ef37030..000000000 --- a/internal/zerrors/error_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package zerrors_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/zitadel/zitadel/internal/zerrors" -) - -func TestContains(t *testing.T) { - err := errors.New("hello world") - world := zerrors.Contains(err, "hello") - assert.True(t, world) - - mars := zerrors.Contains(err, "mars") - assert.False(t, mars) -} diff --git a/internal/zerrors/generate/error.go.tmpl b/internal/zerrors/generate/error.go.tmpl deleted file mode 100644 index 7513451c0..000000000 --- a/internal/zerrors/generate/error.go.tmpl +++ /dev/null @@ -1,34 +0,0 @@ -package errors - -import ( - "fmt" -) - -var ( - _ {{.ErrorName}} = (*{{.ErrorName}}Error)(nil) - _ Error = (*{{.ErrorName}}Error)(nil) -) - -type {{.ErrorName}} interface { - error - Is{{.ErrorName}}() -} - -type {{.ErrorName}}Error struct { - *CaosError -} - -func Throw{{.ErrorName}}(parent error, id, message string) error { - return &{{.ErrorName}}Error{CreateCaosError(parent, id, message)} -} - -func Throw{{.ErrorName}}f(parent error, id, format string, a ...interface{}) error { - return Throw{{.ErrorName}}(parent, id, fmt.Sprintf(format, a...)) -} - -func (err *{{.ErrorName}}Error) Is{{.ErrorName}}() {} - -func Is{{.ErrorName}}(err error) bool { - _, ok := err.({{.ErrorName}}) - return ok -} diff --git a/internal/zerrors/generate/error_creator.go b/internal/zerrors/generate/error_creator.go deleted file mode 100644 index 8e16ad334..000000000 --- a/internal/zerrors/generate/error_creator.go +++ /dev/null @@ -1,75 +0,0 @@ -// go generate -package main - -import ( - "flag" - "fmt" - "html/template" - "log" - "os" - "strings" -) - -func main() { - errorName := readErrorName() - errorName = validateErrorName(errorName) - - data := &Data{ - ErrorName: errorName, - } - - errorFile := data.createFile("error.go.tmpl") - data.createTemplate("error.go.tmpl", errorFile) - if err := errorFile.Close(); err != nil { - log.Fatal(err) - } - - testFile := data.createFile("error_test.go.tmpl") - data.createTemplate("error_test.go.tmpl", testFile) - if err := testFile.Close(); err != nil { - log.Fatal(err) - } - - fmt.Print(` -!!!!! - Add status mapping in internal/api/grpc/zerrors.go -!!!!!`) -} - -type Data struct { - ErrorName string -} - -func (data *Data) createFile(tmplName string) *os.File { - filename := strings.Replace(tmplName, "error", strings.ToLower(data.ErrorName), 1) - filename = filename[:len(filename)-5] - filePath := fmt.Sprintf("../%s", filename) - file, err := os.Create(filePath) - if err != nil { - log.Fatalf("unable to create file (%s): %v", filePath, err) - } - return file -} - -func (data *Data) createTemplate(templateName string, file *os.File) { - tmpl := template.Must(template.New(templateName).ParseFiles(templateName)) - if err := tmpl.Execute(file, data); err != nil { - log.Fatal("unable to execute tmpl: ", err) - } -} - -func readErrorName() (errorName string) { - flag.StringVar(&errorName, "Name", "", "KeyType of the error (e.g. Internal)") - flag.Parse() - return errorName -} - -func validateErrorName(errorName string) string { - if errorName == "" { - log.Fatal("pass argument name") - } - if strings.Contains(errorName, " ") || strings.Contains(errorName, ".") { - log.Fatal("name cannot contain spaces or points") - } - return strings.Title(errorName) -} diff --git a/internal/zerrors/generate/error_interface.go.tmpl b/internal/zerrors/generate/error_interface.go.tmpl deleted file mode 100644 index d67979b96..000000000 --- a/internal/zerrors/generate/error_interface.go.tmpl +++ /dev/null @@ -1,5 +0,0 @@ - -type {{.ErrorName}} interface { - error - Is{{.ErrorName}}() -} diff --git a/internal/zerrors/generate/error_test.go.tmpl b/internal/zerrors/generate/error_test.go.tmpl deleted file mode 100644 index 6d967e1af..000000000 --- a/internal/zerrors/generate/error_test.go.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -package errors_test - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/zitadel/zitadel/internal/zerrors" -) - -func Test{{.ErrorName}}Error(t *testing.T) { - var err interface{} - err = new(zerrors.{{.ErrorName}}Error) - _, ok := err.(*zerrors.{{.ErrorName}}) - assert.True(t, ok) -} - -func TestThrow{{.ErrorName}}f(t *testing.T) { - err := zerrors.Throw{{.ErrorName}}f(nil, "id", "msg") - _, ok := err.(*zerrors.{{.ErrorName}}Error) - assert.True(t, ok) -} - -func TestIs{{.ErrorName}}(t *testing.T) { - err := zerrors.Throw{{.ErrorName}}(nil, "id", "msg") - ok := zerrors.Is{{.ErrorName}}(err) - assert.True(t, ok) - - err = errors.New("I am found!") - ok = zerrors.Is{{.ErrorName}}(err) - assert.False(t, ok) -} diff --git a/internal/zerrors/internal.go b/internal/zerrors/internal.go index 97c164f3e..3edc8165d 100644 --- a/internal/zerrors/internal.go +++ b/internal/zerrors/internal.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ Internal = (*InternalError)(nil) - _ Error = (*InternalError)(nil) -) - -type Internal interface { - error - IsInternal() -} - -type InternalError struct { - *ZitadelError -} +import "fmt" func ThrowInternal(parent error, id, message string) error { - return &InternalError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindInternal, parent, id, message) } -func ThrowInternalf(parent error, id, format string, a ...interface{}) error { - return ThrowInternal(parent, id, fmt.Sprintf(format, a...)) +func ThrowInternalf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindInternal, parent, id, fmt.Sprintf(format, a...)) } -func (err *InternalError) IsInternal() {} - func IsInternal(err error) bool { - _, ok := err.(Internal) - return ok -} - -func (err *InternalError) Is(target error) bool { - t, ok := target.(*InternalError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *InternalError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindInternal } diff --git a/internal/zerrors/internal_test.go b/internal/zerrors/internal_test.go index 3448afbaf..be51619bf 100644 --- a/internal/zerrors/internal_test.go +++ b/internal/zerrors/internal_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestInternalError(t *testing.T) { - var err interface{} = new(zerrors.InternalError) - _, ok := err.(zerrors.Internal) - assert.True(t, ok) -} +func TestInternal(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowInternalf(t *testing.T) { - err := zerrors.ThrowInternalf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.InternalError) - assert.True(t, ok) -} + t.Run("ThrowInternal", func(t *testing.T) { + err := zerrors.ThrowInternal(parentErr, id, message) + assert.NotNil(t, err) -func TestIsInternal(t *testing.T) { - err := zerrors.ThrowInternal(nil, "id", "msg") - ok := zerrors.IsInternal(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindInternal, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsInternal(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowInternalf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowInternalf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindInternal, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsInternal", func(t *testing.T) { + err := zerrors.ThrowInternal(parentErr, id, message) + isInternal := zerrors.IsInternal(err) + assert.True(t, isInternal) + }) } diff --git a/internal/zerrors/invalid_argument.go b/internal/zerrors/invalid_argument.go index e97519e66..6ee9a7da5 100644 --- a/internal/zerrors/invalid_argument.go +++ b/internal/zerrors/invalid_argument.go @@ -1,55 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ InvalidArgument = (*InvalidArgumentError)(nil) - _ Error = (*InvalidArgumentError)(nil) -) - -type InvalidArgument interface { - error - IsInvalidArgument() -} - -type InvalidArgumentError struct { - *ZitadelError -} +import "fmt" func ThrowInvalidArgument(parent error, id, message string) error { - return &InvalidArgumentError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindInvalidArgument, parent, id, message) } -func ThrowInvalidArgumentf(parent error, id, format string, a ...interface{}) error { - return ThrowInvalidArgument(parent, id, fmt.Sprintf(format, a...)) +func ThrowInvalidArgumentf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindInvalidArgument, parent, id, fmt.Sprintf(format, a...)) } -func (err *InvalidArgumentError) IsInvalidArgument() {} - func IsErrorInvalidArgument(err error) bool { - _, ok := err.(InvalidArgument) - return ok -} - -func (err *InvalidArgumentError) Is(target error) bool { - t, ok := target.(*InvalidArgumentError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *InvalidArgumentError) As(target any) bool { - targetErr, ok := target.(*InvalidArgumentError) - if !ok { - return false - } - *targetErr = *err - return true -} - -func (err *InvalidArgumentError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindInvalidArgument } diff --git a/internal/zerrors/invalid_argument_test.go b/internal/zerrors/invalid_argument_test.go index 7c1bd75ae..363c74372 100644 --- a/internal/zerrors/invalid_argument_test.go +++ b/internal/zerrors/invalid_argument_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestInvalidArgumentError(t *testing.T) { - var invalidArgumentError interface{} = new(zerrors.InvalidArgumentError) - _, ok := invalidArgumentError.(zerrors.InvalidArgument) - assert.True(t, ok) -} +func TestInvalidArgument(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowInvalidArgumentf(t *testing.T) { - err := zerrors.ThrowInvalidArgumentf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.InvalidArgumentError) - assert.True(t, ok) -} + t.Run("ThrowInvalidArgument", func(t *testing.T) { + err := zerrors.ThrowInvalidArgument(parentErr, id, message) + assert.NotNil(t, err) -func TestIsErrorInvalidArgument(t *testing.T) { - err := zerrors.ThrowInvalidArgument(nil, "id", "msg") - ok := zerrors.IsErrorInvalidArgument(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindInvalidArgument, zitadelErr.Kind) - err = errors.New("I am invalid!") - ok = zerrors.IsErrorInvalidArgument(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowInvalidArgumentf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowInvalidArgumentf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindInvalidArgument, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsInvalidArgument", func(t *testing.T) { + err := zerrors.ThrowInvalidArgument(parentErr, id, message) + isInvalidArgument := zerrors.IsErrorInvalidArgument(err) + assert.True(t, isInvalidArgument) + }) } diff --git a/internal/zerrors/kind_enumer.go b/internal/zerrors/kind_enumer.go new file mode 100644 index 000000000..a20f57818 --- /dev/null +++ b/internal/zerrors/kind_enumer.go @@ -0,0 +1,153 @@ +// Code generated by "enumer -type=Kind -trimprefix=Kind -json"; DO NOT EDIT. + +package zerrors + +import ( + "encoding/json" + "fmt" + "strings" +) + +const _KindName = "CanceledUnknownInvalidArgumentDeadlineExceededNotFoundAlreadyExistsPermissionDeniedResourceExhaustedPreconditionFailedAbortedOutOfRangeUnimplementedInternalUnavailableDataLossUnauthenticated" + +var _KindIndex = [...]uint8{0, 8, 15, 30, 46, 54, 67, 83, 100, 118, 125, 135, 148, 156, 167, 175, 190} + +const _KindLowerName = "canceledunknowninvalidargumentdeadlineexceedednotfoundalreadyexistspermissiondeniedresourceexhaustedpreconditionfailedabortedoutofrangeunimplementedinternalunavailabledatalossunauthenticated" + +func (i Kind) String() string { + i -= 1 + if i < 0 || i >= Kind(len(_KindIndex)-1) { + return fmt.Sprintf("Kind(%d)", i+1) + } + return _KindName[_KindIndex[i]:_KindIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _KindNoOp() { + var x [1]struct{} + _ = x[KindCanceled-(1)] + _ = x[KindUnknown-(2)] + _ = x[KindInvalidArgument-(3)] + _ = x[KindDeadlineExceeded-(4)] + _ = x[KindNotFound-(5)] + _ = x[KindAlreadyExists-(6)] + _ = x[KindPermissionDenied-(7)] + _ = x[KindResourceExhausted-(8)] + _ = x[KindPreconditionFailed-(9)] + _ = x[KindAborted-(10)] + _ = x[KindOutOfRange-(11)] + _ = x[KindUnimplemented-(12)] + _ = x[KindInternal-(13)] + _ = x[KindUnavailable-(14)] + _ = x[KindDataLoss-(15)] + _ = x[KindUnauthenticated-(16)] +} + +var _KindValues = []Kind{KindCanceled, KindUnknown, KindInvalidArgument, KindDeadlineExceeded, KindNotFound, KindAlreadyExists, KindPermissionDenied, KindResourceExhausted, KindPreconditionFailed, KindAborted, KindOutOfRange, KindUnimplemented, KindInternal, KindUnavailable, KindDataLoss, KindUnauthenticated} + +var _KindNameToValueMap = map[string]Kind{ + _KindName[0:8]: KindCanceled, + _KindLowerName[0:8]: KindCanceled, + _KindName[8:15]: KindUnknown, + _KindLowerName[8:15]: KindUnknown, + _KindName[15:30]: KindInvalidArgument, + _KindLowerName[15:30]: KindInvalidArgument, + _KindName[30:46]: KindDeadlineExceeded, + _KindLowerName[30:46]: KindDeadlineExceeded, + _KindName[46:54]: KindNotFound, + _KindLowerName[46:54]: KindNotFound, + _KindName[54:67]: KindAlreadyExists, + _KindLowerName[54:67]: KindAlreadyExists, + _KindName[67:83]: KindPermissionDenied, + _KindLowerName[67:83]: KindPermissionDenied, + _KindName[83:100]: KindResourceExhausted, + _KindLowerName[83:100]: KindResourceExhausted, + _KindName[100:118]: KindPreconditionFailed, + _KindLowerName[100:118]: KindPreconditionFailed, + _KindName[118:125]: KindAborted, + _KindLowerName[118:125]: KindAborted, + _KindName[125:135]: KindOutOfRange, + _KindLowerName[125:135]: KindOutOfRange, + _KindName[135:148]: KindUnimplemented, + _KindLowerName[135:148]: KindUnimplemented, + _KindName[148:156]: KindInternal, + _KindLowerName[148:156]: KindInternal, + _KindName[156:167]: KindUnavailable, + _KindLowerName[156:167]: KindUnavailable, + _KindName[167:175]: KindDataLoss, + _KindLowerName[167:175]: KindDataLoss, + _KindName[175:190]: KindUnauthenticated, + _KindLowerName[175:190]: KindUnauthenticated, +} + +var _KindNames = []string{ + _KindName[0:8], + _KindName[8:15], + _KindName[15:30], + _KindName[30:46], + _KindName[46:54], + _KindName[54:67], + _KindName[67:83], + _KindName[83:100], + _KindName[100:118], + _KindName[118:125], + _KindName[125:135], + _KindName[135:148], + _KindName[148:156], + _KindName[156:167], + _KindName[167:175], + _KindName[175:190], +} + +// KindString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func KindString(s string) (Kind, error) { + if val, ok := _KindNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _KindNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Kind values", s) +} + +// KindValues returns all values of the enum +func KindValues() []Kind { + return _KindValues +} + +// KindStrings returns a slice of all String values of the enum +func KindStrings() []string { + strs := make([]string, len(_KindNames)) + copy(strs, _KindNames) + return strs +} + +// IsAKind returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Kind) IsAKind() bool { + for _, v := range _KindValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for Kind +func (i Kind) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Kind +func (i *Kind) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("Kind should be a string, got %s", data) + } + + var err error + *i, err = KindString(s) + return err +} diff --git a/internal/zerrors/not_found.go b/internal/zerrors/not_found.go index 450cc1aa6..0701efe11 100644 --- a/internal/zerrors/not_found.go +++ b/internal/zerrors/not_found.go @@ -2,38 +2,15 @@ package zerrors import "fmt" -type NotFound interface { - error - IsNotFound() -} - -type NotFoundError struct { - *ZitadelError -} - func ThrowNotFound(parent error, id, message string) error { - return &NotFoundError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindNotFound, parent, id, message) } -func ThrowNotFoundf(parent error, id, format string, a ...interface{}) error { - return ThrowNotFound(parent, id, fmt.Sprintf(format, a...)) +func ThrowNotFoundf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindNotFound, parent, id, fmt.Sprintf(format, a...)) } -func (err *NotFoundError) IsNotFound() {} - func IsNotFound(err error) bool { - _, ok := err.(NotFound) - return ok -} - -func (err *NotFoundError) Is(target error) bool { - t, ok := target.(*NotFoundError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *NotFoundError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindNotFound } diff --git a/internal/zerrors/not_found_test.go b/internal/zerrors/not_found_test.go index b54c550b2..9660184c8 100644 --- a/internal/zerrors/not_found_test.go +++ b/internal/zerrors/not_found_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestNotFoundError(t *testing.T) { - var notFoundError interface{} = new(zerrors.NotFoundError) - _, ok := notFoundError.(zerrors.NotFound) - assert.True(t, ok) -} +func TestNotFound(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowNotFoundf(t *testing.T) { - err := zerrors.ThrowNotFoundf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.NotFoundError) - assert.True(t, ok) -} + t.Run("ThrowNotFound", func(t *testing.T) { + err := zerrors.ThrowNotFound(parentErr, id, message) + assert.NotNil(t, err) -func TestIsNotFound(t *testing.T) { - err := zerrors.ThrowNotFound(nil, "id", "msg") - ok := zerrors.IsNotFound(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindNotFound, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsNotFound(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowNotFoundf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowNotFoundf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindNotFound, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsNotFound", func(t *testing.T) { + err := zerrors.ThrowNotFound(parentErr, id, message) + isNotFound := zerrors.IsNotFound(err) + assert.True(t, isNotFound) + }) } diff --git a/internal/zerrors/permission_denied.go b/internal/zerrors/permission_denied.go index 62bff08ed..46066d842 100644 --- a/internal/zerrors/permission_denied.go +++ b/internal/zerrors/permission_denied.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ PermissionDenied = (*PermissionDeniedError)(nil) - _ Error = (*PermissionDeniedError)(nil) -) - -type PermissionDenied interface { - error - IsPermissionDenied() -} - -type PermissionDeniedError struct { - *ZitadelError -} +import "fmt" func ThrowPermissionDenied(parent error, id, message string) error { - return &PermissionDeniedError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindPermissionDenied, parent, id, message) } -func ThrowPermissionDeniedf(parent error, id, format string, a ...interface{}) error { - return ThrowPermissionDenied(parent, id, fmt.Sprintf(format, a...)) +func ThrowPermissionDeniedf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindPermissionDenied, parent, id, fmt.Sprintf(format, a...)) } -func (err *PermissionDeniedError) IsPermissionDenied() {} - func IsPermissionDenied(err error) bool { - _, ok := err.(PermissionDenied) - return ok -} - -func (err *PermissionDeniedError) Is(target error) bool { - t, ok := target.(*PermissionDeniedError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *PermissionDeniedError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindPermissionDenied } diff --git a/internal/zerrors/permission_denied_test.go b/internal/zerrors/permission_denied_test.go index e60177055..509436e03 100644 --- a/internal/zerrors/permission_denied_test.go +++ b/internal/zerrors/permission_denied_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestPermissionDeniedError(t *testing.T) { - var err interface{} = new(zerrors.PermissionDeniedError) - _, ok := err.(zerrors.PermissionDenied) - assert.True(t, ok) -} +func TestPermissionDenied(t *testing.T) { + parentErr := errors.New("parent error") + id := "PERMISSION_DENIED" + message := "you do not have permission" -func TestThrowPermissionDeniedf(t *testing.T) { - err := zerrors.ThrowPermissionDeniedf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.PermissionDeniedError) - assert.True(t, ok) -} + t.Run("ThrowPermissionDenied", func(t *testing.T) { + err := zerrors.ThrowPermissionDenied(parentErr, id, message) + assert.NotNil(t, err) -func TestIsPermissionDenied(t *testing.T) { - err := zerrors.ThrowPermissionDenied(nil, "id", "msg") - ok := zerrors.IsPermissionDenied(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindPermissionDenied, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsPermissionDenied(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowPermissionDeniedf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowPermissionDeniedf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindPermissionDenied, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsPermissionDenied", func(t *testing.T) { + err := zerrors.ThrowPermissionDenied(parentErr, id, message) + isPermissionDenied := zerrors.IsPermissionDenied(err) + assert.True(t, isPermissionDenied) + }) } diff --git a/internal/zerrors/precondition_failed.go b/internal/zerrors/precondition_failed.go index ebacb4aa8..c8840eaf8 100644 --- a/internal/zerrors/precondition_failed.go +++ b/internal/zerrors/precondition_failed.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ PreconditionFailed = (*PreconditionFailedError)(nil) - _ Error = (*PreconditionFailedError)(nil) -) - -type PreconditionFailed interface { - error - IsPreconditionFailed() -} - -type PreconditionFailedError struct { - *ZitadelError -} +import "fmt" func ThrowPreconditionFailed(parent error, id, message string) error { - return &PreconditionFailedError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindPreconditionFailed, parent, id, message) } -func ThrowPreconditionFailedf(parent error, id, format string, a ...interface{}) error { - return ThrowPreconditionFailed(parent, id, fmt.Sprintf(format, a...)) +func ThrowPreconditionFailedf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindPreconditionFailed, parent, id, fmt.Sprintf(format, a...)) } -func (err *PreconditionFailedError) IsPreconditionFailed() {} - func IsPreconditionFailed(err error) bool { - _, ok := err.(PreconditionFailed) - return ok -} - -func (err *PreconditionFailedError) Is(target error) bool { - t, ok := target.(*PreconditionFailedError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *PreconditionFailedError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindPreconditionFailed } diff --git a/internal/zerrors/precondition_failed_test.go b/internal/zerrors/precondition_failed_test.go index 33e45dc48..65def5f62 100644 --- a/internal/zerrors/precondition_failed_test.go +++ b/internal/zerrors/precondition_failed_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestPreconditionFailedError(t *testing.T) { - var err interface{} = new(zerrors.PreconditionFailedError) - _, ok := err.(zerrors.PreconditionFailed) - assert.True(t, ok) -} +func TestPreconditionFailed(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowPreconditionFailedf(t *testing.T) { - err := zerrors.ThrowPreconditionFailedf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.PreconditionFailedError) - assert.True(t, ok) -} + t.Run("ThrowPreconditionFailed", func(t *testing.T) { + err := zerrors.ThrowPreconditionFailed(parentErr, id, message) + assert.NotNil(t, err) -func TestIsPreconditionFailed(t *testing.T) { - err := zerrors.ThrowPreconditionFailed(nil, "id", "msg") - ok := zerrors.IsPreconditionFailed(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindPreconditionFailed, zitadelErr.Kind) - err = errors.New("Precondition failed!") - ok = zerrors.IsPreconditionFailed(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowPreconditionFailedf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowPreconditionFailedf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindPreconditionFailed, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsPreconditionFailed", func(t *testing.T) { + err := zerrors.ThrowPreconditionFailed(parentErr, id, message) + isPreconditionFailed := zerrors.IsPreconditionFailed(err) + assert.True(t, isPreconditionFailed) + }) } diff --git a/internal/zerrors/resource_exhausted.go b/internal/zerrors/resource_exhausted.go index 1beac0857..e35bef6ca 100644 --- a/internal/zerrors/resource_exhausted.go +++ b/internal/zerrors/resource_exhausted.go @@ -1,48 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ ResourceExhausted = (*ResourceExhaustedError)(nil) - _ Error = (*ResourceExhaustedError)(nil) -) - -type ResourceExhausted interface { - error - IsResourceExhausted() -} - -type ResourceExhaustedError struct { - *ZitadelError -} +import "fmt" func ThrowResourceExhausted(parent error, id, message string) error { - return &ResourceExhaustedError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindResourceExhausted, parent, id, message) } -func ThrowResourceExhaustedf(parent error, id, format string, a ...interface{}) error { - return ThrowResourceExhausted(parent, id, fmt.Sprintf(format, a...)) +func ThrowResourceExhaustedf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindResourceExhausted, parent, id, fmt.Sprintf(format, a...)) } -func (err *ResourceExhaustedError) IsResourceExhausted() {} - func IsResourceExhausted(err error) bool { - //nolint:errorlint - _, ok := err.(ResourceExhausted) - return ok -} - -func (err *ResourceExhaustedError) Is(target error) bool { - //nolint:errorlint - t, ok := target.(*ResourceExhaustedError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *ResourceExhaustedError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindResourceExhausted } diff --git a/internal/zerrors/resource_exhausted_test.go b/internal/zerrors/resource_exhausted_test.go index 680cf487f..5739df2a2 100644 --- a/internal/zerrors/resource_exhausted_test.go +++ b/internal/zerrors/resource_exhausted_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestResourceExhaustedError(t *testing.T) { - var err interface{} = new(zerrors.ResourceExhaustedError) - _, ok := err.(zerrors.ResourceExhausted) - assert.True(t, ok) -} +func TestResourceExhausted(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowResourceExhaustedf(t *testing.T) { - err := zerrors.ThrowResourceExhaustedf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.ResourceExhaustedError) - assert.True(t, ok) -} + t.Run("ThrowResourceExhausted", func(t *testing.T) { + err := zerrors.ThrowResourceExhausted(parentErr, id, message) + assert.NotNil(t, err) -func TestIsResourceExhausted(t *testing.T) { - err := zerrors.ThrowResourceExhausted(nil, "id", "msg") - ok := zerrors.IsResourceExhausted(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindResourceExhausted, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsResourceExhausted(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowResourceExhaustedf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowResourceExhaustedf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindResourceExhausted, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsResourceExhausted", func(t *testing.T) { + err := zerrors.ThrowResourceExhausted(parentErr, id, message) + isResourceExhausted := zerrors.IsResourceExhausted(err) + assert.True(t, isResourceExhausted) + }) } diff --git a/internal/zerrors/unauthenticated.go b/internal/zerrors/unauthenticated.go index 16a6a9885..be4b78e33 100644 --- a/internal/zerrors/unauthenticated.go +++ b/internal/zerrors/unauthenticated.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ Unauthenticated = (*UnauthenticatedError)(nil) - _ Error = (*UnauthenticatedError)(nil) -) - -type Unauthenticated interface { - error - IsUnauthenticated() -} - -type UnauthenticatedError struct { - *ZitadelError -} +import "fmt" func ThrowUnauthenticated(parent error, id, message string) error { - return &UnauthenticatedError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindUnauthenticated, parent, id, message) } -func ThrowUnauthenticatedf(parent error, id, format string, a ...interface{}) error { - return ThrowUnauthenticated(parent, id, fmt.Sprintf(format, a...)) +func ThrowUnauthenticatedf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindUnauthenticated, parent, id, fmt.Sprintf(format, a...)) } -func (err *UnauthenticatedError) IsUnauthenticated() {} - func IsUnauthenticated(err error) bool { - _, ok := err.(Unauthenticated) - return ok -} - -func (err *UnauthenticatedError) Is(target error) bool { - t, ok := target.(*UnauthenticatedError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *UnauthenticatedError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindUnauthenticated } diff --git a/internal/zerrors/unauthenticated_test.go b/internal/zerrors/unauthenticated_test.go index 9fa9c0a18..a43b8d2da 100644 --- a/internal/zerrors/unauthenticated_test.go +++ b/internal/zerrors/unauthenticated_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestUnauthenticatedError(t *testing.T) { - var err interface{} = new(zerrors.UnauthenticatedError) - _, ok := err.(zerrors.Unauthenticated) - assert.True(t, ok) -} +func TestUnauthenticated(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowUnauthenticatedf(t *testing.T) { - err := zerrors.ThrowUnauthenticatedf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.UnauthenticatedError) - assert.True(t, ok) -} + t.Run("ThrowUnauthenticated", func(t *testing.T) { + err := zerrors.ThrowUnauthenticated(parentErr, id, message) + assert.NotNil(t, err) -func TestIsUnauthenticated(t *testing.T) { - err := zerrors.ThrowUnauthenticated(nil, "id", "msg") - ok := zerrors.IsUnauthenticated(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnauthenticated, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsUnauthenticated(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowUnauthenticatedf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowUnauthenticatedf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnauthenticated, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsUnauthenticated", func(t *testing.T) { + err := zerrors.ThrowUnauthenticated(parentErr, id, message) + isUnauthenticated := zerrors.IsUnauthenticated(err) + assert.True(t, isUnauthenticated) + }) } diff --git a/internal/zerrors/unavailable.go b/internal/zerrors/unavailable.go index 65572881e..bcffdb637 100644 --- a/internal/zerrors/unavailable.go +++ b/internal/zerrors/unavailable.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ Unavailable = (*UnavailableError)(nil) - _ Error = (*UnavailableError)(nil) -) - -type Unavailable interface { - error - IsUnavailable() -} - -type UnavailableError struct { - *ZitadelError -} +import "fmt" func ThrowUnavailable(parent error, id, message string) error { - return &UnavailableError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindUnavailable, parent, id, message) } -func ThrowUnavailablef(parent error, id, format string, a ...interface{}) error { - return ThrowUnavailable(parent, id, fmt.Sprintf(format, a...)) +func ThrowUnavailablef(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindUnavailable, parent, id, fmt.Sprintf(format, a...)) } -func (err *UnavailableError) IsUnavailable() {} - func IsUnavailable(err error) bool { - _, ok := err.(Unavailable) - return ok -} - -func (err *UnavailableError) Is(target error) bool { - t, ok := target.(*UnavailableError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *UnavailableError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindUnavailable } diff --git a/internal/zerrors/unavailable_test.go b/internal/zerrors/unavailable_test.go index f36d25e2c..84a21fd76 100644 --- a/internal/zerrors/unavailable_test.go +++ b/internal/zerrors/unavailable_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestUnavailableError(t *testing.T) { - var err interface{} = new(zerrors.UnavailableError) - _, ok := err.(zerrors.Unavailable) - assert.True(t, ok) -} +func TestUnavailable(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowUnavailablef(t *testing.T) { - err := zerrors.ThrowUnavailablef(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.UnavailableError) - assert.True(t, ok) -} + t.Run("ThrowUnavailable", func(t *testing.T) { + err := zerrors.ThrowUnavailable(parentErr, id, message) + assert.NotNil(t, err) -func TestIsUnavailable(t *testing.T) { - err := zerrors.ThrowUnavailable(nil, "id", "msg") - ok := zerrors.IsUnavailable(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnavailable, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsUnavailable(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowUnavailablef", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowUnavailablef(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnavailable, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsUnavailable", func(t *testing.T) { + err := zerrors.ThrowUnavailable(parentErr, id, message) + isUnavailable := zerrors.IsUnavailable(err) + assert.True(t, isUnavailable) + }) } diff --git a/internal/zerrors/unimplemented.go b/internal/zerrors/unimplemented.go index 544f05ee2..4a6cc5ab6 100644 --- a/internal/zerrors/unimplemented.go +++ b/internal/zerrors/unimplemented.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ Unimplemented = (*UnimplementedError)(nil) - _ Error = (*UnimplementedError)(nil) -) - -type Unimplemented interface { - error - IsUnimplemented() -} - -type UnimplementedError struct { - *ZitadelError -} +import "fmt" func ThrowUnimplemented(parent error, id, message string) error { - return &UnimplementedError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindUnimplemented, parent, id, message) } -func ThrowUnimplementedf(parent error, id, format string, a ...interface{}) error { - return ThrowUnimplemented(parent, id, fmt.Sprintf(format, a...)) +func ThrowUnimplementedf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindUnimplemented, parent, id, fmt.Sprintf(format, a...)) } -func (err *UnimplementedError) IsUnimplemented() {} - func IsUnimplemented(err error) bool { - _, ok := err.(Unimplemented) - return ok -} - -func (err *UnimplementedError) Is(target error) bool { - t, ok := target.(*UnimplementedError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *UnimplementedError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindUnimplemented } diff --git a/internal/zerrors/unimplemented_test.go b/internal/zerrors/unimplemented_test.go index 3786ed2f4..c1e70b798 100644 --- a/internal/zerrors/unimplemented_test.go +++ b/internal/zerrors/unimplemented_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestUnimplementedError(t *testing.T) { - var unimplementedError interface{} = new(zerrors.UnimplementedError) - _, ok := unimplementedError.(zerrors.Unimplemented) - assert.True(t, ok) -} +func TestUnimplemented(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowUnimplementedf(t *testing.T) { - err := zerrors.ThrowUnimplementedf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.UnimplementedError) - assert.True(t, ok) -} + t.Run("ThrowUnimplemented", func(t *testing.T) { + err := zerrors.ThrowUnimplemented(parentErr, id, message) + assert.NotNil(t, err) -func TestIsUnimplemented(t *testing.T) { - err := zerrors.ThrowUnimplemented(nil, "id", "msg") - ok := zerrors.IsUnimplemented(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnimplemented, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsUnimplemented(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowUnimplementedf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowUnimplementedf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnimplemented, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsUnimplemented", func(t *testing.T) { + err := zerrors.ThrowUnimplemented(parentErr, id, message) + isUnimplemented := zerrors.IsUnimplemented(err) + assert.True(t, isUnimplemented) + }) } diff --git a/internal/zerrors/unknown.go b/internal/zerrors/unknown.go index cba44de8a..cbe63462b 100644 --- a/internal/zerrors/unknown.go +++ b/internal/zerrors/unknown.go @@ -1,46 +1,16 @@ package zerrors -import ( - "fmt" -) - -var ( - _ Unknown = (*UnknownError)(nil) - _ Error = (*UnknownError)(nil) -) - -type Unknown interface { - error - IsUnknown() -} - -type UnknownError struct { - *ZitadelError -} +import "fmt" func ThrowUnknown(parent error, id, message string) error { - return &UnknownError{CreateZitadelError(parent, id, message)} + return CreateZitadelError(KindUnknown, parent, id, message) } -func ThrowUnknownf(parent error, id, format string, a ...interface{}) error { - return ThrowUnknown(parent, id, fmt.Sprintf(format, a...)) +func ThrowUnknownf(parent error, id, format string, a ...any) error { + return CreateZitadelError(KindUnknown, parent, id, fmt.Sprintf(format, a...)) } -func (err *UnknownError) IsUnknown() {} - func IsUnknown(err error) bool { - _, ok := err.(Unknown) - return ok -} - -func (err *UnknownError) Is(target error) bool { - t, ok := target.(*UnknownError) - if !ok { - return false - } - return err.ZitadelError.Is(t.ZitadelError) -} - -func (err *UnknownError) Unwrap() error { - return err.ZitadelError + zitadelErr, ok := AsZitadelError(err) + return ok && zitadelErr.Kind == KindUnknown } diff --git a/internal/zerrors/unknown_test.go b/internal/zerrors/unknown_test.go index 973966ce7..820a3e176 100644 --- a/internal/zerrors/unknown_test.go +++ b/internal/zerrors/unknown_test.go @@ -9,25 +9,54 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func TestUnknownError(t *testing.T) { - var err interface{} = new(zerrors.UnknownError) - _, ok := err.(zerrors.Unknown) - assert.True(t, ok) -} +func TestUnknown(t *testing.T) { + parentErr := errors.New("parent error") + id := "test_id" + message := "test message" -func TestThrowUnknownf(t *testing.T) { - err := zerrors.ThrowUnknownf(nil, "id", "msg") - //nolint:errorlint - _, ok := err.(*zerrors.UnknownError) - assert.True(t, ok) -} + t.Run("ThrowUnknown", func(t *testing.T) { + err := zerrors.ThrowUnknown(parentErr, id, message) + assert.NotNil(t, err) -func TestIsUnknown(t *testing.T) { - err := zerrors.ThrowUnknown(nil, "id", "msg") - ok := zerrors.IsUnknown(err) - assert.True(t, ok) + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnknown, zitadelErr.Kind) - err = errors.New("I am found!") - ok = zerrors.IsUnknown(err) - assert.False(t, ok) + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, message, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("ThrowUnknownf", func(t *testing.T) { + format := "formatted %s" + arg := "message" + expectedMessage := "formatted message" + + err := zerrors.ThrowUnknownf(parentErr, id, format, arg) + assert.NotNil(t, err) + + zitadelErr, ok := zerrors.AsZitadelError(err) + assert.True(t, ok) + assert.Equal(t, zerrors.KindUnknown, zitadelErr.Kind) + + zitadelError := new(zerrors.ZitadelError) + if errors.As(err, &zitadelError) { + assert.Equal(t, parentErr, zitadelError.Unwrap()) + assert.Equal(t, id, zitadelError.ID) + assert.Equal(t, expectedMessage, zitadelError.Message) + } else { + t.Errorf("error is not of type ZitadelError") + } + }) + + t.Run("IsUnknown", func(t *testing.T) { + err := zerrors.ThrowUnknown(parentErr, id, message) + isUnknown := zerrors.IsUnknown(err) + assert.True(t, isUnknown) + }) } diff --git a/internal/zerrors/zerror.go b/internal/zerrors/zerror.go index 996f67ce2..5a5de563e 100644 --- a/internal/zerrors/zerror.go +++ b/internal/zerrors/zerror.go @@ -3,23 +3,93 @@ package zerrors import ( "errors" "fmt" - "reflect" + "log/slog" ) -var _ Error = (*ZitadelError)(nil) +//go:generate enumer -type=Kind -trimprefix=Kind -json +type Kind int + +const ( + // KindCanceled indicates that the operation was canceled, typically by the + // caller. + KindCanceled Kind = 1 + + // KindUnknown indicates that the operation failed for an unknown reason. + KindUnknown Kind = 2 + + // KindInvalidArgument indicates that client supplied an invalid argument. + KindInvalidArgument Kind = 3 + + // KindDeadlineExceeded indicates that deadline expired before the operation + // could complete. + KindDeadlineExceeded Kind = 4 + + // KindNotFound indicates that some requested entity (for example, a file or + // directory) was not found. + KindNotFound Kind = 5 + + // KindAlreadyExists indicates that client attempted to create an entity (for + // example, a file or directory) that already exists. + KindAlreadyExists Kind = 6 + + // KindPermissionDenied indicates that the caller doesn't have permission to + // execute the specified operation. + KindPermissionDenied Kind = 7 + + // KindResourceExhausted indicates that some resource has been exhausted. For + // example, a per-user quota may be exhausted or the entire file system may + // be full. + KindResourceExhausted Kind = 8 + + // KindPreconditionFailed indicates that the system is not in a state + // required for the operation's execution. + KindPreconditionFailed Kind = 9 + + // KindAborted indicates that operation was aborted by the system, usually + // because of a concurrency issue such as a sequencer check failure or + // transaction abort. + KindAborted Kind = 10 + + // KindOutOfRange indicates that the operation was attempted past the valid + // range (for example, seeking past end-of-file). + KindOutOfRange Kind = 11 + + // KindUnimplemented indicates that the operation isn't implemented, + // supported, or enabled in this service. + KindUnimplemented Kind = 12 + + // KindInternal indicates that some invariants expected by the underlying + // system have been broken. This Kind is reserved for serious errors. + KindInternal Kind = 13 + + // KindUnavailable indicates that the service is currently unavailable. This + // is usually temporary, so clients can back off and retry idempotent + // operations. + KindUnavailable Kind = 14 + + // KindDataLoss indicates that the operation has resulted in unrecoverable + // data loss or corruption. + KindDataLoss Kind = 15 + + // KindUnauthenticated indicates that the request does not have valid + // authentication credentials for the operation. + KindUnauthenticated Kind = 16 +) type ZitadelError struct { + Kind Kind Parent error Message string ID string } func ThrowError(parent error, id, message string) error { - return CreateZitadelError(parent, id, message) + return CreateZitadelError(KindUnknown, parent, id, message) } -func CreateZitadelError(parent error, id, message string) *ZitadelError { +func CreateZitadelError(kind Kind, parent error, id, message string) *ZitadelError { return &ZitadelError{ + Kind: kind, Parent: parent, ID: id, Message: message, @@ -58,6 +128,9 @@ func (err *ZitadelError) Is(target error) bool { if !ok { return false } + if t.Kind != err.Kind { + return false + } if t.ID != "" && t.ID != err.ID { return false } @@ -71,16 +144,24 @@ func (err *ZitadelError) Is(target error) bool { return true } -func (err *ZitadelError) As(target interface{}) bool { - _, ok := target.(**ZitadelError) - if !ok { - return false - } - reflect.Indirect(reflect.ValueOf(target)).Set(reflect.ValueOf(err)) - return true -} - func IsZitadelError(err error) bool { zitadelErr := new(ZitadelError) return errors.As(err, &zitadelErr) } + +func AsZitadelError(err error) (*ZitadelError, bool) { + zitadelErr := new(ZitadelError) + ok := errors.As(err, &zitadelErr) + return zitadelErr, ok +} + +var _ slog.LogValuer = (*ZitadelError)(nil) + +func (err *ZitadelError) LogValue() slog.Value { + return slog.GroupValue( + slog.String("kind", err.Kind.String()), + slog.String("message", err.Message), + slog.String("id", err.ID), + slog.Any("parent", err.Parent), + ) +} diff --git a/internal/zerrors/zerror_test.go b/internal/zerrors/zerror_test.go index 517f938ee..e03f66c29 100644 --- a/internal/zerrors/zerror_test.go +++ b/internal/zerrors/zerror_test.go @@ -19,26 +19,49 @@ func TestErrorMethod(t *testing.T) { assert.Equal(t, subExptected, err.Error()) } -func TestIsZitadelError(t *testing.T) { +func TestZitadelError_Is(t *testing.T) { + parent := errors.New("parent error") + target := zerrors.CreateZitadelError(zerrors.KindAborted, parent, "id", "message") tests := []struct { - name string + name string // description of this test case err error want bool }{ { - name: "zitadel error", - err: zerrors.ThrowInvalidArgument(nil, "id", "msg"), - want: true, + name: "wrong type", + err: errors.New("some other error"), + want: false, }, { - name: "other error", - err: errors.New("just a random error"), + name: "different kind", + err: zerrors.CreateZitadelError(zerrors.KindNotFound, parent, "id", "message"), want: false, }, + { + name: "different id", + err: zerrors.CreateZitadelError(zerrors.KindAborted, parent, "otherID", "message"), + want: false, + }, + { + name: "different message", + err: zerrors.CreateZitadelError(zerrors.KindAborted, parent, "id", "other message"), + want: false, + }, + { + name: "different parent", + err: zerrors.CreateZitadelError(zerrors.KindAborted, errors.New("other parent"), "id", "message"), + want: false, + }, + { + name: "same error", + err: zerrors.CreateZitadelError(zerrors.KindAborted, parent, "id", "message"), + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, zerrors.IsZitadelError(tt.err), "IsZitadelError(%v)", tt.err) + got := errors.Is(tt.err, target) + assert.Equal(t, tt.want, got) }) } } diff --git a/main.go b/main.go index 97689bbe4..06b10bd86 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,14 @@ package main import ( "os" - "github.com/spf13/cobra" - "github.com/zitadel/zitadel/cmd" ) func main() { args := os.Args[1:] rootCmd := cmd.New(os.Stdout, os.Stdin, args, nil) - cobra.CheckErr(rootCmd.Execute()) + if err := rootCmd.Execute(); err != nil { + // error is logged by the command itself + os.Exit(1) + } } diff --git a/pkg/grpc/admin/event.go b/pkg/grpc/admin/event.go index bc1972406..f49dc1380 100644 --- a/pkg/grpc/admin/event.go +++ b/pkg/grpc/admin/event.go @@ -3,10 +3,10 @@ package admin import ( "context" + "github.com/zitadel/zitadel/backend/v3/instrumentation/tracing" event_grpc "github.com/zitadel/zitadel/internal/api/grpc/event" "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/telemetry/tracing" event_pb "github.com/zitadel/zitadel/pkg/grpc/event" )