effect-cf 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,6 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@cloudflare/vitest-pool-workers": "^0.15.0",
43
43
  "@cloudflare/workers-types": "^4.20260421.1",
44
+ "@effect/platform-node": "4.0.0-beta.70",
44
45
  "@effect/sql-d1": "catalog:",
45
46
  "@effect/sql-pg": "catalog:",
46
47
  "@effect/sql-sqlite-do": "catalog:",
@@ -0,0 +1,516 @@
1
+ import { Config, ConfigProvider, Context, Effect, Layer, Option, Schema } from "effect";
2
+ import type * as Duration from "effect/Duration";
3
+ import type * as Tracer from "effect/Tracer";
4
+ import { FetchHttpClient, type Headers } from "effect/unstable/http";
5
+ import {
6
+ Otlp,
7
+ OtlpLogger,
8
+ OtlpMetrics,
9
+ OtlpSerialization,
10
+ OtlpTracer,
11
+ } from "effect/unstable/observability";
12
+
13
+ import { DurableObjectState } from "./DurableObjectState";
14
+ import { WorkerConfig, WorkerEnvironment } from "./Environment";
15
+
16
+ /** Telemetry signal groups supported by the Cloudflare OTLP layers. */
17
+ export type Signal = "logs" | "traces" | "metrics";
18
+
19
+ /** OTLP payload serialization used by the Effect OTLP exporters. */
20
+ export type Serialization = "json" | "protobuf";
21
+
22
+ /**
23
+ * Resolved OpenTelemetry settings used by the Cloudflare OTLP layers.
24
+ *
25
+ * The default {@link settingsLayer} reads the standard OpenTelemetry
26
+ * environment variable names from `WorkerEnvironment`. Applications that use
27
+ * different binding names can provide {@link CloudflareOtlpSettings} directly
28
+ * with a custom layer.
29
+ */
30
+ export interface Settings {
31
+ /** Generic OTLP base endpoint, read from `OTEL_EXPORTER_OTLP_ENDPOINT`. */
32
+ readonly endpoint: Option.Option<string>;
33
+ /** Logs-specific OTLP endpoint, read from `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`. */
34
+ readonly logsEndpoint: Option.Option<string>;
35
+ /** Traces-specific OTLP endpoint, read from `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. */
36
+ readonly tracesEndpoint: Option.Option<string>;
37
+ /** Metrics-specific OTLP endpoint, read from `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`. */
38
+ readonly metricsEndpoint: Option.Option<string>;
39
+ /** Comma-separated OTLP headers, read from `OTEL_EXPORTER_OTLP_HEADERS`. */
40
+ readonly headers: Option.Option<string>;
41
+ /** Logs-specific OTLP headers, read from `OTEL_EXPORTER_OTLP_LOGS_HEADERS`. */
42
+ readonly logsHeaders: Option.Option<string>;
43
+ /** Traces-specific OTLP headers, read from `OTEL_EXPORTER_OTLP_TRACES_HEADERS`. */
44
+ readonly tracesHeaders: Option.Option<string>;
45
+ /** Metrics-specific OTLP headers, read from `OTEL_EXPORTER_OTLP_METRICS_HEADERS`. */
46
+ readonly metricsHeaders: Option.Option<string>;
47
+ /** Service name, read from `OTEL_SERVICE_NAME`. */
48
+ readonly serviceName: Option.Option<string>;
49
+ /** Service version, read from the effect-cf convenience alias `OTEL_SERVICE_VERSION`. */
50
+ readonly serviceVersion: Option.Option<string>;
51
+ /** Resource attributes, read from `OTEL_RESOURCE_ATTRIBUTES`. */
52
+ readonly resourceAttributes: Option.Option<Record<string, string>>;
53
+ }
54
+
55
+ /**
56
+ * Supported OpenTelemetry environment configuration for Cloudflare OTLP export.
57
+ *
58
+ * This config intentionally uses the conventional `OTEL_*` variable names so
59
+ * deployments can share the same settings used by other OpenTelemetry SDKs.
60
+ * It covers the OTLP HTTP settings supported by the Effect OTLP layers:
61
+ *
62
+ * - `OTEL_EXPORTER_OTLP_ENDPOINT`
63
+ * - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`
64
+ * - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
65
+ * - `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`
66
+ * - `OTEL_EXPORTER_OTLP_HEADERS`
67
+ * - `OTEL_EXPORTER_OTLP_LOGS_HEADERS`
68
+ * - `OTEL_EXPORTER_OTLP_TRACES_HEADERS`
69
+ * - `OTEL_EXPORTER_OTLP_METRICS_HEADERS`
70
+ * - `OTEL_SERVICE_NAME`
71
+ * - `OTEL_RESOURCE_ATTRIBUTES`
72
+ *
73
+ * `OTEL_SERVICE_VERSION` is also accepted as an effect-cf convenience alias.
74
+ * The standard OpenTelemetry way to configure service version is
75
+ * `OTEL_RESOURCE_ATTRIBUTES=service.version=...`.
76
+ *
77
+ * Consumers with non-standard binding names should map those names into
78
+ * {@link CloudflareOtlpSettings} rather than changing this config globally.
79
+ */
80
+ export const settingsConfig = Config.all({
81
+ endpoint: Config.string("OTEL_EXPORTER_OTLP_ENDPOINT").pipe(Config.option),
82
+ logsEndpoint: Config.string("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT").pipe(Config.option),
83
+ tracesEndpoint: Config.string("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").pipe(Config.option),
84
+ metricsEndpoint: Config.string("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT").pipe(Config.option),
85
+ headers: Config.string("OTEL_EXPORTER_OTLP_HEADERS").pipe(Config.option),
86
+ logsHeaders: Config.string("OTEL_EXPORTER_OTLP_LOGS_HEADERS").pipe(Config.option),
87
+ tracesHeaders: Config.string("OTEL_EXPORTER_OTLP_TRACES_HEADERS").pipe(Config.option),
88
+ metricsHeaders: Config.string("OTEL_EXPORTER_OTLP_METRICS_HEADERS").pipe(Config.option),
89
+ serviceName: Config.string("OTEL_SERVICE_NAME").pipe(Config.option),
90
+ serviceVersion: Config.string("OTEL_SERVICE_VERSION").pipe(Config.option),
91
+ resourceAttributes: Config.schema(
92
+ Config.Record(Schema.String, Schema.String),
93
+ "OTEL_RESOURCE_ATTRIBUTES",
94
+ ).pipe(Config.option),
95
+ });
96
+
97
+ /**
98
+ * Service carrying already-resolved Cloudflare OTLP settings.
99
+ *
100
+ * Provide this service directly when an application needs to map custom
101
+ * Cloudflare binding names, compute settings dynamically, or override settings
102
+ * in tests. When this service is present, the OTLP layers use it instead of
103
+ * reading {@link settingsConfig}.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const CustomOtlpSettings = Layer.effect(
108
+ * CloudflareOtlp.CloudflareOtlpSettings,
109
+ * Config.all({
110
+ * ...CloudflareOtlp.settingsConfig,
111
+ * endpoint: Config.string("MY_OTLP_ENDPOINT").pipe(Config.option),
112
+ * }),
113
+ * ).pipe(Layer.provide(WorkerConfig.layer));
114
+ * ```
115
+ */
116
+ export class CloudflareOtlpSettings extends Context.Service<CloudflareOtlpSettings, Settings>()(
117
+ "effect-cf/CloudflareOtlpSettings",
118
+ ) {}
119
+
120
+ /** Resource metadata shared by Worker and Durable Object OTLP layers. */
121
+ export interface ResourceOptions {
122
+ /** Explicit service name. Overrides `OTEL_SERVICE_NAME` when provided. */
123
+ readonly serviceName?: string;
124
+ /** Explicit service version. Overrides `OTEL_SERVICE_VERSION` when provided. */
125
+ readonly serviceVersion?: string;
126
+ /** Additional resource attributes attached to all exported telemetry. */
127
+ readonly attributes?: Record<string, unknown>;
128
+ }
129
+
130
+ /** Resource metadata specific to Cloudflare Workers. */
131
+ export interface WorkerResourceOptions extends ResourceOptions {
132
+ /** Cloudflare Worker name to attach as `cloudflare.worker.name`. */
133
+ readonly workerName?: string;
134
+ }
135
+
136
+ /** Resource metadata specific to Cloudflare Durable Objects. */
137
+ export interface DurableObjectResourceOptions extends ResourceOptions {
138
+ /** Durable Object class name to attach as `cloudflare.durable_object.class`. */
139
+ readonly className?: string;
140
+ /**
141
+ * Include the Durable Object id as `cloudflare.durable_object.id`.
142
+ *
143
+ * This is disabled by default because object ids can be high-cardinality.
144
+ */
145
+ readonly includeObjectId?: boolean;
146
+ }
147
+
148
+ /** Options shared by all Cloudflare OTLP telemetry layers. */
149
+ export interface LayerOptions extends ResourceOptions {
150
+ /** Telemetry signals to export. Defaults to logs, traces, and metrics. */
151
+ readonly signals?: ReadonlyArray<Signal>;
152
+ /** OTLP payload serialization. Defaults to protobuf. */
153
+ readonly serialization?: Serialization;
154
+ /** Maximum batch size for log and trace exporters. */
155
+ readonly maxBatchSize?: number;
156
+ /** Maximum time to wait for exporter shutdown/flush when a scope closes. */
157
+ readonly shutdownTimeout?: Duration.Input;
158
+ /** Export interval for the logs exporter. */
159
+ readonly loggerExportInterval?: Duration.Input;
160
+ /** Exclude log records emitted for spans when exporting logs. */
161
+ readonly loggerExcludeLogSpans?: boolean;
162
+ /** Merge the OTLP logger with existing loggers instead of replacing them. */
163
+ readonly loggerMergeWithExisting?: boolean;
164
+ /** Export interval for the traces exporter. */
165
+ readonly tracerExportInterval?: Duration.Input;
166
+ /** Custom trace context lookup used by the Effect OTLP tracer. */
167
+ readonly tracerContext?: <X>(primitive: Tracer.EffectPrimitive<X>, span: Tracer.AnySpan) => X;
168
+ /** Export interval for the metrics exporter. */
169
+ readonly metricsExportInterval?: Duration.Input;
170
+ /** Aggregation temporality for exported metrics. */
171
+ readonly metricsTemporality?: OtlpMetrics.AggregationTemporality;
172
+ }
173
+
174
+ const allSignals: ReadonlyArray<Signal> = ["logs", "traces", "metrics"];
175
+
176
+ const optionToUndefined = <A>(option: Option.Option<A>): A | undefined =>
177
+ Option.isSome(option) ? option.value : undefined;
178
+
179
+ const nonEmptyStringOption = (option: Option.Option<string>): Option.Option<string> =>
180
+ Option.flatMap(option, (value) => {
181
+ const trimmed = value.trim();
182
+ return trimmed === "" ? Option.none() : Option.some(trimmed);
183
+ });
184
+
185
+ const nonEmptyRecordOption = (
186
+ option: Option.Option<Record<string, string>>,
187
+ ): Option.Option<Record<string, string>> =>
188
+ Option.filter(option, (value) => Object.keys(value).length > 0);
189
+
190
+ const normalizeSettings = (settings: Settings): Settings => ({
191
+ endpoint: nonEmptyStringOption(settings.endpoint),
192
+ logsEndpoint: nonEmptyStringOption(settings.logsEndpoint),
193
+ tracesEndpoint: nonEmptyStringOption(settings.tracesEndpoint),
194
+ metricsEndpoint: nonEmptyStringOption(settings.metricsEndpoint),
195
+ headers: nonEmptyStringOption(settings.headers),
196
+ logsHeaders: nonEmptyStringOption(settings.logsHeaders),
197
+ tracesHeaders: nonEmptyStringOption(settings.tracesHeaders),
198
+ metricsHeaders: nonEmptyStringOption(settings.metricsHeaders),
199
+ serviceName: nonEmptyStringOption(settings.serviceName),
200
+ serviceVersion: nonEmptyStringOption(settings.serviceVersion),
201
+ resourceAttributes: nonEmptyRecordOption(settings.resourceAttributes),
202
+ });
203
+
204
+ /**
205
+ * Layer that reads standard OpenTelemetry settings from the current Cloudflare
206
+ * `env` object.
207
+ *
208
+ * The layer depends on {@link WorkerEnvironment} and installs a
209
+ * Cloudflare-backed Effect `ConfigProvider`, so `settingsConfig` reads from
210
+ * Worker vars/secrets instead of Node process environment variables.
211
+ */
212
+ export const settingsLayer: Layer.Layer<
213
+ CloudflareOtlpSettings,
214
+ Config.ConfigError,
215
+ WorkerEnvironment
216
+ > = Layer.effect(CloudflareOtlpSettings, settingsConfig.pipe(Config.map(normalizeSettings))).pipe(
217
+ Layer.provide(WorkerConfig.layer),
218
+ );
219
+
220
+ const selectedSignals = (signals: ReadonlyArray<Signal> | undefined): ReadonlySet<Signal> =>
221
+ new Set(signals ?? allSignals);
222
+
223
+ const withDefinedAttributes = (attributes: Record<string, unknown>): Record<string, unknown> => {
224
+ const out: Record<string, unknown> = {};
225
+ for (const [key, value] of Object.entries(attributes)) {
226
+ if (value !== undefined) {
227
+ out[key] = value;
228
+ }
229
+ }
230
+ return out;
231
+ };
232
+
233
+ const parseHeaders = (input: Option.Option<string>): Record<string, string> | undefined => {
234
+ if (Option.isNone(input) || input.value.trim() === "") {
235
+ return undefined;
236
+ }
237
+
238
+ const headers: Record<string, string> = {};
239
+ for (const part of input.value.split(",")) {
240
+ const index = part.indexOf("=");
241
+ if (index <= 0) {
242
+ continue;
243
+ }
244
+
245
+ const key = part.slice(0, index).trim();
246
+ const value = part.slice(index + 1).trim();
247
+ if (key !== "") {
248
+ headers[decodeHeaderComponent(key)] = decodeHeaderComponent(value);
249
+ }
250
+ }
251
+
252
+ return headers;
253
+ };
254
+
255
+ const mergeHeaders = (
256
+ common: Option.Option<string>,
257
+ signalSpecific: Option.Option<string>,
258
+ ): Headers.Input | undefined => {
259
+ const commonHeaders = parseHeaders(common);
260
+ const signalHeaders = parseHeaders(signalSpecific);
261
+ if (commonHeaders === undefined && signalHeaders === undefined) {
262
+ return undefined;
263
+ }
264
+
265
+ return {
266
+ ...commonHeaders,
267
+ ...signalHeaders,
268
+ };
269
+ };
270
+
271
+ const decodeHeaderComponent = (value: string): string => {
272
+ try {
273
+ return decodeURIComponent(value);
274
+ } catch {
275
+ return value;
276
+ }
277
+ };
278
+
279
+ const appendOtlpPath = (baseUrl: string, path: "/v1/logs" | "/v1/traces" | "/v1/metrics") =>
280
+ `${baseUrl.replace(/\/+$/, "")}${path}`;
281
+
282
+ const makeResource = (
283
+ settings: Settings,
284
+ options: LayerOptions,
285
+ runtimeAttributes: Record<string, unknown>,
286
+ ) => ({
287
+ serviceName: options.serviceName ?? optionToUndefined(settings.serviceName),
288
+ serviceVersion: options.serviceVersion ?? optionToUndefined(settings.serviceVersion),
289
+ attributes: {
290
+ ...optionToUndefined(settings.resourceAttributes),
291
+ ...withDefinedAttributes(runtimeAttributes),
292
+ ...options.attributes,
293
+ },
294
+ });
295
+
296
+ const commonOtlpOptions = (
297
+ settings: Settings,
298
+ options: LayerOptions,
299
+ runtimeAttributes: Record<string, unknown>,
300
+ ) => ({
301
+ resource: makeResource(settings, options, runtimeAttributes),
302
+ maxBatchSize: options.maxBatchSize,
303
+ loggerExportInterval: options.loggerExportInterval,
304
+ loggerExcludeLogSpans: options.loggerExcludeLogSpans,
305
+ loggerMergeWithExisting: options.loggerMergeWithExisting,
306
+ tracerExportInterval: options.tracerExportInterval,
307
+ tracerContext: options.tracerContext,
308
+ metricsExportInterval: options.metricsExportInterval,
309
+ metricsTemporality: options.metricsTemporality,
310
+ shutdownTimeout: options.shutdownTimeout,
311
+ });
312
+
313
+ const serializationLayer = (serialization: Serialization | undefined) =>
314
+ serialization === "json" ? OtlpSerialization.layerJson : OtlpSerialization.layerProtobuf;
315
+
316
+ const resolveSettings: Effect.Effect<Settings, Config.ConfigError> = Effect.gen(function* () {
317
+ const settings = yield* Effect.serviceOption(CloudflareOtlpSettings);
318
+ if (Option.isSome(settings)) {
319
+ return normalizeSettings(settings.value);
320
+ }
321
+
322
+ const env = yield* Effect.serviceOption(WorkerEnvironment);
323
+ if (Option.isSome(env)) {
324
+ const settings = yield* settingsConfig.pipe(
325
+ Effect.provide(ConfigProvider.layer(Effect.succeed(WorkerConfig.providerFromEnv(env.value)))),
326
+ );
327
+ return normalizeSettings(settings);
328
+ }
329
+
330
+ return normalizeSettings(yield* settingsConfig);
331
+ });
332
+
333
+ const mergeSignalLayers = (
334
+ layers: ReadonlyArray<Layer.Layer<never, never, never>>,
335
+ ): Layer.Layer<never, never, never> => {
336
+ if (layers.length === 0) {
337
+ return Layer.empty;
338
+ }
339
+
340
+ let merged = layers[0]!;
341
+ for (let index = 1; index < layers.length; index++) {
342
+ merged = Layer.mergeAll(merged, layers[index]!);
343
+ }
344
+ return merged;
345
+ };
346
+
347
+ const emptyLayer: Layer.Layer<never, never, never> = Layer.empty;
348
+
349
+ const makeLayerFromSettings = (
350
+ options: LayerOptions = {},
351
+ runtimeAttributes: Record<string, unknown> = {},
352
+ ): Layer.Layer<never, never, never> =>
353
+ Layer.unwrap(
354
+ Effect.map(resolveSettings, (settings) => {
355
+ const signals = selectedSignals(options.signals);
356
+ const endpoint = optionToUndefined(settings.endpoint);
357
+ const logsEndpoint = optionToUndefined(settings.logsEndpoint);
358
+ const tracesEndpoint = optionToUndefined(settings.tracesEndpoint);
359
+ const metricsEndpoint = optionToUndefined(settings.metricsEndpoint);
360
+ const hasSpecificEndpoint =
361
+ logsEndpoint !== undefined || tracesEndpoint !== undefined || metricsEndpoint !== undefined;
362
+ const hasSpecificHeaders =
363
+ Option.isSome(settings.logsHeaders) ||
364
+ Option.isSome(settings.tracesHeaders) ||
365
+ Option.isSome(settings.metricsHeaders);
366
+ const common = commonOtlpOptions(settings, options, runtimeAttributes);
367
+
368
+ if (
369
+ endpoint !== undefined &&
370
+ !hasSpecificEndpoint &&
371
+ !hasSpecificHeaders &&
372
+ signals.size === 3
373
+ ) {
374
+ return Otlp.layer({
375
+ baseUrl: endpoint,
376
+ headers: parseHeaders(settings.headers),
377
+ ...common,
378
+ }).pipe(
379
+ Layer.provide(serializationLayer(options.serialization)),
380
+ Layer.provide(FetchHttpClient.layer),
381
+ );
382
+ }
383
+
384
+ const layers: Array<Layer.Layer<never, never, never>> = [];
385
+ if (signals.has("logs")) {
386
+ const url =
387
+ logsEndpoint ??
388
+ (endpoint === undefined ? undefined : appendOtlpPath(endpoint, "/v1/logs"));
389
+ if (url !== undefined) {
390
+ layers.push(
391
+ OtlpLogger.layer({
392
+ url,
393
+ resource: common.resource,
394
+ headers: mergeHeaders(settings.headers, settings.logsHeaders),
395
+ exportInterval: common.loggerExportInterval,
396
+ maxBatchSize: common.maxBatchSize,
397
+ shutdownTimeout: common.shutdownTimeout,
398
+ excludeLogSpans: common.loggerExcludeLogSpans,
399
+ mergeWithExisting: common.loggerMergeWithExisting,
400
+ }).pipe(
401
+ Layer.provide(serializationLayer(options.serialization)),
402
+ Layer.provide(FetchHttpClient.layer),
403
+ ),
404
+ );
405
+ }
406
+ }
407
+
408
+ if (signals.has("traces")) {
409
+ const url =
410
+ tracesEndpoint ??
411
+ (endpoint === undefined ? undefined : appendOtlpPath(endpoint, "/v1/traces"));
412
+ if (url !== undefined) {
413
+ layers.push(
414
+ OtlpTracer.layer({
415
+ url,
416
+ resource: common.resource,
417
+ headers: mergeHeaders(settings.headers, settings.tracesHeaders),
418
+ exportInterval: common.tracerExportInterval,
419
+ maxBatchSize: common.maxBatchSize,
420
+ context: common.tracerContext,
421
+ shutdownTimeout: common.shutdownTimeout,
422
+ }).pipe(
423
+ Layer.provide(serializationLayer(options.serialization)),
424
+ Layer.provide(FetchHttpClient.layer),
425
+ ),
426
+ );
427
+ }
428
+ }
429
+
430
+ if (signals.has("metrics")) {
431
+ const url =
432
+ metricsEndpoint ??
433
+ (endpoint === undefined ? undefined : appendOtlpPath(endpoint, "/v1/metrics"));
434
+ if (url !== undefined) {
435
+ layers.push(
436
+ OtlpMetrics.layer({
437
+ url,
438
+ resource: common.resource,
439
+ headers: mergeHeaders(settings.headers, settings.metricsHeaders),
440
+ exportInterval: common.metricsExportInterval,
441
+ shutdownTimeout: common.shutdownTimeout,
442
+ temporality: common.metricsTemporality,
443
+ }).pipe(
444
+ Layer.provide(serializationLayer(options.serialization)),
445
+ Layer.provide(FetchHttpClient.layer),
446
+ ),
447
+ );
448
+ }
449
+ }
450
+
451
+ return mergeSignalLayers(layers);
452
+ }).pipe(
453
+ Effect.catch((error: Config.ConfigError) =>
454
+ Effect.logWarning("CloudflareOtlp disabled because OTLP configuration is invalid", {
455
+ error: String(error),
456
+ }).pipe(Effect.as(emptyLayer)),
457
+ ),
458
+ ),
459
+ );
460
+
461
+ const makeLayer = (
462
+ options: LayerOptions = {},
463
+ runtimeAttributes: Record<string, unknown> = {},
464
+ ): Layer.Layer<never, never, never> => makeLayerFromSettings(options, runtimeAttributes);
465
+
466
+ /**
467
+ * Base OTLP telemetry layer for Cloudflare-compatible runtimes.
468
+ *
469
+ * The layer reads {@link CloudflareOtlpSettings} when provided. Otherwise it
470
+ * reads standard OpenTelemetry config from `WorkerEnvironment` when available,
471
+ * then falls back to the ambient Effect `ConfigProvider`.
472
+ *
473
+ * When only `OTEL_EXPORTER_OTLP_ENDPOINT` is configured, per-signal paths are
474
+ * derived by appending `/v1/logs`, `/v1/traces`, and `/v1/metrics`. Signal
475
+ * specific endpoints are used as-is, matching the OpenTelemetry OTLP exporter
476
+ * convention.
477
+ */
478
+ export const layer = (options: LayerOptions = {}) => makeLayer(options);
479
+
480
+ /** Base OTLP telemetry layer forced to JSON serialization. */
481
+ export const layerJson = (options: Omit<LayerOptions, "serialization"> = {}) =>
482
+ layer({ ...options, serialization: "json" });
483
+
484
+ /** Base OTLP telemetry layer forced to protobuf serialization. */
485
+ export const layerProtobuf = (options: Omit<LayerOptions, "serialization"> = {}) =>
486
+ layer({ ...options, serialization: "protobuf" });
487
+
488
+ /**
489
+ * OTLP layer with Cloudflare Worker resource attributes.
490
+ *
491
+ * Provide this layer at runtime scope for long-lived metrics aggregation, or at
492
+ * handler scope when traces/logs should flush as the Cloudflare event finishes.
493
+ */
494
+ export const workerLayer = (options: LayerOptions & WorkerResourceOptions = {}) =>
495
+ makeLayer(options, {
496
+ "cloudflare.resource_type": "worker",
497
+ "cloudflare.worker.name": options.workerName,
498
+ });
499
+
500
+ /**
501
+ * OTLP layer with Cloudflare Durable Object resource attributes.
502
+ *
503
+ * The layer reads {@link DurableObjectState} so it can optionally include the
504
+ * Durable Object id. Prefer leaving `includeObjectId` disabled unless the
505
+ * backend can tolerate high-cardinality resource attributes.
506
+ */
507
+ export const durableObjectLayer = (options: LayerOptions & DurableObjectResourceOptions = {}) =>
508
+ Layer.unwrap(
509
+ Effect.map(DurableObjectState, (state) =>
510
+ makeLayer(options, {
511
+ "cloudflare.resource_type": "durable_object",
512
+ "cloudflare.durable_object.class": options.className,
513
+ "cloudflare.durable_object.id": options.includeObjectId ? state.id.toString() : undefined,
514
+ }),
515
+ ),
516
+ );