effect-cf 0.43.0 → 0.44.1
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 +14 -0
- package/README.md +76 -6
- package/dist/CloudflareTracer.d.mts +57 -12
- package/dist/CloudflareTracer.mjs +175 -22
- package/dist/internal/NativeCallback.mjs +1 -1
- package/package.json +1 -1
- package/src/CloudflareTracer.ts +285 -49
- package/src/internal/NativeCallback.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
+
## 0.44.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#180](https://github.com/danieljvdm/effect-cf/pull/180) [`c94adc5`](https://github.com/danieljvdm/effect-cf/commit/c94adc506142d5d30095e9a05d449fe407c568b3) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Keep Durable Object native callbacks on microtask scheduling through completion so restoring the caller context cannot leave an input gate waiting on a blocked timer.
|
|
8
|
+
|
|
9
|
+
## 0.44.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#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.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
3
17
|
## 0.43.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -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
|
|
@@ -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
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 };
|
|
@@ -23,7 +23,7 @@ const runNativeCallback = Effect.fnUntraced(function* (operation) {
|
|
|
23
23
|
const runPromise = yield* FiberSet.makeRuntimePromise();
|
|
24
24
|
return yield* Effect.callback((resume) => {
|
|
25
25
|
try {
|
|
26
|
-
nativePromise = operation((effect) => runPromise(Effect.provideContext(Effect.exit(effect), callbackContext)));
|
|
26
|
+
nativePromise = operation((effect) => runPromise(Effect.provideContext(Effect.exit(effect), callbackContext), { scheduler: callbackScheduler }));
|
|
27
27
|
} catch (cause) {
|
|
28
28
|
resume(Effect.fail(cause));
|
|
29
29
|
return;
|
package/package.json
CHANGED
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();
|
|
@@ -50,7 +50,9 @@ export const runNativeCallback = Effect.fnUntraced(function* <A, E, R, B>(
|
|
|
50
50
|
return yield* Effect.callback<B, unknown>((resume) => {
|
|
51
51
|
try {
|
|
52
52
|
nativePromise = operation((effect) =>
|
|
53
|
-
runPromise(Effect.provideContext(Effect.exit(effect), callbackContext)
|
|
53
|
+
runPromise(Effect.provideContext(Effect.exit(effect), callbackContext), {
|
|
54
|
+
scheduler: callbackScheduler,
|
|
55
|
+
}),
|
|
54
56
|
);
|
|
55
57
|
} catch (cause) {
|
|
56
58
|
resume(Effect.fail(cause));
|