@systemfsoftware/trace-spec 0.1.0 → 0.2.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 +17 -0
- package/README.md +31 -8
- package/dist/index.d.ts +69 -7
- package/dist/index.mjs +371 -18
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @systemfsoftware/trace-spec
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Update peer and runtime dependency on `effect` and companion packages to `4.0.0-rc.117`.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies:
|
|
10
|
+
- @systemfsoftware/trace-taxonomy@0.0.1
|
|
11
|
+
|
|
12
|
+
## 0.2.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- Judge contracts against traces produced in another process. `RemoteObservation.layer(source, { interval, settle, timeout })` provides `Observation` by reading a trace store until no read has added a span for `settle`. `TempoTraceStore.source({ baseUrl })` reads Grafana Tempo's `/api/v2/traces/<traceId>` through the `HttpClient` you provide; put auth and tenant headers on that client.
|
|
17
|
+
|
|
18
|
+
New failures `Observation.IncompleteObservationError` (spans never settled, or the store marked the trace partial) and `Observation.TransportObservationError` (the store could not be read) join `Collector.collect`, `Contract.JudgeFailure`, and `Suite.CaseFailure`. This is a breaking type change: code that matches every case of these unions, or restates them by hand, must handle both.
|
|
19
|
+
|
|
3
20
|
## 0.1.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ In a system with queues, workers, and background consumers, a `200` is not the o
|
|
|
10
10
|
pnpm add @systemfsoftware/trace-spec @systemfsoftware/trace-taxonomy effect
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The package exports
|
|
13
|
+
The package exports nine namespaces — `Contract`, `Graph`, `Observation`, `ObservationWindow`, `RemoteObservation`, `Rel`, `Stimulus`, `Suite`, `TempoTraceStore`. Errors and verdicts belong to the capability that raises them: `Contract.ContractDecodeError`, `Contract.TraceDisparityError`, `Observation.EmptyObservationError`, `Observation.IncompleteObservationError`, `Observation.TransportObservationError`, `Suite.StimulusFailure`, and `Rel.Hold`/`Rel.Break`/`Rel.Verdict`.
|
|
14
14
|
|
|
15
15
|
## Write a contract
|
|
16
16
|
|
|
@@ -38,15 +38,17 @@ const paymentUnderCheckout = Contract.of(checkout)
|
|
|
38
38
|
|
|
39
39
|
## Run the contract
|
|
40
40
|
|
|
41
|
-
`Contract.judge(contract, input)` stimulates the behaviour under its minted trace, collects that trace's spans, decodes them against the taxonomy (`Graph.decode` answers `Result<TraceGraph, Contract.ContractDecodeError>`), applies the relation, and on a break writes the decoded graph under `artifacts/traces/`. It answers a `Contract.Judgment` — the run, the verdict, and the dump path — so a break is a value, not a failure. Behaviour failures and infrastructure refusals (`Contract.ContractDecodeError
|
|
41
|
+
`Contract.judge(contract, input)` stimulates the behaviour under its minted trace, collects that trace's spans, decodes them against the taxonomy (`Graph.decode` answers `Result<TraceGraph, Contract.ContractDecodeError>`), applies the relation, and on a break writes the decoded graph under `artifacts/traces/`. It answers a `Contract.Judgment` — the run, the verdict, and the dump path — so a break is a value, not a failure. Behaviour failures and infrastructure refusals (`Contract.ContractDecodeError` and the three `Observation` failures) stay on the error channel.
|
|
42
42
|
|
|
43
43
|
`Contract.check(contract, input)` is the test edge over `judge`: a break fails with `Contract.TraceDisparityError`. Navigation over the decoded graph is standalone: `Graph.byId`, `Graph.children`, and `Graph.descendants`.
|
|
44
44
|
|
|
45
|
-
| Outcome | Failure
|
|
46
|
-
| --------------------------------------------- |
|
|
47
|
-
| The relation broke | `Contract.TraceDisparityError`
|
|
48
|
-
| A contracted span lacked a required attribute | `Contract.ContractDecodeError`
|
|
49
|
-
| Nothing ran under the owned trace | `Observation.EmptyObservationError`
|
|
45
|
+
| Outcome | Failure | Meaning |
|
|
46
|
+
| --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| The relation broke | `Contract.TraceDisparityError` | names the broken conjunct, the spans inspected, and where the decoded graph was written |
|
|
48
|
+
| A contracted span lacked a required attribute | `Contract.ContractDecodeError` | names the declaration and the attribute — never reported as a broken relation |
|
|
49
|
+
| Nothing ran under the owned trace | `Observation.EmptyObservationError` | the observation was empty, which is its own outcome, not a break |
|
|
50
|
+
| The trace never finished arriving | `Observation.IncompleteObservationError` | spans were read but kept changing, or the store marked the trace partial; carries the span count |
|
|
51
|
+
| The trace store could not be read | `Observation.TransportObservationError` | the store refused, was unreachable, or answered a body that does not decode; names the request URL |
|
|
50
52
|
|
|
51
53
|
## Register cases
|
|
52
54
|
|
|
@@ -66,7 +68,7 @@ TraceSuite('checkout.place_order')
|
|
|
66
68
|
})
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
`Suite.make({ it, layer })(name)` stages a suite as a Pipeable builder, and `withLayer`/`withScenarioLayer` are `dual` combinators whose terminal `body` registers the cases. The scenario layer is built fresh per case and must provide `Observation.Observation` and a `FileSystem` for failure dumps, plus whatever the stimulated behaviour needs — the requirement is enforced at the type level. A failing case fails with the harness's own error channel — the
|
|
71
|
+
`Suite.make({ it, layer })(name)` stages a suite as a Pipeable builder, and `withLayer`/`withScenarioLayer` are `dual` combinators whose terminal `body` registers the cases. The scenario layer is built fresh per case and must provide `Observation.Observation` and a `FileSystem` for failure dumps, plus whatever the stimulated behaviour needs — the requirement is enforced at the type level. A failing case fails with the harness's own error channel — the outcomes above, or `Suite.StimulusFailure` when the behaviour itself failed before its trace could be judged. Cases run on the live clock.
|
|
70
72
|
|
|
71
73
|
`.withLayer(shared)` registers the suite over a layer built once for the whole suite; the shared layer then carries the observation harness, and `.withScenarioLayer(scenario)` can follow it for what each case needs fresh.
|
|
72
74
|
|
|
@@ -76,6 +78,27 @@ TraceSuite('checkout.place_order')
|
|
|
76
78
|
|
|
77
79
|
`ObservationWindow.make(serviceName)` is the cold resource; the service name is required. `.scoped` acquires an `ObservationWindow` handle for one scope — an OpenTelemetry in-memory exporter behind a simple span processor and an always-on sampler, shut down when the scope closes — and `ObservationWindow.collect(window, traceId)` reads one trace back. `.layer` binds a window as `Observation.Observation` plus the Effect tracer, one window per layer build, so two acquisitions never see each other's spans. Only the handle module imports the OpenTelemetry SDK. The export happens outside any test-clock boundary.
|
|
78
80
|
|
|
81
|
+
## Observe a remote store
|
|
82
|
+
|
|
83
|
+
When the behaviour runs in another process — a CLI binary, a worker, a container — its spans never reach an in-memory window. Export them to a trace store and read them back with `RemoteObservation.layer`, which provides `Observation.Observation` over a trace source:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { RemoteObservation, TempoTraceStore } from '@systemfsoftware/trace-spec'
|
|
87
|
+
import { Layer } from 'effect'
|
|
88
|
+
import { FetchHttpClient } from 'effect/unstable/http'
|
|
89
|
+
|
|
90
|
+
const observation = RemoteObservation.layer(
|
|
91
|
+
TempoTraceStore.source({ baseUrl: 'http://127.0.0.1:3200' }),
|
|
92
|
+
{ interval: '50 millis', settle: '500 millis', timeout: '10 seconds' },
|
|
93
|
+
).pipe(Layer.provide(FetchHttpClient.layer))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`collect(traceId)` reads the source every `interval` and keeps every span it has seen, keyed by span id. It answers once no read has added a span for `settle`, because no trace store says when a trace is complete: a trace read too early looks finished but is missing spans still in flight. A read that returns fewer spans than an earlier one never drops a span. The whole read runs under `timeout`, and a read still in flight at the deadline is cancelled: nothing read by then fails with `Observation.EmptyObservationError`, spans that never settled fail with `Observation.IncompleteObservationError`. A source failure fails at once, without retrying. Every duration must be positive.
|
|
97
|
+
|
|
98
|
+
`TempoTraceStore.source({ baseUrl })` reads Grafana Tempo's `GET /api/v2/traces/<traceId>` once. It needs only an `HttpClient`, so auth and tenant headers go on the client you provide: `HttpClient.mapRequest(client, HttpClientRequest.setHeader('X-Scope-OrgID', tenant))`. A trace Tempo marks `PARTIAL` fails with `Observation.IncompleteObservationError`; a non-2xx answer, including `404`, or a body that does not decode fails with `Observation.TransportObservationError`.
|
|
99
|
+
|
|
100
|
+
Another store needs only another source: a `RemoteObservation.TraceSource<R>` is one read that answers the spans the store holds for a trace id now — empty when it holds none — or fails with `Observation.TransportObservationError` or `Observation.IncompleteObservationError`. It never polls, waits, or retries; the layer owns the cadence.
|
|
101
|
+
|
|
79
102
|
## Relations
|
|
80
103
|
|
|
81
104
|
`Rel.exists`, `Rel.absent`, `Rel.unique`, `Rel.child`, `Rel.descendant`, `Rel.status`, `Rel.errorType`, `Rel.attrs`, `Rel.durationLessThan`, `Rel.forall`, `Rel.event`, `Rel.order`, `Rel.any`, `Rel.not`, `Rel.soft`, `Rel.all`, `Rel.fromTaxonomy`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Context, Effect, FileSystem, Layer, Result, Schema } from "effect";
|
|
1
|
+
import { Context, Duration, Effect, FileSystem, Layer, Result, Schema } from "effect";
|
|
2
2
|
import { Pipeable } from "effect/Pipeable";
|
|
3
3
|
import { Span, Taxonomy } from "@systemfsoftware/trace-taxonomy";
|
|
4
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
4
5
|
import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
|
|
5
6
|
import { BasicTracerProvider, InMemorySpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
6
7
|
import { Suite } from "@systemfsoftware/effect-spec-runtime";
|
|
@@ -23,6 +24,14 @@ declare const EmptyObservationError_base: Schema.Class<EmptyObservationError, Sc
|
|
|
23
24
|
}>, import("effect/Cause").YieldableError>;
|
|
24
25
|
declare class EmptyObservationError extends EmptyObservationError_base {}
|
|
25
26
|
//#endregion
|
|
27
|
+
//#region src/IncompleteObservationError.schema.d.ts
|
|
28
|
+
declare const IncompleteObservationError_base: Schema.Class<IncompleteObservationError, Schema.TaggedStruct<"IncompleteObservationError", {
|
|
29
|
+
readonly traceId: Schema.String;
|
|
30
|
+
readonly spanCount: Schema.Finite;
|
|
31
|
+
readonly detail: Schema.String;
|
|
32
|
+
}>, import("effect/Cause").YieldableError>;
|
|
33
|
+
declare class IncompleteObservationError extends IncompleteObservationError_base {}
|
|
34
|
+
//#endregion
|
|
26
35
|
//#region src/TraceGraph.schema.d.ts
|
|
27
36
|
declare const Status: Schema.Literals<readonly ["ok", "unset", "error"]>;
|
|
28
37
|
type Status = typeof Status.Type;
|
|
@@ -127,11 +136,20 @@ declare const decode: {
|
|
|
127
136
|
(spans: ReadonlyArray<SpanRecord>, taxonomy: Taxonomy.Taxonomy): (traceId: string) => Result.Result<TraceGraph, ContractDecodeError>;
|
|
128
137
|
(traceId: string, spans: ReadonlyArray<SpanRecord>, taxonomy: Taxonomy.Taxonomy): Result.Result<TraceGraph, ContractDecodeError>;
|
|
129
138
|
};
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/TransportObservationError.schema.d.ts
|
|
141
|
+
declare const TransportObservationError_base: Schema.Class<TransportObservationError, Schema.TaggedStruct<"TransportObservationError", {
|
|
142
|
+
readonly traceId: Schema.String;
|
|
143
|
+
readonly source: Schema.String;
|
|
144
|
+
readonly detail: Schema.String;
|
|
145
|
+
}>, import("effect/Cause").YieldableError>;
|
|
146
|
+
declare class TransportObservationError extends TransportObservationError_base {}
|
|
130
147
|
declare namespace Observation_service_d_exports {
|
|
131
|
-
export { Collector, EmptyObservationError, Observation };
|
|
148
|
+
export { Collector, EmptyObservationError, IncompleteObservationError, Observation, ObservationFailure, TransportObservationError };
|
|
132
149
|
}
|
|
150
|
+
type ObservationFailure = EmptyObservationError | IncompleteObservationError | TransportObservationError;
|
|
133
151
|
interface Collector {
|
|
134
|
-
readonly collect: (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecord>,
|
|
152
|
+
readonly collect: (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecord>, ObservationFailure>;
|
|
135
153
|
}
|
|
136
154
|
declare const Observation_base: Context.ServiceClass<Observation, "@systemfsoftware/trace-spec/Observation", Collector>;
|
|
137
155
|
declare class Observation extends Observation_base {}
|
|
@@ -252,7 +270,7 @@ declare const TraceDisparityError_base: Schema.Class<TraceDisparityError, Schema
|
|
|
252
270
|
}>, import("effect/Cause").YieldableError>;
|
|
253
271
|
declare class TraceDisparityError extends TraceDisparityError_base {}
|
|
254
272
|
declare namespace Contract_d_exports {
|
|
255
|
-
export { CheckFailure, CheckOptions, Contract, ContractDecodeError, Declared$1 as Declared, EmptyObservationError, JudgeFailure, Judgment, Services, Stimulated, TraceDisparityError, TypeId$3 as TypeId, check, holds, judge, of, stimulate };
|
|
273
|
+
export { CheckFailure, CheckOptions, Contract, ContractDecodeError, Declared$1 as Declared, EmptyObservationError, IncompleteObservationError, JudgeFailure, Judgment, Services, Stimulated, TraceDisparityError, TransportObservationError, TypeId$3 as TypeId, check, holds, judge, of, stimulate };
|
|
256
274
|
}
|
|
257
275
|
declare const TypeId$3: unique symbol;
|
|
258
276
|
type TypeId$3 = typeof TypeId$3;
|
|
@@ -290,7 +308,7 @@ interface Judgment<Input, Output> {
|
|
|
290
308
|
interface CheckOptions {
|
|
291
309
|
readonly dumpName?: string | undefined;
|
|
292
310
|
}
|
|
293
|
-
type JudgeFailure<E> = E | ContractDecodeError |
|
|
311
|
+
type JudgeFailure<E> = E | ContractDecodeError | ObservationFailure;
|
|
294
312
|
type CheckFailure<E> = JudgeFailure<E> | TraceDisparityError;
|
|
295
313
|
type Services<R> = R | Observation | FileSystem.FileSystem;
|
|
296
314
|
declare const judge: {
|
|
@@ -301,6 +319,46 @@ declare const check: {
|
|
|
301
319
|
<Input>(input: Input, options?: CheckOptions): <Output, E, R>(self: Contract<Input, Output, E, R>) => Effect.Effect<Judgment<Input, Output>, CheckFailure<E>, Services<R>>;
|
|
302
320
|
<Input, Output, E, R>(self: Contract<Input, Output, E, R>, input: Input, options?: CheckOptions): Effect.Effect<Judgment<Input, Output>, CheckFailure<E>, Services<R>>;
|
|
303
321
|
};
|
|
322
|
+
declare namespace RemoteObservation_d_exports {
|
|
323
|
+
export { Options, TraceSource, layer };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The cadence of the poll, the quiet window the observed span set must hold
|
|
327
|
+
* before the read is trusted, and the deadline the whole poll runs under.
|
|
328
|
+
* Durations are refused unless they are positive, at layer construction.
|
|
329
|
+
*/
|
|
330
|
+
interface Options {
|
|
331
|
+
readonly interval: Duration.Input;
|
|
332
|
+
readonly settle: Duration.Input;
|
|
333
|
+
readonly timeout: Duration.Input;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* One read against one store: the spans that store currently holds for a trace
|
|
337
|
+
* id, or the store's own transport or unfinished answer. A source never polls,
|
|
338
|
+
* waits, or retries — the cadence belongs to {@link layer}.
|
|
339
|
+
*/
|
|
340
|
+
type TraceSource<R> = (traceId: string) => Effect.Effect<ReadonlyArray<SpanRecord>, IncompleteObservationError | TransportObservationError, R>;
|
|
341
|
+
/**
|
|
342
|
+
* The `Observation` service over one trace source. `collect` reads the source at
|
|
343
|
+
* `options.interval`, keeps every span it has seen keyed by span id, and answers
|
|
344
|
+
* when that union has stayed unchanged for `options.settle`; a trace the store
|
|
345
|
+
* never served is `EmptyObservationError`, one that never settled is
|
|
346
|
+
* `IncompleteObservationError`, and the source's own failure ends the poll
|
|
347
|
+
* immediately, unchanged. Provides `Observation` alone and requires exactly what
|
|
348
|
+
* the source requires (`Scope.Scope` is supplied by the layer machinery, as
|
|
349
|
+
* every Effect layer constructor scopes it away).
|
|
350
|
+
*/
|
|
351
|
+
declare const layer: {
|
|
352
|
+
(options: Options): <R>(source: TraceSource<R>) => Layer.Layer<Observation, never, Exclude<R, Scope.Scope>>;
|
|
353
|
+
<R>(source: TraceSource<R>, options: Options): Layer.Layer<Observation, never, Exclude<R, Scope.Scope>>;
|
|
354
|
+
};
|
|
355
|
+
declare namespace tempo_trace_store_d_exports {
|
|
356
|
+
export { source };
|
|
357
|
+
}
|
|
358
|
+
/** One read against one Tempo endpoint. Its only requirement is the caller's HTTP client. */
|
|
359
|
+
declare const source: (options: {
|
|
360
|
+
readonly baseUrl: string;
|
|
361
|
+
}) => TraceSource<HttpClient.HttpClient>;
|
|
304
362
|
//#endregion
|
|
305
363
|
//#region src/ObservationWindowSpec.schema.d.ts
|
|
306
364
|
declare const ObservationWindowSpec_base: Schema.Class<ObservationWindowSpec, Schema.Struct<{
|
|
@@ -341,6 +399,10 @@ interface ObservationWindowResource extends Pipeable {
|
|
|
341
399
|
}
|
|
342
400
|
declare const make$1: (serviceName: string) => ObservationWindowResource;
|
|
343
401
|
//#endregion
|
|
402
|
+
//#region src/HarnessFailure.schema.d.ts
|
|
403
|
+
declare const HarnessFailure: Schema.Union<readonly [typeof ContractDecodeError, typeof EmptyObservationError, typeof IncompleteObservationError, typeof TransportObservationError, typeof TraceDisparityError]>;
|
|
404
|
+
type HarnessFailure = typeof HarnessFailure.Type;
|
|
405
|
+
//#endregion
|
|
344
406
|
//#region src/StimulusFailure.schema.d.ts
|
|
345
407
|
declare const StimulusFailure_base: Schema.Class<StimulusFailure, Schema.TaggedStruct<"StimulusFailure", {
|
|
346
408
|
readonly stimulus: Schema.String;
|
|
@@ -352,7 +414,7 @@ declare namespace Suite_d_exports {
|
|
|
352
414
|
}
|
|
353
415
|
declare const TypeId: unique symbol;
|
|
354
416
|
type TypeId = typeof TypeId;
|
|
355
|
-
type CaseFailure =
|
|
417
|
+
type CaseFailure = HarnessFailure | StimulusFailure;
|
|
356
418
|
type Harness = Observation | FileSystem.FileSystem;
|
|
357
419
|
type ArbitraryInput<Input> = Schema.Schema<Input> | Arbitrary.Arbitrary<Input>;
|
|
358
420
|
interface CaseRegistrar<Provided> {
|
|
@@ -385,4 +447,4 @@ declare const withScenarioLayer: {
|
|
|
385
447
|
};
|
|
386
448
|
declare const make: (bindings: Suite.Bindings) => (name: string) => Declared;
|
|
387
449
|
//#endregion
|
|
388
|
-
export { Contract_d_exports as Contract, Graph_d_exports as Graph, Observation_service_d_exports as Observation, observation_window_resource_d_exports as ObservationWindow, Rel_d_exports as Rel, Stimulus_d_exports as Stimulus, Suite_d_exports as Suite };
|
|
450
|
+
export { Contract_d_exports as Contract, Graph_d_exports as Graph, Observation_service_d_exports as Observation, observation_window_resource_d_exports as ObservationWindow, Rel_d_exports as Rel, RemoteObservation_d_exports as RemoteObservation, Stimulus_d_exports as Stimulus, Suite_d_exports as Suite, tempo_trace_store_d_exports as TempoTraceStore };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import { Cause, Context, Effect, Equal, FileSystem, Layer, Match, Option, Predicate, Result, Schema } from "effect";
|
|
2
|
+
import { Array as Array$1, Cause, Clock, Context, Duration, Effect, Encoding, Equal, FileSystem, Layer, Match, Option, Predicate, Ref, Result, Schema, SchemaGetter, SchemaTransformation } from "effect";
|
|
3
3
|
import { dual } from "effect/Function";
|
|
4
4
|
import { Prototype } from "effect/Pipeable";
|
|
5
5
|
import "effect/PlatformError";
|
|
6
6
|
import "@systemfsoftware/trace-taxonomy";
|
|
7
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
8
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
9
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
7
10
|
import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
|
|
8
11
|
import * as OtelResource from "@effect/opentelemetry/Resource";
|
|
9
12
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
@@ -221,10 +224,26 @@ const decodeImpl = (traceId, spans, taxonomy) => {
|
|
|
221
224
|
*/
|
|
222
225
|
const decode = dual(3, decodeImpl);
|
|
223
226
|
//#endregion
|
|
227
|
+
//#region src/IncompleteObservationError.schema.ts
|
|
228
|
+
var IncompleteObservationError = class extends Schema.TaggedError()("IncompleteObservationError", {
|
|
229
|
+
traceId: Schema.String,
|
|
230
|
+
spanCount: Schema.Finite,
|
|
231
|
+
detail: Schema.String
|
|
232
|
+
}) {};
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/TransportObservationError.schema.ts
|
|
235
|
+
var TransportObservationError = class extends Schema.TaggedError()("TransportObservationError", {
|
|
236
|
+
traceId: Schema.String,
|
|
237
|
+
source: Schema.String,
|
|
238
|
+
detail: Schema.String
|
|
239
|
+
}) {};
|
|
240
|
+
//#endregion
|
|
224
241
|
//#region src/Observation.service.ts
|
|
225
242
|
var Observation_service_exports = /* @__PURE__ */ __exportAll({
|
|
226
243
|
EmptyObservationError: () => EmptyObservationError,
|
|
227
|
-
|
|
244
|
+
IncompleteObservationError: () => IncompleteObservationError,
|
|
245
|
+
Observation: () => Observation,
|
|
246
|
+
TransportObservationError: () => TransportObservationError
|
|
228
247
|
});
|
|
229
248
|
var Observation = class extends Context.Service()("@systemfsoftware/trace-spec/Observation") {};
|
|
230
249
|
//#endregion
|
|
@@ -240,7 +259,9 @@ var TraceDisparityError = class extends Schema.TaggedError()("TraceDisparityErro
|
|
|
240
259
|
var Contract_exports = /* @__PURE__ */ __exportAll({
|
|
241
260
|
ContractDecodeError: () => ContractDecodeError,
|
|
242
261
|
EmptyObservationError: () => EmptyObservationError,
|
|
262
|
+
IncompleteObservationError: () => IncompleteObservationError,
|
|
243
263
|
TraceDisparityError: () => TraceDisparityError,
|
|
264
|
+
TransportObservationError: () => TransportObservationError,
|
|
244
265
|
TypeId: () => TypeId$3,
|
|
245
266
|
check: () => check,
|
|
246
267
|
holds: () => holds,
|
|
@@ -308,6 +329,219 @@ const refuseBreak = (relationId, judgment) => Match.value(judgment.verdict).pipe
|
|
|
308
329
|
const checkDual = (self, input, options) => Effect.flatMap(judgeDual(self, input, options), (judgment) => refuseBreak(self.relation.id, judgment));
|
|
309
330
|
const check = dual((args) => isContract(args[0]), checkDual);
|
|
310
331
|
//#endregion
|
|
332
|
+
//#region src/drivers/tempo-trace.schema.ts
|
|
333
|
+
/** Proto bytes (base64 on the wire) as the lowercase hex ids traces are addressed by. */
|
|
334
|
+
const HexId = Schema.Uint8ArrayFromBase64.pipe(Schema.decodeTo(Schema.String, {
|
|
335
|
+
decode: SchemaGetter.transform(Encoding.encodeHex),
|
|
336
|
+
encode: SchemaGetter.decodeHex()
|
|
337
|
+
}));
|
|
338
|
+
const secondsAndNanos = (nanos) => {
|
|
339
|
+
const text = nanos.padStart(10, "0");
|
|
340
|
+
return [Number(text.slice(0, -9)), Number(text.slice(-9))];
|
|
341
|
+
};
|
|
342
|
+
const millisOf$2 = (nanos) => {
|
|
343
|
+
const [seconds, subMillis] = secondsAndNanos(nanos);
|
|
344
|
+
return seconds * 1e3 + subMillis / 1e6;
|
|
345
|
+
};
|
|
346
|
+
const MillisFromNanos = Schema.String.pipe(Schema.decodeTo(Schema.Finite, SchemaTransformation.transform({
|
|
347
|
+
decode: millisOf$2,
|
|
348
|
+
encode: (millis) => String(Math.round(millis * 1e6))
|
|
349
|
+
})));
|
|
350
|
+
const unsetParent = (parent) => parent === "" || /^0+$/.test(parent);
|
|
351
|
+
const heldParent = (parent) => unsetParent(parent) ? null : parent;
|
|
352
|
+
const parentOf$1 = (parent) => heldParent(parent ?? "");
|
|
353
|
+
const EMPTY_STATUS = {};
|
|
354
|
+
const codeOf = (status) => status.code ?? "STATUS_CODE_UNSET";
|
|
355
|
+
const STATUS_BY_CODE$1 = {
|
|
356
|
+
STATUS_CODE_UNSET: "unset",
|
|
357
|
+
STATUS_CODE_OK: "ok",
|
|
358
|
+
STATUS_CODE_ERROR: "error"
|
|
359
|
+
};
|
|
360
|
+
const statusOf$1 = (status) => STATUS_BY_CODE$1[codeOf(status ?? EMPTY_STATUS)];
|
|
361
|
+
const stringValueOf = (value) => value.stringValue ?? null;
|
|
362
|
+
const numberOf = (value) => value.doubleValue ?? intToNumber(value.intValue);
|
|
363
|
+
const intToNumber = (text) => text === void 0 ? null : Number(text);
|
|
364
|
+
const boolOrNumberOf = (value) => value.boolValue ?? numberOf(value);
|
|
365
|
+
const scalarOf = (value) => stringValueOf(value) ?? boolOrNumberOf(value);
|
|
366
|
+
const isScalarMember = (member) => member !== null;
|
|
367
|
+
const arrayValuesOf = (value) => value.arrayValue?.values;
|
|
368
|
+
const scalarMembersOf = (values) => values.map(scalarOf).filter(isScalarMember);
|
|
369
|
+
const arrayValueOf = (values) => values === void 0 ? null : scalarMembersOf(values);
|
|
370
|
+
const presentValueOf = (value) => scalarOf(value) ?? arrayValueOf(arrayValuesOf(value));
|
|
371
|
+
const isPresent$1 = (entry) => entry[1] !== null;
|
|
372
|
+
const entryOf = (attribute) => [attribute.key, attributeValueOf(attribute.value)];
|
|
373
|
+
const attributeValueOf = (value) => value === void 0 ? null : presentValueOf(value);
|
|
374
|
+
const attributesOf = (attributes) => Object.fromEntries((attributes ?? []).map(entryOf).filter(isPresent$1));
|
|
375
|
+
const errorTypeOf$1 = (attributes) => {
|
|
376
|
+
const value = attributes["error.type"];
|
|
377
|
+
return typeof value === "string" ? value : null;
|
|
378
|
+
};
|
|
379
|
+
const eventOf = (event) => ({
|
|
380
|
+
name: event.name,
|
|
381
|
+
attributes: attributesOf(event.attributes)
|
|
382
|
+
});
|
|
383
|
+
const linkIdOf = (id) => id ?? "";
|
|
384
|
+
const linkOf = (link) => ({
|
|
385
|
+
traceId: linkIdOf(link.traceId),
|
|
386
|
+
spanId: linkIdOf(link.spanId)
|
|
387
|
+
});
|
|
388
|
+
const eventsOf$1 = (span) => (span.events ?? []).map(eventOf);
|
|
389
|
+
const linksOf$1 = (span) => (span.links ?? []).map(linkOf);
|
|
390
|
+
const spanRecordOf$1 = (span) => {
|
|
391
|
+
const attributes = attributesOf(span.attributes);
|
|
392
|
+
return {
|
|
393
|
+
traceId: span.traceId,
|
|
394
|
+
spanId: span.spanId,
|
|
395
|
+
parentSpanId: parentOf$1(span.parentSpanId),
|
|
396
|
+
name: span.name,
|
|
397
|
+
status: statusOf$1(span.status),
|
|
398
|
+
errorType: errorTypeOf$1(attributes),
|
|
399
|
+
startMillis: span.startTimeUnixNano,
|
|
400
|
+
durationMillis: span.endTimeUnixNano - span.startTimeUnixNano,
|
|
401
|
+
attributes,
|
|
402
|
+
events: eventsOf$1(span),
|
|
403
|
+
links: linksOf$1(span)
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
const wireTextOf = (value) => typeof value === "string" ? { stringValue: value } : void 0;
|
|
407
|
+
const wireBoolOf = (value) => typeof value === "boolean" ? { boolValue: value } : void 0;
|
|
408
|
+
const isValueArray = (value) => Array.isArray(value);
|
|
409
|
+
const wireArrayOf = (value) => ({ arrayValue: { values: isValueArray(value) ? value.map(wireValueOf) : [] } });
|
|
410
|
+
const wireNumberOrArrayOf = (value) => typeof value === "number" ? { doubleValue: value } : wireArrayOf(value);
|
|
411
|
+
const wireBoolOrNumberOf = (value) => wireBoolOf(value) ?? wireNumberOrArrayOf(value);
|
|
412
|
+
const wireValueOf = (value) => wireTextOf(value) ?? wireBoolOrNumberOf(value);
|
|
413
|
+
const wireAttributesOf = (attributes) => Object.entries(attributes).map(([key, value]) => ({
|
|
414
|
+
key,
|
|
415
|
+
value: wireValueOf(value)
|
|
416
|
+
}));
|
|
417
|
+
const STATUS_CODE_BY_STATUS = {
|
|
418
|
+
unset: "STATUS_CODE_UNSET",
|
|
419
|
+
ok: "STATUS_CODE_OK",
|
|
420
|
+
error: "STATUS_CODE_ERROR"
|
|
421
|
+
};
|
|
422
|
+
const wireStatusOf = (status) => ({ code: STATUS_CODE_BY_STATUS[status] });
|
|
423
|
+
const wireSpanOf = (record) => ({
|
|
424
|
+
traceId: record.traceId,
|
|
425
|
+
spanId: record.spanId,
|
|
426
|
+
parentSpanId: record.parentSpanId ?? void 0,
|
|
427
|
+
name: record.name,
|
|
428
|
+
startTimeUnixNano: record.startMillis,
|
|
429
|
+
endTimeUnixNano: record.startMillis + record.durationMillis,
|
|
430
|
+
attributes: wireAttributesOf(record.attributes),
|
|
431
|
+
events: record.events.map((event) => ({
|
|
432
|
+
name: event.name,
|
|
433
|
+
attributes: wireAttributesOf(event.attributes)
|
|
434
|
+
})),
|
|
435
|
+
links: record.links.map((link) => ({
|
|
436
|
+
traceId: link.traceId,
|
|
437
|
+
spanId: link.spanId
|
|
438
|
+
})),
|
|
439
|
+
status: wireStatusOf(record.status)
|
|
440
|
+
});
|
|
441
|
+
const WireAnyValue = Schema.Struct({
|
|
442
|
+
stringValue: Schema.optional(Schema.String),
|
|
443
|
+
boolValue: Schema.optional(Schema.Boolean),
|
|
444
|
+
intValue: Schema.optional(Schema.String),
|
|
445
|
+
doubleValue: Schema.optional(Schema.Finite),
|
|
446
|
+
arrayValue: Schema.optional(Schema.Struct({ values: Schema.optional(Schema.Array(Schema.suspend(() => WireAnyValue))) }))
|
|
447
|
+
});
|
|
448
|
+
const WireAttribute = Schema.Struct({
|
|
449
|
+
key: Schema.String,
|
|
450
|
+
value: Schema.optional(WireAnyValue)
|
|
451
|
+
});
|
|
452
|
+
const WireStatus = Schema.Struct({
|
|
453
|
+
message: Schema.optional(Schema.String),
|
|
454
|
+
code: Schema.optional(Schema.Literals([
|
|
455
|
+
"STATUS_CODE_UNSET",
|
|
456
|
+
"STATUS_CODE_OK",
|
|
457
|
+
"STATUS_CODE_ERROR"
|
|
458
|
+
]))
|
|
459
|
+
});
|
|
460
|
+
const WireEvent = Schema.Struct({
|
|
461
|
+
name: Schema.String,
|
|
462
|
+
timeUnixNano: Schema.optional(MillisFromNanos),
|
|
463
|
+
attributes: Schema.optional(Schema.Array(WireAttribute))
|
|
464
|
+
});
|
|
465
|
+
const WireLink = Schema.Struct({
|
|
466
|
+
traceId: Schema.optional(HexId),
|
|
467
|
+
spanId: Schema.optional(HexId)
|
|
468
|
+
});
|
|
469
|
+
const WireSpan = Schema.Struct({
|
|
470
|
+
traceId: HexId,
|
|
471
|
+
spanId: HexId,
|
|
472
|
+
parentSpanId: Schema.optional(HexId),
|
|
473
|
+
name: Schema.String,
|
|
474
|
+
kind: Schema.optional(Schema.String),
|
|
475
|
+
startTimeUnixNano: MillisFromNanos,
|
|
476
|
+
endTimeUnixNano: MillisFromNanos,
|
|
477
|
+
attributes: Schema.optional(Schema.Array(WireAttribute)),
|
|
478
|
+
events: Schema.optional(Schema.Array(WireEvent)),
|
|
479
|
+
links: Schema.optional(Schema.Array(WireLink)),
|
|
480
|
+
status: Schema.optional(WireStatus),
|
|
481
|
+
traceState: Schema.optional(Schema.String),
|
|
482
|
+
flags: Schema.optional(Schema.Finite)
|
|
483
|
+
}).pipe(Schema.decodeTo(SpanRecord, SchemaTransformation.transform({
|
|
484
|
+
decode: spanRecordOf$1,
|
|
485
|
+
encode: wireSpanOf
|
|
486
|
+
})));
|
|
487
|
+
const WireScopeSpans = Schema.Struct({ spans: Schema.optional(Schema.Array(WireSpan)) });
|
|
488
|
+
const WireResourceSpans = Schema.Struct({ scopeSpans: Schema.optional(Schema.Array(WireScopeSpans)) });
|
|
489
|
+
/** One trace-by-id answer: the spans, plus the store's completeness status when it is not complete. */
|
|
490
|
+
const TempoV2TraceResponse = Schema.Struct({
|
|
491
|
+
trace: Schema.Struct({ resourceSpans: Schema.optional(Schema.Array(WireResourceSpans)) }),
|
|
492
|
+
status: Schema.optional(Schema.String),
|
|
493
|
+
message: Schema.optional(Schema.String)
|
|
494
|
+
});
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/drivers/tempo-trace-store.ts
|
|
497
|
+
var tempo_trace_store_exports = /* @__PURE__ */ __exportAll({ source: () => source });
|
|
498
|
+
/**
|
|
499
|
+
* The Grafana Tempo trace source: one read of one trace by id against one
|
|
500
|
+
* store endpoint. It never polls, waits, or retries — completeness belongs
|
|
501
|
+
* to the remote observation layer. Any refusal (unreachable endpoint,
|
|
502
|
+
* non-2xx answer, undecodable body) fails with the transport failure whose
|
|
503
|
+
* `source` is the request URL; an answer the store marked incomplete fails
|
|
504
|
+
* with the incomplete failure, since waiting cannot complete it.
|
|
505
|
+
*/
|
|
506
|
+
const parseBody = Schema.decodeResult(Schema.fromJsonString(TempoV2TraceResponse));
|
|
507
|
+
const orEmpty = (values) => values ?? [];
|
|
508
|
+
const resourceSpansOf = (response) => orEmpty(response.trace.resourceSpans);
|
|
509
|
+
const scopeSpansOf = (resourceSpans) => orEmpty(resourceSpans.scopeSpans);
|
|
510
|
+
const spansOfScope = (scopeSpans) => orEmpty(scopeSpans.spans);
|
|
511
|
+
const spansOf = (response) => resourceSpansOf(response).flatMap((resourceSpans) => scopeSpansOf(resourceSpans).flatMap(spansOfScope));
|
|
512
|
+
const transportFailureOf = (traceId, url, cause) => new TransportObservationError({
|
|
513
|
+
traceId,
|
|
514
|
+
source: url,
|
|
515
|
+
detail: cause
|
|
516
|
+
});
|
|
517
|
+
const refusedOf = (traceId, url, error) => transportFailureOf(traceId, url, error.message);
|
|
518
|
+
const undecodableOf = (traceId, url, error) => transportFailureOf(traceId, url, `undecodable trace body: ${error.message}`);
|
|
519
|
+
const incompleteOf = (traceId, status, spanCount) => new IncompleteObservationError({
|
|
520
|
+
traceId,
|
|
521
|
+
spanCount,
|
|
522
|
+
detail: `the store answered ${status}: waiting cannot complete the trace`
|
|
523
|
+
});
|
|
524
|
+
const isComplete = (status) => status === void 0 || status === "COMPLETE";
|
|
525
|
+
const markedOf = (response) => response.status ?? "INCOMPLETE";
|
|
526
|
+
const recordsOf$1 = (traceId, response) => {
|
|
527
|
+
const spans = spansOf(response);
|
|
528
|
+
return isComplete(response.status) ? Effect.succeed(spans) : Effect.fail(incompleteOf(traceId, markedOf(response), spans.length));
|
|
529
|
+
};
|
|
530
|
+
const readTrace = (traceId, url, body) => {
|
|
531
|
+
const decoded = parseBody(body);
|
|
532
|
+
return Result.isFailure(decoded) ? Effect.fail(undecodableOf(traceId, url, decoded.failure)) : recordsOf$1(traceId, decoded.success);
|
|
533
|
+
};
|
|
534
|
+
/** One read against one Tempo endpoint. Its only requirement is the caller's HTTP client. */
|
|
535
|
+
const source = (options) => {
|
|
536
|
+
const urlOf = (traceId) => `${options.baseUrl}/api/v2/traces/${traceId}`;
|
|
537
|
+
return (traceId) => {
|
|
538
|
+
const url = urlOf(traceId);
|
|
539
|
+
return HttpClient.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.flatMap((response) => response.text), Effect.mapError((error) => refusedOf(traceId, url, error)), Effect.flatMap((body) => readTrace(traceId, url, body)));
|
|
540
|
+
};
|
|
541
|
+
};
|
|
542
|
+
Schema.decodeEffect(HexId);
|
|
543
|
+
Schema.encodeEffect(HexId);
|
|
544
|
+
//#endregion
|
|
311
545
|
//#region src/observation-window.handle.ts
|
|
312
546
|
const TypeId$2 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow");
|
|
313
547
|
const ExporterId = Symbol.for("~systemfsoftware/trace-spec/ObservationWindow/exporter");
|
|
@@ -361,7 +595,7 @@ const linksOf = (span) => span.links.map((link) => ({
|
|
|
361
595
|
traceId: link.context.traceId,
|
|
362
596
|
spanId: link.context.spanId
|
|
363
597
|
}));
|
|
364
|
-
const millisOf = (duration) => duration[0] * 1e3 + duration[1] / 1e6;
|
|
598
|
+
const millisOf$1 = (duration) => duration[0] * 1e3 + duration[1] / 1e6;
|
|
365
599
|
const spanRecordOf = (span) => {
|
|
366
600
|
const attributes = attributeRecordOf(span.attributes);
|
|
367
601
|
return {
|
|
@@ -371,8 +605,8 @@ const spanRecordOf = (span) => {
|
|
|
371
605
|
name: span.name,
|
|
372
606
|
status: statusOf(span.status.code),
|
|
373
607
|
errorType: errorTypeOf(attributes),
|
|
374
|
-
startMillis: millisOf(span.startTime),
|
|
375
|
-
durationMillis: millisOf(span.duration),
|
|
608
|
+
startMillis: millisOf$1(span.startTime),
|
|
609
|
+
durationMillis: millisOf$1(span.duration),
|
|
376
610
|
attributes,
|
|
377
611
|
events: eventsOf(span),
|
|
378
612
|
links: linksOf(span)
|
|
@@ -402,7 +636,7 @@ var observation_window_resource_exports = /* @__PURE__ */ __exportAll({
|
|
|
402
636
|
});
|
|
403
637
|
const TypeId$1 = Symbol.for("~systemfsoftware/trace-spec/ObservationWindowResource");
|
|
404
638
|
const scoped = (spec) => Effect.acquireRelease(Effect.sync(() => make$3(spec)), shutdown);
|
|
405
|
-
const layer = (spec) => OtelTracer.layer.pipe(Layer.provideMerge(OtelResource.layer({ serviceName: spec.serviceName })), Layer.provideMerge(Layer.effectContext(Effect.map(scoped(spec), context))));
|
|
639
|
+
const layer$1 = (spec) => OtelTracer.layer.pipe(Layer.provideMerge(OtelResource.layer({ serviceName: spec.serviceName })), Layer.provideMerge(Layer.effectContext(Effect.map(scoped(spec), context))));
|
|
406
640
|
const makeProto = (spec) => ({
|
|
407
641
|
[TypeId$1]: TypeId$1,
|
|
408
642
|
spec,
|
|
@@ -410,7 +644,7 @@ const makeProto = (spec) => ({
|
|
|
410
644
|
return scoped(spec);
|
|
411
645
|
},
|
|
412
646
|
get layer() {
|
|
413
|
-
return layer(spec);
|
|
647
|
+
return layer$1(spec);
|
|
414
648
|
},
|
|
415
649
|
...Prototype
|
|
416
650
|
});
|
|
@@ -449,7 +683,7 @@ const inspectedOf = (nodes) => nodes.map((node) => node.spanId);
|
|
|
449
683
|
const matchedAll = (items, predicate) => items.length > 0 && items.every(predicate);
|
|
450
684
|
const isBreak$1 = (verdict) => Schema.is(Break)(verdict);
|
|
451
685
|
const isHold$1 = (verdict) => Schema.is(Hold)(verdict);
|
|
452
|
-
const verdictOf = (options) => options.holds ? Hold.make({
|
|
686
|
+
const verdictOf$1 = (options) => options.holds ? Hold.make({
|
|
453
687
|
conjunct: options.id,
|
|
454
688
|
inspected: options.inspected
|
|
455
689
|
}) : Break.make({
|
|
@@ -463,7 +697,7 @@ const declared = (declaration) => relation({
|
|
|
463
697
|
softenable: declaration.softenable,
|
|
464
698
|
evaluate: (graph) => {
|
|
465
699
|
const inspected = declaration.spans.flatMap((span) => inspectedOf(byId(graph, span)));
|
|
466
|
-
return verdictOf({
|
|
700
|
+
return verdictOf$1({
|
|
467
701
|
id: declaration.id,
|
|
468
702
|
inspected,
|
|
469
703
|
holds: declaration.holds(graph),
|
|
@@ -576,7 +810,7 @@ const forallImpl = (spec, predicate, detail) => {
|
|
|
576
810
|
softenable: true,
|
|
577
811
|
evaluate: (graph) => {
|
|
578
812
|
const nodes = matchedNodes(graph, spec);
|
|
579
|
-
return verdictOf({
|
|
813
|
+
return verdictOf$1({
|
|
580
814
|
id,
|
|
581
815
|
inspected: inspectedOf(nodes),
|
|
582
816
|
holds: matchedAll(nodes, predicate),
|
|
@@ -667,7 +901,7 @@ const placementOf = (edge) => {
|
|
|
667
901
|
evaluate: (graph) => {
|
|
668
902
|
const placed = matchedNodes(graph, edge.child);
|
|
669
903
|
const reached = REACH_BY_RELATION[edge.relation](graph, matchedNodes(graph, edge.parent));
|
|
670
|
-
return verdictOf({
|
|
904
|
+
return verdictOf$1({
|
|
671
905
|
id,
|
|
672
906
|
inspected: inspectedOf(placed),
|
|
673
907
|
holds: placed.every((node) => reached.has(node.spanId)),
|
|
@@ -689,6 +923,120 @@ const fromTaxonomyImpl = (taxonomy, options) => combined({
|
|
|
689
923
|
});
|
|
690
924
|
const fromTaxonomy = dual(2, fromTaxonomyImpl);
|
|
691
925
|
//#endregion
|
|
926
|
+
//#region src/RemoteObservation.ts
|
|
927
|
+
var RemoteObservation_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
|
|
928
|
+
const NEVER_READ = {
|
|
929
|
+
spans: [],
|
|
930
|
+
addedAtMillis: 0
|
|
931
|
+
};
|
|
932
|
+
const idOf = (span) => span.spanId;
|
|
933
|
+
const sameId = (left, right) => left.spanId === right.spanId;
|
|
934
|
+
const idsOf = (spans) => new Set(spans.map(idOf));
|
|
935
|
+
const firstOfEachId = (read) => Array$1.dedupeWith(read, sameId);
|
|
936
|
+
const freshOf = (settlement, read) => {
|
|
937
|
+
const known = idsOf(settlement.spans);
|
|
938
|
+
return firstOfEachId(read).filter((span) => !known.has(span.spanId));
|
|
939
|
+
};
|
|
940
|
+
const absorbed = (settlement, read, elapsedMillis) => {
|
|
941
|
+
const fresh = freshOf(settlement, read);
|
|
942
|
+
return fresh.length === 0 ? settlement : {
|
|
943
|
+
spans: [...settlement.spans, ...fresh],
|
|
944
|
+
addedAtMillis: elapsedMillis
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
const quietForLongEnough = (settlement, elapsedMillis, windows) => settlement.spans.length > 0 && elapsedMillis - settlement.addedAtMillis >= windows.settleMillis;
|
|
948
|
+
const pastDeadline = (elapsedMillis, windows) => elapsedMillis >= windows.timeoutMillis;
|
|
949
|
+
const settledVerdict = (spans) => Option.some(Result.succeed(spans));
|
|
950
|
+
const refusedVerdict = (refusal) => Option.some(Result.fail(refusal));
|
|
951
|
+
const verdictOf = (settlement, elapsedMillis, windows) => Match.value({
|
|
952
|
+
quiet: quietForLongEnough(settlement, elapsedMillis, windows),
|
|
953
|
+
vacant: settlement.spans.length === 0,
|
|
954
|
+
late: pastDeadline(elapsedMillis, windows)
|
|
955
|
+
}).pipe(Match.when({ quiet: true }, () => settledVerdict(settlement.spans)), Match.when({
|
|
956
|
+
vacant: true,
|
|
957
|
+
late: true
|
|
958
|
+
}, () => refusedVerdict("absent")), Match.when({ late: true }, () => refusedVerdict("unfinished")), Match.orElse(() => Option.none()));
|
|
959
|
+
const settleStep = (settlement, read, elapsedMillis, windows) => {
|
|
960
|
+
const advanced = absorbed(settlement, read, elapsedMillis);
|
|
961
|
+
return {
|
|
962
|
+
settlement: advanced,
|
|
963
|
+
verdict: verdictOf(advanced, elapsedMillis, windows)
|
|
964
|
+
};
|
|
965
|
+
};
|
|
966
|
+
const lateRefusal = (settlement) => settlement.spans.length === 0 ? "absent" : "unfinished";
|
|
967
|
+
const deadlineVerdict = (settlement, elapsedMillis, windows) => pastDeadline(elapsedMillis, windows) ? refusedVerdict(lateRefusal(settlement)) : Option.none();
|
|
968
|
+
const millisOf = (input) => Duration.toMillis(input);
|
|
969
|
+
const windowsOf = (options) => ({
|
|
970
|
+
settleMillis: millisOf(options.settle),
|
|
971
|
+
timeoutMillis: millisOf(options.timeout)
|
|
972
|
+
});
|
|
973
|
+
const nonPositive = (options) => [
|
|
974
|
+
options.interval,
|
|
975
|
+
options.settle,
|
|
976
|
+
options.timeout
|
|
977
|
+
].some((input) => millisOf(input) <= 0);
|
|
978
|
+
const requirePositive = (options) => nonPositive(options) ? Effect.die(/* @__PURE__ */ new Error(`remote observation durations must be positive milliseconds: interval=${millisOf(options.interval)}, settle=${millisOf(options.settle)}, timeout=${millisOf(options.timeout)}`)) : Effect.void;
|
|
979
|
+
const absence = (traceId, windows) => new EmptyObservationError({
|
|
980
|
+
traceId,
|
|
981
|
+
detail: `the remote trace store served no span for trace ${traceId} before the ${windows.timeoutMillis}ms timeout`
|
|
982
|
+
});
|
|
983
|
+
const incompleteness = (traceId, spans, windows) => new IncompleteObservationError({
|
|
984
|
+
traceId,
|
|
985
|
+
spanCount: spans.length,
|
|
986
|
+
detail: `the remote trace store served ${spans.length} span(s) for trace ${traceId}, which never stayed unchanged for ${windows.settleMillis}ms before the ${windows.timeoutMillis}ms timeout`
|
|
987
|
+
});
|
|
988
|
+
const answer = (verdict, settlement, traceId, windows) => Result.match(verdict, {
|
|
989
|
+
onSuccess: (spans) => Effect.succeed(spans),
|
|
990
|
+
onFailure: (refusal) => refusal === "absent" ? Effect.fail(absence(traceId, windows)) : Effect.fail(incompleteness(traceId, settlement.spans, windows))
|
|
991
|
+
});
|
|
992
|
+
const watching = (windows) => Effect.gen(function* () {
|
|
993
|
+
const startedAtMillis = yield* Clock.currentTimeMillis;
|
|
994
|
+
return {
|
|
995
|
+
settlement: yield* Ref.make(NEVER_READ),
|
|
996
|
+
windows,
|
|
997
|
+
startedAtMillis
|
|
998
|
+
};
|
|
999
|
+
});
|
|
1000
|
+
const advanceAt = (watch, read) => Effect.gen(function* () {
|
|
1001
|
+
const elapsedMillis = (yield* Clock.currentTimeMillis) - watch.startedAtMillis;
|
|
1002
|
+
return settleStep(yield* Ref.get(watch.settlement), read, elapsedMillis, watch.windows);
|
|
1003
|
+
});
|
|
1004
|
+
const elapsedFor = (watch) => Effect.map(Clock.currentTimeMillis, (now) => now - watch.startedAtMillis);
|
|
1005
|
+
const poll = (source, traceId, options, watch) => Effect.gen(function* () {
|
|
1006
|
+
const settlement = yield* Ref.get(watch.settlement);
|
|
1007
|
+
const elapsedMillis = yield* elapsedFor(watch);
|
|
1008
|
+
return yield* Option.match(deadlineVerdict(settlement, elapsedMillis, watch.windows), {
|
|
1009
|
+
onNone: () => readOnce(source, traceId, options, watch, watch.windows.timeoutMillis - elapsedMillis),
|
|
1010
|
+
onSome: (reached) => answer(reached, settlement, traceId, watch.windows)
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
const overdue = (traceId, watch) => Effect.flatMap(Ref.get(watch.settlement), (settlement) => answer(Result.fail(lateRefusal(settlement)), settlement, traceId, watch.windows));
|
|
1014
|
+
const readOnce = (source, traceId, options, watch, remainingMillis) => Effect.flatMap(Effect.timeoutOption(source(traceId), remainingMillis), (answered) => Option.match(answered, {
|
|
1015
|
+
onNone: () => overdue(traceId, watch),
|
|
1016
|
+
onSome: (read) => judgeRead(source, traceId, options, watch, read)
|
|
1017
|
+
}));
|
|
1018
|
+
const judgeRead = (source, traceId, options, watch, read) => Effect.flatMap(advanceAt(watch, read), (advance) => Option.match(advance.verdict, {
|
|
1019
|
+
onNone: () => keepPolling(source, traceId, options, watch, advance),
|
|
1020
|
+
onSome: (verdict) => answer(verdict, advance.settlement, traceId, watch.windows)
|
|
1021
|
+
}));
|
|
1022
|
+
const keepPolling = (source, traceId, options, watch, advance) => Effect.gen(function* () {
|
|
1023
|
+
yield* Ref.set(watch.settlement, advance.settlement);
|
|
1024
|
+
yield* Effect.sleep(options.interval);
|
|
1025
|
+
return yield* Effect.suspend(() => poll(source, traceId, options, watch));
|
|
1026
|
+
});
|
|
1027
|
+
const collector = (source, options, context) => ({ collect: (traceId) => Effect.flatMap(watching(windowsOf(options)), (watch) => poll(source, traceId, options, watch)).pipe(Effect.provideContext(context)) });
|
|
1028
|
+
/**
|
|
1029
|
+
* The `Observation` service over one trace source. `collect` reads the source at
|
|
1030
|
+
* `options.interval`, keeps every span it has seen keyed by span id, and answers
|
|
1031
|
+
* when that union has stayed unchanged for `options.settle`; a trace the store
|
|
1032
|
+
* never served is `EmptyObservationError`, one that never settled is
|
|
1033
|
+
* `IncompleteObservationError`, and the source's own failure ends the poll
|
|
1034
|
+
* immediately, unchanged. Provides `Observation` alone and requires exactly what
|
|
1035
|
+
* the source requires (`Scope.Scope` is supplied by the layer machinery, as
|
|
1036
|
+
* every Effect layer constructor scopes it away).
|
|
1037
|
+
*/
|
|
1038
|
+
const layer = dual(2, (source, options) => Layer.effectContext(Effect.map(Effect.andThen(requirePositive(options), Effect.context()), (context) => Context.make(Observation, collector(source, options, context)))));
|
|
1039
|
+
//#endregion
|
|
692
1040
|
//#region src/Stimulus.ts
|
|
693
1041
|
var Stimulus_exports = /* @__PURE__ */ __exportAll({
|
|
694
1042
|
make: () => make$1,
|
|
@@ -727,6 +1075,15 @@ const make$1 = (options) => {
|
|
|
727
1075
|
return stimulus;
|
|
728
1076
|
};
|
|
729
1077
|
//#endregion
|
|
1078
|
+
//#region src/HarnessFailure.schema.ts
|
|
1079
|
+
const HarnessFailure = Schema.Union([
|
|
1080
|
+
ContractDecodeError,
|
|
1081
|
+
EmptyObservationError,
|
|
1082
|
+
IncompleteObservationError,
|
|
1083
|
+
TransportObservationError,
|
|
1084
|
+
TraceDisparityError
|
|
1085
|
+
]);
|
|
1086
|
+
//#endregion
|
|
730
1087
|
//#region src/Prop.ts
|
|
731
1088
|
const isHold = Schema.is(Hold);
|
|
732
1089
|
const isBreak = Schema.is(Break);
|
|
@@ -778,12 +1135,8 @@ var Suite_exports = /* @__PURE__ */ __exportAll({
|
|
|
778
1135
|
withScenarioLayer: () => withScenarioLayer
|
|
779
1136
|
});
|
|
780
1137
|
const TypeId = Symbol.for("@systemfsoftware/trace-spec/Suite");
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
EmptyObservationError,
|
|
784
|
-
TraceDisparityError
|
|
785
|
-
]));
|
|
786
|
-
const caseFailureOf = (stimulus) => (failure) => isCheckFailure(failure) ? failure : new StimulusFailure({
|
|
1138
|
+
const isHarnessFailure = Schema.is(HarnessFailure);
|
|
1139
|
+
const caseFailureOf = (stimulus) => (failure) => isHarnessFailure(failure) ? failure : new StimulusFailure({
|
|
787
1140
|
stimulus,
|
|
788
1141
|
detail: Cause.pretty(Cause.fail(failure))
|
|
789
1142
|
});
|
|
@@ -828,4 +1181,4 @@ const make = (bindings) => (name) => ({
|
|
|
828
1181
|
...Prototype
|
|
829
1182
|
});
|
|
830
1183
|
//#endregion
|
|
831
|
-
export { Contract_exports as Contract, Graph_exports as Graph, Observation_service_exports as Observation, observation_window_resource_exports as ObservationWindow, Rel_exports as Rel, Stimulus_exports as Stimulus, Suite_exports as Suite };
|
|
1184
|
+
export { Contract_exports as Contract, Graph_exports as Graph, Observation_service_exports as Observation, observation_window_resource_exports as ObservationWindow, Rel_exports as Rel, RemoteObservation_exports as RemoteObservation, Stimulus_exports as Stimulus, Suite_exports as Suite, tempo_trace_store_exports as TempoTraceStore };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/trace-spec",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -33,25 +33,25 @@
|
|
|
33
33
|
"dist"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@effect/opentelemetry": "4.0.0-rc.
|
|
36
|
+
"@effect/opentelemetry": "4.0.0-rc.117",
|
|
37
37
|
"@opentelemetry/api": "^1.9.0",
|
|
38
38
|
"@opentelemetry/resources": "^2",
|
|
39
39
|
"@opentelemetry/sdk-trace-base": "^2",
|
|
40
40
|
"@opentelemetry/semantic-conventions": "^1.33",
|
|
41
|
-
"@systemfsoftware/effect-spec-runtime": "^0.1.
|
|
42
|
-
"@systemfsoftware/trace-taxonomy": "^0.0.
|
|
41
|
+
"@systemfsoftware/effect-spec-runtime": "^0.1.1",
|
|
42
|
+
"@systemfsoftware/trace-taxonomy": "^0.0.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@effect/platform-node": "4.0.0-rc.
|
|
46
|
-
"@effect/vitest": "4.0.0-rc.
|
|
45
|
+
"@effect/platform-node": "4.0.0-rc.117",
|
|
46
|
+
"@effect/vitest": "4.0.0-rc.117",
|
|
47
47
|
"@systemfsoftware/arethetypeswrong-cli": "^4.2.0",
|
|
48
|
-
"@systemfsoftware/effect-gherkin-spec": "^4.3.
|
|
49
|
-
"@systemfsoftware/oxlint-config-recommended": "^2.0.
|
|
50
|
-
"@systemfsoftware/tsconfig": "^2.0.
|
|
48
|
+
"@systemfsoftware/effect-gherkin-spec": "^4.3.1",
|
|
49
|
+
"@systemfsoftware/oxlint-config-recommended": "^2.0.2",
|
|
50
|
+
"@systemfsoftware/tsconfig": "^2.0.1",
|
|
51
51
|
"@systemfsoftware/tsdown-config": "^0.1.0",
|
|
52
52
|
"@systemfsoftware/vitest-config": "^0.1.0",
|
|
53
53
|
"@types/node": "^26",
|
|
54
|
-
"effect": "4.0.0-rc.
|
|
54
|
+
"effect": "4.0.0-rc.117",
|
|
55
55
|
"oxlint": "~1.82.0",
|
|
56
56
|
"rimraf": "^6.1.3",
|
|
57
57
|
"tsdown": "^0.23.0",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"vitest": "^5"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
|
-
"@effect/vitest": "4.0.0-rc.
|
|
63
|
-
"effect": "4.0.0-rc.
|
|
62
|
+
"@effect/vitest": "4.0.0-rc.117",
|
|
63
|
+
"effect": "4.0.0-rc.117",
|
|
64
64
|
"vitest": "^5"
|
|
65
65
|
},
|
|
66
66
|
"publishConfig": {
|