@pikku/core 0.12.15 → 0.12.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/telemetry.d.ts +47 -0
- package/dist/middleware/telemetry.js +106 -0
- package/dist/middleware-runner.js +31 -4
- package/dist/types/core.types.d.ts +13 -0
- package/dist/types/core.types.js +7 -1
- package/dist/wirings/cli/cli-runner.js +2 -1
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +3 -0
- package/dist/wirings/mcp/mcp.types.d.ts +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/middleware/index.ts +1 -0
- package/src/middleware/telemetry.ts +110 -0
- package/src/middleware-runner.test.ts +149 -1
- package/src/middleware-runner.ts +48 -4
- package/src/types/core.types.ts +28 -1
- package/src/wirings/channel/local/local-channel-runner.test.ts +5 -1
- package/src/wirings/cli/cli-runner.ts +2 -1
- package/src/wirings/http/http.types.ts +1 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +4 -0
- package/src/wirings/mcp/mcp.types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## 0.12.4
|
|
2
2
|
|
|
3
|
+
## 0.12.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fbcf5b9: Add middleware priority system, telemetry middleware, and statusCode getter. Middleware now supports named priority levels (highest, high, medium, low, lowest) that control execution order regardless of registration order. Includes telemetryOuter and telemetryInner middleware for observability instrumentation via structured console.log output. PikkuHTTPResponse now exposes a readonly `statusCode` getter across all response implementations.
|
|
8
|
+
|
|
3
9
|
## 0.12.15
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module @pikku/core
|
|
3
3
|
*/
|
|
4
|
-
export type { CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuWiringTypes, RequireAtLeastOne, SerializedError, WireServices, } from './types/core.types.js';
|
|
4
|
+
export type { CommonWireMeta, CoreConfig, CorePikkuMiddleware, CorePikkuMiddlewareConfig, CorePikkuMiddlewareFactory, CorePikkuMiddlewareGroup, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, FunctionMeta, FunctionRuntimeMeta, FunctionServicesMeta, FunctionWiresMeta, FunctionsMeta, FunctionsRuntimeMeta, JSONPrimitive, JSONValue, MakeRequired, MiddlewareMetadata, MiddlewarePriority, PermissionMetadata, PickOptional, PickRequired, PikkuAIMiddlewareHooks, PikkuWire, PikkuWiringTypes, RequireAtLeastOne, SerializedError, WireServices, } from './types/core.types.js';
|
|
5
5
|
export { pikkuAIMiddleware, pikkuChannelMiddleware, pikkuChannelMiddlewareFactory, pikkuMiddleware, pikkuMiddlewareFactory, } from './types/core.types.js';
|
|
6
6
|
export type { CorePikkuAuth, CorePikkuAuthConfig, CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuPermission, CorePikkuPermissionConfig, CorePikkuPermissionFactory, CorePikkuApprovalDescription, CorePermissionGroup, ZodLike, } from './function/functions.types.js';
|
|
7
7
|
export { pikkuAuth, pikkuPermission, pikkuPermissionFactory, pikkuApprovalDescription, } from './function/functions.types.js';
|
|
@@ -3,5 +3,6 @@ export { authCookie } from './auth-cookie.js';
|
|
|
3
3
|
export { authBearer } from './auth-bearer.js';
|
|
4
4
|
export { pikkuRemoteAuthMiddleware } from './remote-auth.js';
|
|
5
5
|
export { cors } from './cors.js';
|
|
6
|
+
export { telemetryOuter, telemetryInner } from './telemetry.js';
|
|
6
7
|
export { addMiddleware, runMiddleware } from '../middleware-runner.js';
|
|
7
8
|
export { addPermission } from '../permissions.js';
|
package/dist/middleware/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export { authCookie } from './auth-cookie.js';
|
|
|
3
3
|
export { authBearer } from './auth-bearer.js';
|
|
4
4
|
export { pikkuRemoteAuthMiddleware } from './remote-auth.js';
|
|
5
5
|
export { cors } from './cors.js';
|
|
6
|
+
export { telemetryOuter, telemetryInner } from './telemetry.js';
|
|
6
7
|
export { addMiddleware, runMiddleware } from '../middleware-runner.js';
|
|
7
8
|
export { addPermission } from '../permissions.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outer telemetry middleware that captures total request duration and outcome.
|
|
3
|
+
* Runs at `highest` priority (outermost in the middleware chain).
|
|
4
|
+
*
|
|
5
|
+
* Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
|
|
6
|
+
* - Total duration (including all middleware)
|
|
7
|
+
* - Outcome (ok/error)
|
|
8
|
+
* - Wire metadata (wireType, wireId, traceId)
|
|
9
|
+
* - HTTP details (method, path, status) if applicable
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { telemetryOuter } from '@pikku/core/middleware'
|
|
14
|
+
* addMiddleware('myTag', [telemetryOuter()])
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const telemetryOuter: import("../types/core.types.js").CorePikkuMiddlewareFactory<void | {
|
|
18
|
+
environmentId?: string;
|
|
19
|
+
orgId?: string;
|
|
20
|
+
}, import("../types/core.types.js").CoreSingletonServices<{
|
|
21
|
+
logLevel?: import("../services/logger.js").LogLevel;
|
|
22
|
+
secrets?: {};
|
|
23
|
+
workflow?: import("../wirings/workflow/workflow.types.js").WorkflowServiceConfig;
|
|
24
|
+
}>, import("../types/core.types.js").CoreUserSession>;
|
|
25
|
+
/**
|
|
26
|
+
* Inner telemetry middleware that captures function execution duration and user context.
|
|
27
|
+
* Runs at `lowest` priority (innermost, closest to the function).
|
|
28
|
+
*
|
|
29
|
+
* Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
|
|
30
|
+
* - Function-only duration (excluding outer middleware like auth)
|
|
31
|
+
* - User identity (pikkuUserId) if authenticated
|
|
32
|
+
* - Wire metadata (wireType, wireId, traceId)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { telemetryInner } from '@pikku/core/middleware'
|
|
37
|
+
* addMiddleware('myTag', [telemetryInner()])
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare const telemetryInner: import("../types/core.types.js").CorePikkuMiddlewareFactory<void | {
|
|
41
|
+
environmentId?: string;
|
|
42
|
+
orgId?: string;
|
|
43
|
+
}, import("../types/core.types.js").CoreSingletonServices<{
|
|
44
|
+
logLevel?: import("../services/logger.js").LogLevel;
|
|
45
|
+
secrets?: {};
|
|
46
|
+
workflow?: import("../wirings/workflow/workflow.types.js").WorkflowServiceConfig;
|
|
47
|
+
}>, import("../types/core.types.js").CoreUserSession>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Outer telemetry middleware that captures total request duration and outcome.
|
|
4
|
+
* Runs at `highest` priority (outermost in the middleware chain).
|
|
5
|
+
*
|
|
6
|
+
* Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
|
|
7
|
+
* - Total duration (including all middleware)
|
|
8
|
+
* - Outcome (ok/error)
|
|
9
|
+
* - Wire metadata (wireType, wireId, traceId)
|
|
10
|
+
* - HTTP details (method, path, status) if applicable
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { telemetryOuter } from '@pikku/core/middleware'
|
|
15
|
+
* addMiddleware('myTag', [telemetryOuter()])
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const telemetryOuter = pikkuMiddlewareFactory(({ environmentId, orgId } = {}) => {
|
|
19
|
+
return pikkuMiddleware({
|
|
20
|
+
name: 'telemetry-outer',
|
|
21
|
+
priority: 'highest',
|
|
22
|
+
func: async (services, wire, next) => {
|
|
23
|
+
const start = performance.now();
|
|
24
|
+
let outcome = 'ok';
|
|
25
|
+
let errorMessage;
|
|
26
|
+
try {
|
|
27
|
+
await next();
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
outcome = 'error';
|
|
31
|
+
errorMessage = e instanceof Error ? e.message : String(e);
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
services.logger.info({
|
|
36
|
+
__pikku_telemetry: 'end',
|
|
37
|
+
__pikku_layer: 'outer',
|
|
38
|
+
traceId: wire.traceId,
|
|
39
|
+
wireType: wire.wireType,
|
|
40
|
+
wireId: wire.wireId,
|
|
41
|
+
totalDuration: Math.round(performance.now() - start),
|
|
42
|
+
outcome,
|
|
43
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
44
|
+
...(wire.http
|
|
45
|
+
? {
|
|
46
|
+
httpStatus: wire.http.response?.statusCode,
|
|
47
|
+
httpMethod: wire.http.request?.method(),
|
|
48
|
+
httpPath: wire.http.request?.path(),
|
|
49
|
+
}
|
|
50
|
+
: {}),
|
|
51
|
+
...(environmentId ? { environmentId } : {}),
|
|
52
|
+
...(orgId ? { orgId } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Inner telemetry middleware that captures function execution duration and user context.
|
|
60
|
+
* Runs at `lowest` priority (innermost, closest to the function).
|
|
61
|
+
*
|
|
62
|
+
* Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
|
|
63
|
+
* - Function-only duration (excluding outer middleware like auth)
|
|
64
|
+
* - User identity (pikkuUserId) if authenticated
|
|
65
|
+
* - Wire metadata (wireType, wireId, traceId)
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { telemetryInner } from '@pikku/core/middleware'
|
|
70
|
+
* addMiddleware('myTag', [telemetryInner()])
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export const telemetryInner = pikkuMiddlewareFactory(({ environmentId, orgId } = {}) => {
|
|
74
|
+
return pikkuMiddleware({
|
|
75
|
+
name: 'telemetry-inner',
|
|
76
|
+
priority: 'lowest',
|
|
77
|
+
func: async (services, wire, next) => {
|
|
78
|
+
const start = performance.now();
|
|
79
|
+
let outcome = 'ok';
|
|
80
|
+
let errorMessage;
|
|
81
|
+
try {
|
|
82
|
+
await next();
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
outcome = 'error';
|
|
86
|
+
errorMessage = e instanceof Error ? e.message : String(e);
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
services.logger.info({
|
|
91
|
+
__pikku_telemetry: 'end',
|
|
92
|
+
__pikku_layer: 'inner',
|
|
93
|
+
traceId: wire.traceId,
|
|
94
|
+
wireType: wire.wireType,
|
|
95
|
+
wireId: wire.wireId,
|
|
96
|
+
functionDuration: Math.round(performance.now() - start),
|
|
97
|
+
outcome,
|
|
98
|
+
pikkuUserId: wire.pikkuUserId,
|
|
99
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
100
|
+
...(environmentId ? { environmentId } : {}),
|
|
101
|
+
...(orgId ? { orgId } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { pikkuState } from './pikku-state.js';
|
|
2
2
|
import { freezeDedupe, getTagGroups } from './utils.js';
|
|
3
|
+
const PRIORITY_ORDER = {
|
|
4
|
+
highest: 0,
|
|
5
|
+
high: 1,
|
|
6
|
+
medium: 2,
|
|
7
|
+
low: 3,
|
|
8
|
+
lowest: 4,
|
|
9
|
+
};
|
|
10
|
+
const getMiddlewarePriority = (fn) => {
|
|
11
|
+
const priority = fn.__priority;
|
|
12
|
+
return PRIORITY_ORDER[priority ?? 'medium'];
|
|
13
|
+
};
|
|
14
|
+
const sortByPriority = (middlewares) => {
|
|
15
|
+
return middlewares.sort((a, b) => getMiddlewarePriority(a) - getMiddlewarePriority(b));
|
|
16
|
+
};
|
|
3
17
|
/**
|
|
4
18
|
* Runs a chain of middleware functions in sequence before executing the main function.
|
|
5
19
|
*
|
|
@@ -18,11 +32,14 @@ import { freezeDedupe, getTagGroups } from './utils.js';
|
|
|
18
32
|
* );
|
|
19
33
|
*/
|
|
20
34
|
export const runMiddleware = async (services, wire, middlewares, main) => {
|
|
21
|
-
//
|
|
35
|
+
// Sort by priority if not already sorted (cached middleware is pre-sorted)
|
|
36
|
+
const sorted = isSortedByPriority(middlewares)
|
|
37
|
+
? middlewares
|
|
38
|
+
: [...middlewares].sort((a, b) => getMiddlewarePriority(a) - getMiddlewarePriority(b));
|
|
22
39
|
let result;
|
|
23
40
|
const dispatch = async (index) => {
|
|
24
|
-
if (
|
|
25
|
-
return await
|
|
41
|
+
if (sorted && index < sorted.length) {
|
|
42
|
+
return await sorted[index](services, wire, () => dispatch(index + 1));
|
|
26
43
|
}
|
|
27
44
|
else if (main) {
|
|
28
45
|
result = await main();
|
|
@@ -31,6 +48,15 @@ export const runMiddleware = async (services, wire, middlewares, main) => {
|
|
|
31
48
|
await dispatch(0);
|
|
32
49
|
return result;
|
|
33
50
|
};
|
|
51
|
+
const isSortedByPriority = (middlewares) => {
|
|
52
|
+
for (let i = 1; i < middlewares.length; i++) {
|
|
53
|
+
if (getMiddlewarePriority(middlewares[i]) <
|
|
54
|
+
getMiddlewarePriority(middlewares[i - 1])) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
34
60
|
/**
|
|
35
61
|
* Registers global middleware for a specific tag.
|
|
36
62
|
*
|
|
@@ -176,7 +202,8 @@ export const combineMiddleware = (wireType, uid, { wireInheritedMiddleware, wire
|
|
|
176
202
|
if (funcMiddleware) {
|
|
177
203
|
resolved.push(...funcMiddleware);
|
|
178
204
|
}
|
|
179
|
-
//
|
|
205
|
+
// Sort by priority, deduplicate, and freeze
|
|
206
|
+
sortByPriority(resolved);
|
|
180
207
|
middlewareCache[wireType][uid] = freezeDedupe(resolved);
|
|
181
208
|
return middlewareCache[wireType][uid];
|
|
182
209
|
};
|
|
@@ -224,6 +224,17 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
|
|
|
224
224
|
* A function that can wrap an wire and be called before or after
|
|
225
225
|
*/
|
|
226
226
|
export type CorePikkuMiddleware<SingletonServices extends CoreSingletonServices = CoreSingletonServices, UserSession extends CoreUserSession = CoreUserSession> = (services: SingletonServices, wires: PikkuWire<unknown, unknown, false, UserSession>, next: () => Promise<void>) => Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Priority levels for middleware execution order.
|
|
229
|
+
* Lower priority runs first (outermost in the onion model).
|
|
230
|
+
*
|
|
231
|
+
* - `highest` — Runs first (outermost). Use for telemetry, request tracing.
|
|
232
|
+
* - `high` — Runs early. Use for CORS, rate limiting.
|
|
233
|
+
* - `medium` — Default. Use for auth, most user middleware.
|
|
234
|
+
* - `low` — Runs late. Use for post-auth processing.
|
|
235
|
+
* - `lowest` — Runs last (innermost, closest to function). Use for inner telemetry.
|
|
236
|
+
*/
|
|
237
|
+
export type MiddlewarePriority = 'highest' | 'high' | 'medium' | 'low' | 'lowest';
|
|
227
238
|
/**
|
|
228
239
|
* Configuration object for creating middleware with metadata
|
|
229
240
|
*
|
|
@@ -237,6 +248,8 @@ export type CorePikkuMiddlewareConfig<SingletonServices extends CoreSingletonSer
|
|
|
237
248
|
name?: string;
|
|
238
249
|
/** Optional description of what the middleware does */
|
|
239
250
|
description?: string;
|
|
251
|
+
/** Execution priority. Lower runs first (outermost). Defaults to 'medium'. */
|
|
252
|
+
priority?: MiddlewarePriority;
|
|
240
253
|
};
|
|
241
254
|
/**
|
|
242
255
|
* A factory function that takes input and returns middleware
|
package/dist/types/core.types.js
CHANGED
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
export const pikkuMiddleware = (middleware) => {
|
|
27
|
-
|
|
27
|
+
if (typeof middleware === 'function')
|
|
28
|
+
return middleware;
|
|
29
|
+
const func = middleware.func;
|
|
30
|
+
if (middleware.priority) {
|
|
31
|
+
func.__priority = middleware.priority;
|
|
32
|
+
}
|
|
33
|
+
return func;
|
|
28
34
|
};
|
|
29
35
|
/**
|
|
30
36
|
* Factory function for creating middleware factories
|
|
@@ -323,8 +323,9 @@ export async function executeCLI({ programName, args, createConfig, createSingle
|
|
|
323
323
|
const config = createConfig
|
|
324
324
|
? await createConfig(new LocalVariablesService(), data)
|
|
325
325
|
: {};
|
|
326
|
-
// Create services with config
|
|
326
|
+
// Create services with config and register in global state
|
|
327
327
|
const singletonServices = await createSingletonServices(config);
|
|
328
|
+
pikkuState(null, 'package', 'singletonServices', singletonServices);
|
|
328
329
|
// Execute the command
|
|
329
330
|
await runCLICommand({
|
|
330
331
|
program: programName,
|
|
@@ -167,6 +167,7 @@ export interface PikkuHTTPRequest<In = unknown> {
|
|
|
167
167
|
query(): PikkuQuery;
|
|
168
168
|
}
|
|
169
169
|
export interface PikkuHTTPResponse<Out = unknown> {
|
|
170
|
+
readonly statusCode: number;
|
|
170
171
|
status(code: number): this;
|
|
171
172
|
cookie(name: string, value: string | null, options: SerializeOptions): this;
|
|
172
173
|
header(name: string, value: string | string[]): this;
|
|
@@ -3,6 +3,7 @@ import type { SerializeOptions as CookieSerializeOptions } from 'cookie';
|
|
|
3
3
|
export declare class PikkuFetchHTTPResponse implements PikkuHTTPResponse {
|
|
4
4
|
#private;
|
|
5
5
|
setMode(mode: 'stream'): void;
|
|
6
|
+
get statusCode(): number;
|
|
6
7
|
status(code: number): this;
|
|
7
8
|
cookie(name: string, value: string, flags: CookieSerializeOptions): this;
|
|
8
9
|
header(name: string, value: string | string[]): this;
|
|
@@ -80,7 +80,7 @@ export type CoreMCPResource<PikkuFunctionConfig = CorePikkuFunctionConfig<CorePi
|
|
|
80
80
|
export type CoreMCPTool<PikkuFunctionConfig = CorePikkuFunctionConfig<CorePikkuFunctionSessionless<any, any>>, PikkuPermission = CorePikkuPermission<any, any>, PikkuMiddleware = CorePikkuMiddleware<any>> = {
|
|
81
81
|
name: string;
|
|
82
82
|
title?: string;
|
|
83
|
-
description
|
|
83
|
+
description?: string;
|
|
84
84
|
summary?: string;
|
|
85
85
|
errors?: string[];
|
|
86
86
|
func: PikkuFunctionConfig;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/middleware/index.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { authCookie } from './auth-cookie.js'
|
|
|
3
3
|
export { authBearer } from './auth-bearer.js'
|
|
4
4
|
export { pikkuRemoteAuthMiddleware } from './remote-auth.js'
|
|
5
5
|
export { cors } from './cors.js'
|
|
6
|
+
export { telemetryOuter, telemetryInner } from './telemetry.js'
|
|
6
7
|
export { addMiddleware, runMiddleware } from '../middleware-runner.js'
|
|
7
8
|
export { addPermission } from '../permissions.js'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Outer telemetry middleware that captures total request duration and outcome.
|
|
5
|
+
* Runs at `highest` priority (outermost in the middleware chain).
|
|
6
|
+
*
|
|
7
|
+
* Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
|
|
8
|
+
* - Total duration (including all middleware)
|
|
9
|
+
* - Outcome (ok/error)
|
|
10
|
+
* - Wire metadata (wireType, wireId, traceId)
|
|
11
|
+
* - HTTP details (method, path, status) if applicable
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { telemetryOuter } from '@pikku/core/middleware'
|
|
16
|
+
* addMiddleware('myTag', [telemetryOuter()])
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const telemetryOuter = pikkuMiddlewareFactory<{
|
|
20
|
+
environmentId?: string
|
|
21
|
+
orgId?: string
|
|
22
|
+
} | void>(({ environmentId, orgId } = {}) => {
|
|
23
|
+
return pikkuMiddleware({
|
|
24
|
+
name: 'telemetry-outer',
|
|
25
|
+
priority: 'highest',
|
|
26
|
+
func: async (services, wire, next) => {
|
|
27
|
+
const start = performance.now()
|
|
28
|
+
let outcome = 'ok'
|
|
29
|
+
let errorMessage: string | undefined
|
|
30
|
+
try {
|
|
31
|
+
await next()
|
|
32
|
+
} catch (e) {
|
|
33
|
+
outcome = 'error'
|
|
34
|
+
errorMessage = e instanceof Error ? e.message : String(e)
|
|
35
|
+
throw e
|
|
36
|
+
} finally {
|
|
37
|
+
services.logger.info({
|
|
38
|
+
__pikku_telemetry: 'end',
|
|
39
|
+
__pikku_layer: 'outer',
|
|
40
|
+
traceId: wire.traceId,
|
|
41
|
+
wireType: wire.wireType,
|
|
42
|
+
wireId: wire.wireId,
|
|
43
|
+
totalDuration: Math.round(performance.now() - start),
|
|
44
|
+
outcome,
|
|
45
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
46
|
+
...(wire.http
|
|
47
|
+
? {
|
|
48
|
+
httpStatus: wire.http.response?.statusCode,
|
|
49
|
+
httpMethod: wire.http.request?.method(),
|
|
50
|
+
httpPath: wire.http.request?.path(),
|
|
51
|
+
}
|
|
52
|
+
: {}),
|
|
53
|
+
...(environmentId ? { environmentId } : {}),
|
|
54
|
+
...(orgId ? { orgId } : {}),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inner telemetry middleware that captures function execution duration and user context.
|
|
63
|
+
* Runs at `lowest` priority (innermost, closest to the function).
|
|
64
|
+
*
|
|
65
|
+
* Emits a structured JSON log entry with `__pikku_telemetry: 'end'` containing:
|
|
66
|
+
* - Function-only duration (excluding outer middleware like auth)
|
|
67
|
+
* - User identity (pikkuUserId) if authenticated
|
|
68
|
+
* - Wire metadata (wireType, wireId, traceId)
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { telemetryInner } from '@pikku/core/middleware'
|
|
73
|
+
* addMiddleware('myTag', [telemetryInner()])
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const telemetryInner = pikkuMiddlewareFactory<{
|
|
77
|
+
environmentId?: string
|
|
78
|
+
orgId?: string
|
|
79
|
+
} | void>(({ environmentId, orgId } = {}) => {
|
|
80
|
+
return pikkuMiddleware({
|
|
81
|
+
name: 'telemetry-inner',
|
|
82
|
+
priority: 'lowest',
|
|
83
|
+
func: async (services, wire, next) => {
|
|
84
|
+
const start = performance.now()
|
|
85
|
+
let outcome = 'ok'
|
|
86
|
+
let errorMessage: string | undefined
|
|
87
|
+
try {
|
|
88
|
+
await next()
|
|
89
|
+
} catch (e) {
|
|
90
|
+
outcome = 'error'
|
|
91
|
+
errorMessage = e instanceof Error ? e.message : String(e)
|
|
92
|
+
throw e
|
|
93
|
+
} finally {
|
|
94
|
+
services.logger.info({
|
|
95
|
+
__pikku_telemetry: 'end',
|
|
96
|
+
__pikku_layer: 'inner',
|
|
97
|
+
traceId: wire.traceId,
|
|
98
|
+
wireType: wire.wireType,
|
|
99
|
+
wireId: wire.wireId,
|
|
100
|
+
functionDuration: Math.round(performance.now() - start),
|
|
101
|
+
outcome,
|
|
102
|
+
pikkuUserId: wire.pikkuUserId,
|
|
103
|
+
...(errorMessage ? { errorMessage } : {}),
|
|
104
|
+
...(environmentId ? { environmentId } : {}),
|
|
105
|
+
...(orgId ? { orgId } : {}),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -6,7 +6,27 @@ import {
|
|
|
6
6
|
runMiddleware,
|
|
7
7
|
} from './middleware-runner.js'
|
|
8
8
|
import { resetPikkuState } from './pikku-state.js'
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
CorePikkuMiddleware,
|
|
11
|
+
MiddlewarePriority,
|
|
12
|
+
} from './types/core.types.js'
|
|
13
|
+
import { pikkuMiddleware } from './types/core.types.js'
|
|
14
|
+
|
|
15
|
+
const withPriority = (
|
|
16
|
+
name: string,
|
|
17
|
+
priority: MiddlewarePriority,
|
|
18
|
+
log: string[]
|
|
19
|
+
): CorePikkuMiddleware => {
|
|
20
|
+
return pikkuMiddleware({
|
|
21
|
+
name,
|
|
22
|
+
priority,
|
|
23
|
+
func: async (_services, _wire, next) => {
|
|
24
|
+
log.push(`start:${name}`)
|
|
25
|
+
await next()
|
|
26
|
+
log.push(`end:${name}`)
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
}
|
|
10
30
|
|
|
11
31
|
beforeEach(() => {
|
|
12
32
|
resetPikkuState()
|
|
@@ -329,6 +349,56 @@ describe('combineMiddleware', () => {
|
|
|
329
349
|
assert.equal(result.length, 1)
|
|
330
350
|
assert.equal(result[0], middleware)
|
|
331
351
|
})
|
|
352
|
+
|
|
353
|
+
test('should sort middleware by priority', () => {
|
|
354
|
+
const log: string[] = []
|
|
355
|
+
const low = withPriority('low', 'low', log)
|
|
356
|
+
const highest = withPriority('highest', 'highest', log)
|
|
357
|
+
const medium = withPriority('medium', 'medium', log)
|
|
358
|
+
|
|
359
|
+
const result = combineMiddleware('http', Math.random().toString(), {
|
|
360
|
+
wireMiddleware: [low, highest, medium],
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
assert.equal(result.length, 3)
|
|
364
|
+
assert.equal(result[0], highest)
|
|
365
|
+
assert.equal(result[1], medium)
|
|
366
|
+
assert.equal(result[2], low)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
test('should default unprioritized middleware to medium', () => {
|
|
370
|
+
const log: string[] = []
|
|
371
|
+
const highest = withPriority('highest', 'highest', log)
|
|
372
|
+
const lowest = withPriority('lowest', 'lowest', log)
|
|
373
|
+
const noPriority: CorePikkuMiddleware = async (_services, _wire, next) => {
|
|
374
|
+
await next()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = combineMiddleware('http', Math.random().toString(), {
|
|
378
|
+
wireMiddleware: [lowest, noPriority, highest],
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
assert.equal(result.length, 3)
|
|
382
|
+
assert.equal(result[0], highest)
|
|
383
|
+
assert.equal(result[1], noPriority) // defaults to medium
|
|
384
|
+
assert.equal(result[2], lowest)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test('should preserve registration order within same priority', () => {
|
|
388
|
+
const log: string[] = []
|
|
389
|
+
const first = withPriority('first', 'medium', log)
|
|
390
|
+
const second = withPriority('second', 'medium', log)
|
|
391
|
+
const third = withPriority('third', 'medium', log)
|
|
392
|
+
|
|
393
|
+
const result = combineMiddleware('http', Math.random().toString(), {
|
|
394
|
+
wireMiddleware: [first, second, third],
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
assert.equal(result.length, 3)
|
|
398
|
+
assert.equal(result[0], first)
|
|
399
|
+
assert.equal(result[1], second)
|
|
400
|
+
assert.equal(result[2], third)
|
|
401
|
+
})
|
|
332
402
|
})
|
|
333
403
|
|
|
334
404
|
describe('runMiddleware', () => {
|
|
@@ -415,4 +485,82 @@ describe('runMiddleware', () => {
|
|
|
415
485
|
|
|
416
486
|
assert.deepEqual(executionOrder, ['middleware'])
|
|
417
487
|
})
|
|
488
|
+
|
|
489
|
+
test('should execute middleware in priority order (highest first, lowest last)', async () => {
|
|
490
|
+
const log: string[] = []
|
|
491
|
+
const low = withPriority('low', 'low', log)
|
|
492
|
+
const highest = withPriority('highest', 'highest', log)
|
|
493
|
+
const medium = withPriority('medium', 'medium', log)
|
|
494
|
+
|
|
495
|
+
await runMiddleware(
|
|
496
|
+
{} as any,
|
|
497
|
+
{} as any,
|
|
498
|
+
[low, highest, medium],
|
|
499
|
+
async () => {
|
|
500
|
+
log.push('main')
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
assert.deepEqual(log, [
|
|
505
|
+
'start:highest',
|
|
506
|
+
'start:medium',
|
|
507
|
+
'start:low',
|
|
508
|
+
'main',
|
|
509
|
+
'end:low',
|
|
510
|
+
'end:medium',
|
|
511
|
+
'end:highest',
|
|
512
|
+
])
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
test('should sort unsorted middleware passed directly to runMiddleware', async () => {
|
|
516
|
+
const log: string[] = []
|
|
517
|
+
const lowest = withPriority('lowest', 'lowest', log)
|
|
518
|
+
const highest = withPriority('highest', 'highest', log)
|
|
519
|
+
|
|
520
|
+
await runMiddleware({} as any, {} as any, [lowest, highest], async () => {
|
|
521
|
+
log.push('main')
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// highest should run first (outermost), lowest last (innermost)
|
|
525
|
+
assert.deepEqual(log, [
|
|
526
|
+
'start:highest',
|
|
527
|
+
'start:lowest',
|
|
528
|
+
'main',
|
|
529
|
+
'end:lowest',
|
|
530
|
+
'end:highest',
|
|
531
|
+
])
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
test('should handle all five priority levels in correct order', async () => {
|
|
535
|
+
const log: string[] = []
|
|
536
|
+
const lowest = withPriority('lowest', 'lowest', log)
|
|
537
|
+
const low = withPriority('low', 'low', log)
|
|
538
|
+
const medium = withPriority('medium', 'medium', log)
|
|
539
|
+
const high = withPriority('high', 'high', log)
|
|
540
|
+
const highest = withPriority('highest', 'highest', log)
|
|
541
|
+
|
|
542
|
+
// Pass in reverse order to verify sorting
|
|
543
|
+
await runMiddleware(
|
|
544
|
+
{} as any,
|
|
545
|
+
{} as any,
|
|
546
|
+
[lowest, low, medium, high, highest],
|
|
547
|
+
async () => {
|
|
548
|
+
log.push('main')
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
assert.deepEqual(log, [
|
|
553
|
+
'start:highest',
|
|
554
|
+
'start:high',
|
|
555
|
+
'start:medium',
|
|
556
|
+
'start:low',
|
|
557
|
+
'start:lowest',
|
|
558
|
+
'main',
|
|
559
|
+
'end:lowest',
|
|
560
|
+
'end:low',
|
|
561
|
+
'end:medium',
|
|
562
|
+
'end:high',
|
|
563
|
+
'end:highest',
|
|
564
|
+
])
|
|
565
|
+
})
|
|
418
566
|
})
|