Files
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

165 lines
6.1 KiB
Go

package telemetry
import (
"context"
"fmt"
"net/http"
"os"
"github.com/centrifugal/centrifugo/v6/internal/build"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/oauth"
)
// googleCloudAuthScope is the OAuth2 scope used when authenticating to Google
// Cloud's OTLP endpoint (telemetry.googleapis.com) with Application Default
// Credentials.
const googleCloudAuthScope = "https://www.googleapis.com/auth/cloud-platform"
// newResource builds the OTel resource attached to telemetry providers.
// instanceID (Centrifugo node ID) becomes the default service.instance.id —
// backends that require points of a time series to arrive in order (notably
// Google Cloud Managed Service for Prometheus) reject or collapse metrics when
// several instances report under the same identity, so every process must
// carry a distinct one. Optional detectors run first, so both Centrifugo
// defaults and environment values override what they detect.
// OTEL_RESOURCE_ATTRIBUTES / OTEL_SERVICE_NAME are merged with precedence over
// Centrifugo defaults; the SDK's WithResource merges environment attributes
// too, but with the opposite precedence (explicitly passed attributes win
// there), so the env override must happen here.
func newResource(ctx context.Context, instanceID string, detectors ...resource.Detector) (*resource.Resource, error) {
serviceName := os.Getenv("OTEL_SERVICE_NAME")
if serviceName == "" {
serviceName = "centrifugo"
}
rs, err := resource.New(ctx,
resource.WithSchemaURL(semconv.SchemaURL),
resource.WithDetectors(detectors...),
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceInstanceIDKey.String(instanceID),
attribute.String("version", build.Version),
),
resource.WithFromEnv(),
)
if err != nil {
// resource.New returns a usable partial resource together with the
// error (malformed OTEL_RESOURCE_ATTRIBUTES, failed detector), but
// exporting telemetry under a wrong or incomplete identity is worse
// than failing startup: both causes are actionable, and orchestrator
// restarts cover transient ones (e.g. metadata server not ready yet).
return nil, fmt.Errorf("error building opentelemetry resource: %w", err)
}
return rs, nil
}
func SetupTracing(ctx context.Context, cfg configtypes.OpenTelemetry, instanceID string) (*trace.TracerProvider, error) {
exporterProtocol := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL")
if exporterProtocol == "" {
exporterProtocol = "http/protobuf"
}
exporter, err := createExporter(ctx, exporterProtocol, cfg.GoogleCloudADCAuth)
if err != nil {
return nil, err
}
detectors, err := resourceDetectors(cfg)
if err != nil {
return nil, err
}
// labels/tags/resources that are common to all traces.
rs, err := newResource(ctx, instanceID, detectors...)
if err != nil {
return nil, err
}
provider := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(rs),
)
otel.SetTracerProvider(provider)
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{}, // W3C Trace Context format; https://www.w3.org/TR/trace-context/
),
)
otel.SetErrorHandler(&ErrorHandlerImpl{})
return provider, nil
}
type ErrorHandlerImpl struct{}
func (e ErrorHandlerImpl) Handle(err error) {
log.Err(err).Msg("opentelemetry error")
}
func createExporter(ctx context.Context, exporterProtocol string, googleCloudADCAuth bool) (*otlptrace.Exporter, error) {
if exporterProtocol == "grpc" {
var opts []otlptracegrpc.Option
if googleCloudADCAuth {
// Inject Google Cloud Application Default Credentials as per-RPC
// OAuth2 tokens so the exporter can authenticate against Google
// Cloud's OTLP endpoint (telemetry.googleapis.com). The access
// token is minted lazily on first export and then cached and
// auto-refreshed. Note: resolving ADC here (at startup) may do a
// one-time metadata-server probe when running on GCE without an
// explicit credentials file.
creds, err := oauth.NewApplicationDefault(ctx, googleCloudAuthScope)
if err != nil {
return nil, fmt.Errorf("error creating Google Cloud application default credentials: %w", err)
}
opts = append(opts, otlptracegrpc.WithDialOption(grpc.WithPerRPCCredentials(creds)))
}
return otlptracegrpc.New(ctx, opts...)
}
if exporterProtocol == "http/protobuf" {
var opts []otlptracehttp.Option
if googleCloudADCAuth {
// Same as the gRPC path, but ADC tokens are attached by an OAuth2
// http.Client transport (which also refreshes them automatically)
// since the HTTP exporter has no per-RPC credentials concept.
client, err := googleCloudADCHTTPClient(ctx)
if err != nil {
return nil, err
}
opts = append(opts, otlptracehttp.WithHTTPClient(client))
}
return otlptracehttp.New(ctx, opts...)
}
return nil, fmt.Errorf("unsupported exporter protocol: %s", exporterProtocol)
}
// googleCloudADCHTTPClient returns an *http.Client that authenticates outgoing
// requests with Google Cloud Application Default Credentials. The OAuth2
// transport mints the access token lazily on first request and then caches and
// auto-refreshes it. Resolving ADC (here, at startup) may perform a one-time
// metadata-server probe when running on GCE without an explicit credentials file.
func googleCloudADCHTTPClient(ctx context.Context) (*http.Client, error) {
ts, err := google.DefaultTokenSource(ctx, googleCloudAuthScope)
if err != nil {
return nil, fmt.Errorf("error creating Google Cloud application default credentials: %w", err)
}
return oauth2.NewClient(ctx, ts), nil
}