Repository navigation
feat(traces,metrics): honor OTEL_SERVICE_NAME over APP_NAME, guard nil metrics Logger - #4343
Conversation
…l metrics Logger OTEL_SERVICE_NAME -- and a non-empty service.name entry inside OTEL_RESOURCE_ATTRIBUTES -- now sets the resource attribute service.name, overriding APP_NAME, in pkg/gofr/traces/exporters and pkg/gofr/metrics/exporters together. Moving one signal alone would report one name to the trace backend and another to the metric backend, breaking the trace-metric join; that is why gofr-dev#4206 kept APP_NAME and named this change as the way out. Each package resolves the name once through resource.Environment(), so the SDK's own ranking (OTEL_SERVICE_NAME ahead of OTEL_RESOURCE_ATTRIBUTES) is inherited rather than reimplemented, and the resolved value is always placed in attrs. The resource.Option order is unchanged -- WithFromEnv() before WithAttributes() -- so framework_version stays out of the environment's reach. An empty env value is not an override: OTEL_RESOURCE_ATTRIBUTES="service.name=" parses to a valid attribute with an empty value, which would otherwise ship a nameless service. An override is logged at info level from both packages, because tracing is off by default and a metrics-only app never reaches the tracing resource code. The instrumentation scope name (mp.Meter), the health endpoint, MCP and startup telemetry deliberately stay on APP_NAME: the scope name surfaces as the otel_scope_name label on every series, and the others describe the application rather than the OTel resource. Also substitutes noopLogger for a nil Logger in metrics' Build, matching what panicked, even though noopLogger has existed in the package all along documented as the fallback for callers that do not supply one.
|
Thanks for the thorough write-up and the mutation table — the SDK claims all check out against the pinned Before going line by line I would like your opinion on the direction, because my inclination is to keep the v1.62.0 behaviour:
I would also not add Is there a deployment or a user request behind this that I am missing? If there is a concrete case where setting If we do keep
One test note either way: with |
Description:
OTEL_SERVICE_NAME— and a non-emptyservice.nameentry insideOTEL_RESOURCE_ATTRIBUTES— now sets the OTel resource attributeservice.name, overridingAPP_NAME, in bothpkg/gofr/traces/exportersandpkg/gofr/metrics/exporters.This reverses the precedence decided in #4206, and deliberately so. That PR kept
APP_NAMEfor one reason:metrics/exportershad already shipped resolvingservice.namefromAPP_NAME, so flipping traces alone would have reported one name to the trace backend and another to the metric backend, breaking the trace↔metric join. Moving both signals in one PR is what that review left as the way out, and this is that PR. The standard variables now do what the OpenTelemetry specification says they do.How it works — resolve once, always set, rather than reordering the
resource.Options:resolveServiceName(appName, logger)readsresource.Environment(), so the SDK's own ranking (OTEL_SERVICE_NAMEahead of theOTEL_RESOURCE_ATTRIBUTESform) is inherited rather than reimplemented.Environment()runs only thefromEnvdetector, so it carries nounknown_service:<exe>default to mistake for an operator's value.WithFromEnv()thenWithAttributes(attrs...)). Simply movingWithFromEnv()last would be a one-line fix, but it would also let any environment key overwriteframework_version, which the current order protects on purpose. Only the value placed inattrschanges.OTEL_RESOURCE_ATTRIBUTES="service.name="parses to a valid attribute with an empty value (sdk/resource/env.go), and shipping that would leave the backend with a nameless service.OTEL_SERVICE_NAME=""is already trimmed to unset by the SDK, so only that form is exposed.APP_NAME. Both packages log because traces'Buildreturns the NeverSample provider beforebuildResourcewhenTRACE_EXPORTERis unset: a metrics-only app, which is the default, never reaches the tracing resource code.The helper is duplicated per package rather than shared. The two
buildResources are already deliberate near-duplicates with divergent bodies (traces omitsWithHostID, with a comment saying why), the packages do not import each other, and each defines its ownLoggerinterface — sharing would need a third logger type or ananyparameter, and would introduce the firstpkg/gofr/internal/...in the tree to save fifteen lines. Drift is mitigated by an identical six-row test table in both packages and a cross-reference comment in each helper.Left on
APP_NAMEon purpose, each with a comment:mp.Meter(cfg.AppName, ...)is the instrumentation scope name, which surfaces as theotel_scope_namelabel on every series — following the service name would silently rename a label on all metrics and break dashboards and recording rules for no conformance gain. The health endpoint, MCP and startup telemetry describe the application rather than the OTel resource, andOTEL_SERVICE_NAMEis specified as a resource attribute, so extending it there would be GoFr inventing semantics. The configs reference documents that divergence.Second change, same area:
metrics/exporters.Buildnow substitutesnoopLoggerfor a nilLogger, matching what #4206 did for traces.buildResourcelogs unguarded, soBuild(ctx, cfg, nil)panicked — even thoughnoopLoggerhas existed in the package all along, documented as the fallback for "callers that do not supply one", and was never applied. The deprecated exportedPrometheushelper is one such caller.Breaking Changes (if applicable):
No API changes — this is a behaviour change for one configuration.
OTEL_SERVICE_NAMEis not a new variable: the OpenTelemetry SDK already reads it, and in v1.61.0 both signals picked it up and then silently overwrote it withAPP_NAME(metrics via theWithAttributesafterWithFromEnv; traces becausesdktrace.WithResourcemergesresource.Environment()first and GoFr'sAPP_NAMEresource over it).Affected: deployments that set
OTEL_SERVICE_NAME(orservice.name=inOTEL_RESOURCE_ATTRIBUTES) to a value different fromAPP_NAME. For example,APP_NAME=orderswithOTEL_SERVICE_NAME=orders-prodreportedordersbefore and reportsorders-prodafter, to both the trace and the metric backend. Both signals change together, so the trace↔metric join is preserved. Unsetting the environment variable restores the previous name. Deployments that set onlyAPP_NAMEsee no change.Additional Information:
Mutation-checked by hand; each mutation was applied, the suites run, then reverted:
resolveServiceNamereverted tocfg.AppNameWithFromEnv()moved afterWithAttributes(...)framework_version is not overridableandempty service.name falls back to APP_NAME, per package)Loggerguard droppedTest_Build_substitutesNoopLoggerForANilLoggerpanicsCoverage:
metrics/exporters90.4% → 91.1%;traces/exporters97.2% → 97.1%. The traces figure is arithmetic, not a gap:warnIfEnvServiceNameIgnored(fully covered) was replaced by a slightly shorterresolveServiceName(also fully covered), and the only uncovered statements inbuildResourceare the pre-existingresource.Newerror branch and theres == nilfallback, neither of which this PR touches. All new code is covered.Docs updated: the distributed-tracing guide's resource-attributes section, the production-tracing checklist, and the configs reference (
APP_NAMEandOTEL_RESOURCE_ATTRIBUTESrows amended, a newOTEL_SERVICE_NAMErow added).No new dependencies.
Checklist:
goimportandgolangci-lint.