@toa.io/extensions.introspection 1.0.0-alpha.291 → 1.0.0-alpha.293
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/package.json +16 -6
- package/transpiled/Composition.d.ts +2 -7
- package/transpiled/Composition.js +4 -15
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Factory.d.ts +1 -1
- package/transpiled/Factory.js +2 -2
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Reporter.d.ts +1 -1
- package/transpiled/Reporter.js +1 -1
- package/transpiled/Reporter.js.map +1 -1
- package/transpiled/Tenant.js +1 -1
- package/transpiled/Tenant.js.map +1 -1
- package/transpiled/UI.js +1 -1
- package/transpiled/UI.js.map +1 -1
- package/transpiled/index.d.ts +1 -3
- package/transpiled/index.js +0 -2
- package/transpiled/index.js.map +1 -1
- package/transpiled/sample.js +1 -1
- package/transpiled/sample.js.map +1 -1
- package/ui/dist/_app/immutable/chunks/B2kPZfJh.js +1 -0
- package/ui/dist/_app/immutable/chunks/{BM_8gmPc.js → B6dxxHIS.js} +1 -1
- package/ui/dist/_app/immutable/chunks/BQGufqas.js +6 -0
- package/ui/dist/_app/immutable/chunks/{BPmTOjUD.js → CMT3qkok.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{ClEi7LqQ.js → CRq6qlvN.js} +1 -1
- package/ui/dist/_app/immutable/chunks/{BNP4DAhr.js → Cdb6j7Bw.js} +1 -1
- package/ui/dist/_app/immutable/chunks/DFP66uri.js +1 -0
- package/ui/dist/_app/immutable/chunks/{D58WJWRP.js → Qo6KLWZ1.js} +1 -1
- package/ui/dist/_app/immutable/chunks/jxky52kD.js +1 -0
- package/ui/dist/_app/immutable/entry/{app.C__edJT8.js → app.MKQlb7LU.js} +2 -2
- package/ui/dist/_app/immutable/entry/start.RcPu9STL.js +1 -0
- package/ui/dist/_app/immutable/nodes/{0.DqHzPxwp.js → 0.Gc5w_8Fb.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{1.CxKiCrAx.js → 1.CZzwHnag.js} +1 -1
- package/ui/dist/_app/immutable/nodes/{2.DiR-utWy.js → 2.DRxH9AhI.js} +1 -1
- package/ui/dist/_app/immutable/nodes/3.DXaS4-Wc.js +1 -0
- package/ui/dist/_app/immutable/nodes/4.C424lXpX.js +1 -0
- package/ui/dist/_app/immutable/nodes/{5.BjL9k_iK.js → 5.BlPcxfID.js} +1 -1
- package/ui/dist/_app/version.json +1 -1
- package/ui/dist/index.html +12 -12
- package/CHANGELOG.md +0 -35
- package/components/introspection.edges/tsconfig.tsbuildinfo +0 -1
- package/components/introspection.nodes/tsconfig.tsbuildinfo +0 -1
- package/cucumber.mjs +0 -8
- package/features/calls.feature +0 -123
- package/features/nodes.feature +0 -70
- package/features/samples.feature +0 -75
- package/features/shutdown.feature +0 -37
- package/features/site/_app/immutable/asset.js +0 -1
- package/features/site/favicon.ico +0 -0
- package/features/site/index.html +0 -10
- package/features/steps/Map.ts +0 -184
- package/features/steps/UI.ts +0 -98
- package/features/steps/components/probe.discreet/manifest.toa.yaml +0 -16
- package/features/steps/components/probe.discreet/operations/ping.js +0 -3
- package/features/steps/components/probe.quiet/manifest.toa.yaml +0 -15
- package/features/steps/components/probe.quiet/operations/ping.js +0 -3
- package/features/steps/components/probe.source/manifest.toa.yaml +0 -40
- package/features/steps/components/probe.source/operations/crash.js +0 -3
- package/features/steps/components/probe.source/operations/fail.js +0 -8
- package/features/steps/components/probe.source/operations/relay.js +0 -3
- package/features/steps/components/probe.target/manifest.toa.yaml +0 -30
- package/features/steps/components/probe.target/operations/compute.js +0 -3
- package/features/steps/components/probe.target/operations/count.js +0 -5
- package/features/steps/config.ts +0 -27
- package/features/steps/tsconfig.json +0 -9
- package/features/ui.feature +0 -93
- package/schemas/annotation.cos.yaml +0 -31
- package/schemas/declaration.cos.yaml +0 -5
- package/source/Composition.ts +0 -60
- package/source/Explorer.ts +0 -16
- package/source/Factory.ts +0 -152
- package/source/Reporter.ts +0 -209
- package/source/Tenant.ts +0 -46
- package/source/UI.ts +0 -213
- package/source/annotation.ts +0 -87
- package/source/const.ts +0 -46
- package/source/describe.ts +0 -75
- package/source/extension.ts +0 -49
- package/source/index.ts +0 -7
- package/source/keys.ts +0 -0
- package/source/manifest.ts +0 -18
- package/source/model.ts +0 -83
- package/source/sample.ts +0 -47
- package/source/schemas.ts +0 -12
- package/transpiled/annotation.d.ts +0 -40
- package/transpiled/annotation.js +0 -39
- package/transpiled/annotation.js.map +0 -1
- package/transpiled/const.d.ts +0 -30
- package/transpiled/const.js +0 -35
- package/transpiled/const.js.map +0 -1
- package/transpiled/extension.d.ts +0 -14
- package/transpiled/extension.js +0 -42
- package/transpiled/extension.js.map +0 -1
- package/transpiled/manifest.d.ts +0 -9
- package/transpiled/manifest.js +0 -16
- package/transpiled/manifest.js.map +0 -1
- package/transpiled/schemas.d.ts +0 -4
- package/transpiled/schemas.js +0 -7
- package/transpiled/schemas.js.map +0 -1
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/ui/dist/_app/immutable/chunks/B29ORAfk.js +0 -1
- package/ui/dist/_app/immutable/chunks/B2Uho8Qa.js +0 -1
- package/ui/dist/_app/immutable/chunks/DJkgMsju.js +0 -6
- package/ui/dist/_app/immutable/chunks/xuD-gQB1.js +0 -1
- package/ui/dist/_app/immutable/entry/start.1kW6Dv7l.js +0 -1
- package/ui/dist/_app/immutable/nodes/3.C4ocqYxG.js +0 -1
- package/ui/dist/_app/immutable/nodes/4.BxBsqIXE.js +0 -1
package/source/UI.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import * as path from 'node:path'
|
|
4
|
-
import { Connector } from '@toa.io/core'
|
|
5
|
-
import { console } from 'openspan'
|
|
6
|
-
import { UI_PATH } from './const.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Serves the introspection UI: the directory `ui` builds, and nothing else.
|
|
10
|
-
*
|
|
11
|
-
* The page is a single-page application whose mount path is baked into the bundle,
|
|
12
|
-
* so this server routes relative to `UI_PATH` — which is also what the ingress
|
|
13
|
-
* forwards. `/introspection/*` is left alone: that is the components' own API.
|
|
14
|
-
*/
|
|
15
|
-
export class UI extends Connector {
|
|
16
|
-
private readonly server: http.Server = http.createServer()
|
|
17
|
-
private readonly port: number
|
|
18
|
-
private readonly root: string
|
|
19
|
-
|
|
20
|
-
public constructor(port: number, root: string = SITE) {
|
|
21
|
-
super()
|
|
22
|
-
|
|
23
|
-
this.port = port
|
|
24
|
-
this.root = root
|
|
25
|
-
this.server.on('request', (request, response) => {
|
|
26
|
-
this.listen(request, response)
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
protected override async open(): Promise<void> {
|
|
31
|
-
/*
|
|
32
|
-
* A taken port is a real error: uniqueness across services is settled at export
|
|
33
|
-
* time, so nothing here has to negotiate for one.
|
|
34
|
-
*/
|
|
35
|
-
await new Promise<void>((resolve, reject) => {
|
|
36
|
-
const failed = (error: Error): void => {
|
|
37
|
-
this.server.off('listening', listening)
|
|
38
|
-
reject(error)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const listening = (): void => {
|
|
42
|
-
this.server.off('error', failed)
|
|
43
|
-
resolve()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
this.server.once('error', failed)
|
|
47
|
-
this.server.once('listening', listening)
|
|
48
|
-
this.server.listen(this.port)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
if (!isFile(path.join(this.root, 'index.html')))
|
|
52
|
-
console.warn('Introspection UI is not built', { root: this.root })
|
|
53
|
-
|
|
54
|
-
console.info('Introspection UI started', { port: this.port, path: UI_PATH + '/' })
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
protected override async close(): Promise<void> {
|
|
58
|
-
const closed = new Promise<void>((resolve) =>
|
|
59
|
-
this.server.once('close', () => {
|
|
60
|
-
resolve()
|
|
61
|
-
})
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
this.server.close()
|
|
65
|
-
this.server.closeAllConnections()
|
|
66
|
-
|
|
67
|
-
await closed
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private listen(request: http.IncomingMessage, response: http.ServerResponse): void {
|
|
71
|
-
void this.respond(request, response).catch((error: Error) => {
|
|
72
|
-
console.error('Introspection UI failure', { message: error.message })
|
|
73
|
-
|
|
74
|
-
if (!response.writableEnded) response.writeHead(500).end()
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private async respond(
|
|
79
|
-
request: http.IncomingMessage,
|
|
80
|
-
response: http.ServerResponse
|
|
81
|
-
): Promise<void> {
|
|
82
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
83
|
-
response.writeHead(405, { allow: 'GET, HEAD' }).end()
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const pathname = decode(request.url ?? '/')
|
|
89
|
-
|
|
90
|
-
if (pathname === null) {
|
|
91
|
-
response.writeHead(400).end()
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// the port is the explorer's alone, so the root is the way in
|
|
97
|
-
if (pathname === '/') {
|
|
98
|
-
response.writeHead(302, { location: UI_PATH + '/' }).end()
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const file = this.resolve(pathname)
|
|
104
|
-
|
|
105
|
-
if (file === null) response.writeHead(404).end()
|
|
106
|
-
else await this.send(file, request, response)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The file a request lands on, or `null` when nothing does. A path that exists is
|
|
111
|
-
* served as it is; anything else that could be a route falls back to the page,
|
|
112
|
-
* because the client router — not this server — knows what routes there are.
|
|
113
|
-
*/
|
|
114
|
-
private resolve(pathname: string): string | null {
|
|
115
|
-
if (!pathname.startsWith(UI_PATH)) return null
|
|
116
|
-
|
|
117
|
-
const relative = pathname.slice(UI_PATH.length)
|
|
118
|
-
|
|
119
|
-
if (relative !== '' && !relative.startsWith('/')) return null
|
|
120
|
-
|
|
121
|
-
const file = path.join(this.root, relative)
|
|
122
|
-
|
|
123
|
-
if (file !== this.root && !file.startsWith(this.root + path.sep)) return null
|
|
124
|
-
|
|
125
|
-
if (isFile(file)) return file
|
|
126
|
-
|
|
127
|
-
/*
|
|
128
|
-
* A missing asset is missing, but a route can look like one: `identity.passkeys` is a
|
|
129
|
-
* component, not a file with an extension. What this server would have served is what
|
|
130
|
-
* it knows how to serve, so anything else is a route and falls back to the page — as
|
|
131
|
-
* does anything ending in a slash, which is no name for a file at all.
|
|
132
|
-
*/
|
|
133
|
-
const asset = !relative.endsWith('/') && path.extname(relative) in TYPES
|
|
134
|
-
|
|
135
|
-
return asset ? null : path.join(this.root, 'index.html')
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private async send(
|
|
139
|
-
file: string,
|
|
140
|
-
request: http.IncomingMessage,
|
|
141
|
-
response: http.ServerResponse
|
|
142
|
-
): Promise<void> {
|
|
143
|
-
const stats = await fs.promises.stat(file).catch(() => null)
|
|
144
|
-
|
|
145
|
-
if (stats === null) {
|
|
146
|
-
response
|
|
147
|
-
.writeHead(503, { 'content-type': 'text/plain' })
|
|
148
|
-
.end('The introspection UI is not built. Run `npm run build:ui`.\n')
|
|
149
|
-
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
response.writeHead(200, {
|
|
154
|
-
'content-type': TYPES[path.extname(file)] ?? 'application/octet-stream',
|
|
155
|
-
'content-length': stats.size,
|
|
156
|
-
'cache-control': caching(path.relative(this.root, file))
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
if (request.method === 'HEAD') response.end()
|
|
160
|
-
else fs.createReadStream(file).pipe(response)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function decode(url: string): string | null {
|
|
165
|
-
try {
|
|
166
|
-
return decodeURIComponent(url.split('?')[0])
|
|
167
|
-
} catch {
|
|
168
|
-
return null
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function isFile(file: string): boolean {
|
|
173
|
-
return fs.existsSync(file) && fs.statSync(file).isFile()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Where `npm run build:ui` puts the page, from both `source` and `transpiled`. */
|
|
177
|
-
const SITE = path.resolve(import.meta.dirname, '..', 'ui', 'dist')
|
|
178
|
-
|
|
179
|
-
/** Assets under this prefix carry their build hash in the name. */
|
|
180
|
-
const IMMUTABLE = path.join('_app', 'immutable')
|
|
181
|
-
const FOREVER = 'public, max-age=31536000, immutable'
|
|
182
|
-
|
|
183
|
-
/** The icons: named without a hash, so they are re-read, but rarely. */
|
|
184
|
-
const ICONS = new Set(['favicon.ico', 'favicon-96x96.png', 'apple-touch-icon.png'])
|
|
185
|
-
const DAY = 'public, max-age=86400'
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* How long what is served may be held. The page is never cached — it names the assets,
|
|
189
|
-
* and their names carry the build. An icon is named for what it is rather than for its
|
|
190
|
-
* content, so it is asked about again, but not on every page load.
|
|
191
|
-
*/
|
|
192
|
-
function caching(relative: string): string {
|
|
193
|
-
if (relative.startsWith(IMMUTABLE)) return FOREVER
|
|
194
|
-
|
|
195
|
-
return ICONS.has(relative) ? DAY : 'no-cache'
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const TYPES: Record<string, string> = {
|
|
199
|
-
'.css': 'text/css; charset=utf-8',
|
|
200
|
-
'.html': 'text/html; charset=utf-8',
|
|
201
|
-
'.ico': 'image/x-icon',
|
|
202
|
-
'.jpg': 'image/jpeg',
|
|
203
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
204
|
-
'.json': 'application/json; charset=utf-8',
|
|
205
|
-
'.map': 'application/json; charset=utf-8',
|
|
206
|
-
'.png': 'image/png',
|
|
207
|
-
'.svg': 'image/svg+xml',
|
|
208
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
209
|
-
'.webmanifest': 'application/manifest+json',
|
|
210
|
-
'.webp': 'image/webp',
|
|
211
|
-
'.woff': 'font/woff',
|
|
212
|
-
'.woff2': 'font/woff2'
|
|
213
|
-
}
|
package/source/annotation.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_INTERVAL, DEFAULT_THRESHOLD, DENIED, ENV } from './const.js'
|
|
2
|
-
import { environment as variables } from '@toa.io/generic'
|
|
3
|
-
import type { Resources } from '@toa.io/operations'
|
|
4
|
-
|
|
5
|
-
/** `context.toa.yaml` */
|
|
6
|
-
export type Annotation =
|
|
7
|
-
| false
|
|
8
|
-
| {
|
|
9
|
-
/** Capture real payloads. Off by default: this is production data. */
|
|
10
|
-
samples?: boolean
|
|
11
|
-
/** Flush period, seconds. */
|
|
12
|
-
interval?: number
|
|
13
|
-
/** Flush as soon as this many distinct edges are buffered. */
|
|
14
|
-
threshold?: number
|
|
15
|
-
/** Publish the UI. On by default. */
|
|
16
|
-
ui?: boolean
|
|
17
|
-
resources?: Resources
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** `manifest.toa.yaml` */
|
|
21
|
-
export type Declaration =
|
|
22
|
-
| false
|
|
23
|
-
| {
|
|
24
|
-
samples?: boolean
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** What `deployment()` encodes into the environment. */
|
|
28
|
-
export interface Options {
|
|
29
|
-
samples: boolean
|
|
30
|
-
interval: number
|
|
31
|
-
threshold: number
|
|
32
|
-
ui: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The effective per-component decision. */
|
|
36
|
-
export interface Settings {
|
|
37
|
-
enabled: boolean
|
|
38
|
-
samples: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const DISABLED: Settings = { enabled: false, samples: false }
|
|
42
|
-
|
|
43
|
-
export function options(annotation?: Annotation): Options {
|
|
44
|
-
const declaration = annotation === undefined || annotation === false ? {} : annotation
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
samples: declaration.samples === true,
|
|
48
|
-
interval: declaration.interval ?? DEFAULT_INTERVAL,
|
|
49
|
-
threshold: declaration.threshold ?? DEFAULT_THRESHOLD,
|
|
50
|
-
ui: declaration.ui !== false
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Reads what `deployment()` has put into the environment. */
|
|
55
|
-
export function environment(): Options | null {
|
|
56
|
-
const value = variables.get(ENV)
|
|
57
|
-
|
|
58
|
-
if (value === undefined) return null
|
|
59
|
-
|
|
60
|
-
return JSON.parse(value) as Options
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function component(declaration: Declaration | null | undefined): Declaration {
|
|
64
|
-
if (declaration === false) return false
|
|
65
|
-
|
|
66
|
-
// predefined extensions arrive as null for components that say nothing
|
|
67
|
-
if (declaration === null || declaration === undefined) return {}
|
|
68
|
-
|
|
69
|
-
return declaration.samples === undefined ? {} : { samples: declaration.samples }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Both levels must agree, and either can veto: the context is the environment
|
|
74
|
-
* ceiling, the manifest is the component's own call. A component handling
|
|
75
|
-
* personal data opts out for good, and no context flag overrides that.
|
|
76
|
-
*/
|
|
77
|
-
export function settings(
|
|
78
|
-
namespace: string,
|
|
79
|
-
declaration: Declaration,
|
|
80
|
-
opts: Options | null
|
|
81
|
-
): Settings {
|
|
82
|
-
if (opts === null || declaration === false) return DISABLED
|
|
83
|
-
|
|
84
|
-
const samples = opts.samples && declaration.samples !== false && !DENIED.has(namespace)
|
|
85
|
-
|
|
86
|
-
return { enabled: true, samples }
|
|
87
|
-
}
|
package/source/const.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { environment } from '@toa.io/generic'
|
|
2
|
-
|
|
3
|
-
export const ID = 'introspection'
|
|
4
|
-
export const NAMESPACE = 'introspection'
|
|
5
|
-
|
|
6
|
-
export const ENV = 'TOA_INTROSPECTION'
|
|
7
|
-
|
|
8
|
-
export const NODES = 'nodes'
|
|
9
|
-
export const EDGES = 'edges'
|
|
10
|
-
|
|
11
|
-
/** Where the UI is mounted; `/introspection/*` belongs to the components' own API. */
|
|
12
|
-
export const UI_PATH = '/.introspection'
|
|
13
|
-
export const UI_PORT = 8002
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The port the UI is served on. `UI_PORT` is what a deployment publishes and what the chart
|
|
17
|
-
* renders; the variable is for a machine that runs more than one Toa process — a Toa checkout
|
|
18
|
-
* beside an application, or two applications — where one of them has to give it up.
|
|
19
|
-
*/
|
|
20
|
-
export const UI_PORT_ENV = 'TOA_INTROSPECTION_UI_PORT'
|
|
21
|
-
export function uiPort(): number {
|
|
22
|
-
const value = environment.get(UI_PORT_ENV)
|
|
23
|
-
|
|
24
|
-
return value === undefined ? UI_PORT : Number(value)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const DEFAULT_INTERVAL = 300
|
|
28
|
-
export const DEFAULT_THRESHOLD = 1024
|
|
29
|
-
|
|
30
|
-
/** How often a component re-announces its description, so that removed components fade out. */
|
|
31
|
-
export const ANNOUNCE_INTERVAL = 1_800_000
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* `source` arrives over the wire, so the number of distinct edges a process
|
|
35
|
-
* can hold must be bounded regardless of what peers send.
|
|
36
|
-
*/
|
|
37
|
-
export const MAX_EDGES = 4096
|
|
38
|
-
|
|
39
|
-
/** Never capture samples for these namespaces, whatever the annotation says. */
|
|
40
|
-
export const DENIED = new Set(['identity', 'introspection'])
|
|
41
|
-
|
|
42
|
-
/** Keys never stored in a sample, even when sampling is on. */
|
|
43
|
-
export const REDACTED = /^(password|secret|token|credentials?|key|authorization|cookie)$/i
|
|
44
|
-
|
|
45
|
-
/** Serialized size cap of a single sample. */
|
|
46
|
-
export const SAMPLE_LIMIT = 4096
|
package/source/describe.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { Node, Operation, Event, Receiver } from './model.js'
|
|
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 normalized by norm: the prototype chain is collapsed,
|
|
8
|
-
* 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) return null
|
|
27
|
-
|
|
28
|
-
const { properties, required } = manifest.entity
|
|
29
|
-
|
|
30
|
-
// the map publishes a schema, which is what a tool reading it can validate against;
|
|
31
|
-
// a manifest declares the properties and assembles no envelope
|
|
32
|
-
return {
|
|
33
|
-
schema: { type: 'object', properties, required },
|
|
34
|
-
storage: manifest.entity.storage,
|
|
35
|
-
associated: manifest.entity.associated === true
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function operations(manifest: Manifest): Operation[] {
|
|
40
|
-
return Object.entries(manifest.operations ?? {}).map(
|
|
41
|
-
([endpoint, definition]: [string, any]) => ({
|
|
42
|
-
endpoint,
|
|
43
|
-
type: definition.type,
|
|
44
|
-
description: definition.description,
|
|
45
|
-
scope: definition.scope,
|
|
46
|
-
query: definition.query,
|
|
47
|
-
input: definition.input ?? null,
|
|
48
|
-
output: definition.output ?? null,
|
|
49
|
-
errors: definition.errors ?? []
|
|
50
|
-
})
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function events(manifest: Manifest): Event[] {
|
|
55
|
-
return Object.entries(manifest.events ?? {}).map(
|
|
56
|
-
([label, definition]: [string, any]) => ({ label, binding: definition.binding })
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/*
|
|
61
|
-
* Receiver labels contain dots (`identity.bans.created`), which is why nodes keep
|
|
62
|
-
* these as arrays rather than maps — a dot in a document key is a hazard in Mongo.
|
|
63
|
-
*/
|
|
64
|
-
function receivers(manifest: Manifest): Receiver[] {
|
|
65
|
-
return Object.entries(manifest.receivers ?? {}).map(
|
|
66
|
-
([label, definition]: [string, any]) => ({
|
|
67
|
-
label,
|
|
68
|
-
source: definition.source ?? label.split('.').slice(0, 2).join('.'),
|
|
69
|
-
event: label.split('.').pop()!,
|
|
70
|
-
operation: definition.operation,
|
|
71
|
-
conditioned: definition.conditioned === true,
|
|
72
|
-
adaptive: definition.adaptive === true
|
|
73
|
-
})
|
|
74
|
-
)
|
|
75
|
-
}
|
package/source/extension.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { options } from './annotation.js'
|
|
3
|
-
import { components } from './Composition.js'
|
|
4
|
-
import { ENV, UI_PATH, UI_PORT } from './const.js'
|
|
5
|
-
import * as schemas from './schemas.js'
|
|
6
|
-
import type { Annotation } from './annotation.js'
|
|
7
|
-
import type { Dependency, Instances, Service } from '@toa.io/operations'
|
|
8
|
-
|
|
9
|
-
/** Where Toa's release publishes this service's image. An application takes it
|
|
10
|
-
* instead of building one when its context says `registry.services: published`. */
|
|
11
|
-
export const image = 'ghcr.io/toa-io/extension-introspection-explorer'
|
|
12
|
-
|
|
13
|
-
export const standalone = true
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The explorer hosts the introspection components, exactly as the exposition
|
|
17
|
-
* gateway hosts the identity ones. Collection is on unless the context says
|
|
18
|
-
* `introspection: false`, and the environment variable is emitted together
|
|
19
|
-
* with the service — never on its own, or tasks would pile up in a queue
|
|
20
|
-
* nothing consumes.
|
|
21
|
-
*/
|
|
22
|
-
export function deployment(_: Instances<unknown>, annotation?: Annotation): Dependency {
|
|
23
|
-
if (annotation === false) return {}
|
|
24
|
-
|
|
25
|
-
if (annotation !== undefined) schemas.annotation.validate(annotation)
|
|
26
|
-
|
|
27
|
-
const opts = options(annotation)
|
|
28
|
-
|
|
29
|
-
const service: Service = {
|
|
30
|
-
group: 'introspection',
|
|
31
|
-
name: 'explorer',
|
|
32
|
-
image,
|
|
33
|
-
version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
34
|
-
.version,
|
|
35
|
-
components: components().labels,
|
|
36
|
-
resources: annotation?.resources,
|
|
37
|
-
variables: []
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (opts.ui) {
|
|
41
|
-
service.port = UI_PORT
|
|
42
|
-
service.ingress = { path: UI_PATH }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
services: [service],
|
|
47
|
-
variables: { global: [{ name: ENV, value: JSON.stringify(opts) }] }
|
|
48
|
-
}
|
|
49
|
-
}
|
package/source/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { Factory } from './Factory.js'
|
|
2
|
-
export { manifest } from './manifest.js'
|
|
3
|
-
export { components } from './Composition.js'
|
|
4
|
-
export * from './extension.js'
|
|
5
|
-
|
|
6
|
-
export type { Annotation, Declaration, Options, Settings } from './annotation.js'
|
|
7
|
-
export type * from './model.js'
|
package/source/keys.ts
DELETED
|
Binary file
|
package/source/manifest.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { component } from './annotation.js'
|
|
2
|
-
import * as schemas from './schemas.js'
|
|
3
|
-
import type { Declaration } from './annotation.js'
|
|
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) schemas.declaration.validate(normalized)
|
|
16
|
-
|
|
17
|
-
return normalized
|
|
18
|
-
}
|
package/source/model.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { Source } from '@toa.io/core/types'
|
|
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
|
-
description?: string
|
|
22
|
-
scope?: string
|
|
23
|
-
query?: boolean
|
|
24
|
-
input?: object | null
|
|
25
|
-
output?: object | null
|
|
26
|
-
errors?: string[]
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface Event {
|
|
30
|
-
label: string
|
|
31
|
-
binding?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface Receiver {
|
|
35
|
-
label: string
|
|
36
|
-
source: string
|
|
37
|
-
event: string
|
|
38
|
-
operation: string
|
|
39
|
-
conditioned: boolean
|
|
40
|
-
adaptive: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface Entity {
|
|
44
|
-
schema: object
|
|
45
|
-
storage?: string
|
|
46
|
-
associated: boolean
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The static description of a component, derived from its manifest.
|
|
51
|
-
* Sent to `introspection.nodes`.
|
|
52
|
-
*/
|
|
53
|
-
export interface Node {
|
|
54
|
-
namespace: string
|
|
55
|
-
component: string
|
|
56
|
-
version: string
|
|
57
|
-
entity: Entity | null
|
|
58
|
-
operations: Operation[]
|
|
59
|
-
events: Event[]
|
|
60
|
-
receivers: Receiver[]
|
|
61
|
-
extensions: string[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface Sample {
|
|
65
|
-
at: number
|
|
66
|
-
input?: unknown
|
|
67
|
-
outcome: Outcome
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export type Outcome = 'ok' | 'error' | 'exception'
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* A call observed in a process between flushes. What the application declares —
|
|
74
|
-
* which events exist, and which receiver takes which — is a node's business;
|
|
75
|
-
* an edge is what actually happened.
|
|
76
|
-
*
|
|
77
|
-
* Sent to `introspection.edges`.
|
|
78
|
-
*/
|
|
79
|
-
export interface Edge {
|
|
80
|
-
src: Origin
|
|
81
|
-
dst: Target
|
|
82
|
-
sample?: Sample
|
|
83
|
-
}
|
package/source/sample.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { REDACTED, SAMPLE_LIMIT } from './const.js'
|
|
3
|
-
import type { Outcome, Sample } from './model.js'
|
|
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)) return truncate(value.map(redact))
|
|
22
|
-
|
|
23
|
-
const result: Record<string, unknown> = {}
|
|
24
|
-
|
|
25
|
-
for (const [key, property] of Object.entries(value))
|
|
26
|
-
result[key] = REDACTED.test(key) ? '***' : redact(property)
|
|
27
|
-
|
|
28
|
-
return truncate(result)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The map holds one sample per edge indefinitely, so a single oversized
|
|
33
|
-
* payload must not become a permanent tenant of the collection.
|
|
34
|
-
*/
|
|
35
|
-
function truncate(value: unknown): unknown {
|
|
36
|
-
let serialized: string
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
serialized = JSON.stringify(value) ?? ''
|
|
40
|
-
} catch {
|
|
41
|
-
return '[unserializable]'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (serialized.length <= SAMPLE_LIMIT) return value
|
|
45
|
-
|
|
46
|
-
return '[truncated]'
|
|
47
|
-
}
|
package/source/schemas.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import * as schemas from '@toa.io/schemas'
|
|
3
|
-
import type { Schema } from '@toa.io/schemas'
|
|
4
|
-
import type { Annotation, Declaration } from './annotation.js'
|
|
5
|
-
|
|
6
|
-
const path = resolve(import.meta.dirname, '../schemas')
|
|
7
|
-
const namespace = schemas.namespace(path)
|
|
8
|
-
|
|
9
|
-
export const annotation: Schema<Exclude<Annotation, false>> =
|
|
10
|
-
namespace.schema('annotation')
|
|
11
|
-
export const declaration: Schema<Exclude<Declaration, false>> =
|
|
12
|
-
namespace.schema('declaration')
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { Resources } from '@toa.io/operations';
|
|
2
|
-
/** `context.toa.yaml` */
|
|
3
|
-
export type Annotation = false | {
|
|
4
|
-
/** Capture real payloads. Off by default: this is production data. */
|
|
5
|
-
samples?: boolean;
|
|
6
|
-
/** Flush period, seconds. */
|
|
7
|
-
interval?: number;
|
|
8
|
-
/** Flush as soon as this many distinct edges are buffered. */
|
|
9
|
-
threshold?: number;
|
|
10
|
-
/** Publish the UI. On by default. */
|
|
11
|
-
ui?: boolean;
|
|
12
|
-
resources?: Resources;
|
|
13
|
-
};
|
|
14
|
-
/** `manifest.toa.yaml` */
|
|
15
|
-
export type Declaration = false | {
|
|
16
|
-
samples?: boolean;
|
|
17
|
-
};
|
|
18
|
-
/** What `deployment()` encodes into the environment. */
|
|
19
|
-
export interface Options {
|
|
20
|
-
samples: boolean;
|
|
21
|
-
interval: number;
|
|
22
|
-
threshold: number;
|
|
23
|
-
ui: boolean;
|
|
24
|
-
}
|
|
25
|
-
/** The effective per-component decision. */
|
|
26
|
-
export interface Settings {
|
|
27
|
-
enabled: boolean;
|
|
28
|
-
samples: boolean;
|
|
29
|
-
}
|
|
30
|
-
export declare const DISABLED: Settings;
|
|
31
|
-
export declare function options(annotation?: Annotation): Options;
|
|
32
|
-
/** Reads what `deployment()` has put into the environment. */
|
|
33
|
-
export declare function environment(): Options | null;
|
|
34
|
-
export declare function component(declaration: Declaration | null | undefined): Declaration;
|
|
35
|
-
/**
|
|
36
|
-
* Both levels must agree, and either can veto: the context is the environment
|
|
37
|
-
* ceiling, the manifest is the component's own call. A component handling
|
|
38
|
-
* personal data opts out for good, and no context flag overrides that.
|
|
39
|
-
*/
|
|
40
|
-
export declare function settings(namespace: string, declaration: Declaration, opts: Options | null): Settings;
|