From 24465866a2e6187a089b8eec37c3a4177713bed2 Mon Sep 17 00:00:00 2001 From: Jeremie Dayan Date: Mon, 29 Jun 2026 23:37:28 +0200 Subject: [PATCH] fix(opentelemetry): avoid duplicate service.name/service.version in OtlpResource.fromConfig When service.name and/or service.version are set in OTEL_RESOURCE_ATTRIBUTES, fromConfig passed them through to `make`, which also appends them explicitly, producing duplicate KeyValue entries. Filter those keys out of the attributes record before calling `make`. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...x-otlp-resource-duplicate-service-attrs.md | 5 +++ packages/opentelemetry/src/OtlpResource.ts | 8 +++- .../opentelemetry/test/OtlpResource.test.ts | 38 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-otlp-resource-duplicate-service-attrs.md create mode 100644 packages/opentelemetry/test/OtlpResource.test.ts diff --git a/.changeset/fix-otlp-resource-duplicate-service-attrs.md b/.changeset/fix-otlp-resource-duplicate-service-attrs.md new file mode 100644 index 00000000000..078f2c2a8c3 --- /dev/null +++ b/.changeset/fix-otlp-resource-duplicate-service-attrs.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Fix `OtlpResource.fromConfig` duplicating `service.name`/`service.version` attributes when they are provided via `OTEL_RESOURCE_ATTRIBUTES`. These keys are now filtered out of the attributes record before being passed to `make`, which already adds them explicitly. diff --git a/packages/opentelemetry/src/OtlpResource.ts b/packages/opentelemetry/src/OtlpResource.ts index ac72cbee6f5..ed4c4df833e 100644 --- a/packages/opentelemetry/src/OtlpResource.ts +++ b/packages/opentelemetry/src/OtlpResource.ts @@ -5,6 +5,7 @@ import * as Arr from "effect/Array" import * as Config from "effect/Config" import * as Effect from "effect/Effect" import * as Inspectable from "effect/Inspectable" +import * as Record from "effect/Record" const ATTR_SERVICE_NAME = "service.name" const ATTR_SERVICE_VERSION = "service.version" @@ -90,10 +91,15 @@ export const fromConfig: ( (yield* Config.string("OTEL_SERVICE_NAME")) const serviceVersion = options?.serviceVersion ?? attributes[ATTR_SERVICE_VERSION] as string ?? (yield* Config.string("OTEL_SERVICE_VERSION").pipe(Config.withDefault(undefined))) + // service.name and service.version are added explicitly by `make`, so filter + // them out of the attributes record to avoid duplicate entries. return make({ serviceName, serviceVersion, - attributes + attributes: Record.filter( + attributes, + (_, key) => key !== ATTR_SERVICE_NAME && key !== ATTR_SERVICE_VERSION + ) }) }, Effect.orDie) diff --git a/packages/opentelemetry/test/OtlpResource.test.ts b/packages/opentelemetry/test/OtlpResource.test.ts new file mode 100644 index 00000000000..3d87d24fa91 --- /dev/null +++ b/packages/opentelemetry/test/OtlpResource.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "@effect/vitest" +import * as ConfigProvider from "effect/ConfigProvider" +import * as Effect from "effect/Effect" +import * as OtlpResource from "../src/OtlpResource.js" + +const withConfig = (map: Record) => + Effect.withConfigProvider(ConfigProvider.fromMap(new Map(Object.entries(map)))) + +describe("OtlpResource", () => { + describe("fromConfig", () => { + it.effect("does not duplicate service.name/service.version from OTEL_RESOURCE_ATTRIBUTES", () => + Effect.gen(function*() { + const resource = yield* OtlpResource.fromConfig() + const serviceNameAttrs = resource.attributes.filter((attr) => attr.key === "service.name") + const serviceVersionAttrs = resource.attributes.filter((attr) => attr.key === "service.version") + expect(serviceNameAttrs).toHaveLength(1) + expect(serviceNameAttrs[0].value.stringValue).toBe("my-service") + expect(serviceVersionAttrs).toHaveLength(1) + expect(serviceVersionAttrs[0].value.stringValue).toBe("1.2.3") + }).pipe( + withConfig({ + OTEL_RESOURCE_ATTRIBUTES: "service.name=my-service,service.version=1.2.3,deployment.environment=test" + }) + )) + + it.effect("keeps other attributes from OTEL_RESOURCE_ATTRIBUTES", () => + Effect.gen(function*() { + const resource = yield* OtlpResource.fromConfig() + const envAttr = resource.attributes.filter((attr) => attr.key === "deployment.environment") + expect(envAttr).toHaveLength(1) + expect(envAttr[0].value.stringValue).toBe("test") + }).pipe( + withConfig({ + OTEL_RESOURCE_ATTRIBUTES: "service.name=my-service,deployment.environment=test" + }) + )) + }) +})