Skip to content
Merged
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/explicit-otel-service-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Prefer explicit OTLP resource configuration over environment configuration.
31 changes: 20 additions & 11 deletions packages/effect/src/unstable/observability/OtlpResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Comment on lines 108 to 114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate service attributes before using them as strings.

options.attributes accepts unknown values, so these assertions do not validate runtime types. For example, { "service.name": 123 } is passed to make despite serviceName requiring a string, producing invalid resource metadata. Use a runtime string check or schema validation before applying these attributes.

Proposed fix
+  const serviceNameAttribute = options?.attributes?.["service.name"]
+  const serviceVersionAttribute = options?.attributes?.["service.version"]
+
   const serviceName = options?.serviceName
-    ?? options?.attributes?.["service.name"] as string | undefined
+    ?? (typeof serviceNameAttribute === "string" ? serviceNameAttribute : undefined)
...
   const serviceVersion = options?.serviceVersion
-    ?? options?.attributes?.["service.version"] as string | undefined
+    ?? (typeof serviceVersionAttribute === "string" ? serviceVersionAttribute : undefined)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
?? 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
const serviceNameAttribute = options?.attributes?.["service.name"]
const serviceVersionAttribute = options?.attributes?.["service.version"]
const serviceName = options?.serviceName
?? (typeof serviceNameAttribute === "string" ? serviceNameAttribute : undefined)
?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_NAME"))
?? env?.["service.name"] as string | undefined
?? (yield* Config.string("OTEL_SERVICE_NAME"))
const serviceVersion = options?.serviceVersion
?? (typeof serviceVersionAttribute === "string" ? serviceVersionAttribute : undefined)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/effect/src/unstable/observability/OtlpResource.ts` around lines 109
- 115, Validate the "service.name" and "service.version" values read from
options.attributes at runtime before using them in the serviceName and
serviceVersion resolution in make. Only accept values that are strings;
otherwise skip them and continue to the existing configuration or environment
fallbacks, preserving the current precedence for valid attributes.

?? 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"]
Expand Down
46 changes: 35 additions & 11 deletions packages/effect/test/unstable/observability/OtlpResource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,39 @@ const attributesRecord = (resource: OtlpResource.Resource): Record<string, strin

describe("OtlpResource", () => {
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(
ConfigProvider.ConfigProvider,
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",
Expand All @@ -47,22 +48,45 @@ 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"
}
})
)
))

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({
Expand Down
Loading