diff --git a/.changeset/explicit-otel-service-identity.md b/.changeset/explicit-otel-service-identity.md new file mode 100644 index 00000000000..a34030e3364 --- /dev/null +++ b/.changeset/explicit-otel-service-identity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prefer explicit OTLP resource configuration over environment configuration. diff --git a/packages/effect/src/unstable/observability/OtlpResource.ts b/packages/effect/src/unstable/observability/OtlpResource.ts index 8602e53ce9a..20ecffcfa27 100644 --- a/packages/effect/src/unstable/observability/OtlpResource.ts +++ b/packages/effect/src/unstable/observability/OtlpResource.ts @@ -70,11 +70,20 @@ export const make = (options: { * Creates an OTLP resource from explicit options and OpenTelemetry * configuration. * + * **When to use** + * + * Use when resource metadata may be configured in code or by the deployment + * environment. To let operators set the service identity, omit `serviceName`, + * `serviceVersion`, and their matching attributes, then use + * `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES`. + * * **Details** * - * `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, and - * `OTEL_SERVICE_VERSION` override explicit options; missing required - * configuration is converted to a defect. + * Explicit `serviceName` and `serviceVersion` options take precedence over + * matching explicit attributes. Explicit attributes take precedence over + * environment variables. `OTEL_SERVICE_NAME` and `OTEL_SERVICE_VERSION` take + * precedence over matching attributes in `OTEL_RESOURCE_ATTRIBUTES`. Missing + * required configuration is converted to a defect. * * @category constructors * @since 4.0.0 @@ -95,20 +104,20 @@ export const fromConfig: ( "OTEL_RESOURCE_ATTRIBUTES" ) - const serviceName = (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_NAME")) - ?? env?.["service.name"] as string | undefined + const serviceName = options?.serviceName ?? options?.attributes?.["service.name"] as string | undefined - ?? options?.serviceName + ?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_NAME")) + ?? env?.["service.name"] as string | undefined ?? (yield* Config.string("OTEL_SERVICE_NAME")) - const serviceVersion = (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_VERSION")) - ?? env?.["service.version"] as string | undefined + const serviceVersion = options?.serviceVersion ?? options?.attributes?.["service.version"] as string | undefined - ?? options?.serviceVersion + ?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_VERSION")) + ?? env?.["service.version"] as string | undefined const attributes = { - ...options?.attributes, - ...env + ...env, + ...options?.attributes } delete attributes["service.name"] diff --git a/packages/effect/test/unstable/observability/OtlpResource.test.ts b/packages/effect/test/unstable/observability/OtlpResource.test.ts index 5ca89675693..c4de1a60f01 100644 --- a/packages/effect/test/unstable/observability/OtlpResource.test.ts +++ b/packages/effect/test/unstable/observability/OtlpResource.test.ts @@ -7,20 +7,22 @@ const attributesRecord = (resource: OtlpResource.Resource): Record { describe("fromConfig", () => { - it.effect("uses OTEL service variables before explicit options", () => + it.effect("uses explicit service options before attributes and environment variables", () => Effect.gen(function*() { const resource = yield* OtlpResource.fromConfig({ serviceName: "explicit-service", serviceVersion: "explicit-version", attributes: { - "custom.attribute": "explicit" + "custom.attribute": "explicit", + "service.name": "explicit-attribute-service", + "service.version": "explicit-attribute-version" } }) assert.deepStrictEqual(attributesRecord(resource), { "custom.attribute": "explicit", - "service.name": "env-service", - "service.version": "env-version" + "service.name": "explicit-service", + "service.version": "explicit-version" }) }).pipe( Effect.provideService( @@ -28,17 +30,16 @@ describe("OtlpResource", () => { ConfigProvider.fromEnv({ env: { OTEL_SERVICE_NAME: "env-service", - OTEL_SERVICE_VERSION: "env-version" + OTEL_SERVICE_VERSION: "env-version", + OTEL_RESOURCE_ATTRIBUTES: "service.name=env-attribute-service,service.version=env-attribute-version" } }) ) )) - it.effect("uses OTEL resource attributes before explicit options", () => + it.effect("uses explicit attributes before environment variables", () => Effect.gen(function*() { const resource = yield* OtlpResource.fromConfig({ - serviceName: "explicit-service", - serviceVersion: "explicit-version", attributes: { "custom.attribute": "explicit", "service.name": "explicit-attribute-service", @@ -47,15 +48,17 @@ describe("OtlpResource", () => { }) assert.deepStrictEqual(attributesRecord(resource), { - "custom.attribute": "env", - "service.name": "env-attribute-service", - "service.version": "env-attribute-version" + "custom.attribute": "explicit", + "service.name": "explicit-attribute-service", + "service.version": "explicit-attribute-version" }) }).pipe( Effect.provideService( ConfigProvider.ConfigProvider, ConfigProvider.fromEnv({ env: { + OTEL_SERVICE_NAME: "env-service", + OTEL_SERVICE_VERSION: "env-version", OTEL_RESOURCE_ATTRIBUTES: "service.name=env-attribute-service,service.version=env-attribute-version,custom.attribute=env" } @@ -63,6 +66,27 @@ describe("OtlpResource", () => { ) )) + it.effect("uses dedicated service variables before OTEL resource attributes", () => + Effect.gen(function*() { + const resource = yield* OtlpResource.fromConfig() + + assert.deepStrictEqual(attributesRecord(resource), { + "service.name": "env-service", + "service.version": "env-version" + }) + }).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + OTEL_SERVICE_NAME: "env-service", + OTEL_SERVICE_VERSION: "env-version", + OTEL_RESOURCE_ATTRIBUTES: "service.name=env-attribute-service,service.version=env-attribute-version" + } + }) + ) + )) + it.effect("omits service.version when it is not configured", () => Effect.gen(function*() { const resource = yield* OtlpResource.fromConfig({