1/**
2 * OpenTelemetry resource service and layers.
3 *
4 * An OpenTelemetry resource identifies the process or service that emits spans,
5 * metrics, and logs. This module stores that resource in Effect context and
6 * provides layers for creating it from explicit service metadata, from
7 * `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES`, or as an empty resource.
8 * It also includes `configToAttributes` for turning service metadata into raw
9 * OpenTelemetry attributes.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type * as OtelApi from "@opentelemetry/api"
15import * as Resources from "@opentelemetry/resources"
16import * as OtelSemConv from "@opentelemetry/semantic-conventions"
17import * as Arr from "effect/Array"
18import * as Config from "effect/Config"
19import * as Context from "effect/Context"
20import * as Effect from "effect/Effect"
21import * as Layer from "effect/Layer"
22import * as Rec from "effect/Record"
23
24/**
25 * Service tag for OpenTelemetry metadata attached to emitted telemetry.
26 *
27 * **When to use**
28 *
29 * Use to provide process, service, and deployment metadata that should be
30 * attached to spans, metrics, and logs.
31 *
32 * @stability unstable
33 * @category services
34 * @since 4.0.0
35 */
36export class Resource extends Context.Service<
37 Resource,
38 Resources.Resource
39>()("@effect/opentelemetry/Resource") {}
40
41/**
42 * Creates a `Resource` layer from service metadata and additional OpenTelemetry attributes.
43 *
44 * @stability unstable
45 * @category layers
46 * @since 4.0.0
47 */
48export const layer = (config: {
49 readonly serviceName: string
50 readonly serviceVersion?: string
51 readonly attributes?: OtelApi.Attributes
52}) =>
53 Layer.succeed(
54 Resource,
55 Resources.resourceFromAttributes(configToAttributes(config))
56 )
57
58/**
59 * Converts resource configuration into OpenTelemetry attributes, adding service name, optional service version, and telemetry SDK metadata.
60 *
61 * **When to use**
62 *
63 * Use to turn explicit service metadata into a raw OpenTelemetry attribute map
64 * for lower-level resource construction or merging with environment-derived
65 * attributes via `layerFromEnv`.
66 *
67 * **Details**
68 *
69 * The returned record copies `attributes` first, then sets `service.name`,
70 * `telemetry.sdk.name`, and `telemetry.sdk.language`. `service.version` is
71 * included only when `serviceVersion` is provided.
72 *
73 * **Gotchas**
74 *
75 * Custom values for `service.name` and `telemetry.sdk.*` are overwritten by this
76 * helper. An empty `serviceVersion` is treated as absent.
77 *
78 * @see {@link layer} for creating a `Resource` layer from explicit metadata
79 * @see {@link layerFromEnv} for merging attributes with OpenTelemetry environment variables
80 *
81 * @stability unstable
82 * @category converting
83 * @since 4.0.0
84 */
85export const configToAttributes = (options: {
86 readonly serviceName: string
87 readonly serviceVersion?: string
88 readonly attributes?: OtelApi.Attributes
89}): Record<string, string> => {
90 const attributes: Record<string, string> = {
91 ...(options.attributes ?? undefined),
92 [OtelSemConv.ATTR_SERVICE_NAME]: options.serviceName,
93 [OtelSemConv.ATTR_TELEMETRY_SDK_NAME]: "@effect/opentelemetry",
94 [OtelSemConv.ATTR_TELEMETRY_SDK_LANGUAGE]: typeof (globalThis as any).document === "undefined"
95 ? OtelSemConv.TELEMETRY_SDK_LANGUAGE_VALUE_NODEJS
96 : OtelSemConv.TELEMETRY_SDK_LANGUAGE_VALUE_WEBJS
97 }
98 if (options.serviceVersion) {
99 attributes[OtelSemConv.ATTR_SERVICE_VERSION] = options.serviceVersion
100 }
101 return attributes
102}
103
104/**
105 * Creates a `Resource` layer from OpenTelemetry environment variables, optionally merging additional attributes.
106 *
107 * @stability unstable
108 * @category layers
109 * @since 4.0.0
110 */
111export const layerFromEnv = (
112 additionalAttributes?:
113 | OtelApi.Attributes
114 | undefined
115): Layer.Layer<Resource> =>
116 Layer.effect(
117 Resource,
118 Effect.gen(function*() {
119 const serviceName = yield* Config.option(Config.String("OTEL_SERVICE_NAME"))
120 const attributes = yield* Config.String("OTEL_RESOURCE_ATTRIBUTES").pipe(
121 Config.withDefault(""),
122 Config.map((s) => {
123 const attrs = s.split(",")
124 return Arr.reduce(attrs, {} as OtelApi.Attributes, (acc, attr) => {
125 const parts = attr.split("=")
126 if (parts.length !== 2) {
127 return acc
128 }
129 Rec.assignProperty(acc, parts[0].trim(), parts[1].trim())
130 return acc
131 })
132 })
133 )
134 if (serviceName._tag === "Some") {
135 attributes[OtelSemConv.ATTR_SERVICE_NAME] = serviceName.value
136 }
137 return Resources.resourceFromAttributes({
138 ...attributes,
139 ...additionalAttributes
140 })
141 }).pipe(Effect.orDie)
142 )
143
144/**
145 * Layer that provides an empty OpenTelemetry resource.
146 *
147 * @stability unstable
148 * @category layers
149 * @since 4.0.0
150 */
151export const layerEmpty = Layer.succeed(
152 Resource,
153 Resources.emptyResource()
154)