effect-inspect 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -4
- package/app/dist/client/assets/{index-smV05cfr.js → index-T-bCzOWw.js} +2 -2
- package/app/dist/client/assets/routes-C2qO8k2W.js +5 -0
- package/app/dist/server/assets/_tanstack-start-manifest_v-B2BMiICr.js +20 -0
- package/app/dist/server/assets/{router-CN98Ramo.js → router-dMcw-pHq.js} +1 -1
- package/app/dist/server/assets/{routes-eZ4XqxE9.js → routes-BmnrEVoN.js} +203 -157
- package/app/dist/server/server.js +2 -2
- package/dist/cli/QueryCommands.d.ts +127 -0
- package/dist/cli/QueryCommands.js +768 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +150 -8
- package/dist/client/Client.d.ts +59 -5
- package/dist/client/Client.js +196 -16
- package/dist/collector/QueryApi.d.ts +29 -0
- package/dist/collector/QueryApi.js +91 -0
- package/dist/collector/Server.d.ts +1 -1
- package/dist/collector/Server.js +15 -8
- package/dist/collector/Store.d.ts +56 -16
- package/dist/collector/Store.js +37 -11
- package/dist/protocol/Codec.d.ts +10 -0
- package/dist/protocol/Schema.d.ts +118 -1
- package/dist/protocol/Schema.js +53 -1
- package/dist/query/Client.d.ts +30 -0
- package/dist/query/Client.js +74 -0
- package/dist/query/Query.d.ts +599 -0
- package/dist/query/Query.js +876 -0
- package/dist/trace/Timing.d.ts +18 -0
- package/dist/trace/Timing.js +57 -0
- package/dist/trace/TraceFile.d.ts +54 -0
- package/dist/trace/TraceFile.js +86 -0
- package/dist/trace/TraceStore.d.ts +202 -0
- package/dist/trace/TraceStore.js +328 -0
- package/package.json +3 -1
- package/app/dist/client/assets/routes-TKgeFdSW.js +0 -5
- package/app/dist/server/assets/_tanstack-start-manifest_v-Co953HeC.js +0 -20
package/dist/protocol/Schema.js
CHANGED
|
@@ -28,8 +28,24 @@ export const Json = Schema.Union([
|
|
|
28
28
|
]);
|
|
29
29
|
/** Span attributes, log annotations, and metric/event attributes. */
|
|
30
30
|
export const Attributes = Schema.Record(Schema.String, Json);
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Identifies one run of an instrumented program.
|
|
33
|
+
*
|
|
34
|
+
* Any string on the wire, so traces from older clients keep decoding. IDs a
|
|
35
|
+
* caller chooses are held to {@link isValidSessionId} by the client instead.
|
|
36
|
+
*/
|
|
32
37
|
export const SessionId = Schema.String;
|
|
38
|
+
/** What {@link isValidSessionId} accepts, phrased for diagnostics. */
|
|
39
|
+
export const sessionIdRule = '1-128 ASCII letters, digits, ".", "_" or "-", starting with a letter or digit';
|
|
40
|
+
/**
|
|
41
|
+
* Whether a caller-chosen session ID is acceptable, such as `checkout-before-1`.
|
|
42
|
+
*
|
|
43
|
+
* Human-readable rather than UUID-shaped, and needs no quoting as a shell
|
|
44
|
+
* argument. No colon, so a chosen ID can never take the `loaded:` form the
|
|
45
|
+
* webapp gives sessions read from a saved file. A generated UUID also
|
|
46
|
+
* satisfies it.
|
|
47
|
+
*/
|
|
48
|
+
export const isValidSessionId = (id) => /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(id);
|
|
33
49
|
const sessionId = { sessionId: SessionId };
|
|
34
50
|
/** Mirrors `Tracer.SpanKind`. */
|
|
35
51
|
export const SpanKind = Schema.Literals(['internal', 'server', 'client', 'producer', 'consumer']);
|
|
@@ -64,6 +80,13 @@ export const Hello = Schema.Struct({
|
|
|
64
80
|
runtime: Schema.String,
|
|
65
81
|
protocolVersion: Schema.Natural,
|
|
66
82
|
clock: Clock,
|
|
83
|
+
/**
|
|
84
|
+
* Random per client instance, fixed for its lifetime and re-sent on every
|
|
85
|
+
* reconnect. Tells a genuine reconnect (same instance) from an independent
|
|
86
|
+
* run that chose the same `sessionId`. Absent from older clients, which are
|
|
87
|
+
* treated as one instance per `sessionId`.
|
|
88
|
+
*/
|
|
89
|
+
instanceId: Schema.optional(Schema.String),
|
|
67
90
|
});
|
|
68
91
|
/** The protocol version a `Hello` must carry. Bump on any breaking change. */
|
|
69
92
|
export const protocolVersion = 1;
|
|
@@ -249,6 +272,12 @@ export const Session = Schema.Struct({
|
|
|
249
272
|
clock: Clock,
|
|
250
273
|
active: Schema.Boolean,
|
|
251
274
|
endedAtEpochMillis: Schema.optional(Schema.Natural),
|
|
275
|
+
/**
|
|
276
|
+
* Connections refused because a different client instance announced this
|
|
277
|
+
* `sessionId` — an ID collision. Their telemetry was discarded, so this
|
|
278
|
+
* session holds only the first instance's run. Absent when there were none.
|
|
279
|
+
*/
|
|
280
|
+
conflicts: Schema.optional(Schema.Natural),
|
|
252
281
|
});
|
|
253
282
|
/** Every session the collector holds. Sent on connect and on change. */
|
|
254
283
|
export const SessionList = Schema.Struct({
|
|
@@ -302,6 +331,22 @@ export const Unsubscribe = Schema.Struct({
|
|
|
302
331
|
});
|
|
303
332
|
/** What the webapp sends to the collector. */
|
|
304
333
|
export const WebappRequest = Schema.Union([Subscribe, Unsubscribe]);
|
|
334
|
+
/**
|
|
335
|
+
* Collector-side loss counters for one session at snapshot time, carried in
|
|
336
|
+
* a trace file exported from the collector.
|
|
337
|
+
*/
|
|
338
|
+
export const TraceCapture = Schema.Struct({
|
|
339
|
+
/** Messages evicted by the collector's per-session capacity bound. */
|
|
340
|
+
droppedMessages: Schema.Natural,
|
|
341
|
+
/** Lines received for the session that could not be decoded. */
|
|
342
|
+
skippedLines: Schema.Natural,
|
|
343
|
+
/**
|
|
344
|
+
* Whether the collector could refuse an independent run reusing this ID.
|
|
345
|
+
* `false` when the owning client predates instance IDs, so a reused ID
|
|
346
|
+
* would have merged silently and `session.conflicts` proves nothing.
|
|
347
|
+
*/
|
|
348
|
+
conflictDetection: Schema.Boolean,
|
|
349
|
+
});
|
|
305
350
|
/**
|
|
306
351
|
* Line 1 of a saved trace file.
|
|
307
352
|
*
|
|
@@ -322,6 +367,13 @@ export const TraceFileHeader = Schema.Struct({
|
|
|
322
367
|
protocolVersion: Schema.Natural,
|
|
323
368
|
session: Session,
|
|
324
369
|
savedAtEpochMillis: Schema.Natural,
|
|
370
|
+
/**
|
|
371
|
+
* What the collector knew about loss when the file was exported from it.
|
|
372
|
+
* Absent from browser saves and older files: their capture completeness is
|
|
373
|
+
* unknown, not complete. Optional and additive, so older builds, which
|
|
374
|
+
* ignore unknown header keys, still read these files.
|
|
375
|
+
*/
|
|
376
|
+
capture: Schema.optional(TraceCapture),
|
|
325
377
|
});
|
|
326
378
|
/** The trace file layout version. Bump only on a change to the header or the line layout. */
|
|
327
379
|
export const traceFileFormatVersion = 1;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls a running collector's query API (see `collector/QueryApi.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Never fails: transport problems become the same `QueryFailure` shape the
|
|
5
|
+
* collector returns, tagged `CollectorUnavailable` (nothing answered in time)
|
|
6
|
+
* or `CollectorError` (something answered, but not this API), so a caller
|
|
7
|
+
* prints one JSON contract whatever went wrong.
|
|
8
|
+
*/
|
|
9
|
+
import { Effect } from 'effect';
|
|
10
|
+
import { HttpClient } from 'effect/unstable/http';
|
|
11
|
+
import * as Query from './Query.ts';
|
|
12
|
+
/** Where `effect-inspect start` listens by default. */
|
|
13
|
+
export declare const defaultUrl = "http://localhost:34437";
|
|
14
|
+
/** How long a call waits for the collector before `CollectorUnavailable`. */
|
|
15
|
+
export declare const defaultTimeoutMs = 5000;
|
|
16
|
+
export interface Options {
|
|
17
|
+
/** Collector base URL, e.g. {@link defaultUrl}. */
|
|
18
|
+
readonly url: string;
|
|
19
|
+
readonly timeoutMs?: number | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Sends one query request (validated by the collector) and returns its response. */
|
|
22
|
+
export declare const query: (options: Options, request: unknown) => Effect.Effect<Query.QueryResponse, never, HttpClient.HttpClient>;
|
|
23
|
+
/**
|
|
24
|
+
* Downloads one session's frozen snapshot as `.eitrace` text, loss counters
|
|
25
|
+
* included, for offline queries that answer exactly as the live one did.
|
|
26
|
+
*/
|
|
27
|
+
export declare const exportTrace: (options: Options, sessionId: string) => Effect.Effect<{
|
|
28
|
+
readonly ok: true;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
} | Query.QueryFailure, never, HttpClient.HttpClient>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls a running collector's query API (see `collector/QueryApi.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Never fails: transport problems become the same `QueryFailure` shape the
|
|
5
|
+
* collector returns, tagged `CollectorUnavailable` (nothing answered in time)
|
|
6
|
+
* or `CollectorError` (something answered, but not this API), so a caller
|
|
7
|
+
* prints one JSON contract whatever went wrong.
|
|
8
|
+
*/
|
|
9
|
+
import { Duration, Effect } from 'effect';
|
|
10
|
+
import { HttpClient, HttpClientRequest } from 'effect/unstable/http';
|
|
11
|
+
import { defaultPort } from '../collector/Config.js';
|
|
12
|
+
import * as Query from './Query.js';
|
|
13
|
+
/** Where `effect-inspect start` listens by default. */
|
|
14
|
+
export const defaultUrl = `http://localhost:${defaultPort}`;
|
|
15
|
+
/** How long a call waits for the collector before `CollectorUnavailable`. */
|
|
16
|
+
export const defaultTimeoutMs = 5000;
|
|
17
|
+
const unavailable = (op, url, detail) => Query.failure(op, 'CollectorUnavailable', `No collector answered at ${url}: ${detail}`, 'Start one with `effect-inspect start` (EFFECT_INSPECT_PORT sets its port) or point at the right URL; for saved traces query the file instead.', { url });
|
|
18
|
+
const foreign = (op, url, detail) => Query.failure(op, 'CollectorError', `${url} did not answer as an effect-inspect query API: ${detail}`, 'The collector may predate the query API, or another service owns that port. Upgrade and restart the collector, or check the URL.', { url });
|
|
19
|
+
const isResponse = (body) => typeof body === 'object' &&
|
|
20
|
+
body !== null &&
|
|
21
|
+
body.apiVersion === Query.apiVersion &&
|
|
22
|
+
typeof body.ok === 'boolean';
|
|
23
|
+
const opOf = (request) => typeof request === 'object' &&
|
|
24
|
+
request !== null &&
|
|
25
|
+
typeof request.op === 'string'
|
|
26
|
+
? request.op
|
|
27
|
+
: null;
|
|
28
|
+
const withTimeout = (options, op, effect) => {
|
|
29
|
+
const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
30
|
+
return Effect.timeoutOrElse(effect, {
|
|
31
|
+
duration: Duration.millis(timeoutMs),
|
|
32
|
+
orElse: () => Effect.succeed(unavailable(op, options.url, `no answer within ${timeoutMs}ms`)),
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
/** Sends one query request (validated by the collector) and returns its response. */
|
|
36
|
+
export const query = (options, request) => {
|
|
37
|
+
const op = opOf(request);
|
|
38
|
+
// Bounded here too: a failure built from a long URL, or an oversized body
|
|
39
|
+
// from a collector that does not enforce the bound, must not escape it.
|
|
40
|
+
return Effect.map(withTimeout(options, op, Effect.gen(function* () {
|
|
41
|
+
const client = yield* HttpClient.HttpClient;
|
|
42
|
+
const response = yield* client.execute(HttpClientRequest.post(new URL('/api/v1/query', options.url)).pipe(HttpClientRequest.bodyJsonUnsafe(request)));
|
|
43
|
+
const body = yield* response.json;
|
|
44
|
+
return isResponse(body)
|
|
45
|
+
? body
|
|
46
|
+
: foreign(op, options.url, `unexpected body (HTTP ${response.status})`);
|
|
47
|
+
}).pipe(Effect.catchTag('HttpClientError', (error) => Effect.succeed(error.reason._tag === 'TransportError' || error.reason._tag === 'InvalidUrlError'
|
|
48
|
+
? unavailable(op, options.url, error.message)
|
|
49
|
+
: foreign(op, options.url, error.message))))), Query.limitResponse);
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Downloads one session's frozen snapshot as `.eitrace` text, loss counters
|
|
53
|
+
* included, for offline queries that answer exactly as the live one did.
|
|
54
|
+
*/
|
|
55
|
+
export const exportTrace = (options, sessionId) => Effect.map(withTimeout(options, 'export', Effect.gen(function* () {
|
|
56
|
+
const client = yield* HttpClient.HttpClient;
|
|
57
|
+
const url = new URL('/api/v1/export', options.url);
|
|
58
|
+
url.searchParams.set('sessionId', sessionId);
|
|
59
|
+
const response = yield* client.execute(HttpClientRequest.get(url));
|
|
60
|
+
// An older collector hands unknown paths to the web UI, which may answer 200 HTML.
|
|
61
|
+
if (response.status === 200 &&
|
|
62
|
+
response.headers['content-type']?.startsWith('application/x-ndjson') === true) {
|
|
63
|
+
return { ok: true, text: yield* response.text };
|
|
64
|
+
}
|
|
65
|
+
const body = yield* response.json;
|
|
66
|
+
return isResponse(body) && !body.ok
|
|
67
|
+
? body
|
|
68
|
+
: foreign('export', options.url, `unexpected body (HTTP ${response.status})`);
|
|
69
|
+
}).pipe(Effect.catchTag('HttpClientError', (error) => Effect.succeed(error.reason._tag === 'TransportError' || error.reason._tag === 'InvalidUrlError'
|
|
70
|
+
? unavailable('export', options.url, error.message)
|
|
71
|
+
: foreign('export', options.url, error.message))))),
|
|
72
|
+
// The trace text is a lossless artifact and deliberately unbounded;
|
|
73
|
+
// only failures are held to the JSON response bound.
|
|
74
|
+
(result) => (result.ok ? result : Query.limitResponse(result)));
|