effect-cf 0.42.1 → 0.44.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/CHANGELOG.md +48 -0
- package/README.md +77 -7
- package/dist/AnalyticsEngine.mjs +3 -3
- package/dist/CloudflareTracer.d.mts +57 -12
- package/dist/CloudflareTracer.mjs +176 -23
- package/dist/Environment.mjs +5 -5
- package/dist/Worker.mjs +3 -2
- package/package.json +5 -5
- package/src/AnalyticsEngine.ts +3 -3
- package/src/CloudflareTracer.ts +285 -49
- package/src/Environment.ts +5 -5
- package/src/Worker.ts +11 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#178](https://github.com/danieljvdm/effect-cf/pull/178) [`9551619`](https://github.com/danieljvdm/effect-cf/commit/95516194672b7245ce3db77d3ee3aa0359901ebc) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Preserve more Effect telemetry in Cloudflare's native spans: bounded JSON strings for structured attributes, searchable Effect and parent IDs, span kind, up to eight link ID pairs, and safe failure/defect/interruption classification. The `effect.*` attribute namespace is now reserved for adapter metadata; move application annotations using that prefix to an application namespace for native export.
|
|
8
|
+
|
|
9
|
+
Add `CloudflareTracer.layerWith({ formatError, spanEvents })` while retaining `CloudflareTracer.layer`. Error details require a caller-supplied sanitizing formatter. Event forwarding is off by default; opting in emits bounded structured logs in the owning span's async context and adds log volume. Unsupported or oversized metadata and formatter/logging failures cannot fail application operations. Correlation does not change Cloudflare trace IDs, parentage, native status, or graph links.
|
|
10
|
+
|
|
11
|
+
## 0.43.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- [#174](https://github.com/danieljvdm/effect-cf/pull/174) [`10cd4a9`](https://github.com/danieljvdm/effect-cf/commit/10cd4a9cd162ef38599a53c772cdb19f8e241605) Thanks [@danieljvdm](https://github.com/danieljvdm)! - ## effect-cf
|
|
16
|
+
|
|
17
|
+
Require Effect `^4.0.0-rc.115` and matching `@effect/sql-d1`, `@effect/sql-pg`, and
|
|
18
|
+
`@effect/sql-sqlite-do` peers. Upgrade the Effect family together.
|
|
19
|
+
|
|
20
|
+
Preserve Cloudflare span context with Effect's new fiber representation and close
|
|
21
|
+
request scopes when HEAD or status 204, 205, or 304 omits an Effect streaming body.
|
|
22
|
+
Omitted streams are not started, and request resources and telemetry are finalized.
|
|
23
|
+
|
|
24
|
+
HyperdrivePg now uses Effect's native PostgreSQL driver. Consumers must account for
|
|
25
|
+
its new result codecs (`int8` becomes `bigint`, dates become strings, and timestamps
|
|
26
|
+
become epoch milliseconds), wrap JSON parameters with `sql.json`, and submit one
|
|
27
|
+
statement per query. The driver enables named prepared statements by default;
|
|
28
|
+
pass `prepare: false` for poolers that cannot preserve them.
|
|
29
|
+
|
|
30
|
+
## effect-webtransport
|
|
31
|
+
|
|
32
|
+
Require Effect `^4.0.0-rc.115` and migrate the Socket adapter to scoped readers and
|
|
33
|
+
writers. Replace `socket.run(handler)` with a scoped acquisition of `socket.reader`
|
|
34
|
+
and a loop over `reader.pull`. Use `writer.write` and `writer.writeAll` after
|
|
35
|
+
acquiring `socket.writer`.
|
|
36
|
+
|
|
37
|
+
Reads now follow consumer demand without dispatching a fiber per chunk. Reader
|
|
38
|
+
scope cleanup still releases stream locks, sends FIN on success, and aborts writes
|
|
39
|
+
on failure. Releasing a writer scope half-closes the write side. Interrupting a
|
|
40
|
+
batch stops unsent frames while leaving the socket available for later writes.
|
|
41
|
+
|
|
42
|
+
Every close, including peer FIN, fails with a `SocketError` containing
|
|
43
|
+
`SocketCloseError`. Remove `closeCodeIsError`, `FromBidirectionalStreamOptions`,
|
|
44
|
+
and `defaultCloseCodeIsError`; handle close reasons in the read loop and use
|
|
45
|
+
`Effect.retry` around the scoped loop to reconnect. WebSocket fallback options now
|
|
46
|
+
accept `highWaterMark`.
|
|
47
|
+
|
|
48
|
+
MessagePack serialization was removed upstream. Use `RpcSerialization.layerNdjson`
|
|
49
|
+
or `RpcSerialization.layerSchemaBinary()` for the byte-stream transport.
|
|
50
|
+
|
|
3
51
|
## 0.42.1
|
|
4
52
|
|
|
5
53
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Cloudflare entrypoints and bindings as Effect services.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install effect-cf "effect@^4.0.0-rc.
|
|
6
|
+
npm install effect-cf "effect@^4.0.0-rc.115"
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
The repository tests against workerd `1.20260825.1` and `@cloudflare/workers-types@5.20260825.1`. Use `compatibility_date: "2026-08-25"` in Wrangler.
|
|
@@ -120,12 +120,82 @@ Nested spans, concurrent fibers, and resumed work restore the appropriate span
|
|
|
120
120
|
context. Cloudflare handles sampling and export; no exporter endpoint or flush
|
|
121
121
|
is needed.
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
Strings, finite numbers, and booleans are forwarded as native scalars. Null,
|
|
124
|
+
plain objects (including null-prototype objects), and dense arrays are encoded
|
|
125
|
+
as JSON strings under the original attribute key. For example,
|
|
126
|
+
`{ routes: ["email", "sms"], retry: false }` becomes the string
|
|
127
|
+
`'{"routes":["email","sms"],"retry":false}'`, not native object/array metadata.
|
|
128
|
+
There is no flattening or generated attribute key per nested field.
|
|
129
|
+
|
|
130
|
+
Each encoded value is limited to **4096 UTF-8 bytes, four container levels, and
|
|
131
|
+
64 values including the root**. Keys and JSON punctuation count toward bytes.
|
|
132
|
+
The entire value is dropped if it exceeds a limit or contains unsupported input:
|
|
133
|
+
cycles, accessors, undefined, non-finite numbers, bigint, symbols, functions,
|
|
134
|
+
class instances (including errors and Effect `Redacted`), or sparse arrays.
|
|
135
|
+
Only own enumerable string keys and array elements are included; getters and
|
|
136
|
+
`toJSON` are never called. Serialization/host errors are ignored. Dropped updates
|
|
137
|
+
leave any previous native attribute value intact. Original Effect-local values
|
|
138
|
+
remain available. These are adapter limits; Cloudflare can apply further limits.
|
|
139
|
+
Existing scalar strings are forwarded unchanged.
|
|
140
|
+
|
|
141
|
+
The adapter reserves **`effect.*`**; caller attributes in that namespace remain
|
|
142
|
+
Effect-local and cannot overwrite its metadata. Existing annotations using that
|
|
143
|
+
prefix should move to an application namespace if they need native export.
|
|
144
|
+
|
|
145
|
+
| Native attribute | Representation |
|
|
146
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
147
|
+
| `effect.trace_id`, `effect.span_id` | Effect IDs, distinct from Cloudflare's opaque IDs |
|
|
148
|
+
| `effect.parent.trace_id`, `effect.parent.span_id` | Effect parent IDs, when present |
|
|
149
|
+
| `effect.span.kind` | Effect's internal/server/client/producer/consumer kind |
|
|
150
|
+
| `effect.span.links` | JSON array of `{ traceId, spanId }` for the first eight links at span end, including `addLinks` updates |
|
|
151
|
+
| `effect.span.links_dropped` | Omitted link count; the candidate array is dropped whole if it exceeds the JSON limits |
|
|
152
|
+
| `effect.exit` | `success`, `failure`, or `interrupted` |
|
|
153
|
+
| `effect.error.kind` | On failure: `failure`, `defect`, `mixed` (typed failures and defects), or `interrupted` (interruptions only) |
|
|
154
|
+
|
|
155
|
+
Link attributes remain local. Correlation and error metadata add no spans or
|
|
156
|
+
logs. No Cause, error payload, message, stack, schema input, or request body is
|
|
157
|
+
automatically serialized for errors. Optional diagnostics require an explicit
|
|
158
|
+
sanitizing formatter:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const tracing = CloudflareTracer.layerWith({
|
|
162
|
+
formatError: () => ({
|
|
163
|
+
type: "OperationFailed",
|
|
164
|
+
message: "The operation could not complete",
|
|
165
|
+
}),
|
|
166
|
+
spanEvents: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export default Worker.make(Layer.empty, {
|
|
170
|
+
eventLayer: tracing,
|
|
171
|
+
fetch: Effect.sync(() => new Response("Hello")).pipe(Effect.withSpan("greet")),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`formatError` receives the failed span's Cause, including interruption, and may
|
|
176
|
+
return `undefined` or sanitized `type`/`message` strings. These become
|
|
177
|
+
`effect.error.type` and `effect.error.message`; each is dropped above 4096 UTF-8
|
|
178
|
+
bytes. Extra returned fields are ignored. The formatter runs once at end only
|
|
179
|
+
for sampled failed spans; formatter exceptions cannot change the application exit.
|
|
180
|
+
Do not pass through raw errors or use a full Cause formatter here.
|
|
181
|
+
|
|
182
|
+
`spanEvents` defaults to `false`. Opting in forwards at most the first **16 event
|
|
183
|
+
attempts per span** through `console.log` in that span's captured async context.
|
|
184
|
+
Each structured log contains `effect.event` (the original name),
|
|
185
|
+
`effect.event.time_unix_nano` (the original bigint timestamp as a decimal string),
|
|
186
|
+
`effect.trace_id`, `effect.span_id`, and nested `attributes`. The complete log
|
|
187
|
+
uses the same JSON limits, including its envelope; an unsupported/oversized log
|
|
188
|
+
is dropped whole and still consumes one attempt. Unsampled and ended spans emit
|
|
189
|
+
no event logs. Reentrant forwarding and console failures are suppressed. This
|
|
190
|
+
adds log volume and may increase Observability costs; log retention/export also
|
|
191
|
+
depends on your Cloudflare logging configuration.
|
|
192
|
+
|
|
193
|
+
This is searchable correlation and optional **log forwarding**. Cloudflare's
|
|
194
|
+
custom span API does not expose native IDs, manual parent wiring, an outcome
|
|
195
|
+
setter, `addLink`, or `addEvent`. It does not create native graph edges, clickable
|
|
196
|
+
links, native OTel span events, or cross-system propagation. Explicit external
|
|
197
|
+
parents cannot join a Cloudflare trace by ID, and `root: true` starts under the
|
|
198
|
+
invocation's captured context.
|
|
129
199
|
|
|
130
200
|
This layer replaces the active Effect tracer. When combining it with
|
|
131
201
|
`CloudflareOtlp`, select only `logs` and/or `metrics` in the OTLP layer. See
|
package/dist/AnalyticsEngine.mjs
CHANGED
|
@@ -235,9 +235,9 @@ const makeClient = (definition, defaults) => (dataset) => {
|
|
|
235
235
|
};
|
|
236
236
|
};
|
|
237
237
|
const queryConfig = (options) => Config.all({
|
|
238
|
-
accountId: options?.accountId ?? Config.
|
|
239
|
-
apiToken: options?.apiToken ?? Config.
|
|
240
|
-
apiBaseUrl: options?.apiBaseUrl ?? Config.
|
|
238
|
+
accountId: options?.accountId ?? Config.String("CLOUDFLARE_ACCOUNT_ID"),
|
|
239
|
+
apiToken: options?.apiToken ?? Config.Redacted("CLOUDFLARE_API_TOKEN"),
|
|
240
|
+
apiBaseUrl: options?.apiBaseUrl ?? Config.String("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl))
|
|
241
241
|
});
|
|
242
242
|
const queryLayer = (tag, definition) => Layer.effect(tag, makeQueryClient(definition));
|
|
243
243
|
const layerFetch = (tag, definition) => queryLayer(tag, definition).pipe(Layer.provide(FetchHttpClient.layer));
|
|
@@ -1,20 +1,65 @@
|
|
|
1
1
|
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
2
3
|
declare namespace CloudflareTracer_d_exports {
|
|
3
|
-
export { layer };
|
|
4
|
+
export { ErrorDetails, LayerOptions, layer, layerWith };
|
|
4
5
|
}
|
|
6
|
+
/** Explicitly sanitized diagnostics. No error payload or Cause is serialized automatically. */
|
|
7
|
+
interface ErrorDetails {
|
|
8
|
+
readonly type?: string;
|
|
9
|
+
readonly message?: string;
|
|
10
|
+
}
|
|
11
|
+
interface LayerOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Called once for a sampled failed span, including interruption. Return only
|
|
14
|
+
* safe diagnostic strings; each is dropped if over 4096 UTF-8 bytes. Throws
|
|
15
|
+
* are ignored. Defaults to classification only, without messages or stacks.
|
|
16
|
+
*/
|
|
17
|
+
readonly formatError?: (cause: Cause.Cause<unknown>) => ErrorDetails | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Forward the first 16 event attempts per sampled span as structured console
|
|
20
|
+
* logs in its captured context. Defaults to false. Each complete log must fit
|
|
21
|
+
* the JSON limits documented on `layer`. This adds billable log volume.
|
|
22
|
+
*/
|
|
23
|
+
readonly spanEvents?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Configures the per-invocation native tracer. See `layer` for representations
|
|
27
|
+
* and limits. Options do not change Cloudflare sampling or native trace IDs.
|
|
28
|
+
*/
|
|
29
|
+
declare const layerWith: (options?: LayerOptions) => Layer.Layer<never>;
|
|
5
30
|
/**
|
|
6
|
-
* Sends Effect spans to Cloudflare Workers Observability
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
31
|
+
* Sends Effect spans to Cloudflare Workers Observability. Build inside each
|
|
32
|
+
* invocation, e.g. `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
|
|
33
|
+
* Captures async context; never cache the built layer across requests.
|
|
34
|
+
*
|
|
35
|
+
* Strings, finite numbers and booleans remain native scalars. Null, plain
|
|
36
|
+
* objects and arrays become JSON strings under their original keys, limited to
|
|
37
|
+
* 4096 UTF-8 bytes, 4 container levels and 64 values including the root. Values
|
|
38
|
+
* exceeding any limit are dropped whole, as are cycles, accessors, unsupported
|
|
39
|
+
* values (including undefined, bigint, functions, class instances and Redacted)
|
|
40
|
+
* or serialization failures. No getters or toJSON methods are invoked. Only
|
|
41
|
+
* own enumerable string keys / dense array elements are encoded. No flattening.
|
|
42
|
+
*
|
|
43
|
+
* `effect.*` is reserved for this adapter; caller attributes in that namespace
|
|
44
|
+
* remain local. `effect.trace_id`, `effect.span_id`, `effect.parent.trace_id`,
|
|
45
|
+
* `effect.parent.span_id` and `effect.span.kind` describe Effect identities.
|
|
46
|
+
* At end, `effect.span.links` contains JSON for the first 8 linked Effect ID
|
|
47
|
+
* pairs (including addLinks updates), bounded by the same JSON limits, with
|
|
48
|
+
* omitted count in `effect.span.links_dropped`. Link attributes stay local.
|
|
49
|
+
*
|
|
50
|
+
* `effect.exit` is success/failure/interrupted. Failed spans also have
|
|
51
|
+
* `effect.error.kind`: failure, defect, mixed (failures + defects), or interrupted.
|
|
52
|
+
* No error payloads, messages or stacks are exported unless formatError opts in.
|
|
53
|
+
* Events remain local unless `layerWith({ spanEvents: true })` opts into logs.
|
|
54
|
+
* Local NativeSpan storage retains original attributes, links and events.
|
|
10
55
|
*
|
|
11
|
-
* Enable
|
|
12
|
-
* and export; no OTLP exporter or flush is required.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
56
|
+
* Enable observability.traces.enabled in Wrangler. Cloudflare owns sampling
|
|
57
|
+
* and export; no OTLP exporter or flush is required. Metadata creates no extra
|
|
58
|
+
* spans. Cloudflare IDs remain opaque and distinct: this provides lookup, not
|
|
59
|
+
* native graph links or cross-system propagation. There is no native outcome
|
|
60
|
+
* setter or addEvent API. External parents and root: true cannot override the
|
|
61
|
+
* invocation's Cloudflare trace. See Cloudflare's custom spans documentation.
|
|
17
62
|
*/
|
|
18
63
|
declare const layer: Layer.Layer<never>;
|
|
19
64
|
//#endregion
|
|
20
|
-
export { CloudflareTracer_d_exports, layer };
|
|
65
|
+
export { CloudflareTracer_d_exports, ErrorDetails, LayerOptions, layer, layerWith };
|
|
@@ -9,44 +9,163 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
9
9
|
import { tracing } from "cloudflare:workers";
|
|
10
10
|
import * as Tracer from "effect/Tracer";
|
|
11
11
|
//#region src/CloudflareTracer.ts
|
|
12
|
-
var CloudflareTracer_exports = /* @__PURE__ */ __exportAll({
|
|
12
|
+
var CloudflareTracer_exports = /* @__PURE__ */ __exportAll({
|
|
13
|
+
layer: () => layer,
|
|
14
|
+
layerWith: () => layerWith
|
|
15
|
+
});
|
|
16
|
+
const maxBytes = 4096;
|
|
17
|
+
const maxLinks = 8;
|
|
18
|
+
const maxEvents = 16;
|
|
19
|
+
const textEncoder = new TextEncoder();
|
|
20
|
+
const bestEffort = (f) => {
|
|
21
|
+
try {
|
|
22
|
+
f();
|
|
23
|
+
} catch {}
|
|
24
|
+
};
|
|
25
|
+
const encodeJson = (value) => {
|
|
26
|
+
let bytes = 0;
|
|
27
|
+
let values = 0;
|
|
28
|
+
const parts = [];
|
|
29
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30
|
+
const append = (part) => {
|
|
31
|
+
bytes += textEncoder.encode(part).byteLength;
|
|
32
|
+
if (bytes > maxBytes) return false;
|
|
33
|
+
parts.push(part);
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
const string = (text) => text.length <= maxBytes && append(JSON.stringify(text));
|
|
37
|
+
const visit = (current, depth) => {
|
|
38
|
+
if (++values > 64) return false;
|
|
39
|
+
if (Predicate.isString(current)) return string(current);
|
|
40
|
+
if (current === null || Predicate.isBoolean(current)) return append(String(current));
|
|
41
|
+
if (Predicate.isNumber(current)) return Number.isFinite(current) && append(String(current));
|
|
42
|
+
if (!Predicate.isObjectOrArray(current) || depth === 4 || ancestors.has(current)) return false;
|
|
43
|
+
const array = Array.isArray(current);
|
|
44
|
+
const prototype = Object.getPrototypeOf(current);
|
|
45
|
+
if (!array && prototype !== Object.prototype && prototype !== null) return false;
|
|
46
|
+
if (array && current.length > 64 - values) return false;
|
|
47
|
+
ancestors.add(current);
|
|
48
|
+
if (!append(array ? "[" : "{")) return false;
|
|
49
|
+
let count = 0;
|
|
50
|
+
const member = (key) => {
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
52
|
+
if (!descriptor || !Object.hasOwn(descriptor, "value")) return false;
|
|
53
|
+
if (count++ > 0 && !append(",")) return false;
|
|
54
|
+
if (!array && (!string(key) || !append(":"))) return false;
|
|
55
|
+
return visit(descriptor.value, depth + 1);
|
|
56
|
+
};
|
|
57
|
+
if (array) {
|
|
58
|
+
for (let i = 0; i < current.length; i++) if (!member(String(i))) return false;
|
|
59
|
+
} else for (const key in current) if (Object.hasOwn(current, key) && !member(key)) return false;
|
|
60
|
+
ancestors.delete(current);
|
|
61
|
+
return append(array ? "]" : "}");
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
return visit(value, 0) ? parts.join("") : void 0;
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
13
69
|
var Span = class extends Tracer.NativeSpan {
|
|
14
70
|
runInContext;
|
|
71
|
+
config;
|
|
72
|
+
forwardEvent;
|
|
15
73
|
cloudflareSpan;
|
|
16
|
-
|
|
74
|
+
eventAttempts = 0;
|
|
75
|
+
constructor(options, runInContext, config, forwardEvent, cloudflareSpan) {
|
|
17
76
|
super({
|
|
18
77
|
...options,
|
|
19
78
|
sampled: options.sampled && (cloudflareSpan?.isTraced ?? false)
|
|
20
79
|
});
|
|
21
80
|
this.runInContext = runInContext;
|
|
81
|
+
this.config = config;
|
|
82
|
+
this.forwardEvent = forwardEvent;
|
|
22
83
|
this.cloudflareSpan = cloudflareSpan;
|
|
84
|
+
if (!this.sampled) return;
|
|
85
|
+
bestEffort(() => {
|
|
86
|
+
this.setAttribute("effect.trace_id", this.traceId);
|
|
87
|
+
this.setAttribute("effect.span_id", this.spanId);
|
|
88
|
+
this.setAttribute("effect.span.kind", this.kind);
|
|
89
|
+
if (Option.isSome(this.parent)) {
|
|
90
|
+
this.setAttribute("effect.parent.trace_id", this.parent.value.traceId);
|
|
91
|
+
this.setAttribute("effect.parent.span_id", this.parent.value.spanId);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
setAttribute(key, value) {
|
|
96
|
+
bestEffort(() => this.cloudflareSpan?.setAttribute(key, value));
|
|
23
97
|
}
|
|
24
98
|
attribute(key, value) {
|
|
25
99
|
super.attribute(key, value);
|
|
26
|
-
if (
|
|
100
|
+
if (!this.sampled || this.status._tag === "Ended" || key.startsWith("effect.")) return;
|
|
101
|
+
if (Predicate.isString(value) || Predicate.isBoolean(value)) this.setAttribute(key, value);
|
|
102
|
+
else if (Predicate.isNumber(value)) {
|
|
103
|
+
if (Number.isFinite(value)) this.setAttribute(key, value);
|
|
104
|
+
} else {
|
|
105
|
+
const json = encodeJson(value);
|
|
106
|
+
if (json !== void 0) this.setAttribute(key, json);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
event(name, startTime, attributes) {
|
|
110
|
+
super.event(name, startTime, attributes);
|
|
111
|
+
if (!this.config.spanEvents || !this.sampled || this.status._tag === "Ended" || this.eventAttempts >= maxEvents) return;
|
|
112
|
+
this.eventAttempts++;
|
|
113
|
+
bestEffort(() => {
|
|
114
|
+
const json = encodeJson({
|
|
115
|
+
"effect.event": name,
|
|
116
|
+
"effect.event.time_unix_nano": String(startTime),
|
|
117
|
+
"effect.trace_id": this.traceId,
|
|
118
|
+
"effect.span_id": this.spanId,
|
|
119
|
+
attributes: attributes ?? {}
|
|
120
|
+
});
|
|
121
|
+
if (json !== void 0) this.forwardEvent(this.runInContext, json);
|
|
122
|
+
});
|
|
27
123
|
}
|
|
28
124
|
end(endTime, exit) {
|
|
29
125
|
if (this.status._tag === "Ended") return;
|
|
30
126
|
super.end(endTime, exit);
|
|
31
|
-
|
|
32
|
-
this.
|
|
127
|
+
if (!this.sampled) return;
|
|
128
|
+
this.setAttribute("effect.exit", Exit.isSuccess(exit) ? "success" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure");
|
|
129
|
+
bestEffort(() => {
|
|
130
|
+
if (this.links.length > 0) {
|
|
131
|
+
const links = this.links.slice(0, maxLinks).map(({ span }) => ({
|
|
132
|
+
traceId: span.traceId,
|
|
133
|
+
spanId: span.spanId
|
|
134
|
+
}));
|
|
135
|
+
const json = encodeJson(links);
|
|
136
|
+
if (json !== void 0) this.setAttribute("effect.span.links", json);
|
|
137
|
+
this.setAttribute("effect.span.links_dropped", this.links.length - (json ? links.length : 0));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (Exit.isFailure(exit)) {
|
|
141
|
+
this.setAttribute("effect.error.kind", Cause.hasDies(exit.cause) ? Cause.hasFails(exit.cause) ? "mixed" : "defect" : Cause.hasInterruptsOnly(exit.cause) ? "interrupted" : "failure");
|
|
142
|
+
bestEffort(() => {
|
|
143
|
+
const details = this.config.formatError?.(exit.cause);
|
|
144
|
+
for (const key of ["type", "message"]) {
|
|
145
|
+
const value = details?.[key];
|
|
146
|
+
if (Predicate.isString(value) && value.length <= maxBytes && textEncoder.encode(value).byteLength <= maxBytes) this.setAttribute(`effect.error.${key}`, value);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
bestEffort(() => this.cloudflareSpan?.end());
|
|
33
151
|
}
|
|
34
152
|
};
|
|
35
153
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
|
|
39
|
-
* It captures the invocation's async context and must not be cached across requests.
|
|
40
|
-
*
|
|
41
|
-
* Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
|
|
42
|
-
* and export; no OTLP exporter or flush is required. Scalar attributes are
|
|
43
|
-
* forwarded; other attributes, events, and links remain Effect-local. Completion
|
|
44
|
-
* is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
|
|
45
|
-
* Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
|
|
46
|
-
* parents and `root: true` cannot override the invocation's Cloudflare trace.
|
|
154
|
+
* Configures the per-invocation native tracer. See `layer` for representations
|
|
155
|
+
* and limits. Options do not change Cloudflare sampling or native trace IDs.
|
|
47
156
|
*/
|
|
48
|
-
const
|
|
157
|
+
const layerWith = (options = {}) => Layer.effect(Tracer.Tracer, Effect.sync(() => {
|
|
49
158
|
const invocationContext = AsyncLocalStorage.snapshot();
|
|
159
|
+
let forwarding = false;
|
|
160
|
+
const forwardEvent = (context, json) => {
|
|
161
|
+
if (forwarding) return;
|
|
162
|
+
forwarding = true;
|
|
163
|
+
try {
|
|
164
|
+
context(() => console.log(JSON.parse(json)));
|
|
165
|
+
} finally {
|
|
166
|
+
forwarding = false;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
50
169
|
const contextFor = (span) => {
|
|
51
170
|
while (span?._tag === "Span") {
|
|
52
171
|
if (span instanceof Span) return span.runInContext;
|
|
@@ -55,15 +174,49 @@ const layer = Layer.effect(Tracer.Tracer, Effect.sync(() => {
|
|
|
55
174
|
return invocationContext;
|
|
56
175
|
};
|
|
57
176
|
return Tracer.make({
|
|
58
|
-
span(
|
|
59
|
-
const parentContext =
|
|
60
|
-
if (!
|
|
61
|
-
return parentContext(() => tracing.startActiveSpan(
|
|
177
|
+
span(spanOptions) {
|
|
178
|
+
const parentContext = spanOptions.root ? invocationContext : contextFor(Option.getOrUndefined(spanOptions.parent));
|
|
179
|
+
if (!spanOptions.sampled) return new Span(spanOptions, parentContext, options, forwardEvent);
|
|
180
|
+
return parentContext(() => tracing.startActiveSpan(spanOptions.name, (span) => new Span(spanOptions, AsyncLocalStorage.snapshot(), options, forwardEvent, span)));
|
|
62
181
|
},
|
|
63
182
|
context(primitive, fiber) {
|
|
64
|
-
return contextFor(fiber.
|
|
183
|
+
return contextFor(fiber.cache.span)(() => primitive["~effect/Effect/evaluate"](fiber));
|
|
65
184
|
}
|
|
66
185
|
});
|
|
67
186
|
}));
|
|
187
|
+
/**
|
|
188
|
+
* Sends Effect spans to Cloudflare Workers Observability. Build inside each
|
|
189
|
+
* invocation, e.g. `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
|
|
190
|
+
* Captures async context; never cache the built layer across requests.
|
|
191
|
+
*
|
|
192
|
+
* Strings, finite numbers and booleans remain native scalars. Null, plain
|
|
193
|
+
* objects and arrays become JSON strings under their original keys, limited to
|
|
194
|
+
* 4096 UTF-8 bytes, 4 container levels and 64 values including the root. Values
|
|
195
|
+
* exceeding any limit are dropped whole, as are cycles, accessors, unsupported
|
|
196
|
+
* values (including undefined, bigint, functions, class instances and Redacted)
|
|
197
|
+
* or serialization failures. No getters or toJSON methods are invoked. Only
|
|
198
|
+
* own enumerable string keys / dense array elements are encoded. No flattening.
|
|
199
|
+
*
|
|
200
|
+
* `effect.*` is reserved for this adapter; caller attributes in that namespace
|
|
201
|
+
* remain local. `effect.trace_id`, `effect.span_id`, `effect.parent.trace_id`,
|
|
202
|
+
* `effect.parent.span_id` and `effect.span.kind` describe Effect identities.
|
|
203
|
+
* At end, `effect.span.links` contains JSON for the first 8 linked Effect ID
|
|
204
|
+
* pairs (including addLinks updates), bounded by the same JSON limits, with
|
|
205
|
+
* omitted count in `effect.span.links_dropped`. Link attributes stay local.
|
|
206
|
+
*
|
|
207
|
+
* `effect.exit` is success/failure/interrupted. Failed spans also have
|
|
208
|
+
* `effect.error.kind`: failure, defect, mixed (failures + defects), or interrupted.
|
|
209
|
+
* No error payloads, messages or stacks are exported unless formatError opts in.
|
|
210
|
+
* Events remain local unless `layerWith({ spanEvents: true })` opts into logs.
|
|
211
|
+
* Local NativeSpan storage retains original attributes, links and events.
|
|
212
|
+
*
|
|
213
|
+
* Enable observability.traces.enabled in Wrangler. Cloudflare owns sampling
|
|
214
|
+
* and export; no OTLP exporter or flush is required. Metadata creates no extra
|
|
215
|
+
* spans. Cloudflare IDs remain opaque and distinct: this provides lookup, not
|
|
216
|
+
* native graph links or cross-system propagation. There is no native outcome
|
|
217
|
+
* setter or addEvent API. External parents and root: true cannot override the
|
|
218
|
+
* invocation's Cloudflare trace. See Cloudflare's custom spans documentation.
|
|
219
|
+
*/
|
|
220
|
+
const layer = layerWith();
|
|
68
221
|
//#endregion
|
|
69
|
-
export { CloudflareTracer_exports, layer };
|
|
222
|
+
export { CloudflareTracer_exports, layer, layerWith };
|
package/dist/Environment.mjs
CHANGED
|
@@ -15,11 +15,11 @@ var Environment_exports = /* @__PURE__ */ __exportAll({
|
|
|
15
15
|
var WorkerEnvironment = class extends Context.Service()("effect-cf/WorkerEnvironment") {};
|
|
16
16
|
let WorkerConfig;
|
|
17
17
|
(function(_WorkerConfig) {
|
|
18
|
-
_WorkerConfig.string = (name) => Config.
|
|
19
|
-
_WorkerConfig.redacted = (name) => Config.
|
|
20
|
-
_WorkerConfig.number = (name) => Config.
|
|
21
|
-
_WorkerConfig.integer = (name) => Config.
|
|
22
|
-
_WorkerConfig.boolean = (name) => Config.
|
|
18
|
+
_WorkerConfig.string = (name) => Config.String(name);
|
|
19
|
+
_WorkerConfig.redacted = (name) => Config.Redacted(name);
|
|
20
|
+
_WorkerConfig.number = (name) => Config.Number(name);
|
|
21
|
+
_WorkerConfig.integer = (name) => Config.Int(name);
|
|
22
|
+
_WorkerConfig.boolean = (name) => Config.Boolean(name);
|
|
23
23
|
const providerFromEnv = _WorkerConfig.providerFromEnv = (env, options) => {
|
|
24
24
|
const record = {};
|
|
25
25
|
for (const [key, value] of Object.entries(env)) if (Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value)) record[key] = String(value);
|
package/dist/Worker.mjs
CHANGED
|
@@ -58,8 +58,9 @@ const renderFetchSuccess = (effect, eventLayer) => Effect.suspend(() => {
|
|
|
58
58
|
return Effect.succeed(response);
|
|
59
59
|
});
|
|
60
60
|
const handleResponse = (request, response) => nativeResponse !== void 0 ? Effect.void : Effect.map(Effect.context(), (context) => {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const withoutBody = request.method === "HEAD";
|
|
62
|
+
webResponse = HttpServerResponse.toWeb(HttpServerResponse.omitsBody(response, withoutBody) ? response : HttpEffect.scopeTransferToStream(response), {
|
|
63
|
+
withoutBody,
|
|
63
64
|
context
|
|
64
65
|
});
|
|
65
66
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-cf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Effect-native primitives for Cloudflare Workers and bindings.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
"@cloudflare/sandbox": "^0.13.0-next.738.2",
|
|
95
95
|
"@cloudflare/vitest-plugin": "^1.0.0",
|
|
96
96
|
"@cloudflare/workers-types": "^5.20260825.1",
|
|
97
|
-
"@effect/sql-d1": "^4.0.0-rc.
|
|
98
|
-
"@effect/sql-pg": "^4.0.0-rc.
|
|
99
|
-
"@effect/sql-sqlite-do": "^4.0.0-rc.
|
|
97
|
+
"@effect/sql-d1": "^4.0.0-rc.115",
|
|
98
|
+
"@effect/sql-pg": "^4.0.0-rc.115",
|
|
99
|
+
"@effect/sql-sqlite-do": "^4.0.0-rc.115",
|
|
100
100
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
101
|
-
"effect": "^4.0.0-rc.
|
|
101
|
+
"effect": "^4.0.0-rc.115"
|
|
102
102
|
},
|
|
103
103
|
"peerDependenciesMeta": {
|
|
104
104
|
"@cloudflare/computer": {
|
package/src/AnalyticsEngine.ts
CHANGED
|
@@ -729,11 +729,11 @@ export const makeClient =
|
|
|
729
729
|
|
|
730
730
|
export const queryConfig = (options?: QueryConfigOptions) =>
|
|
731
731
|
Config.all({
|
|
732
|
-
accountId: options?.accountId ?? Config.
|
|
733
|
-
apiToken: options?.apiToken ?? Config.
|
|
732
|
+
accountId: options?.accountId ?? Config.String("CLOUDFLARE_ACCOUNT_ID"),
|
|
733
|
+
apiToken: options?.apiToken ?? Config.Redacted("CLOUDFLARE_API_TOKEN"),
|
|
734
734
|
apiBaseUrl:
|
|
735
735
|
options?.apiBaseUrl ??
|
|
736
|
-
Config.
|
|
736
|
+
Config.String("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl)),
|
|
737
737
|
});
|
|
738
738
|
|
|
739
739
|
export const queryLayer = <Self>(
|
package/src/CloudflareTracer.ts
CHANGED
|
@@ -11,31 +11,185 @@ import * as Tracer from "effect/Tracer";
|
|
|
11
11
|
type SpanOptions = Parameters<Tracer.Tracer["span"]>[0];
|
|
12
12
|
type RunInContext = ReturnType<typeof AsyncLocalStorage.snapshot>;
|
|
13
13
|
type CloudflareSpan = Parameters<Parameters<typeof tracing.startActiveSpan>[1]>[0];
|
|
14
|
+
type ForwardEvent = (context: RunInContext, json: string) => void;
|
|
15
|
+
|
|
16
|
+
/** Explicitly sanitized diagnostics. No error payload or Cause is serialized automatically. */
|
|
17
|
+
export interface ErrorDetails {
|
|
18
|
+
readonly type?: string;
|
|
19
|
+
readonly message?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LayerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Called once for a sampled failed span, including interruption. Return only
|
|
25
|
+
* safe diagnostic strings; each is dropped if over 4096 UTF-8 bytes. Throws
|
|
26
|
+
* are ignored. Defaults to classification only, without messages or stacks.
|
|
27
|
+
*/
|
|
28
|
+
readonly formatError?: (cause: Cause.Cause<unknown>) => ErrorDetails | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Forward the first 16 event attempts per sampled span as structured console
|
|
31
|
+
* logs in its captured context. Defaults to false. Each complete log must fit
|
|
32
|
+
* the JSON limits documented on `layer`. This adds billable log volume.
|
|
33
|
+
*/
|
|
34
|
+
readonly spanEvents?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const maxBytes = 4096;
|
|
38
|
+
const maxLinks = 8;
|
|
39
|
+
const maxEvents = 16;
|
|
40
|
+
const textEncoder = new TextEncoder();
|
|
41
|
+
|
|
42
|
+
const bestEffort = (f: () => void): void => {
|
|
43
|
+
try {
|
|
44
|
+
f();
|
|
45
|
+
} catch {
|
|
46
|
+
// Telemetry must not change the application's exit, including formatter/host failures.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// This bounded JSON writer deliberately avoids coercion, getters and toJSON.
|
|
51
|
+
// Schema decoding / JSON.stringify on arbitrary input would traverse it before
|
|
52
|
+
// enforcing our work/depth limits, and could invoke application code.
|
|
53
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters
|
|
54
|
+
const encodeJson = (value: unknown): string | undefined => {
|
|
55
|
+
let bytes = 0;
|
|
56
|
+
let values = 0;
|
|
57
|
+
const parts: Array<string> = [];
|
|
58
|
+
const ancestors = new Set<object>();
|
|
59
|
+
const append = (part: string): boolean => {
|
|
60
|
+
bytes += textEncoder.encode(part).byteLength;
|
|
61
|
+
if (bytes > maxBytes) return false;
|
|
62
|
+
parts.push(part);
|
|
63
|
+
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
const string = (text: string): boolean => text.length <= maxBytes && append(JSON.stringify(text));
|
|
67
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters
|
|
68
|
+
const visit = (current: unknown, depth: number): boolean => {
|
|
69
|
+
if (++values > 64) return false;
|
|
70
|
+
if (Predicate.isString(current)) return string(current);
|
|
71
|
+
if (current === null || Predicate.isBoolean(current)) return append(String(current));
|
|
72
|
+
if (Predicate.isNumber(current)) return Number.isFinite(current) && append(String(current));
|
|
73
|
+
if (!Predicate.isObjectOrArray(current) || depth === 4 || ancestors.has(current)) return false;
|
|
74
|
+
|
|
75
|
+
const array = Array.isArray(current);
|
|
76
|
+
const prototype = Object.getPrototypeOf(current);
|
|
77
|
+
|
|
78
|
+
if (!array && prototype !== Object.prototype && prototype !== null) return false;
|
|
79
|
+
if (array && current.length > 64 - values) return false;
|
|
80
|
+
ancestors.add(current);
|
|
81
|
+
if (!append(array ? "[" : "{")) return false;
|
|
82
|
+
|
|
83
|
+
let count = 0;
|
|
84
|
+
const member = (key: string): boolean => {
|
|
85
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
86
|
+
|
|
87
|
+
if (!descriptor || !Object.hasOwn(descriptor, "value")) return false;
|
|
88
|
+
if (count++ > 0 && !append(",")) return false;
|
|
89
|
+
if (!array && (!string(key) || !append(":"))) return false;
|
|
90
|
+
|
|
91
|
+
return visit(descriptor.value, depth + 1);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (array) {
|
|
95
|
+
for (let i = 0; i < current.length; i++) {
|
|
96
|
+
if (!member(String(i))) return false;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
for (const key in current) {
|
|
100
|
+
if (Object.hasOwn(current, key) && !member(key)) return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
ancestors.delete(current);
|
|
104
|
+
|
|
105
|
+
return append(array ? "]" : "}");
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return visit(value, 0) ? parts.join("") : undefined;
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
14
114
|
|
|
15
115
|
class Span extends Tracer.NativeSpan {
|
|
116
|
+
private eventAttempts = 0;
|
|
117
|
+
|
|
16
118
|
constructor(
|
|
17
119
|
options: SpanOptions,
|
|
18
120
|
readonly runInContext: RunInContext,
|
|
121
|
+
readonly config: LayerOptions,
|
|
122
|
+
readonly forwardEvent: ForwardEvent,
|
|
19
123
|
readonly cloudflareSpan?: CloudflareSpan,
|
|
20
124
|
) {
|
|
21
125
|
super({ ...options, sampled: options.sampled && (cloudflareSpan?.isTraced ?? false) });
|
|
126
|
+
if (!this.sampled) return;
|
|
127
|
+
|
|
128
|
+
bestEffort(() => {
|
|
129
|
+
this.setAttribute("effect.trace_id", this.traceId);
|
|
130
|
+
this.setAttribute("effect.span_id", this.spanId);
|
|
131
|
+
this.setAttribute("effect.span.kind", this.kind);
|
|
132
|
+
if (Option.isSome(this.parent)) {
|
|
133
|
+
this.setAttribute("effect.parent.trace_id", this.parent.value.traceId);
|
|
134
|
+
this.setAttribute("effect.parent.span_id", this.parent.value.spanId);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private setAttribute(key: string, value: string | number | boolean): void {
|
|
140
|
+
bestEffort(() => this.cloudflareSpan?.setAttribute(key, value));
|
|
22
141
|
}
|
|
23
142
|
|
|
24
|
-
// Effect's tracer contract accepts arbitrary attributes
|
|
143
|
+
// Effect's tracer contract accepts arbitrary attributes.
|
|
25
144
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters
|
|
26
145
|
override attribute(key: string, value: unknown): void {
|
|
27
146
|
super.attribute(key, value);
|
|
147
|
+
if (!this.sampled || this.status._tag === "Ended" || key.startsWith("effect.")) return;
|
|
148
|
+
|
|
149
|
+
if (Predicate.isString(value) || Predicate.isBoolean(value)) {
|
|
150
|
+
this.setAttribute(key, value);
|
|
151
|
+
} else if (Predicate.isNumber(value)) {
|
|
152
|
+
if (Number.isFinite(value)) this.setAttribute(key, value);
|
|
153
|
+
} else {
|
|
154
|
+
const json = encodeJson(value);
|
|
28
155
|
|
|
29
|
-
|
|
30
|
-
this.cloudflareSpan?.setAttribute(key, value);
|
|
156
|
+
if (json !== undefined) this.setAttribute(key, json);
|
|
31
157
|
}
|
|
32
158
|
}
|
|
33
159
|
|
|
160
|
+
override event(
|
|
161
|
+
name: string,
|
|
162
|
+
startTime: bigint,
|
|
163
|
+
attributes?: Parameters<Tracer.Span["event"]>[2],
|
|
164
|
+
): void {
|
|
165
|
+
super.event(name, startTime, attributes);
|
|
166
|
+
if (
|
|
167
|
+
!this.config.spanEvents ||
|
|
168
|
+
!this.sampled ||
|
|
169
|
+
this.status._tag === "Ended" ||
|
|
170
|
+
this.eventAttempts >= maxEvents
|
|
171
|
+
)
|
|
172
|
+
return;
|
|
173
|
+
this.eventAttempts++;
|
|
174
|
+
bestEffort(() => {
|
|
175
|
+
const json = encodeJson({
|
|
176
|
+
"effect.event": name,
|
|
177
|
+
"effect.event.time_unix_nano": String(startTime),
|
|
178
|
+
"effect.trace_id": this.traceId,
|
|
179
|
+
"effect.span_id": this.spanId,
|
|
180
|
+
attributes: attributes ?? {},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (json !== undefined) this.forwardEvent(this.runInContext, json);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
34
187
|
override end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
|
|
35
188
|
if (this.status._tag === "Ended") return;
|
|
36
|
-
|
|
37
189
|
super.end(endTime, exit);
|
|
38
|
-
this.
|
|
190
|
+
if (!this.sampled) return;
|
|
191
|
+
|
|
192
|
+
this.setAttribute(
|
|
39
193
|
"effect.exit",
|
|
40
194
|
Exit.isSuccess(exit)
|
|
41
195
|
? "success"
|
|
@@ -43,56 +197,138 @@ class Span extends Tracer.NativeSpan {
|
|
|
43
197
|
? "interrupted"
|
|
44
198
|
: "failure",
|
|
45
199
|
);
|
|
46
|
-
|
|
200
|
+
bestEffort(() => {
|
|
201
|
+
if (this.links.length > 0) {
|
|
202
|
+
const links = this.links.slice(0, maxLinks).map(({ span }) => ({
|
|
203
|
+
traceId: span.traceId,
|
|
204
|
+
spanId: span.spanId,
|
|
205
|
+
}));
|
|
206
|
+
const json = encodeJson(links);
|
|
207
|
+
|
|
208
|
+
if (json !== undefined) this.setAttribute("effect.span.links", json);
|
|
209
|
+
this.setAttribute(
|
|
210
|
+
"effect.span.links_dropped",
|
|
211
|
+
this.links.length - (json ? links.length : 0),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
if (Exit.isFailure(exit)) {
|
|
216
|
+
this.setAttribute(
|
|
217
|
+
"effect.error.kind",
|
|
218
|
+
Cause.hasDies(exit.cause)
|
|
219
|
+
? Cause.hasFails(exit.cause)
|
|
220
|
+
? "mixed"
|
|
221
|
+
: "defect"
|
|
222
|
+
: Cause.hasInterruptsOnly(exit.cause)
|
|
223
|
+
? "interrupted"
|
|
224
|
+
: "failure",
|
|
225
|
+
);
|
|
226
|
+
bestEffort(() => {
|
|
227
|
+
const details = this.config.formatError?.(exit.cause);
|
|
228
|
+
|
|
229
|
+
for (const key of ["type", "message"] as const) {
|
|
230
|
+
const value = details?.[key];
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
Predicate.isString(value) &&
|
|
234
|
+
value.length <= maxBytes &&
|
|
235
|
+
textEncoder.encode(value).byteLength <= maxBytes
|
|
236
|
+
) {
|
|
237
|
+
this.setAttribute(`effect.error.${key}`, value);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
bestEffort(() => this.cloudflareSpan?.end());
|
|
47
243
|
}
|
|
48
244
|
}
|
|
49
245
|
|
|
50
246
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
|
|
54
|
-
* It captures the invocation's async context and must not be cached across requests.
|
|
55
|
-
*
|
|
56
|
-
* Enable `observability.traces.enabled` in Wrangler. Cloudflare owns sampling
|
|
57
|
-
* and export; no OTLP exporter or flush is required. Scalar attributes are
|
|
58
|
-
* forwarded; other attributes, events, and links remain Effect-local. Completion
|
|
59
|
-
* is recorded as `effect.exit`, since Cloudflare has no outcome-setting API.
|
|
60
|
-
* Effect trace/span IDs remain independent of Cloudflare's opaque IDs. External
|
|
61
|
-
* parents and `root: true` cannot override the invocation's Cloudflare trace.
|
|
247
|
+
* Configures the per-invocation native tracer. See `layer` for representations
|
|
248
|
+
* and limits. Options do not change Cloudflare sampling or native trace IDs.
|
|
62
249
|
*/
|
|
63
|
-
export const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
250
|
+
export const layerWith = (options: LayerOptions = {}): Layer.Layer<never> =>
|
|
251
|
+
Layer.effect(
|
|
252
|
+
Tracer.Tracer,
|
|
253
|
+
Effect.sync(() => {
|
|
254
|
+
const invocationContext = AsyncLocalStorage.snapshot();
|
|
255
|
+
let forwarding = false;
|
|
256
|
+
const forwardEvent: ForwardEvent = (context, json) => {
|
|
257
|
+
if (forwarding) return;
|
|
258
|
+
forwarding = true;
|
|
259
|
+
try {
|
|
260
|
+
// Parse only our own bounded JSON to give console a structured object.
|
|
261
|
+
context(() => console.log(JSON.parse(json)));
|
|
262
|
+
} finally {
|
|
263
|
+
forwarding = false;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const contextFor = (span: Tracer.AnySpan | undefined): RunInContext => {
|
|
267
|
+
while (span?._tag === "Span") {
|
|
268
|
+
if (span instanceof Span) return span.runInContext;
|
|
74
269
|
|
|
75
|
-
|
|
76
|
-
|
|
270
|
+
// Effect can install a no-op span while tracing is locally disabled.
|
|
271
|
+
span = Option.getOrUndefined(span.parent);
|
|
272
|
+
}
|
|
77
273
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const parentContext = options.root
|
|
81
|
-
? invocationContext
|
|
82
|
-
: contextFor(Option.getOrUndefined(options.parent));
|
|
274
|
+
return invocationContext;
|
|
275
|
+
};
|
|
83
276
|
|
|
84
|
-
|
|
277
|
+
return Tracer.make({
|
|
278
|
+
span(spanOptions) {
|
|
279
|
+
const parentContext = spanOptions.root
|
|
280
|
+
? invocationContext
|
|
281
|
+
: contextFor(Option.getOrUndefined(spanOptions.parent));
|
|
85
282
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
283
|
+
if (!spanOptions.sampled)
|
|
284
|
+
return new Span(spanOptions, parentContext, options, forwardEvent);
|
|
285
|
+
|
|
286
|
+
return parentContext(() =>
|
|
287
|
+
tracing.startActiveSpan(
|
|
288
|
+
spanOptions.name,
|
|
289
|
+
(span) =>
|
|
290
|
+
new Span(spanOptions, AsyncLocalStorage.snapshot(), options, forwardEvent, span),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
context(primitive, fiber) {
|
|
295
|
+
return contextFor(fiber.cache.span)(() => primitive["~effect/Effect/evaluate"](fiber));
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Sends Effect spans to Cloudflare Workers Observability. Build inside each
|
|
303
|
+
* invocation, e.g. `Worker.make(Services, { eventLayer: CloudflareTracer.layer, fetch })`.
|
|
304
|
+
* Captures async context; never cache the built layer across requests.
|
|
305
|
+
*
|
|
306
|
+
* Strings, finite numbers and booleans remain native scalars. Null, plain
|
|
307
|
+
* objects and arrays become JSON strings under their original keys, limited to
|
|
308
|
+
* 4096 UTF-8 bytes, 4 container levels and 64 values including the root. Values
|
|
309
|
+
* exceeding any limit are dropped whole, as are cycles, accessors, unsupported
|
|
310
|
+
* values (including undefined, bigint, functions, class instances and Redacted)
|
|
311
|
+
* or serialization failures. No getters or toJSON methods are invoked. Only
|
|
312
|
+
* own enumerable string keys / dense array elements are encoded. No flattening.
|
|
313
|
+
*
|
|
314
|
+
* `effect.*` is reserved for this adapter; caller attributes in that namespace
|
|
315
|
+
* remain local. `effect.trace_id`, `effect.span_id`, `effect.parent.trace_id`,
|
|
316
|
+
* `effect.parent.span_id` and `effect.span.kind` describe Effect identities.
|
|
317
|
+
* At end, `effect.span.links` contains JSON for the first 8 linked Effect ID
|
|
318
|
+
* pairs (including addLinks updates), bounded by the same JSON limits, with
|
|
319
|
+
* omitted count in `effect.span.links_dropped`. Link attributes stay local.
|
|
320
|
+
*
|
|
321
|
+
* `effect.exit` is success/failure/interrupted. Failed spans also have
|
|
322
|
+
* `effect.error.kind`: failure, defect, mixed (failures + defects), or interrupted.
|
|
323
|
+
* No error payloads, messages or stacks are exported unless formatError opts in.
|
|
324
|
+
* Events remain local unless `layerWith({ spanEvents: true })` opts into logs.
|
|
325
|
+
* Local NativeSpan storage retains original attributes, links and events.
|
|
326
|
+
*
|
|
327
|
+
* Enable observability.traces.enabled in Wrangler. Cloudflare owns sampling
|
|
328
|
+
* and export; no OTLP exporter or flush is required. Metadata creates no extra
|
|
329
|
+
* spans. Cloudflare IDs remain opaque and distinct: this provides lookup, not
|
|
330
|
+
* native graph links or cross-system propagation. There is no native outcome
|
|
331
|
+
* setter or addEvent API. External parents and root: true cannot override the
|
|
332
|
+
* invocation's Cloudflare trace. See Cloudflare's custom spans documentation.
|
|
333
|
+
*/
|
|
334
|
+
export const layer: Layer.Layer<never> = layerWith();
|
package/src/Environment.ts
CHANGED
|
@@ -33,19 +33,19 @@ export namespace WorkerConfig {
|
|
|
33
33
|
export type Key = ScalarConfigKey;
|
|
34
34
|
|
|
35
35
|
/** Read a scalar Cloudflare var or secret as a string. */
|
|
36
|
-
export const string = <const Name extends Key>(name: Name) => Config.
|
|
36
|
+
export const string = <const Name extends Key>(name: Name) => Config.String(name);
|
|
37
37
|
|
|
38
38
|
/** Read a scalar Cloudflare secret as a redacted string. */
|
|
39
|
-
export const redacted = <const Name extends Key>(name: Name) => Config.
|
|
39
|
+
export const redacted = <const Name extends Key>(name: Name) => Config.Redacted(name);
|
|
40
40
|
|
|
41
41
|
/** Read a scalar Cloudflare var or secret as a number. */
|
|
42
|
-
export const number = <const Name extends Key>(name: Name) => Config.
|
|
42
|
+
export const number = <const Name extends Key>(name: Name) => Config.Number(name);
|
|
43
43
|
|
|
44
44
|
/** Read a scalar Cloudflare var or secret as an integer. */
|
|
45
|
-
export const integer = <const Name extends Key>(name: Name) => Config.
|
|
45
|
+
export const integer = <const Name extends Key>(name: Name) => Config.Int(name);
|
|
46
46
|
|
|
47
47
|
/** Read a scalar Cloudflare var or secret as a boolean. */
|
|
48
|
-
export const boolean = <const Name extends Key>(name: Name) => Config.
|
|
48
|
+
export const boolean = <const Name extends Key>(name: Name) => Config.Boolean(name);
|
|
49
49
|
|
|
50
50
|
export interface ProviderOptions {
|
|
51
51
|
/**
|
package/src/Worker.ts
CHANGED
|
@@ -277,10 +277,17 @@ const renderFetchSuccess = <E, R, REvent, EventLayerError, EventLayerRequirement
|
|
|
277
277
|
nativeResponse !== undefined
|
|
278
278
|
? Effect.void
|
|
279
279
|
: Effect.map(Effect.context<never>(), (context) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
const withoutBody = request.method === "HEAD";
|
|
281
|
+
|
|
282
|
+
webResponse = HttpServerResponse.toWeb(
|
|
283
|
+
HttpServerResponse.omitsBody(response, withoutBody)
|
|
284
|
+
? response
|
|
285
|
+
: HttpEffect.scopeTransferToStream(response),
|
|
286
|
+
{
|
|
287
|
+
withoutBody,
|
|
288
|
+
context,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
284
291
|
});
|
|
285
292
|
const telemetryMiddleware = HttpMiddleware.make((self) =>
|
|
286
293
|
RpcTargets.withScope(
|