Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-otlp-resource-duplicate-service-attrs.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 7 additions & 1 deletion packages/opentelemetry/src/OtlpResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
38 changes: 38 additions & 0 deletions packages/opentelemetry/test/OtlpResource.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) =>
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"
})
))
})
})