@toa.io/extensions.introspection 1.0.0-alpha.259
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/LICENSE +22 -0
- package/components/introspection.edges/manifest.toa.yaml +48 -0
- package/components/introspection.edges/operations/lib/types.d.ts +28 -0
- package/components/introspection.edges/operations/lib/types.js +3 -0
- package/components/introspection.edges/operations/lib/types.js.map +1 -0
- package/components/introspection.edges/operations/merge.d.ts +9 -0
- package/components/introspection.edges/operations/merge.js +25 -0
- package/components/introspection.edges/operations/merge.js.map +1 -0
- package/components/introspection.edges/operations/tsconfig.tsbuildinfo +1 -0
- package/components/introspection.edges/source/lib/types.ts +33 -0
- package/components/introspection.edges/source/merge.ts +26 -0
- package/components/introspection.edges/tsconfig.json +9 -0
- package/components/introspection.nodes/manifest.toa.yaml +49 -0
- package/components/introspection.nodes/operations/lib/types.d.ts +16 -0
- package/components/introspection.nodes/operations/lib/types.js +3 -0
- package/components/introspection.nodes/operations/lib/types.js.map +1 -0
- package/components/introspection.nodes/operations/merge.d.ts +9 -0
- package/components/introspection.nodes/operations/merge.js +21 -0
- package/components/introspection.nodes/operations/merge.js.map +1 -0
- package/components/introspection.nodes/operations/tsconfig.tsbuildinfo +1 -0
- package/components/introspection.nodes/source/lib/types.ts +18 -0
- package/components/introspection.nodes/source/merge.ts +21 -0
- package/components/introspection.nodes/tsconfig.json +9 -0
- package/cucumber.js +8 -0
- package/features/calls.feature +123 -0
- package/features/nodes.feature +70 -0
- package/features/samples.feature +75 -0
- package/features/shutdown.feature +37 -0
- package/features/site/_app/immutable/asset.js +1 -0
- package/features/site/index.html +6 -0
- package/features/steps/Map.ts +151 -0
- package/features/steps/UI.ts +88 -0
- package/features/steps/components/probe.discreet/manifest.toa.yaml +12 -0
- package/features/steps/components/probe.discreet/operations/ping.js +7 -0
- package/features/steps/components/probe.quiet/manifest.toa.yaml +11 -0
- package/features/steps/components/probe.quiet/operations/ping.js +7 -0
- package/features/steps/components/probe.source/manifest.toa.yaml +27 -0
- package/features/steps/components/probe.source/operations/crash.js +7 -0
- package/features/steps/components/probe.source/operations/fail.js +12 -0
- package/features/steps/components/probe.source/operations/relay.js +7 -0
- package/features/steps/components/probe.target/manifest.toa.yaml +24 -0
- package/features/steps/components/probe.target/operations/compute.js +7 -0
- package/features/steps/components/probe.target/operations/count.js +9 -0
- package/features/steps/config.ts +20 -0
- package/features/steps/tsconfig.json +9 -0
- package/features/ui.feature +85 -0
- package/package.json +32 -0
- package/readme.md +135 -0
- package/schemas/annotation.cos.yaml +21 -0
- package/schemas/declaration.cos.yaml +2 -0
- package/source/Composition.ts +61 -0
- package/source/Explorer.ts +16 -0
- package/source/Factory.ts +119 -0
- package/source/Reporter.ts +210 -0
- package/source/Tenant.ts +47 -0
- package/source/UI.ts +197 -0
- package/source/annotation.ts +83 -0
- package/source/const.ts +32 -0
- package/source/describe.ts +68 -0
- package/source/extension.ts +45 -0
- package/source/index.ts +7 -0
- package/source/keys.ts +0 -0
- package/source/manifest.ts +19 -0
- package/source/model.ts +82 -0
- package/source/sample.ts +49 -0
- package/source/schemas.ts +10 -0
- package/transpiled/Composition.d.ts +21 -0
- package/transpiled/Composition.js +48 -0
- package/transpiled/Composition.js.map +1 -0
- package/transpiled/Explorer.d.ts +9 -0
- package/transpiled/Explorer.js +19 -0
- package/transpiled/Explorer.js.map +1 -0
- package/transpiled/Factory.d.ts +22 -0
- package/transpiled/Factory.js +93 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Reporter.d.ts +44 -0
- package/transpiled/Reporter.js +187 -0
- package/transpiled/Reporter.js.map +1 -0
- package/transpiled/Tenant.d.ts +18 -0
- package/transpiled/Tenant.js +40 -0
- package/transpiled/Tenant.js.map +1 -0
- package/transpiled/UI.d.ts +25 -0
- package/transpiled/UI.js +186 -0
- package/transpiled/UI.js.map +1 -0
- package/transpiled/annotation.d.ts +40 -0
- package/transpiled/annotation.js +46 -0
- package/transpiled/annotation.js.map +1 -0
- package/transpiled/const.d.ts +23 -0
- package/transpiled/const.js +27 -0
- package/transpiled/const.js.map +1 -0
- package/transpiled/describe.d.ts +9 -0
- package/transpiled/describe.js +64 -0
- package/transpiled/describe.js.map +1 -0
- package/transpiled/extension.d.ts +11 -0
- package/transpiled/extension.js +64 -0
- package/transpiled/extension.js.map +1 -0
- package/transpiled/index.d.ts +6 -0
- package/transpiled/index.js +25 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/keys.d.ts +9 -0
- package/transpiled/keys.js +0 -0
- package/transpiled/keys.js.map +1 -0
- package/transpiled/manifest.d.ts +9 -0
- package/transpiled/manifest.js +43 -0
- package/transpiled/manifest.js.map +1 -0
- package/transpiled/model.d.ts +71 -0
- package/transpiled/model.js +3 -0
- package/transpiled/model.js.map +1 -0
- package/transpiled/sample.d.ts +7 -0
- package/transpiled/sample.js +44 -0
- package/transpiled/sample.js.map +1 -0
- package/transpiled/schemas.d.ts +4 -0
- package/transpiled/schemas.js +13 -0
- package/transpiled/schemas.js.map +1 -0
- package/transpiled/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { decode } from '@toa.io/generic'
|
|
2
|
+
import { DEFAULT_INTERVAL, DEFAULT_THRESHOLD, DENIED, ENV } from './const'
|
|
3
|
+
import type { Resources } from '@toa.io/operations'
|
|
4
|
+
|
|
5
|
+
/** `context.toa.yaml` */
|
|
6
|
+
export type Annotation = false | {
|
|
7
|
+
/** Capture real payloads. Off by default: this is production data. */
|
|
8
|
+
samples?: boolean
|
|
9
|
+
/** Flush period, seconds. */
|
|
10
|
+
interval?: number
|
|
11
|
+
/** Flush as soon as this many distinct edges are buffered. */
|
|
12
|
+
threshold?: number
|
|
13
|
+
/** Publish the UI. On by default. */
|
|
14
|
+
ui?: boolean
|
|
15
|
+
resources?: Resources
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `manifest.toa.yaml` */
|
|
19
|
+
export type Declaration = false | {
|
|
20
|
+
samples?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What `deployment()` encodes into the environment. */
|
|
24
|
+
export interface Options {
|
|
25
|
+
samples: boolean
|
|
26
|
+
interval: number
|
|
27
|
+
threshold: number
|
|
28
|
+
ui: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The effective per-component decision. */
|
|
32
|
+
export interface Settings {
|
|
33
|
+
enabled: boolean
|
|
34
|
+
samples: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DISABLED: Settings = { enabled: false, samples: false }
|
|
38
|
+
|
|
39
|
+
export function options (annotation?: Annotation): Options {
|
|
40
|
+
const declaration = annotation === undefined || annotation === false ? {} : annotation
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
samples: declaration.samples === true,
|
|
44
|
+
interval: declaration.interval ?? DEFAULT_INTERVAL,
|
|
45
|
+
threshold: declaration.threshold ?? DEFAULT_THRESHOLD,
|
|
46
|
+
ui: declaration.ui !== false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Reads what `deployment()` has put into the environment. */
|
|
51
|
+
export function environment (): Options | null {
|
|
52
|
+
const value = process.env[ENV]
|
|
53
|
+
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return null
|
|
56
|
+
|
|
57
|
+
return decode<Options>(value)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function component (declaration: Declaration | null | undefined): Declaration {
|
|
61
|
+
if (declaration === false)
|
|
62
|
+
return false
|
|
63
|
+
|
|
64
|
+
// predefined extensions arrive as null for components that say nothing
|
|
65
|
+
if (declaration === null || declaration === undefined)
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
return declaration.samples === undefined ? {} : { samples: declaration.samples }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Both levels must agree, and either can veto: the context is the environment
|
|
73
|
+
* ceiling, the manifest is the component's own call. A component handling
|
|
74
|
+
* personal data opts out for good, and no context flag overrides that.
|
|
75
|
+
*/
|
|
76
|
+
export function settings (namespace: string, declaration: Declaration, opts: Options | null): Settings {
|
|
77
|
+
if (opts === null || declaration === false)
|
|
78
|
+
return DISABLED
|
|
79
|
+
|
|
80
|
+
const samples = opts.samples && declaration.samples !== false && !DENIED.has(namespace)
|
|
81
|
+
|
|
82
|
+
return { enabled: true, samples }
|
|
83
|
+
}
|
package/source/const.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const ID = 'introspection'
|
|
2
|
+
export const NAMESPACE = 'introspection'
|
|
3
|
+
|
|
4
|
+
export const ENV = 'TOA_INTROSPECTION'
|
|
5
|
+
|
|
6
|
+
export const NODES = 'nodes'
|
|
7
|
+
export const EDGES = 'edges'
|
|
8
|
+
|
|
9
|
+
/** Where the UI is mounted; `/introspection/*` belongs to the components' own API. */
|
|
10
|
+
export const UI_PATH = '/.introspection'
|
|
11
|
+
export const UI_PORT = 8002
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_INTERVAL = 300
|
|
14
|
+
export const DEFAULT_THRESHOLD = 1024
|
|
15
|
+
|
|
16
|
+
/** How often a component re-announces its description, so that removed components fade out. */
|
|
17
|
+
export const ANNOUNCE_INTERVAL = 1_800_000
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `source` arrives over the wire, so the number of distinct edges a process
|
|
21
|
+
* can hold must be bounded regardless of what peers send.
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_EDGES = 4096
|
|
24
|
+
|
|
25
|
+
/** Never capture samples for these namespaces, whatever the annotation says. */
|
|
26
|
+
export const DENIED = new Set(['identity', 'introspection'])
|
|
27
|
+
|
|
28
|
+
/** Keys never stored in a sample, even when sampling is on. */
|
|
29
|
+
export const REDACTED = /^(password|secret|token|credentials?|key|authorization|cookie)$/i
|
|
30
|
+
|
|
31
|
+
/** Serialized size cap of a single sample. */
|
|
32
|
+
export const SAMPLE_LIMIT = 4096
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Node, Operation, Event, Receiver } from './model'
|
|
2
|
+
import type { Manifest } from '@toa.io/norm'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Turns a normalized component manifest into the node of the map.
|
|
6
|
+
*
|
|
7
|
+
* Everything here is already expanded by norm: concise schemas are resolved and
|
|
8
|
+
* the prototype chain is collapsed, so this is the shape the runtime actually runs.
|
|
9
|
+
*/
|
|
10
|
+
export function describe (manifest: Manifest): Node {
|
|
11
|
+
return {
|
|
12
|
+
namespace: manifest.namespace,
|
|
13
|
+
component: manifest.name,
|
|
14
|
+
version: manifest.version,
|
|
15
|
+
entity: entity(manifest),
|
|
16
|
+
operations: operations(manifest),
|
|
17
|
+
events: events(manifest),
|
|
18
|
+
receivers: receivers(manifest),
|
|
19
|
+
|
|
20
|
+
// names only: extension declarations carry configuration and secrets
|
|
21
|
+
extensions: Object.keys(manifest.extensions ?? {})
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function entity (manifest: Manifest): Node['entity'] {
|
|
26
|
+
if (manifest.entity === undefined)
|
|
27
|
+
return null
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
schema: manifest.entity.schema,
|
|
31
|
+
storage: manifest.entity.storage,
|
|
32
|
+
associated: manifest.entity.associated === true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function operations (manifest: Manifest): Operation[] {
|
|
37
|
+
return Object.entries(manifest.operations ?? {})
|
|
38
|
+
.map(([endpoint, definition]: [string, any]) => ({
|
|
39
|
+
endpoint,
|
|
40
|
+
type: definition.type,
|
|
41
|
+
scope: definition.scope,
|
|
42
|
+
query: definition.query,
|
|
43
|
+
input: definition.input ?? null,
|
|
44
|
+
output: definition.output ?? null,
|
|
45
|
+
errors: definition.errors ?? []
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function events (manifest: Manifest): Event[] {
|
|
50
|
+
return Object.entries(manifest.events ?? {})
|
|
51
|
+
.map(([label, definition]: [string, any]) => ({ label, binding: definition.binding }))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* Receiver labels contain dots (`identity.bans.created`), which is why nodes keep
|
|
56
|
+
* these as arrays rather than maps — a dot in a document key is a hazard in Mongo.
|
|
57
|
+
*/
|
|
58
|
+
function receivers (manifest: Manifest): Receiver[] {
|
|
59
|
+
return Object.entries(manifest.receivers ?? {})
|
|
60
|
+
.map(([label, definition]: [string, any]) => ({
|
|
61
|
+
label,
|
|
62
|
+
source: definition.source ?? label.split('.').slice(0, 2).join('.'),
|
|
63
|
+
event: label.split('.').pop()!,
|
|
64
|
+
operation: definition.operation,
|
|
65
|
+
conditioned: definition.conditioned === true,
|
|
66
|
+
adaptive: definition.adaptive === true
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { encode } from '@toa.io/generic'
|
|
2
|
+
import { options } from './annotation'
|
|
3
|
+
import { components } from './Composition'
|
|
4
|
+
import { ENV, UI_PATH, UI_PORT } from './const'
|
|
5
|
+
import * as schemas from './schemas'
|
|
6
|
+
import type { Annotation } from './annotation'
|
|
7
|
+
import type { Dependency, Instances, Service } from '@toa.io/operations'
|
|
8
|
+
|
|
9
|
+
export const standalone = true
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The explorer hosts the introspection components, exactly as the exposition
|
|
13
|
+
* gateway hosts the identity ones. Collection is on unless the context says
|
|
14
|
+
* `introspection: false`, and the environment variable is emitted together
|
|
15
|
+
* with the service — never on its own, or tasks would pile up in a queue
|
|
16
|
+
* nothing consumes.
|
|
17
|
+
*/
|
|
18
|
+
export function deployment (_: Instances<unknown>, annotation?: Annotation): Dependency {
|
|
19
|
+
if (annotation === false)
|
|
20
|
+
return {}
|
|
21
|
+
|
|
22
|
+
if (annotation !== undefined)
|
|
23
|
+
schemas.annotation.validate(annotation)
|
|
24
|
+
|
|
25
|
+
const opts = options(annotation)
|
|
26
|
+
|
|
27
|
+
const service: Service = {
|
|
28
|
+
group: 'introspection',
|
|
29
|
+
name: 'explorer',
|
|
30
|
+
version: require('../package.json').version,
|
|
31
|
+
components: components().labels,
|
|
32
|
+
resources: annotation?.resources,
|
|
33
|
+
variables: []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (opts.ui) {
|
|
37
|
+
service.port = UI_PORT
|
|
38
|
+
service.ingress = { path: UI_PATH }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
services: [service],
|
|
43
|
+
variables: { global: [{ name: ENV, value: encode(opts) }] }
|
|
44
|
+
}
|
|
45
|
+
}
|
package/source/index.ts
ADDED
package/source/keys.ts
ADDED
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { component } from './annotation'
|
|
2
|
+
import * as schemas from './schemas'
|
|
3
|
+
import type { Declaration } from './annotation'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The component level of the annotation.
|
|
7
|
+
*
|
|
8
|
+
* The extension is predefined, so most components say nothing and the
|
|
9
|
+
* declaration arrives as `null` — which still has to produce a value,
|
|
10
|
+
* or norm rejects the extension.
|
|
11
|
+
*/
|
|
12
|
+
export function manifest (declaration: Declaration | null | undefined): Declaration {
|
|
13
|
+
const normalized = component(declaration)
|
|
14
|
+
|
|
15
|
+
if (normalized !== false)
|
|
16
|
+
schemas.declaration.validate(normalized)
|
|
17
|
+
|
|
18
|
+
return normalized
|
|
19
|
+
}
|
package/source/model.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Source } from '@toa.io/core'
|
|
2
|
+
|
|
3
|
+
export type { Source }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The caller side of an edge: the operation that made the call, the event that
|
|
7
|
+
* caused it, or the service the call came from.
|
|
8
|
+
*/
|
|
9
|
+
export type Origin = Source
|
|
10
|
+
|
|
11
|
+
/** The callee side of an edge. */
|
|
12
|
+
export interface Target {
|
|
13
|
+
namespace: string
|
|
14
|
+
component: string
|
|
15
|
+
operation: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Operation {
|
|
19
|
+
endpoint: string
|
|
20
|
+
type: string
|
|
21
|
+
scope?: string
|
|
22
|
+
query?: boolean
|
|
23
|
+
input?: object | null
|
|
24
|
+
output?: object | null
|
|
25
|
+
errors?: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Event {
|
|
29
|
+
label: string
|
|
30
|
+
binding?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Receiver {
|
|
34
|
+
label: string
|
|
35
|
+
source: string
|
|
36
|
+
event: string
|
|
37
|
+
operation: string
|
|
38
|
+
conditioned: boolean
|
|
39
|
+
adaptive: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Entity {
|
|
43
|
+
schema: object
|
|
44
|
+
storage?: string
|
|
45
|
+
associated: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The static description of a component, derived from its manifest.
|
|
50
|
+
* Sent to `introspection.nodes`.
|
|
51
|
+
*/
|
|
52
|
+
export interface Node {
|
|
53
|
+
namespace: string
|
|
54
|
+
component: string
|
|
55
|
+
version: string
|
|
56
|
+
entity: Entity | null
|
|
57
|
+
operations: Operation[]
|
|
58
|
+
events: Event[]
|
|
59
|
+
receivers: Receiver[]
|
|
60
|
+
extensions: string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface Sample {
|
|
64
|
+
at: number
|
|
65
|
+
input?: unknown
|
|
66
|
+
outcome: Outcome
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type Outcome = 'ok' | 'error' | 'exception'
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A call observed in a process between flushes. What the application declares —
|
|
73
|
+
* which events exist, and which receiver takes which — is a node's business;
|
|
74
|
+
* an edge is what actually happened.
|
|
75
|
+
*
|
|
76
|
+
* Sent to `introspection.edges`.
|
|
77
|
+
*/
|
|
78
|
+
export interface Edge {
|
|
79
|
+
src: Origin
|
|
80
|
+
dst: Target
|
|
81
|
+
sample?: Sample
|
|
82
|
+
}
|
package/source/sample.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { REDACTED, SAMPLE_LIMIT } from './const'
|
|
3
|
+
import type { Outcome, Sample } from './model'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Captures the payload of a call. Only reached when both the context and the
|
|
7
|
+
* component have opted in, and never for a denied namespace.
|
|
8
|
+
*/
|
|
9
|
+
export function capture (input: unknown, outcome: Outcome): Sample {
|
|
10
|
+
return { at: Date.now(), input: redact(input), outcome }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function samplable (input: unknown): boolean {
|
|
14
|
+
return !(input instanceof Readable)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function redact (value: unknown): unknown {
|
|
18
|
+
if (value === null || value === undefined || typeof value !== 'object')
|
|
19
|
+
return truncate(value)
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return truncate(value.map(redact))
|
|
23
|
+
|
|
24
|
+
const result: Record<string, unknown> = {}
|
|
25
|
+
|
|
26
|
+
for (const [key, property] of Object.entries(value))
|
|
27
|
+
result[key] = REDACTED.test(key) ? '***' : redact(property)
|
|
28
|
+
|
|
29
|
+
return truncate(result)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The map holds one sample per edge indefinitely, so a single oversized
|
|
34
|
+
* payload must not become a permanent tenant of the collection.
|
|
35
|
+
*/
|
|
36
|
+
function truncate (value: unknown): unknown {
|
|
37
|
+
let serialized: string
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
serialized = JSON.stringify(value) ?? ''
|
|
41
|
+
} catch {
|
|
42
|
+
return '[unserializable]'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (serialized.length <= SAMPLE_LIMIT)
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
return '[truncated]'
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import schemas from '@toa.io/schemas'
|
|
3
|
+
import type { Schema } from '@toa.io/schemas'
|
|
4
|
+
import type { Annotation, Declaration } from './annotation'
|
|
5
|
+
|
|
6
|
+
const path = resolve(__dirname, '../schemas')
|
|
7
|
+
const namespace = schemas.namespace(path)
|
|
8
|
+
|
|
9
|
+
export const annotation: Schema<Exclude<Annotation, false>> = namespace.schema('annotation')
|
|
10
|
+
export const declaration: Schema<Exclude<Declaration, false>> = namespace.schema('declaration')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import { type Bootloader } from './Factory';
|
|
3
|
+
import type { Annotation } from './annotation';
|
|
4
|
+
/** Hosts the introspection components in the explorer process. */
|
|
5
|
+
export declare class Composition extends Connector {
|
|
6
|
+
private readonly boot;
|
|
7
|
+
constructor(boot: Bootloader);
|
|
8
|
+
protected open(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare function find(): string[];
|
|
11
|
+
/**
|
|
12
|
+
* The extension is predefined, so an application that turns introspection off
|
|
13
|
+
* must not end up with the explorer components — nor with the exposition
|
|
14
|
+
* dependency they bring in.
|
|
15
|
+
*/
|
|
16
|
+
export declare function components(annotation?: Annotation): Components;
|
|
17
|
+
interface Components {
|
|
18
|
+
labels: string[];
|
|
19
|
+
paths: string[];
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.components = exports.find = exports.Composition = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const core_1 = require("@toa.io/core");
|
|
7
|
+
/** Hosts the introspection components in the explorer process. */
|
|
8
|
+
class Composition extends core_1.Connector {
|
|
9
|
+
boot;
|
|
10
|
+
constructor(boot) {
|
|
11
|
+
super();
|
|
12
|
+
this.boot = boot;
|
|
13
|
+
}
|
|
14
|
+
async open() {
|
|
15
|
+
const paths = find();
|
|
16
|
+
const composition = await this.boot.composition(paths);
|
|
17
|
+
await composition.connect();
|
|
18
|
+
this.depends(composition);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.Composition = Composition;
|
|
22
|
+
function find() {
|
|
23
|
+
return entries().map((entry) => (0, node_path_1.resolve)(ROOT, entry.name));
|
|
24
|
+
}
|
|
25
|
+
exports.find = find;
|
|
26
|
+
function entries() {
|
|
27
|
+
const entries = (0, node_fs_1.readdirSync)(ROOT, { withFileTypes: true });
|
|
28
|
+
return entries.filter((entry) => entry.isDirectory());
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The extension is predefined, so an application that turns introspection off
|
|
32
|
+
* must not end up with the explorer components — nor with the exposition
|
|
33
|
+
* dependency they bring in.
|
|
34
|
+
*/
|
|
35
|
+
function components(annotation) {
|
|
36
|
+
if (annotation === false)
|
|
37
|
+
return { labels: [], paths: [] };
|
|
38
|
+
const labels = [];
|
|
39
|
+
const paths = [];
|
|
40
|
+
for (const entry of entries()) {
|
|
41
|
+
labels.push(entry.name.replace('.', '-'));
|
|
42
|
+
paths.push((0, node_path_1.resolve)(ROOT, entry.name));
|
|
43
|
+
}
|
|
44
|
+
return { labels, paths };
|
|
45
|
+
}
|
|
46
|
+
exports.components = components;
|
|
47
|
+
const ROOT = (0, node_path_1.resolve)(__dirname, '../components/');
|
|
48
|
+
//# sourceMappingURL=Composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Composition.js","sourceRoot":"","sources":["../source/Composition.ts"],"names":[],"mappings":";;;AAAA,qCAAkD;AAClD,yCAAmC;AACnC,uCAAwC;AAIxC,kEAAkE;AAClE,MAAa,WAAY,SAAQ,gBAAS;IACvB,IAAI,CAAY;IAEjC,YAAoB,IAAgB;QAClC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;QACpB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;QAEtD,MAAM,WAAW,CAAC,OAAO,EAAE,CAAA;QAE3B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3B,CAAC;CACF;AAhBD,kCAgBC;AAED,SAAgB,IAAI;IAClB,OAAO,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,mBAAO,EAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5D,CAAC;AAFD,oBAEC;AAED,SAAS,OAAO;IACd,MAAM,OAAO,GAAG,IAAA,qBAAW,EAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IAE1D,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;AACvD,CAAC;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAE,UAAuB;IACjD,IAAI,UAAU,KAAK,KAAK;QACtB,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IAElC,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACzC,KAAK,CAAC,IAAI,CAAC,IAAA,mBAAO,EAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IACvC,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AAC1B,CAAC;AAbD,gCAaC;AAOD,MAAM,IAAI,GAAG,IAAA,mBAAO,EAAC,SAAS,EAAE,gBAAgB,CAAC,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
/**
|
|
3
|
+
* The explorer process. It hosts the introspection components — the map is read
|
|
4
|
+
* through their own operations — and serves the UI.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Explorer extends Connector {
|
|
7
|
+
protected open(): Promise<void>;
|
|
8
|
+
protected dispose(): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Explorer = void 0;
|
|
4
|
+
const core_1 = require("@toa.io/core");
|
|
5
|
+
const openspan_1 = require("openspan");
|
|
6
|
+
/**
|
|
7
|
+
* The explorer process. It hosts the introspection components — the map is read
|
|
8
|
+
* through their own operations — and serves the UI.
|
|
9
|
+
*/
|
|
10
|
+
class Explorer extends core_1.Connector {
|
|
11
|
+
async open() {
|
|
12
|
+
openspan_1.console.info('Introspection explorer started');
|
|
13
|
+
}
|
|
14
|
+
dispose() {
|
|
15
|
+
openspan_1.console.info('Introspection explorer is closed');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.Explorer = Explorer;
|
|
19
|
+
//# sourceMappingURL=Explorer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Explorer.js","sourceRoot":"","sources":["../source/Explorer.ts"],"names":[],"mappings":";;;AAAA,uCAAwC;AACxC,uCAAkC;AAElC;;;GAGG;AACH,MAAa,QAAS,SAAQ,gBAAS;IAClB,KAAK,CAAC,IAAI;QAC3B,kBAAO,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;IAChD,CAAC;IAEkB,OAAO;QACxB,kBAAO,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAA;IAClD,CAAC;CACF;AARD,4BAQC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Declaration } from './annotation';
|
|
3
|
+
import type { Manifest } from '@toa.io/norm';
|
|
4
|
+
import type { Component, Locator, extensions } from '@toa.io/core';
|
|
5
|
+
export declare class Factory implements extensions.Factory {
|
|
6
|
+
private readonly boot;
|
|
7
|
+
private readonly options;
|
|
8
|
+
private readonly settings;
|
|
9
|
+
private reporter;
|
|
10
|
+
constructor(boot: Bootloader);
|
|
11
|
+
tenant(locator: Locator, decl: Declaration | null, manifest: Manifest): Connector;
|
|
12
|
+
component(component: Component): Component;
|
|
13
|
+
service(): Connector | null;
|
|
14
|
+
/**
|
|
15
|
+
* `tenant()` runs before any component is created, so settings are warm.
|
|
16
|
+
* A component booted on its own (without a composition) falls back to
|
|
17
|
+
* the environment, with sampling off.
|
|
18
|
+
*/
|
|
19
|
+
private resolve;
|
|
20
|
+
private collector;
|
|
21
|
+
}
|
|
22
|
+
export type Bootloader = typeof import('@toa.io/boot');
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Factory = void 0;
|
|
4
|
+
const core_1 = require("@toa.io/core");
|
|
5
|
+
const annotation_1 = require("./annotation");
|
|
6
|
+
const const_1 = require("./const");
|
|
7
|
+
const describe_1 = require("./describe");
|
|
8
|
+
const Reporter_1 = require("./Reporter");
|
|
9
|
+
const Tenant_1 = require("./Tenant");
|
|
10
|
+
const Composition_1 = require("./Composition");
|
|
11
|
+
const Explorer_1 = require("./Explorer");
|
|
12
|
+
const UI_1 = require("./UI");
|
|
13
|
+
const sample_1 = require("./sample");
|
|
14
|
+
class Factory {
|
|
15
|
+
boot;
|
|
16
|
+
options;
|
|
17
|
+
settings = {};
|
|
18
|
+
reporter = null;
|
|
19
|
+
constructor(boot) {
|
|
20
|
+
this.boot = boot;
|
|
21
|
+
this.options = (0, annotation_1.environment)();
|
|
22
|
+
}
|
|
23
|
+
tenant(locator, decl, manifest) {
|
|
24
|
+
const resolved = (0, annotation_1.settings)(locator.namespace, (0, annotation_1.component)(decl), this.options);
|
|
25
|
+
this.settings[locator.id] = resolved;
|
|
26
|
+
if (!resolved.enabled || locator.namespace === const_1.NAMESPACE)
|
|
27
|
+
return new core_1.Connector();
|
|
28
|
+
return new Tenant_1.Tenant(this.collector(), (0, describe_1.describe)(manifest));
|
|
29
|
+
}
|
|
30
|
+
component(component) {
|
|
31
|
+
const locator = component.locator;
|
|
32
|
+
const resolved = this.resolve(locator);
|
|
33
|
+
if (!resolved.enabled)
|
|
34
|
+
return component;
|
|
35
|
+
const reporter = this.collector();
|
|
36
|
+
const invoke = component.invoke.bind(component);
|
|
37
|
+
component.invoke = async (endpoint, request) => {
|
|
38
|
+
let outcome = 'ok';
|
|
39
|
+
let reply;
|
|
40
|
+
try {
|
|
41
|
+
reply = await invoke(endpoint, request);
|
|
42
|
+
if (reply?.exception !== undefined)
|
|
43
|
+
outcome = 'exception';
|
|
44
|
+
else if (reply?.error !== undefined)
|
|
45
|
+
outcome = 'error';
|
|
46
|
+
return reply;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
outcome = 'exception';
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
// a call that failed is still a connection between two components
|
|
54
|
+
const src = request?.source ?? UNKNOWN;
|
|
55
|
+
const dst = { namespace: locator.namespace, component: locator.name, operation: endpoint };
|
|
56
|
+
const sample = resolved.samples && (0, sample_1.samplable)(request?.input)
|
|
57
|
+
? (0, sample_1.capture)(request?.input, outcome)
|
|
58
|
+
: undefined;
|
|
59
|
+
reporter.observe({ src, dst, sample });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
component.depends(reporter);
|
|
63
|
+
return component;
|
|
64
|
+
}
|
|
65
|
+
service() {
|
|
66
|
+
if (this.options === null)
|
|
67
|
+
return null;
|
|
68
|
+
const composition = new Composition_1.Composition(this.boot);
|
|
69
|
+
const explorer = new Explorer_1.Explorer();
|
|
70
|
+
explorer.depends(composition);
|
|
71
|
+
if (this.options.ui)
|
|
72
|
+
explorer.depends(new UI_1.UI(const_1.UI_PORT));
|
|
73
|
+
return explorer;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `tenant()` runs before any component is created, so settings are warm.
|
|
77
|
+
* A component booted on its own (without a composition) falls back to
|
|
78
|
+
* the environment, with sampling off.
|
|
79
|
+
*/
|
|
80
|
+
resolve(locator) {
|
|
81
|
+
if (locator.namespace === const_1.NAMESPACE)
|
|
82
|
+
return annotation_1.DISABLED;
|
|
83
|
+
return this.settings[locator.id] ??
|
|
84
|
+
(0, annotation_1.settings)(locator.namespace, {}, this.options === null ? null : { ...this.options, samples: false });
|
|
85
|
+
}
|
|
86
|
+
collector() {
|
|
87
|
+
this.reporter ??= new Reporter_1.Reporter(this.boot, this.options);
|
|
88
|
+
return this.reporter;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.Factory = Factory;
|
|
92
|
+
const UNKNOWN = { service: 'unknown' };
|
|
93
|
+
//# sourceMappingURL=Factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":";;;AAAA,uCAAwC;AACxC,6CAAwF;AACxF,mCAA4C;AAC5C,yCAAqC;AACrC,yCAAqC;AACrC,qCAAiC;AACjC,+CAA2C;AAC3C,yCAAqC;AACrC,6BAAyB;AACzB,qCAA6C;AAM7C,MAAa,OAAO;IACD,IAAI,CAAY;IAChB,OAAO,CAAgB;IACvB,QAAQ,GAA6B,EAAE,CAAA;IAChD,QAAQ,GAAoB,IAAI,CAAA;IAExC,YAAoB,IAAgB;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,OAAO,GAAG,IAAA,wBAAW,GAAE,CAAA;IAC9B,CAAC;IAEM,MAAM,CAAE,OAAgB,EAAE,IAAwB,EAAE,QAAkB;QAC3E,MAAM,QAAQ,GAAG,IAAA,qBAAQ,EAAC,OAAO,CAAC,SAAS,EAAE,IAAA,sBAAW,EAAC,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QAE7E,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAA;QAEpC,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,SAAS,KAAK,iBAAS;YACtD,OAAO,IAAI,gBAAS,EAAE,CAAA;QAExB,OAAO,IAAI,eAAM,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,IAAA,mBAAQ,EAAC,QAAQ,CAAC,CAAC,CAAA;IACzD,CAAC;IAEM,SAAS,CAAE,SAAoB;QACpC,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAA;QACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAEtC,IAAI,CAAC,QAAQ,CAAC,OAAO;YACnB,OAAO,SAAS,CAAA;QAElB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,CAAA;QACjC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAE/C,SAAS,CAAC,MAAM,GAAG,KAAK,EAAE,QAAgB,EAAE,OAAgB,EAAgB,EAAE;YAC5E,IAAI,OAAO,GAAY,IAAI,CAAA;YAC3B,IAAI,KAAwB,CAAA;YAE5B,IAAI,CAAC;gBACH,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;gBAEvC,IAAI,KAAK,EAAE,SAAS,KAAK,SAAS;oBAAE,OAAO,GAAG,WAAW,CAAA;qBACpD,IAAI,KAAK,EAAE,KAAK,KAAK,SAAS;oBAAE,OAAO,GAAG,OAAO,CAAA;gBAEtD,OAAO,KAAK,CAAA;YACd,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,GAAG,WAAW,CAAA;gBAErB,MAAM,KAAK,CAAA;YACb,CAAC;oBAAS,CAAC;gBACT,kEAAkE;gBAClE,MAAM,GAAG,GAAW,OAAO,EAAE,MAAM,IAAI,OAAO,CAAA;gBAC9C,MAAM,GAAG,GAAW,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAA;gBAElG,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,IAAI,IAAA,kBAAS,EAAC,OAAO,EAAE,KAAK,CAAC;oBAC1D,CAAC,CAAC,IAAA,gBAAO,EAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;oBAClC,CAAC,CAAC,SAAS,CAAA;gBAEb,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;YACxC,CAAC;QACH,CAAC,CAAA;QAED,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QAE3B,OAAO,SAAS,CAAA;IAClB,CAAC;IAEM,OAAO;QACZ,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YACvB,OAAO,IAAI,CAAA;QAEb,MAAM,WAAW,GAAG,IAAI,yBAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,MAAM,QAAQ,GAAG,IAAI,mBAAQ,EAAE,CAAA;QAE/B,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAE7B,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE;YACjB,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAE,CAAC,eAAO,CAAC,CAAC,CAAA;QAEnC,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED;;;;OAIG;IACK,OAAO,CAAE,OAAgB;QAC/B,IAAI,OAAO,CAAC,SAAS,KAAK,iBAAS;YACjC,OAAO,qBAAQ,CAAA;QAEjB,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,IAAA,qBAAQ,EAAC,OAAO,CAAC,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;IACvG,CAAC;IAEO,SAAS;QACf,IAAI,CAAC,QAAQ,KAAK,IAAI,mBAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAQ,CAAC,CAAA;QAExD,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;CACF;AAlGD,0BAkGC;AAED,MAAM,OAAO,GAAG,EAAE,OAAO,EAAE,SAAS,EAAW,CAAA"}
|