effect-cf 0.13.1 → 0.15.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.
@@ -1,9 +1,7 @@
1
- import { Config, ConfigProvider, Context, Effect, Layer, Option, Schema } from "effect";
2
- import type * as Duration from "effect/Duration";
1
+ import { ConfigProvider, Effect, Layer, Option } from "effect";
3
2
  import type * as Tracer from "effect/Tracer";
4
3
  import { FetchHttpClient, type Headers } from "effect/unstable/http";
5
4
  import {
6
- Otlp,
7
5
  OtlpLogger,
8
6
  OtlpMetrics,
9
7
  OtlpSerialization,
@@ -19,122 +17,52 @@ export type Signal = "logs" | "traces" | "metrics";
19
17
  /** OTLP payload serialization used by the Effect OTLP exporters. */
20
18
  export type Serialization = "json" | "protobuf";
21
19
 
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
20
  /** Resource metadata shared by Worker and Durable Object OTLP layers. */
121
21
  export interface ResourceOptions {
122
- /** Explicit service name. Overrides `OTEL_SERVICE_NAME` when provided. */
22
+ /** Explicit service name. OTEL resource environment variables take precedence. */
123
23
  readonly serviceName?: string;
124
- /** Explicit service version. Overrides `OTEL_SERVICE_VERSION` when provided. */
24
+ /** Explicit service version. OTEL resource environment variables take precedence. */
125
25
  readonly serviceVersion?: string;
126
- /** Additional resource attributes attached to all exported telemetry. */
26
+ /** Additional resource attributes attached to exported telemetry. */
127
27
  readonly attributes?: Record<string, unknown>;
128
28
  }
129
29
 
30
+ /** Options shared by all Cloudflare OTLP telemetry layers. */
31
+ export interface LayerOptions {
32
+ /**
33
+ * Telemetry signals to export. Defaults to logs, traces, and metrics.
34
+ *
35
+ * Each selected signal is still controlled by standard OTEL exporter config,
36
+ * such as `OTEL_TRACES_EXPORTER=otlp`.
37
+ */
38
+ readonly signals?: ReadonlyArray<Signal>;
39
+ /** OTLP payload serialization. Defaults to protobuf. */
40
+ readonly serialization?: Serialization;
41
+ /** Resource metadata forwarded to Effect's OTLP resource resolver. */
42
+ readonly resource?: ResourceOptions;
43
+ /**
44
+ * Explicit headers for every selected signal.
45
+ *
46
+ * When omitted, Effect reads `OTEL_EXPORTER_OTLP_HEADERS` and the
47
+ * signal-specific `OTEL_EXPORTER_OTLP_*_HEADERS` variables.
48
+ */
49
+ readonly headers?: Headers.Input;
50
+ /** Exclude log records emitted for spans when exporting logs. */
51
+ readonly loggerExcludeLogSpans?: boolean;
52
+ /** Merge the OTLP logger with existing loggers instead of replacing them. */
53
+ readonly loggerMergeWithExisting?: boolean;
54
+ /** Custom trace context lookup used by the Effect OTLP tracer. */
55
+ readonly tracerContext?: <X>(primitive: Tracer.EffectPrimitive<X>, span: Tracer.AnySpan) => X;
56
+ }
57
+
130
58
  /** Resource metadata specific to Cloudflare Workers. */
131
- export interface WorkerResourceOptions extends ResourceOptions {
59
+ export interface WorkerLayerOptions extends LayerOptions {
132
60
  /** Cloudflare Worker name to attach as `cloudflare.worker.name`. */
133
61
  readonly workerName?: string;
134
62
  }
135
63
 
136
64
  /** Resource metadata specific to Cloudflare Durable Objects. */
137
- export interface DurableObjectResourceOptions extends ResourceOptions {
65
+ export interface DurableObjectLayerOptions extends LayerOptions {
138
66
  /** Durable Object class name to attach as `cloudflare.durable_object.class`. */
139
67
  readonly className?: string;
140
68
  /**
@@ -145,76 +73,15 @@ export interface DurableObjectResourceOptions extends ResourceOptions {
145
73
  readonly includeObjectId?: boolean;
146
74
  }
147
75
 
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
76
  const allSignals: ReadonlyArray<Signal> = ["logs", "traces", "metrics"];
175
77
 
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
- });
78
+ const emptyConfigProvider = ConfigProvider.fromUnknown({});
203
79
 
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),
80
+ const cloudflareConfigProviderLayer = ConfigProvider.layerAdd(
81
+ Effect.map(Effect.serviceOption(WorkerEnvironment), (env) =>
82
+ Option.isSome(env) ? WorkerConfig.providerFromEnv(env.value) : emptyConfigProvider,
83
+ ),
84
+ { asPrimary: true },
218
85
  );
219
86
 
220
87
  const selectedSignals = (signals: ReadonlyArray<Signal> | undefined): ReadonlySet<Signal> =>
@@ -230,109 +97,24 @@ const withDefinedAttributes = (attributes: Record<string, unknown>): Record<stri
230
97
  return out;
231
98
  };
232
99
 
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
100
  const makeResource = (
283
- settings: Settings,
284
101
  options: LayerOptions,
285
102
  runtimeAttributes: Record<string, unknown>,
286
- ) => ({
287
- serviceName: options.serviceName ?? optionToUndefined(settings.serviceName),
288
- serviceVersion: options.serviceVersion ?? optionToUndefined(settings.serviceVersion),
103
+ ): ResourceOptions => ({
104
+ serviceName: options.resource?.serviceName,
105
+ serviceVersion: options.resource?.serviceVersion,
289
106
  attributes: {
290
- ...optionToUndefined(settings.resourceAttributes),
291
107
  ...withDefinedAttributes(runtimeAttributes),
292
- ...options.attributes,
108
+ ...options.resource?.attributes,
293
109
  },
294
110
  });
295
111
 
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
112
  const serializationLayer = (serialization: Serialization | undefined) =>
314
113
  serialization === "json" ? OtlpSerialization.layerJson : OtlpSerialization.layerProtobuf;
315
114
 
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
- }
115
+ type SignalLayer = ReturnType<typeof OtlpLogger.layerFromConfig>;
321
116
 
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> => {
117
+ const mergeSignalLayers = (layers: ReadonlyArray<SignalLayer>): SignalLayer => {
336
118
  if (layers.length === 0) {
337
119
  return Layer.empty;
338
120
  }
@@ -344,136 +126,58 @@ const mergeSignalLayers = (
344
126
  return merged;
345
127
  };
346
128
 
347
- const emptyLayer: Layer.Layer<never, never, never> = Layer.empty;
348
-
349
- const makeLayerFromSettings = (
129
+ const makeLayer = (
350
130
  options: LayerOptions = {},
351
131
  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
- }
132
+ ): Layer.Layer<never, never, never> => {
133
+ const signals = selectedSignals(options.signals);
134
+ const resource = makeResource(options, runtimeAttributes);
135
+ const layers: Array<SignalLayer> = [];
136
+
137
+ if (signals.has("logs")) {
138
+ layers.push(
139
+ OtlpLogger.layerFromConfig({
140
+ resource,
141
+ headers: options.headers,
142
+ excludeLogSpans: options.loggerExcludeLogSpans,
143
+ mergeWithExisting: options.loggerMergeWithExisting,
144
+ }),
145
+ );
146
+ }
407
147
 
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
- }
148
+ if (signals.has("traces")) {
149
+ layers.push(
150
+ OtlpTracer.layerFromConfig({
151
+ resource,
152
+ headers: options.headers,
153
+ context: options.tracerContext,
154
+ }),
155
+ );
156
+ }
429
157
 
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
- }
158
+ if (signals.has("metrics")) {
159
+ layers.push(
160
+ OtlpMetrics.layerFromConfig({
161
+ resource,
162
+ headers: options.headers,
163
+ }),
164
+ );
165
+ }
450
166
 
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
- ),
167
+ return mergeSignalLayers(layers).pipe(
168
+ Layer.provide(cloudflareConfigProviderLayer),
169
+ Layer.provide(serializationLayer(options.serialization)),
170
+ Layer.provide(FetchHttpClient.layer),
459
171
  );
460
-
461
- const makeLayer = (
462
- options: LayerOptions = {},
463
- runtimeAttributes: Record<string, unknown> = {},
464
- ): Layer.Layer<never, never, never> => makeLayerFromSettings(options, runtimeAttributes);
172
+ };
465
173
 
466
174
  /**
467
175
  * Base OTLP telemetry layer for Cloudflare-compatible runtimes.
468
176
  *
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.
177
+ * Standard OpenTelemetry environment variables are resolved by Effect's OTLP
178
+ * layers. In Cloudflare Workers and Durable Objects, the current `env` object is
179
+ * installed as the primary `ConfigProvider`; outside Cloudflare, the ambient
180
+ * Effect `ConfigProvider` remains the fallback.
477
181
  */
478
182
  export const layer = (options: LayerOptions = {}) => makeLayer(options);
479
183
 
@@ -491,7 +195,7 @@ export const layerProtobuf = (options: Omit<LayerOptions, "serialization"> = {})
491
195
  * Provide this layer at runtime scope for long-lived metrics aggregation, or at
492
196
  * handler scope when traces/logs should flush as the Cloudflare event finishes.
493
197
  */
494
- export const workerLayer = (options: LayerOptions & WorkerResourceOptions = {}) =>
198
+ export const workerLayer = (options: WorkerLayerOptions = {}) =>
495
199
  makeLayer(options, {
496
200
  "cloudflare.resource_type": "worker",
497
201
  "cloudflare.worker.name": options.workerName,
@@ -504,7 +208,7 @@ export const workerLayer = (options: LayerOptions & WorkerResourceOptions = {})
504
208
  * Durable Object id. Prefer leaving `includeObjectId` disabled unless the
505
209
  * backend can tolerate high-cardinality resource attributes.
506
210
  */
507
- export const durableObjectLayer = (options: LayerOptions & DurableObjectResourceOptions = {}) =>
211
+ export const durableObjectLayer = (options: DurableObjectLayerOptions = {}) =>
508
212
  Layer.unwrap(
509
213
  Effect.map(DurableObjectState, (state) =>
510
214
  makeLayer(options, {
@@ -1,9 +1,9 @@
1
1
  import { DurableObject as CloudflareDurableObject } from "cloudflare:workers";
2
- import { Effect, Layer, ManagedRuntime, type Context, type Scope } from "effect";
2
+ import { ConfigProvider, Effect, Layer, ManagedRuntime, type Context, type Scope } from "effect";
3
3
  import type { Schema as S } from "effect";
4
4
 
5
5
  import { NativeRequest } from "./Worker";
6
- import { WorkerEnvironment, type WorkerEnv } from "./Environment";
6
+ import { WorkerConfig, WorkerEnvironment, type WorkerEnv } from "./Environment";
7
7
  import { DurableObjectState, fromDurableObjectState } from "./DurableObjectState";
8
8
  import { fromWebSocket, type DurableWebSocket } from "./DurableObjectWebSocket";
9
9
  import type * as Binding from "./Binding";
@@ -163,6 +163,7 @@ export const make = <
163
163
 
164
164
  const services = Layer.mergeAll(
165
165
  CloudflareClock.layer,
166
+ ConfigProvider.layer(Effect.succeed(WorkerConfig.providerFromEnv(env))),
166
167
  Layer.succeed(DurableObjectState, fromDurableObjectState(state)),
167
168
  Layer.succeed(WorkerEnvironment, env),
168
169
  );