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" )