From 79ccf33774f157c03e5b71c9cf0b926cc2b6754a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 11 Apr 2026 21:14:02 -0700 Subject: [PATCH] feat(logger,tracing): share OTel resource for logs and traces Factor the resource detector list into internal/otlpresource so the logger's OTLP provider and the trace provider describe the same process with the same attributes. OTLP log records now carry the full resource (process.pid, service.name, service.version, host.*, os.*, etc.) instead of no resource attributes at all. --- internal/otlpresource/resource.go | 52 +++++++++++++++++++++++++++++++ logger/logger.go | 11 +++++++ tracing/tracing.go | 39 ++++++----------------- 3 files changed, 73 insertions(+), 29 deletions(-) create mode 100644 internal/otlpresource/resource.go diff --git a/internal/otlpresource/resource.go b/internal/otlpresource/resource.go new file mode 100644 index 0000000..a228d6c --- /dev/null +++ b/internal/otlpresource/resource.go @@ -0,0 +1,52 @@ +// Package otlpresource builds the OpenTelemetry resource shared between the +// tracing and logger packages so spans, metrics, and logs describe the same +// process with the same attributes (service.name, process.pid, host.*, etc.). +// Keep the detector list in sync across signals. +package otlpresource + +import ( + "context" + "errors" + "log/slog" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" +) + +// Options carries optional attributes that only some callers supply. +// Zero values are skipped. +type Options struct { + ServiceVersion string // added as semconv.ServiceVersionKey + Environment string // added as attribute "environment" +} + +// New builds the shared OTel resource. Non-fatal partial/schema errors are +// logged via the provided slog logger and a usable resource is still returned. +func New(ctx context.Context, log *slog.Logger, opts Options) (*resource.Resource, error) { + detectors := []resource.Option{ + resource.WithFromEnv(), // OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES + resource.WithTelemetrySDK(), // telemetry.sdk.* + resource.WithProcess(), // process.pid + process.executable.* + process.runtime.* + resource.WithOS(), + resource.WithContainer(), + resource.WithHost(), + } + if opts.ServiceVersion != "" { + detectors = append(detectors, + resource.WithAttributes(semconv.ServiceVersionKey.String(opts.ServiceVersion))) + } + if opts.Environment != "" { + detectors = append(detectors, + resource.WithAttributes(attribute.String("environment", opts.Environment))) + } + + res, err := resource.New(ctx, detectors...) + if errors.Is(err, resource.ErrPartialResource) || errors.Is(err, resource.ErrSchemaURLConflict) { + if log != nil { + log.Warn("otel resource setup", "err", err) + } + return res, nil + } + return res, err +} diff --git a/logger/logger.go b/logger/logger.go index 9955b18..d801270 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -36,6 +36,8 @@ import ( slogtraceid "github.com/remychantenay/slog-otel" slogmulti "github.com/samber/slog-multi" + "go.ntppool.org/common/internal/otlpresource" + "go.ntppool.org/common/version" "go.opentelemetry.io/contrib/bridges/otelslog" "go.opentelemetry.io/otel/log/global" otellog "go.opentelemetry.io/otel/sdk/log" @@ -167,9 +169,18 @@ func setupOtlpLogger() *slog.Logger { otellog.WithExportMaxBatchSize(512), ) + // Build the shared OTel resource (process.pid, host.*, service.name + // from OTEL_SERVICE_NAME, service.version, etc.). Environment is only + // known to the tracing package and is intentionally omitted here; see + // common/internal/otlpresource. + res, _ := otlpresource.New(context.Background(), Setup(), otlpresource.Options{ + ServiceVersion: version.Version(), + }) + // Create logger provider provider := otellog.NewLoggerProvider( otellog.WithProcessor(processor), + otellog.WithResource(res), ) // Set global provider diff --git a/tracing/tracing.go b/tracing/tracing.go index c055eca..0804015 100644 --- a/tracing/tracing.go +++ b/tracing/tracing.go @@ -45,19 +45,18 @@ import ( "slices" "time" + "go.ntppool.org/common/internal/otlpresource" "go.ntppool.org/common/internal/tracerconfig" "go.ntppool.org/common/version" "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/propagation" sdklog "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "go.opentelemetry.io/otel/trace" ) @@ -177,33 +176,15 @@ func SetupSDK(ctx context.Context, cfg *TracerConfig) (shutdown TpShutdownFunc, } } - resources := []resource.Option{ - resource.WithFromEnv(), // Discover and provide attributes from OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME environment variables. - resource.WithTelemetrySDK(), // Discover and provide information about the OpenTelemetry SDK used. - resource.WithProcess(), // Discover and provide process information. - resource.WithOS(), // Discover and provide OS information. - resource.WithContainer(), // Discover and provide container information. - resource.WithHost(), // Discover and provide host information. - - // set above via os.Setenv() for WithFromEnv to find - // resource.WithAttributes(semconv.ServiceNameKey.String(cfg.ServiceName)), - - resource.WithAttributes(semconv.ServiceVersionKey.String(version.Version())), - } - - if len(cfg.Environment) > 0 { - resources = append(resources, - resource.WithAttributes(attribute.String("environment", cfg.Environment)), - ) - } - - res, err := resource.New( - context.Background(), - resources..., - ) - if errors.Is(err, resource.ErrPartialResource) || errors.Is(err, resource.ErrSchemaURLConflict) { - log.Warn("otel resource setup", "err", err) // Log non-fatal issues. - } else if err != nil { + // Build the shared OTel resource. Kept in sync between traces, metrics, + // and logs via common/internal/otlpresource — update the detector list + // there, not here. service.name is supplied via OTEL_SERVICE_NAME (set + // above when cfg.ServiceName is provided). + res, err := otlpresource.New(ctx, log, otlpresource.Options{ + ServiceVersion: version.Version(), + Environment: cfg.Environment, + }) + if err != nil { log.Error("otel resource setup", "err", err) return }