1/**
2 * Builds OTLP resource metadata shared by exported telemetry.
3 *
4 * An OTLP resource describes the service and other attributes attached to every
5 * exported log, metric, or trace. This module builds resources from explicit
6 * options or OpenTelemetry environment variables and converts JavaScript values
7 * into OTLP attribute values.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Config from "../Config.ts"
13import * as Effect from "../Effect.ts"
14import { format } from "../Formatter.ts"
15import * as Schema from "../Schema.ts"
16
17/**
18 * OTLP resource metadata attached to exported logs, metrics, and traces.
19 *
20 * @stability unstable
21 * @category models
22 * @since 4.0.0
23 */
24export interface Resource {
25 /** Resource attributes */
26 attributes: Array<KeyValue>
27 /** Resource droppedAttributesCount */
28 droppedAttributesCount: number
29}
30
31/**
32 * Creates an OTLP resource from service metadata and additional attributes.
33 *
34 * **Details**
35 *
36 * The resource always includes `service.name`, includes `service.version` when
37 * provided, and converts custom attributes into OTLP attribute values.
38 *
39 * @stability unstable
40 * @category constructors
41 * @since 4.0.0
42 */
43export const make = (options: {
44 readonly serviceName: string
45 readonly serviceVersion?: string | undefined
46 readonly attributes?: Record<string, unknown> | undefined
47}): Resource => {
48 const resourceAttributes = options.attributes
49 ? entriesToAttributes(Object.entries(options.attributes))
50 : []
51 resourceAttributes.push({
52 key: "service.name",
53 value: {
54 stringValue: options.serviceName
55 }
56 })
57 if (options.serviceVersion) {
58 resourceAttributes.push({
59 key: "service.version",
60 value: {
61 stringValue: options.serviceVersion
62 }
63 })
64 }
65
66 return {
67 attributes: resourceAttributes,
68 droppedAttributesCount: 0
69 }
70}
71
72/**
73 * Creates an OTLP resource from explicit options and OpenTelemetry
74 * configuration.
75 *
76 * **When to use**
77 *
78 * Use when resource metadata may be configured in code or by the deployment
79 * environment. To let operators set the service identity, omit `serviceName`,
80 * `serviceVersion`, and their matching attributes, then use
81 * `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES`.
82 *
83 * **Details**
84 *
85 * Explicit `serviceName` and `serviceVersion` options take precedence over
86 * matching explicit attributes. Explicit attributes take precedence over
87 * environment variables. `OTEL_SERVICE_NAME` and `OTEL_SERVICE_VERSION` take
88 * precedence over matching attributes in `OTEL_RESOURCE_ATTRIBUTES`. Missing
89 * required configuration is converted to a defect.
90 *
91 * @stability unstable
92 * @category constructors
93 * @since 4.0.0
94 */
95export const fromConfig: (
96 options?: {
97 readonly serviceName?: string | undefined
98 readonly serviceVersion?: string | undefined
99 readonly attributes?: Record<string, unknown> | undefined
100 } | undefined
101) => Effect.Effect<Resource> = Effect.fnUntraced(function*(options?: {
102 readonly serviceName?: string | undefined
103 readonly serviceVersion?: string | undefined
104 readonly attributes?: Record<string, unknown> | undefined
105}) {
106 const env = yield* Config.Record(
107 Schema.StringFromUriComponent,
108 Schema.StringFromUriComponent,
109 "OTEL_RESOURCE_ATTRIBUTES"
110 ).pipe(Config.withDefault(undefined))
111
112 const serviceName = options?.serviceName
113 ?? options?.attributes?.["service.name"] as string | undefined
114 ?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_NAME"))
115 ?? env?.["service.name"] as string | undefined
116 ?? (yield* Config.String("OTEL_SERVICE_NAME"))
117
118 const serviceVersion = options?.serviceVersion
119 ?? options?.attributes?.["service.version"] as string | undefined
120 ?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_VERSION"))
121 ?? env?.["service.version"] as string | undefined
122
123 const attributes = {
124 ...env,
125 ...options?.attributes
126 }
127
128 delete attributes["service.name"]
129 delete attributes["service.version"]
130
131 return make({
132 serviceName,
133 serviceVersion,
134 attributes
135 })
136}, Effect.orDie)
137
138/**
139 * Returns the `service.name` attribute from an OTLP resource.
140 *
141 * **When to use**
142 *
143 * Use when an OTLP resource is known to contain a string `service.name` and
144 * throwing is acceptable if that invariant is broken.
145 *
146 * **Gotchas**
147 *
148 * Throws if the resource does not contain a string `service.name` attribute.
149 *
150 * @stability unstable
151 * @category attributes
152 * @since 4.0.0
153 */
154export const serviceNameUnsafe = (resource: Resource): string => {
155 const serviceNameAttribute = resource.attributes.find(
156 (attr) => attr.key === "service.name"
157 )
158 if (!serviceNameAttribute || !serviceNameAttribute.value.stringValue) {
159 throw new Error("Resource does not contain a service name")
160 }
161 return serviceNameAttribute.value.stringValue
162}
163
164/**
165 * Converts key/value entries into OTLP `KeyValue` attributes.
166 *
167 * @stability unstable
168 * @category attributes
169 * @since 4.0.0
170 */
171export const entriesToAttributes = (entries: Iterable<[string, unknown]>): Array<KeyValue> => {
172 const attributes: Array<KeyValue> = []
173 for (const [key, value] of entries) {
174 attributes.push({
175 key,
176 value: unknownToAttributeValue(value)
177 })
178 }
179 return attributes
180}
181
182/**
183 * Converts an arbitrary JavaScript value into an OTLP `AnyValue`.
184 *
185 * **Details**
186 *
187 * Arrays are converted recursively, primitive values use their matching OTLP
188 * fields, and unsupported values are formatted as strings.
189 *
190 * @stability unstable
191 * @category attributes
192 * @since 4.0.0
193 */
194export const unknownToAttributeValue = (value: unknown): AnyValue => {
195 if (Array.isArray(value)) {
196 return {
197 arrayValue: {
198 values: value.map(unknownToAttributeValue)
199 }
200 }
201 }
202 switch (typeof value) {
203 case "string":
204 return {
205 stringValue: value
206 }
207 case "bigint":
208 return {
209 intValue: String(value)
210 }
211 case "number":
212 return Number.isInteger(value)
213 ? {
214 intValue: value
215 }
216 : {
217 doubleValue: value
218 }
219 case "boolean":
220 return {
221 boolValue: value
222 }
223 default:
224 return {
225 stringValue: format(value)
226 }
227 }
228}
229
230/**
231 * An OTLP attribute represented as a string key and typed value.
232 *
233 * @stability unstable
234 * @category models
235 * @since 4.0.0
236 */
237export interface KeyValue {
238 /** KeyValue key */
239 key: string
240 /** KeyValue value */
241 value: AnyValue
242}
243
244/**
245 * OTLP `AnyValue` payload for scalar, array, key/value-list, or byte values.
246 *
247 * @stability unstable
248 * @category models
249 * @since 4.0.0
250 */
251export interface AnyValue {
252 /** AnyValue stringValue */
253 stringValue?: string | null
254 /** AnyValue boolValue */
255 boolValue?: boolean | null
256 /** AnyValue intValue */
257 intValue?: string | number | null
258 /** AnyValue doubleValue */
259 doubleValue?: number | null
260 /** AnyValue arrayValue */
261 arrayValue?: ArrayValue
262 /** AnyValue kvlistValue */
263 kvlistValue?: KeyValueList
264 /** AnyValue bytesValue */
265 bytesValue?: Uint8Array
266}
267
268/**
269 * OTLP array value containing nested `AnyValue` entries.
270 *
271 * @stability unstable
272 * @category models
273 * @since 4.0.0
274 */
275export interface ArrayValue {
276 /** ArrayValue values */
277 values: Array<AnyValue>
278}
279
280/**
281 * OTLP key/value-list value containing nested attributes.
282 *
283 * @stability unstable
284 * @category models
285 * @since 4.0.0
286 */
287export interface KeyValueList {
288 /** KeyValueList values */
289 values: Array<KeyValue>
290}
291
292/**
293 * Low and high 32-bit parts of a 64-bit integer value.
294 *
295 * @stability unstable
296 * @category models
297 * @since 4.0.0
298 */
299export interface LongBits {
300 low: number
301 high: number
302}
303
304/**
305 * Accepted runtime representations for an OTLP/protobuf fixed 64-bit value.
306 *
307 * @stability unstable
308 * @category models
309 * @since 4.0.0
310 */
311export type Fixed64 = LongBits | string | number