Files
zitadel/backend/v3/instrumentation/instrumentation.go
T
8b87ca50d4 feat(telemetry): auto detect google attributes (#12694)
# Which Problems Are Solved

We run multiple deployments (regions) under a single GCP project. Within
these deployments we scale containers. Currently all metrics target the
same time series in GCP, regardless or region or container. All metics
ended up in `generic_node` and Cloud Monitoring rejects the duplicate
and out-of-order points.

This can be fixed by setting vendor specific attributes such as
`faas.name` (K_SERVICE), `faas.version` (K_REVISION), `faas.instance`
(instance id from the metadata server), and `cloud.region` (metadata
instance/region).


# How the Problems Are Solved

Use `go.opentelemetry.io/contrib/detectors/gcp` to auto-detect values as
mentioned above. A new `Instrumentation.Detectors` config entry is
added, which accepts and array of enum for detecotrs. At the moment only
Google is accepted, other entries from
[contrib](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/detectors)
can be added if deemed usefull.

I've considered using `autodetect` contrib package, identifiers and
constructor, but this pulls in the full AWS SDK, increasing zitadel's
binary size by 50MiB.

By default detection is switched off, as it relies on unauthenticated
link-local calls that are insecure anywhere outside of the
infrastructure it targets. It also prevents breaking existing
deployments that rely on detection being off already. (May break
existing trace and logging pipelines).

# Additional Changes

- none

# Additional Context

- (internal only): https://github.com/caos/infra/pull/1813

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Claude Opus 5 <[email protected]>
2026-09-07 12:14:09 +00:00

86 lines
2.4 KiB
Go

// 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"
"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
// Detectors enrich the resource of every exported signal with attributes
// describing the platform we run on. Empty by default.
Detectors []DetectorType
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...),
}
}
// RequestFilter returns an [otelhttp.Filter] that excludes requests
// with paths starting with any of the specified prefixes.
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)
},
)
}
}