Files
zitadel/internal/zerrors/zerror.go
T
Tim MöhlmannandGitHub eb22b58756 feat(telemetry): improved instrumentation for observability (#11159)
# Which Problems Are Solved

Zitadel did not provide easy correlation between errors, logs, traces
and metrics. The configuration for those instrumentations was also not
consistent, with some supporting different exporters then others.
Implementation and parsing of config was also spaghettified over
multiple packages, with awkward parsing and inconsistent naming of
options.

# How the Problems Are Solved

All telemetry is now merged under the name "instrumentation". Why?
1. We thought it was a good idea in the past to call the milestone
exporter `Telemtry` in the runtime config. Calling this `TelemetryV2`
looks weird.
2. Not everything is a meter and not everything is sent (tele...). 
3. It's also
[defined](https://opentelemetry.io/docs/concepts/instrumentation/) as
such by the OTEL documentation.

## New features

- Adds structured, context based logging with trace-ID awareness
- Static log fields are added to the context, such as service and
request path
- Static log fields are injected in each logline emitted by the
application
- Structured logs can also be send to an otel exporter
- Structured logs can be printed to StdErr in text and JSON format
- Error sinks make sure every error is logged at the correct level:
- Warnings for client side errors (HTTP 400 range, Invalid request etc)
  - Error for server side errors (Internal server errors)
- Metrics can now also be send to a OTEL collector. (previously they
could only be scraped from `/debug/metrics` with prometheus)

## Exporters

This change adds all the exporters supported by OTEL upstream and some
google specific exporters for our cloud deployment.

- StdOut / StdErr: all instrumentations
- OTEL gRPC / HTTP: all instrumentations
- Google: all instrumentations except logging
- Prometheus (pull-based): only metrics

The exception is profiling, which only supports the google exporting due
to lack of support by OTEL upstream.

## Configuration and structure 

- All instrumentation is moved into the new `backend/v3/instrumentation`
package. It reuses configuration types, so both code and runtime
configuration are easier to understand.
- The `internal/telemetry` packages are removed.
- Instrumentation is started with a single function and a proper
shutdown function is now provided.
- Legacy configuration is still parsed from the runtime config, as long
as the new configuration is disabled. This allows backporting this
feature to v4 without breaking existing configurations.

# Additional Changes

- Devcontainer: set `$PATH` variable so installed go binaries can be run
individually, without NX.
- NX: install GCI tool to fix imports

# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/8408
- Closes https://github.com/zitadel/zitadel/issues/6664
- Backport to v4
2026-01-12 05:51:39 +00:00

168 lines
4.3 KiB
Go

package zerrors
import (
"errors"
"fmt"
"log/slog"
)
//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(KindUnknown, parent, id, message)
}
func CreateZitadelError(kind Kind, parent error, id, message string) *ZitadelError {
return &ZitadelError{
Kind: kind,
Parent: parent,
ID: id,
Message: message,
}
}
func (err *ZitadelError) Error() string {
if err.Parent != nil {
return fmt.Sprintf("ID=%s Message=%s Parent=(%v)", err.ID, err.Message, err.Parent)
}
return fmt.Sprintf("ID=%s Message=%s", err.ID, err.Message)
}
func (err *ZitadelError) Unwrap() error {
return err.GetParent()
}
func (err *ZitadelError) GetParent() error {
return err.Parent
}
func (err *ZitadelError) GetMessage() string {
return err.Message
}
func (err *ZitadelError) SetMessage(msg string) {
err.Message = msg
}
func (err *ZitadelError) GetID() string {
return err.ID
}
func (err *ZitadelError) Is(target error) bool {
t, ok := target.(*ZitadelError)
if !ok {
return false
}
if t.Kind != err.Kind {
return false
}
if t.ID != "" && t.ID != err.ID {
return false
}
if t.Message != "" && t.Message != err.Message {
return false
}
if t.Parent != nil && !errors.Is(err.Parent, t.Parent) {
return false
}
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),
)
}