@friggframework/core 2.0.0--canary.622.5e1352f.0 → 2.0.0--canary.622.16ee19d.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/application/commands/usage-commands.js +21 -1
- package/application/index.js +16 -1
- package/core/create-handler.js +8 -0
- package/handlers/app-definition-loader.js +1 -0
- package/package.json +5 -5
- package/telemetry/README.md +36 -3
- package/telemetry/index.js +8 -0
- package/telemetry/plugin-subscribers-singleton.js +51 -0
- package/telemetry/plugin-subscribers.js +77 -0
- package/telemetry/telemetry-config.js +26 -0
|
@@ -14,8 +14,9 @@ const {
|
|
|
14
14
|
createUsageRepository,
|
|
15
15
|
} = require('../../usage/repositories/usage-repository-factory');
|
|
16
16
|
const { computeUsageWindows } = require('../../telemetry/usage-windows');
|
|
17
|
+
const { resolveNorthStarEntry } = require('../../telemetry/north-star');
|
|
17
18
|
|
|
18
|
-
function createUsageCommands({ usageRepository } = {}) {
|
|
19
|
+
function createUsageCommands({ usageRepository, northStar = null } = {}) {
|
|
19
20
|
const repository = usageRepository || createUsageRepository();
|
|
20
21
|
|
|
21
22
|
return {
|
|
@@ -50,6 +51,25 @@ function createUsageCommands({ usageRepository } = {}) {
|
|
|
50
51
|
async series(args) {
|
|
51
52
|
return repository.series(args);
|
|
52
53
|
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* First-class North Star read (ADR-011 Decision 5). Resolves the
|
|
57
|
+
* configured counter for an integration type (byType wins over default),
|
|
58
|
+
* then returns its totals from the durable usage store. Returns `null`
|
|
59
|
+
* when no North Star is configured, so callers can branch without
|
|
60
|
+
* knowing the counter key. Trends read via `series({ metric })`.
|
|
61
|
+
*/
|
|
62
|
+
async northStar({ integrationType, since, groupBy = 'integrationType', bucket } = {}) {
|
|
63
|
+
const entry = resolveNorthStarEntry(northStar, integrationType);
|
|
64
|
+
if (!entry) return null;
|
|
65
|
+
const totals = await repository.totals({
|
|
66
|
+
metric: entry.name,
|
|
67
|
+
groupBy,
|
|
68
|
+
since,
|
|
69
|
+
bucket,
|
|
70
|
+
});
|
|
71
|
+
return { metric: entry.name, totals };
|
|
72
|
+
},
|
|
53
73
|
};
|
|
54
74
|
}
|
|
55
75
|
|
package/application/index.js
CHANGED
|
@@ -8,6 +8,21 @@ const { createCredentialCommands } = require('./commands/credential-commands');
|
|
|
8
8
|
const { createProcessCommands } = require('./commands/process-commands');
|
|
9
9
|
const { createSchedulerCommands } = require('./commands/scheduler-commands');
|
|
10
10
|
const { createUsageCommands } = require('./commands/usage-commands');
|
|
11
|
+
const { loadAppDefinition } = require('../handlers/app-definition-loader');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the adopter's North Star config from the app definition so
|
|
15
|
+
* `frigg.usage.northStar(...)` can read it (ADR-011 Decision 5). Guarded: a
|
|
16
|
+
* missing/unloadable app definition (e.g. in unit tests) must never break the
|
|
17
|
+
* command factory — usage reads simply have no North Star.
|
|
18
|
+
*/
|
|
19
|
+
function resolveNorthStarConfig() {
|
|
20
|
+
try {
|
|
21
|
+
return loadAppDefinition().telemetry?.northStar ?? null;
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
/**
|
|
13
28
|
* Create a unified command factory with all CRUD operations
|
|
@@ -53,7 +68,7 @@ function createFriggCommands({ integrationClass }) {
|
|
|
53
68
|
...processCommands,
|
|
54
69
|
|
|
55
70
|
// Usage read/write (ADR-011) — nested to match `frigg.usage.*`
|
|
56
|
-
usage: createUsageCommands(),
|
|
71
|
+
usage: createUsageCommands({ northStar: resolveNorthStarConfig() }),
|
|
57
72
|
};
|
|
58
73
|
}
|
|
59
74
|
|
package/core/create-handler.js
CHANGED
|
@@ -8,6 +8,9 @@ const { getTelemetry } = require('../telemetry/telemetry-singleton');
|
|
|
8
8
|
const {
|
|
9
9
|
getUsageRollupSubscriber,
|
|
10
10
|
} = require('../telemetry/usage-rollup-singleton');
|
|
11
|
+
const {
|
|
12
|
+
getPluginTelemetrySubscribers,
|
|
13
|
+
} = require('../telemetry/plugin-subscribers-singleton');
|
|
11
14
|
|
|
12
15
|
// Bounds the tail latency telemetry adds to every warm invocation. Kept low so
|
|
13
16
|
// an unreachable OTLP endpoint (e.g. a VPC Lambda with no NAT/egress) costs at
|
|
@@ -139,6 +142,11 @@ const createHandler = (optionByName = {}) => {
|
|
|
139
142
|
? usageRollup
|
|
140
143
|
: getUsageRollupSubscriber();
|
|
141
144
|
|
|
145
|
+
// Wire adopter-declared telemetry subscribers once per cold start
|
|
146
|
+
// (ADR-011 Decision 6). Memoized in the singleton, so this is a cheap
|
|
147
|
+
// no-op after the first invocation.
|
|
148
|
+
getPluginTelemetrySubscribers();
|
|
149
|
+
|
|
142
150
|
try {
|
|
143
151
|
console.info(`[createHandler] ${eventName}: handler entry`, {
|
|
144
152
|
eventName,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.622.
|
|
4
|
+
"version": "2.0.0--canary.622.16ee19d.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@friggframework/eslint-config": "2.0.0--canary.622.
|
|
49
|
-
"@friggframework/prettier-config": "2.0.0--canary.622.
|
|
50
|
-
"@friggframework/test": "2.0.0--canary.622.
|
|
48
|
+
"@friggframework/eslint-config": "2.0.0--canary.622.16ee19d.0",
|
|
49
|
+
"@friggframework/prettier-config": "2.0.0--canary.622.16ee19d.0",
|
|
50
|
+
"@friggframework/test": "2.0.0--canary.622.16ee19d.0",
|
|
51
51
|
"@prisma/client": "^6.19.3",
|
|
52
52
|
"@types/lodash": "4.17.15",
|
|
53
53
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"publishConfig": {
|
|
88
88
|
"access": "public"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "16ee19d11c975c256a996503bafc70c062440ff0"
|
|
91
91
|
}
|
package/telemetry/README.md
CHANGED
|
@@ -200,12 +200,45 @@ telemetry: {
|
|
|
200
200
|
}
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
Read it
|
|
203
|
+
Read it as a first-class metric without knowing the configured key — the North
|
|
204
|
+
Star resolves per integration type (`byType` wins over `default`):
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
// Resolves the configured counter for the type, then returns its totals.
|
|
208
|
+
await frigg.usage.northStar({ integrationType: 'hubspot', since: daysAgo(30) });
|
|
209
|
+
// → { metric: 'contacts_synced', totals: [{ integrationType: 'hubspot', value: 900 }] }
|
|
210
|
+
// → null when no North Star is configured (caller branches without knowing keys)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Or read it like any counter once you know the key:
|
|
214
|
+
`frigg.usage.totals({ metric: 'contacts_synced' })`; trends via `frigg.usage.series({ metric })`.
|
|
204
215
|
|
|
205
216
|
## Plugin / extension tap
|
|
206
217
|
|
|
207
|
-
Telemetry flows onto an internal event stream
|
|
208
|
-
|
|
218
|
+
Telemetry flows onto an internal event stream (independent of OTel export, so
|
|
219
|
+
taps fire even with `exporter: none`). Two ways to subscribe:
|
|
220
|
+
|
|
221
|
+
**Declarative (app definition)** — the framework wires these once per cold start,
|
|
222
|
+
each guarded so a bad subscriber can't break emission or its siblings:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// backend/index.js
|
|
226
|
+
const Definition = {
|
|
227
|
+
telemetry: {
|
|
228
|
+
subscribers: [
|
|
229
|
+
// (a) declarative object — `event` optional; omit to receive both:
|
|
230
|
+
{ event: 'metric', handler: ({ name, value, attributes, context }) => {
|
|
231
|
+
forwardToStatsd(name, value, attributes);
|
|
232
|
+
} },
|
|
233
|
+
// (b) factory — gets the telemetry service, registers itself, may
|
|
234
|
+
// return an unsubscribe:
|
|
235
|
+
(telemetry) => telemetry.on('event', (payload) => auditSink.write(payload)),
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Imperative** — subscribe from anywhere that runs at startup:
|
|
209
242
|
|
|
210
243
|
```js
|
|
211
244
|
const { getTelemetry } = require('@friggframework/core');
|
package/telemetry/index.js
CHANGED
|
@@ -24,6 +24,11 @@ const {
|
|
|
24
24
|
CANONICAL_COUNTERS,
|
|
25
25
|
isCanonicalCounter,
|
|
26
26
|
} = require('./canonical-counters');
|
|
27
|
+
const { wireTelemetrySubscribers } = require('./plugin-subscribers');
|
|
28
|
+
const {
|
|
29
|
+
getPluginTelemetrySubscribers,
|
|
30
|
+
resetPluginTelemetrySubscribersForTests,
|
|
31
|
+
} = require('./plugin-subscribers-singleton');
|
|
27
32
|
|
|
28
33
|
module.exports = {
|
|
29
34
|
createTelemetry,
|
|
@@ -45,4 +50,7 @@ module.exports = {
|
|
|
45
50
|
createNorthStarDerivationSubscriber,
|
|
46
51
|
CANONICAL_COUNTERS,
|
|
47
52
|
isCanonicalCounter,
|
|
53
|
+
wireTelemetrySubscribers,
|
|
54
|
+
getPluginTelemetrySubscribers,
|
|
55
|
+
resetPluginTelemetrySubscribersForTests,
|
|
48
56
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { wireTelemetrySubscribers } = require('./plugin-subscribers');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Process-wide wiring of adopter-declared telemetry subscribers (ADR-011
|
|
5
|
+
* Decision 6). Built once per cold start and reused for the life of the Lambda
|
|
6
|
+
* container, mirroring the usage-rollup singleton. Independent of the usage
|
|
7
|
+
* rollup: adopter taps wire even when no integration declares `Definition.usage`.
|
|
8
|
+
* Never throws — a bad/absent app definition just means no adopter taps.
|
|
9
|
+
*/
|
|
10
|
+
let wired = null;
|
|
11
|
+
let initialized = false;
|
|
12
|
+
|
|
13
|
+
function getPluginTelemetrySubscribers() {
|
|
14
|
+
if (initialized) return wired;
|
|
15
|
+
initialized = true;
|
|
16
|
+
try {
|
|
17
|
+
const {
|
|
18
|
+
loadAppDefinition,
|
|
19
|
+
} = require('../handlers/app-definition-loader');
|
|
20
|
+
const { telemetry = {} } = loadAppDefinition();
|
|
21
|
+
const subscribers = telemetry.subscribers || [];
|
|
22
|
+
if (subscribers.length === 0) {
|
|
23
|
+
wired = [];
|
|
24
|
+
return wired;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { getTelemetry } = require('./telemetry-singleton');
|
|
28
|
+
wired = wireTelemetrySubscribers({
|
|
29
|
+
telemetry: getTelemetry(),
|
|
30
|
+
subscribers,
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(
|
|
34
|
+
`[Frigg][telemetry] plugin subscribers disabled: ${
|
|
35
|
+
error && error.message
|
|
36
|
+
}`
|
|
37
|
+
);
|
|
38
|
+
wired = [];
|
|
39
|
+
}
|
|
40
|
+
return wired;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetPluginTelemetrySubscribersForTests() {
|
|
44
|
+
wired = null;
|
|
45
|
+
initialized = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
getPluginTelemetrySubscribers,
|
|
50
|
+
resetPluginTelemetrySubscribersForTests,
|
|
51
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative plugin/extension telemetry taps (ADR-011 Decision 6).
|
|
3
|
+
*
|
|
4
|
+
* The built-in usage rollup and North Star derivation subscribe to the telemetry
|
|
5
|
+
* bus in framework code; this lets an ADOPTER subscribe declaratively from the
|
|
6
|
+
* app definition (`Definition.telemetry.subscribers: [...]`) — forward to a
|
|
7
|
+
* custom sink, compute aggregates, persist counters — without writing wiring code.
|
|
8
|
+
*
|
|
9
|
+
* A subscriber is one of:
|
|
10
|
+
* - a **factory** `fn(telemetry)` that registers itself (e.g. calls
|
|
11
|
+
* `telemetry.on('metric', ...)`) and MAY return an unsubscribe function; or
|
|
12
|
+
* - a **declarative object** `{ event?: 'metric'|'event', handler(payload, eventType) }`
|
|
13
|
+
* — when `event` is omitted the handler receives both metric and event payloads.
|
|
14
|
+
*
|
|
15
|
+
* Every attach and every handler invocation is guarded so a misbehaving
|
|
16
|
+
* subscriber can never break emission, its siblings, or a handler (mirrors the
|
|
17
|
+
* bus's own per-subscriber isolation).
|
|
18
|
+
*
|
|
19
|
+
* @param {object} params
|
|
20
|
+
* @param {object} params.telemetry Telemetry service exposing `on(eventType, cb)`.
|
|
21
|
+
* @param {Array<Function|{event?: string, handler: Function}>} [params.subscribers]
|
|
22
|
+
* @returns {Array<Function>} unsubscribe functions for the wired subscribers.
|
|
23
|
+
*/
|
|
24
|
+
const EVENT_TYPES = ['metric', 'event'];
|
|
25
|
+
|
|
26
|
+
function wireTelemetrySubscribers({ telemetry, subscribers = [] } = {}) {
|
|
27
|
+
if (
|
|
28
|
+
!telemetry ||
|
|
29
|
+
typeof telemetry.on !== 'function' ||
|
|
30
|
+
!Array.isArray(subscribers)
|
|
31
|
+
) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const unsubscribes = [];
|
|
36
|
+
|
|
37
|
+
for (const subscriber of subscribers) {
|
|
38
|
+
try {
|
|
39
|
+
if (typeof subscriber === 'function') {
|
|
40
|
+
const off = subscriber(telemetry);
|
|
41
|
+
if (typeof off === 'function') unsubscribes.push(off);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (subscriber && typeof subscriber.handler === 'function') {
|
|
46
|
+
const events = subscriber.event
|
|
47
|
+
? [subscriber.event]
|
|
48
|
+
: EVENT_TYPES;
|
|
49
|
+
for (const eventType of events) {
|
|
50
|
+
const off = telemetry.on(eventType, (payload) => {
|
|
51
|
+
try {
|
|
52
|
+
subscriber.handler(payload, eventType);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(
|
|
55
|
+
`[Frigg][telemetry] subscriber handler for "${eventType}" threw: ${
|
|
56
|
+
error && error.message
|
|
57
|
+
}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (typeof off === 'function') unsubscribes.push(off);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// A single bad subscriber must never break wiring of the others.
|
|
66
|
+
console.warn(
|
|
67
|
+
`[Frigg][telemetry] failed to wire a telemetry subscriber: ${
|
|
68
|
+
error && error.message
|
|
69
|
+
}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return unsubscribes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { wireTelemetrySubscribers };
|
|
@@ -60,6 +60,31 @@ function validateNorthStarEntry(entry, where) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Adopter-declared telemetry subscribers (Decision 6). Each is either a factory
|
|
65
|
+
* function `fn(telemetry)` or a declarative `{ event?, handler }` object. Kept as
|
|
66
|
+
* an array so `wireTelemetrySubscribers` can attach them to the bus at runtime.
|
|
67
|
+
*/
|
|
68
|
+
function resolveSubscribers(subscribers) {
|
|
69
|
+
if (subscribers === undefined || subscribers === null) return [];
|
|
70
|
+
if (!Array.isArray(subscribers)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
'[Frigg][telemetry] subscribers must be an array of functions or { handler } objects'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
for (const subscriber of subscribers) {
|
|
76
|
+
const ok =
|
|
77
|
+
typeof subscriber === 'function' ||
|
|
78
|
+
(subscriber && typeof subscriber.handler === 'function');
|
|
79
|
+
if (!ok) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'[Frigg][telemetry] each subscriber must be a function or an object with a handler function'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return subscribers;
|
|
86
|
+
}
|
|
87
|
+
|
|
63
88
|
function resolveNorthStar(northStar) {
|
|
64
89
|
if (!northStar) return null;
|
|
65
90
|
if (northStar.default) validateNorthStarEntry(northStar.default, 'default');
|
|
@@ -86,6 +111,7 @@ function resolveTelemetryConfig(appDefinition = {}, ctx = {}) {
|
|
|
86
111
|
exporter: resolveExporter(telemetry.exporter, stage),
|
|
87
112
|
northStar: resolveNorthStar(telemetry.northStar),
|
|
88
113
|
sampleRatio: resolveSampleRatio(telemetry.sampleRatio),
|
|
114
|
+
subscribers: resolveSubscribers(telemetry.subscribers),
|
|
89
115
|
};
|
|
90
116
|
}
|
|
91
117
|
|