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" + }) + )) + }) +})