mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# 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]>
202 lines
6.4 KiB
Go
202 lines
6.4 KiB
Go
package instrumentation
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
google_detector "go.opentelemetry.io/contrib/detectors/gcp"
|
|
"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
|
|
ExporterTypeAuto
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
//go:generate enumer -type=DetectorType -trimprefix=DetectorType -text -linecomment
|
|
type DetectorType int
|
|
|
|
// DetectorType defines a resource detector that describes the platform ZITADEL
|
|
// runs on. Detected attributes are attached to every trace, metric and log we
|
|
// export. Detectors are opt-in: each one probes its platform on startup, and the
|
|
// attributes it finds identify the account and host we run on to whichever
|
|
// backend we export to.
|
|
const (
|
|
// Empty line comment sets empty string of unspecified value
|
|
DetectorTypeUnspecified DetectorType = iota //
|
|
DetectorTypeGoogle
|
|
)
|
|
|
|
// 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 := newResource(ctx, cfg.ServiceName, cfg.Detectors)
|
|
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{},
|
|
)
|
|
}
|
|
|
|
// newResource builds the resource that identifies this process on every signal
|
|
// it emits. Detectors are applied in order and the later ones win, so the
|
|
// service attributes we are explicitly configured with always take precedence
|
|
// over anything a platform detector found.
|
|
func newResource(ctx context.Context, serviceName string, detectorTypes []DetectorType) (*resource.Resource, error) {
|
|
// OTEL_SERVICE_NAME takes priority over programmatic config per the
|
|
// OpenTelemetry specification.
|
|
if envName := os.Getenv("OTEL_SERVICE_NAME"); envName != "" {
|
|
serviceName = envName
|
|
}
|
|
attributes := []attribute.KeyValue{
|
|
semconv.ServiceNameKey.String(serviceName),
|
|
}
|
|
if build.Version() != "" {
|
|
attributes = append(attributes, semconv.ServiceVersionKey.String(build.Version()))
|
|
}
|
|
detectors, err := newDetectors(detectorTypes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resource.New(ctx,
|
|
resource.WithDetectors(detectors...),
|
|
resource.WithTelemetrySDK(),
|
|
resource.WithFromEnv(),
|
|
resource.WithAttributes(attributes...),
|
|
)
|
|
}
|
|
|
|
// newDetectors resolves the configured detector types, rejecting any value
|
|
// that does not name a detector. An empty list yields no detectors, which is
|
|
// the default: detection is opt-in because it costs a platform probe on startup
|
|
// and adds attributes that identify the account and host we run on to
|
|
// everything we export.
|
|
func newDetectors(types []DetectorType) ([]resource.Detector, error) {
|
|
detectors := make([]resource.Detector, 0, len(types))
|
|
for _, typ := range types {
|
|
switch typ {
|
|
case DetectorTypeGoogle:
|
|
// Fills in faas.*, cloud.* and host.* attributes when we run on
|
|
// Cloud Run, GKE, GCE, App Engine or Cloud Functions. Google Cloud
|
|
// Monitoring derives the monitored resource a metric is written to
|
|
// from exactly those attributes, so without them every process
|
|
// reports as the same empty generic_node and the series overlap.
|
|
detectors = append(detectors, google_detector.NewDetector())
|
|
case DetectorTypeUnspecified:
|
|
// The zero value is not a valid entry: an empty string in the list
|
|
// is a typo, not a way to switch detection off. An empty list is.
|
|
return nil, errDetectorType(typ)
|
|
default:
|
|
return nil, errDetectorType(typ)
|
|
}
|
|
}
|
|
return detectors, nil
|
|
}
|
|
|
|
func errExporterType(typ ExporterType, instrument string) error {
|
|
return fmt.Errorf("exporter type \"%v\" unsupported for %s", typ, instrument)
|
|
}
|
|
|
|
func errDetectorType(typ DetectorType) error {
|
|
return fmt.Errorf("detector type \"%v\" unsupported", typ)
|
|
}
|