shokupan 0.6.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -2
- package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
- package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
- package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
- package/dist/analyzer-Ce_7JxZh.js.map +1 -0
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +58 -23
- package/dist/{server-adapter-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
- package/dist/http-server-BEMPIs33.cjs.map +1 -0
- package/dist/{server-adapter-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
- package/dist/http-server-CCeagTyU.js.map +1 -0
- package/dist/index.cjs +1940 -917
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +18 -17
- package/dist/index.js +1948 -925
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
- package/dist/plugins/application/cluster.d.ts +33 -0
- package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +42 -0
- package/dist/plugins/application/dashboard/static/charts.js +328 -0
- package/dist/plugins/application/dashboard/static/failures.js +85 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
- package/dist/plugins/application/dashboard/static/poll.js +146 -0
- package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
- package/dist/plugins/application/dashboard/static/registry.css +131 -0
- package/dist/plugins/application/dashboard/static/registry.js +269 -0
- package/dist/plugins/application/dashboard/static/requests.js +118 -0
- package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
- package/dist/plugins/application/dashboard/static/styles.css +175 -0
- package/dist/plugins/application/dashboard/static/tables.js +92 -0
- package/dist/plugins/application/dashboard/static/tabs.js +113 -0
- package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
- package/dist/plugins/application/dashboard/template.eta +246 -0
- package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
- package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
- package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
- package/dist/plugins/application/scalar.d.ts +36 -0
- package/dist/plugins/application/socket-io.d.ts +14 -0
- package/dist/plugins/middleware/compression.d.ts +17 -0
- package/dist/plugins/middleware/cors.d.ts +34 -0
- package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
- package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
- package/dist/plugins/middleware/proxy.d.ts +37 -0
- package/dist/plugins/middleware/rate-limit.d.ts +58 -0
- package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
- package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
- package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
- package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
- package/dist/router.d.ts +17 -5
- package/dist/shokupan.d.ts +31 -5
- package/dist/util/async-hooks.d.ts +8 -2
- package/dist/util/datastore.d.ts +4 -3
- package/dist/{decorators.d.ts → util/decorators.d.ts} +6 -1
- package/dist/util/http-error.d.ts +38 -0
- package/dist/util/http-status.d.ts +32 -0
- package/dist/util/instrumentation.d.ts +1 -1
- package/dist/{request.d.ts → util/request.d.ts} +1 -1
- package/dist/util/symbol.d.ts +34 -0
- package/dist/{router → util}/trie.d.ts +1 -1
- package/dist/{types.d.ts → util/types.d.ts} +38 -2
- package/package.json +9 -6
- package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
- package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
- package/dist/plugins/compression.d.ts +0 -5
- package/dist/plugins/cors.d.ts +0 -11
- package/dist/plugins/debugview/plugin.d.ts +0 -29
- package/dist/plugins/proxy.d.ts +0 -11
- package/dist/plugins/rate-limit.d.ts +0 -15
- package/dist/plugins/scalar.d.ts +0 -15
- package/dist/server-adapter-0xH174zz.js.map +0 -1
- package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
- package/dist/symbol.d.ts +0 -15
- /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
- /package/dist/{di.d.ts → util/di.d.ts} +0 -0
- /package/dist/{response.d.ts → util/response.d.ts} +0 -0
package/dist/middleware.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { ShokupanContext } from '
|
|
2
|
-
import { ShokupanRouter } from '
|
|
1
|
+
import { ShokupanContext } from '../../context';
|
|
2
|
+
import { ShokupanRouter } from '../../router';
|
|
3
|
+
import { Shokupan } from '../../shokupan';
|
|
4
|
+
import { ShokupanPlugin, ShokupanPluginOptions } from '../../util/types';
|
|
3
5
|
export interface AuthUser {
|
|
4
6
|
id: string;
|
|
5
7
|
email?: string;
|
|
@@ -9,29 +11,92 @@ export interface AuthUser {
|
|
|
9
11
|
raw?: any;
|
|
10
12
|
}
|
|
11
13
|
export interface ProviderConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Client ID
|
|
16
|
+
*/
|
|
12
17
|
clientId: string;
|
|
18
|
+
/**
|
|
19
|
+
* Client secret
|
|
20
|
+
*/
|
|
13
21
|
clientSecret: string;
|
|
22
|
+
/**
|
|
23
|
+
* Redirect URI
|
|
24
|
+
*/
|
|
14
25
|
redirectUri: string;
|
|
26
|
+
/**
|
|
27
|
+
* Scopes
|
|
28
|
+
*/
|
|
15
29
|
scopes?: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Tenant ID (MSFT AD)
|
|
32
|
+
*/
|
|
16
33
|
tenantId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Domain (Auth0, Okta)
|
|
36
|
+
*/
|
|
17
37
|
domain?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Team ID (Apple)
|
|
40
|
+
*/
|
|
18
41
|
teamId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Key ID (Apple)
|
|
44
|
+
*/
|
|
19
45
|
keyId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Auth URL (Generic OAuth2)
|
|
48
|
+
*/
|
|
20
49
|
authUrl?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Token URL (Generic OAuth2)
|
|
52
|
+
*/
|
|
21
53
|
tokenUrl?: string;
|
|
54
|
+
/**
|
|
55
|
+
* User info URL (Generic OAuth2)
|
|
56
|
+
*/
|
|
22
57
|
userInfoUrl?: string;
|
|
23
58
|
}
|
|
24
59
|
export interface AuthConfig {
|
|
60
|
+
/**
|
|
61
|
+
* JWT secret
|
|
62
|
+
*/
|
|
25
63
|
jwtSecret: string | Uint8Array;
|
|
64
|
+
/**
|
|
65
|
+
* JWT expiration
|
|
66
|
+
*/
|
|
26
67
|
jwtExpiration?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Cookie options
|
|
70
|
+
*/
|
|
27
71
|
cookieOptions?: {
|
|
72
|
+
/**
|
|
73
|
+
* HTTP only
|
|
74
|
+
*/
|
|
28
75
|
httpOnly?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Secure
|
|
78
|
+
*/
|
|
29
79
|
secure?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Same site
|
|
82
|
+
*/
|
|
30
83
|
sameSite?: "Strict" | "Lax" | "None";
|
|
84
|
+
/**
|
|
85
|
+
* Path
|
|
86
|
+
*/
|
|
31
87
|
path?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Max age
|
|
90
|
+
*/
|
|
32
91
|
maxAge?: number;
|
|
33
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* Success callback
|
|
95
|
+
*/
|
|
34
96
|
onSuccess?: (user: AuthUser, ctx: ShokupanContext) => Promise<any> | any;
|
|
97
|
+
/**
|
|
98
|
+
* Providers
|
|
99
|
+
*/
|
|
35
100
|
providers: {
|
|
36
101
|
github?: ProviderConfig;
|
|
37
102
|
google?: ProviderConfig;
|
|
@@ -43,10 +108,14 @@ export interface AuthConfig {
|
|
|
43
108
|
[key: string]: ProviderConfig | undefined;
|
|
44
109
|
};
|
|
45
110
|
}
|
|
46
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Authentication plugin
|
|
113
|
+
*/
|
|
114
|
+
export declare class AuthPlugin extends ShokupanRouter<any> implements ShokupanPlugin {
|
|
47
115
|
private authConfig;
|
|
48
116
|
private secret;
|
|
49
117
|
constructor(authConfig: AuthConfig);
|
|
118
|
+
onInit(app: Shokupan, options?: ShokupanPluginOptions): void;
|
|
50
119
|
private getProviderInstance;
|
|
51
120
|
private createSession;
|
|
52
121
|
private init;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Shokupan } from '../../shokupan';
|
|
2
|
+
import { ShokupanPlugin } from '../../util/types';
|
|
3
|
+
export interface ClusterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Number of workers to spawn.
|
|
6
|
+
* Set to -1 or 'auto' to spawn one worker per available CPU.
|
|
7
|
+
* @default 'auto'
|
|
8
|
+
*/
|
|
9
|
+
workers?: number | 'auto';
|
|
10
|
+
/**
|
|
11
|
+
* Whether to pipe stdout/stderr to the parent process.
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
silent?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Enable sticky sessions (useful for Socket.io).
|
|
17
|
+
* Currently only supported in Node.js runtime.
|
|
18
|
+
* @default false
|
|
19
|
+
*/
|
|
20
|
+
sticky?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Cluster Plugin
|
|
24
|
+
*
|
|
25
|
+
* Automatically manages clustering for Node.js and Bun.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ClusterPlugin implements ShokupanPlugin {
|
|
28
|
+
private options;
|
|
29
|
+
constructor(options?: ClusterOptions);
|
|
30
|
+
onInit(app: Shokupan): void;
|
|
31
|
+
private handleBun;
|
|
32
|
+
private handleNode;
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class MetricsCollector {
|
|
2
|
+
private currentIntervalStart;
|
|
3
|
+
private pendingDetails;
|
|
4
|
+
private eventLoopHistogram;
|
|
5
|
+
private timer;
|
|
6
|
+
constructor();
|
|
7
|
+
recordRequest(duration: number, isError: boolean): void;
|
|
8
|
+
private alignTimestamp;
|
|
9
|
+
private collect;
|
|
10
|
+
private flushInterval;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HeadersInit } from 'bun';
|
|
2
|
+
import { ShokupanHooks, ShokupanPlugin } from '../../../util/types';
|
|
3
|
+
export interface RequestLog {
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
status: number;
|
|
7
|
+
duration: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
handlerStack?: any[];
|
|
10
|
+
}
|
|
11
|
+
export interface DashboardConfig {
|
|
12
|
+
getRequestHeaders?: () => HeadersInit;
|
|
13
|
+
path?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Retention time in milliseconds
|
|
16
|
+
*/
|
|
17
|
+
retentionMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class Dashboard implements ShokupanPlugin {
|
|
20
|
+
private readonly dashboardConfig;
|
|
21
|
+
private static __dirname;
|
|
22
|
+
private static getBasePath;
|
|
23
|
+
private router;
|
|
24
|
+
private metrics;
|
|
25
|
+
private eta;
|
|
26
|
+
private startTime;
|
|
27
|
+
private instrumented;
|
|
28
|
+
private metricsCollector;
|
|
29
|
+
constructor(dashboardConfig?: DashboardConfig);
|
|
30
|
+
onInit(app: any, options?: {
|
|
31
|
+
path?: string;
|
|
32
|
+
}): void;
|
|
33
|
+
private setupRoutes;
|
|
34
|
+
private instrumentApp;
|
|
35
|
+
private assignIdsToRegistry;
|
|
36
|
+
recordNodeMetric(id: string, type: string, duration: number, isError: boolean): void;
|
|
37
|
+
recordEdgeMetric(from: string, to: string): void;
|
|
38
|
+
private getLinkPattern;
|
|
39
|
+
getHooks(): ShokupanHooks;
|
|
40
|
+
private updateTiming;
|
|
41
|
+
}
|
|
42
|
+
export default function DebugDashboard(config?: DashboardConfig): Dashboard;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
|
|
2
|
+
// Common chart config
|
|
3
|
+
const commonOptions = {
|
|
4
|
+
responsive: true,
|
|
5
|
+
maintainAspectRatio: false,
|
|
6
|
+
plugins: {
|
|
7
|
+
legend: { labels: { color: '#94a3b8' } }
|
|
8
|
+
},
|
|
9
|
+
scales: {
|
|
10
|
+
x: {
|
|
11
|
+
ticks: { color: '#94a3b8' },
|
|
12
|
+
grid: { color: '#334155' }
|
|
13
|
+
},
|
|
14
|
+
y: {
|
|
15
|
+
ticks: { color: '#94a3b8' },
|
|
16
|
+
grid: { color: '#334155' },
|
|
17
|
+
beginAtZero: true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
animation: { duration: 0 },
|
|
21
|
+
interaction: {
|
|
22
|
+
mode: 'index',
|
|
23
|
+
intersect: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Get request headers from global function if available
|
|
28
|
+
const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
|
|
29
|
+
|
|
30
|
+
// Determine base path for API requests
|
|
31
|
+
const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
|
|
32
|
+
const url = basePath + '/';
|
|
33
|
+
|
|
34
|
+
// Chart instances
|
|
35
|
+
|
|
36
|
+
// --- Latency Chart ---
|
|
37
|
+
const latencyCtx = document.getElementById('latencyChart').getContext('2d');
|
|
38
|
+
const latencyChart = new Chart(latencyCtx, {
|
|
39
|
+
type: 'line',
|
|
40
|
+
data: {
|
|
41
|
+
labels: [],
|
|
42
|
+
datasets: [
|
|
43
|
+
{
|
|
44
|
+
label: 'Msg (Avg)',
|
|
45
|
+
data: [],
|
|
46
|
+
borderColor: '#3b82f6',
|
|
47
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
48
|
+
borderWidth: 2,
|
|
49
|
+
tension: 0.4,
|
|
50
|
+
fill: false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'p95',
|
|
54
|
+
data: [],
|
|
55
|
+
borderColor: '#eab308',
|
|
56
|
+
backgroundColor: 'rgba(234, 179, 8, 0.1)',
|
|
57
|
+
borderWidth: 2,
|
|
58
|
+
tension: 0.4,
|
|
59
|
+
fill: false
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'p99',
|
|
63
|
+
data: [],
|
|
64
|
+
borderColor: '#ef4444',
|
|
65
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
66
|
+
borderWidth: 2,
|
|
67
|
+
tension: 0.4,
|
|
68
|
+
fill: false
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
options: commonOptions
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- RPS Chart ---
|
|
76
|
+
const rpsCtx = document.getElementById('rpsChart').getContext('2d');
|
|
77
|
+
const rpsChart = new Chart(rpsCtx, {
|
|
78
|
+
type: 'line',
|
|
79
|
+
data: {
|
|
80
|
+
labels: [],
|
|
81
|
+
datasets: [
|
|
82
|
+
{
|
|
83
|
+
label: 'Total Requests',
|
|
84
|
+
data: [],
|
|
85
|
+
borderColor: '#10b981',
|
|
86
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
87
|
+
borderWidth: 2,
|
|
88
|
+
tension: 0.4,
|
|
89
|
+
fill: true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'Errors',
|
|
93
|
+
data: [],
|
|
94
|
+
borderColor: '#ef4444',
|
|
95
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
96
|
+
borderWidth: 2,
|
|
97
|
+
tension: 0.4,
|
|
98
|
+
fill: true
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
options: commonOptions
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// --- CPU Chart ---
|
|
106
|
+
const cpuCtx = document.getElementById('cpuChart').getContext('2d');
|
|
107
|
+
const cpuChart = new Chart(cpuCtx, {
|
|
108
|
+
type: 'line',
|
|
109
|
+
data: {
|
|
110
|
+
labels: [],
|
|
111
|
+
datasets: [
|
|
112
|
+
{
|
|
113
|
+
label: 'CPU Load (1m)',
|
|
114
|
+
data: [],
|
|
115
|
+
borderColor: '#8b5cf6',
|
|
116
|
+
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
|
117
|
+
borderWidth: 2,
|
|
118
|
+
tension: 0.4,
|
|
119
|
+
fill: true
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
options: commonOptions
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- Memory Chart ---
|
|
127
|
+
const memoryCtx = document.getElementById('memoryChart').getContext('2d');
|
|
128
|
+
const memoryChart = new Chart(memoryCtx, {
|
|
129
|
+
type: 'line',
|
|
130
|
+
data: {
|
|
131
|
+
labels: [],
|
|
132
|
+
datasets: [
|
|
133
|
+
{
|
|
134
|
+
label: 'Heap Used (MB)',
|
|
135
|
+
data: [],
|
|
136
|
+
borderColor: '#f97316',
|
|
137
|
+
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
|
138
|
+
borderWidth: 2,
|
|
139
|
+
tension: 0.4,
|
|
140
|
+
fill: true
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: 'RSS (MB)',
|
|
144
|
+
data: [],
|
|
145
|
+
borderColor: '#06b6d4',
|
|
146
|
+
backgroundColor: 'rgba(6, 182, 212, 0.1)',
|
|
147
|
+
borderWidth: 2,
|
|
148
|
+
tension: 0.4,
|
|
149
|
+
fill: true
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
options: commonOptions
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// --- Heap Chart ---
|
|
157
|
+
const heapCtx = document.getElementById('heapChart').getContext('2d');
|
|
158
|
+
const heapChart = new Chart(heapCtx, {
|
|
159
|
+
type: 'line',
|
|
160
|
+
data: {
|
|
161
|
+
labels: [],
|
|
162
|
+
datasets: [
|
|
163
|
+
{
|
|
164
|
+
label: 'Heap Used (MB)',
|
|
165
|
+
data: [],
|
|
166
|
+
borderColor: '#f97316',
|
|
167
|
+
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
|
168
|
+
borderWidth: 2,
|
|
169
|
+
tension: 0.4,
|
|
170
|
+
fill: true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: 'Heap Total (MB)',
|
|
174
|
+
data: [],
|
|
175
|
+
borderColor: '#fb923c',
|
|
176
|
+
backgroundColor: 'rgba(251, 146, 60, 0.1)',
|
|
177
|
+
borderWidth: 2,
|
|
178
|
+
tension: 0.4,
|
|
179
|
+
fill: false
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
options: commonOptions
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// --- Event Loop Latency Chart ---
|
|
187
|
+
const eventLoopCtx = document.getElementById('eventLoopChart').getContext('2d');
|
|
188
|
+
const eventLoopChart = new Chart(eventLoopCtx, {
|
|
189
|
+
type: 'line',
|
|
190
|
+
data: {
|
|
191
|
+
labels: [],
|
|
192
|
+
datasets: [
|
|
193
|
+
{
|
|
194
|
+
label: 'Mean (ms)',
|
|
195
|
+
data: [],
|
|
196
|
+
borderColor: '#3b82f6',
|
|
197
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
198
|
+
borderWidth: 2,
|
|
199
|
+
tension: 0.4,
|
|
200
|
+
fill: false
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: 'p95 (ms)',
|
|
204
|
+
data: [],
|
|
205
|
+
borderColor: '#eab308',
|
|
206
|
+
backgroundColor: 'rgba(234, 179, 8, 0.1)',
|
|
207
|
+
borderWidth: 2,
|
|
208
|
+
tension: 0.4,
|
|
209
|
+
fill: false
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
label: 'p99 (ms)',
|
|
213
|
+
data: [],
|
|
214
|
+
borderColor: '#ef4444',
|
|
215
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
216
|
+
borderWidth: 2,
|
|
217
|
+
tension: 0.4,
|
|
218
|
+
fill: false
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
options: commonOptions
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// --- Error Rate Chart ---
|
|
226
|
+
const errorRateCtx = document.getElementById('errorRateChart').getContext('2d');
|
|
227
|
+
const errorRateChart = new Chart(errorRateCtx, {
|
|
228
|
+
type: 'line',
|
|
229
|
+
data: {
|
|
230
|
+
labels: [],
|
|
231
|
+
datasets: [
|
|
232
|
+
{
|
|
233
|
+
label: 'Success Rate (%)',
|
|
234
|
+
data: [],
|
|
235
|
+
borderColor: '#10b981',
|
|
236
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
237
|
+
borderWidth: 2,
|
|
238
|
+
tension: 0.4,
|
|
239
|
+
fill: true
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: 'Error Rate (%)',
|
|
243
|
+
data: [],
|
|
244
|
+
borderColor: '#ef4444',
|
|
245
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
246
|
+
borderWidth: 2,
|
|
247
|
+
tension: 0.4,
|
|
248
|
+
fill: true
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
options: commonOptions
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
async function updateCharts() {
|
|
256
|
+
const period = document.getElementById('time-range-selector').value || '1m';
|
|
257
|
+
try {
|
|
258
|
+
const res = await fetch(`${url}metrics/history?interval=${period}`, { headers });
|
|
259
|
+
const data = await res.json();
|
|
260
|
+
const metrics = data.metrics || [];
|
|
261
|
+
|
|
262
|
+
const labels = metrics.map(m => new Date(m.timestamp).toLocaleTimeString());
|
|
263
|
+
|
|
264
|
+
// Latency
|
|
265
|
+
latencyChart.data.labels = labels;
|
|
266
|
+
latencyChart.data.datasets[0].data = metrics.map(m => m.responseTime.avg);
|
|
267
|
+
latencyChart.data.datasets[1].data = metrics.map(m => m.responseTime.p95);
|
|
268
|
+
latencyChart.data.datasets[2].data = metrics.map(m => m.responseTime.p99);
|
|
269
|
+
latencyChart.update();
|
|
270
|
+
|
|
271
|
+
// RPS
|
|
272
|
+
rpsChart.data.labels = labels;
|
|
273
|
+
rpsChart.data.datasets[0].data = metrics.map(m => m.requests.total);
|
|
274
|
+
rpsChart.data.datasets[1].data = metrics.map(m => m.requests.error);
|
|
275
|
+
rpsChart.update();
|
|
276
|
+
|
|
277
|
+
// CPU
|
|
278
|
+
cpuChart.data.labels = labels;
|
|
279
|
+
cpuChart.data.datasets[0].data = metrics.map(m => m.cpu);
|
|
280
|
+
cpuChart.update();
|
|
281
|
+
|
|
282
|
+
// Memory
|
|
283
|
+
memoryChart.data.labels = labels;
|
|
284
|
+
memoryChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
|
|
285
|
+
memoryChart.data.datasets[1].data = metrics.map(m => (m.memory.used / 1024 / 1024).toFixed(2));
|
|
286
|
+
memoryChart.update();
|
|
287
|
+
|
|
288
|
+
// Heap
|
|
289
|
+
heapChart.data.labels = labels;
|
|
290
|
+
heapChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
|
|
291
|
+
heapChart.data.datasets[1].data = metrics.map(m => (m.memory.heapTotal / 1024 / 1024).toFixed(2));
|
|
292
|
+
heapChart.update();
|
|
293
|
+
|
|
294
|
+
// Event Loop Latency
|
|
295
|
+
eventLoopChart.data.labels = labels;
|
|
296
|
+
eventLoopChart.data.datasets[0].data = metrics.map(m => m.eventLoopLatency.mean);
|
|
297
|
+
eventLoopChart.data.datasets[1].data = metrics.map(m => m.eventLoopLatency.p95);
|
|
298
|
+
eventLoopChart.data.datasets[2].data = metrics.map(m => m.eventLoopLatency.p99);
|
|
299
|
+
eventLoopChart.update();
|
|
300
|
+
|
|
301
|
+
// Error Rate
|
|
302
|
+
errorRateChart.data.labels = labels;
|
|
303
|
+
errorRateChart.data.datasets[0].data = metrics.map(m => {
|
|
304
|
+
const total = m.requests.success + m.requests.error;
|
|
305
|
+
return total > 0 ? ((m.requests.success / total) * 100).toFixed(2) : 100;
|
|
306
|
+
});
|
|
307
|
+
errorRateChart.data.datasets[1].data = metrics.map(m => {
|
|
308
|
+
const total = m.requests.success + m.requests.error;
|
|
309
|
+
return total > 0 ? ((m.requests.error / total) * 100).toFixed(2) : 0;
|
|
310
|
+
});
|
|
311
|
+
errorRateChart.update();
|
|
312
|
+
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error("Failed to fetch metrics", e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Initial load
|
|
319
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
320
|
+
updateCharts();
|
|
321
|
+
// Poll every 10s for short intervals
|
|
322
|
+
setInterval(() => {
|
|
323
|
+
const period = document.getElementById('time-range-selector').value;
|
|
324
|
+
if (['1m', '5m', '30m', '1h', '2h'].includes(period)) {
|
|
325
|
+
updateCharts();
|
|
326
|
+
}
|
|
327
|
+
}, 10000);
|
|
328
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
// --- Failures Tab Logic ---
|
|
3
|
+
const failuresTable = new Tabulator("#failures-table-container", {
|
|
4
|
+
layout: "fitColumns",
|
|
5
|
+
height: "500px",
|
|
6
|
+
placeholder: "No failed requests found",
|
|
7
|
+
data: [],
|
|
8
|
+
columns: [
|
|
9
|
+
{ title: "Time", field: "timestamp", width: 180, formatter: (cell) => new Date(cell.getValue()).toLocaleString() },
|
|
10
|
+
{ title: "Method", field: "method", width: 100 },
|
|
11
|
+
{ title: "URL", field: "url" },
|
|
12
|
+
{ title: "Status", field: "status", width: 90, formatter: (cell) => `<span style="color: #ef4444; font-weight: bold;">${cell.getValue()}</span>` },
|
|
13
|
+
{
|
|
14
|
+
title: "Actions", formatter: (cell) => {
|
|
15
|
+
return `
|
|
16
|
+
<button class="replay-btn" style="background:#3b82f6; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer; margin-right:4px;">Replay</button>
|
|
17
|
+
<button class="export-btn" style="background:#64748b; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer;">Export</button>
|
|
18
|
+
`;
|
|
19
|
+
}, cellClick: (e, cell) => {
|
|
20
|
+
if (e.target.classList.contains('replay-btn')) {
|
|
21
|
+
replayRequest(cell.getRow().getData());
|
|
22
|
+
} else if (e.target.classList.contains('export-btn')) {
|
|
23
|
+
exportFailure(cell.getRow().getData());
|
|
24
|
+
}
|
|
25
|
+
}, width: 140, headerSort: false
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
function exportFailure(data) {
|
|
32
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
33
|
+
const url = URL.createObjectURL(blob);
|
|
34
|
+
const a = document.createElement('a');
|
|
35
|
+
a.href = url;
|
|
36
|
+
a.download = `failure-${data.timestamp}.json`;
|
|
37
|
+
a.click();
|
|
38
|
+
URL.revokeObjectURL(url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function importFailure() {
|
|
42
|
+
const input = document.createElement('input');
|
|
43
|
+
input.type = 'file';
|
|
44
|
+
input.accept = '.json';
|
|
45
|
+
input.onchange = e => {
|
|
46
|
+
const file = e.target.files[0];
|
|
47
|
+
if (!file) return;
|
|
48
|
+
const reader = new FileReader();
|
|
49
|
+
reader.onload = ev => {
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(ev.target.result);
|
|
52
|
+
replayRequest(data);
|
|
53
|
+
} catch (err) { alert("Invalid JSON: " + err); }
|
|
54
|
+
};
|
|
55
|
+
reader.readAsText(file);
|
|
56
|
+
};
|
|
57
|
+
input.click();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function replayRequest(req) {
|
|
61
|
+
if (!confirm(`Replay ${req.method} ${req.url}?`)) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const headers = getRequestHeaders ? getRequestHeaders() : {};
|
|
65
|
+
const basePath = window.location.pathname.endsWith('/') ? '' : window.location.pathname;
|
|
66
|
+
const url = basePath + (basePath.endsWith('/') ? 'replay' : '/replay');
|
|
67
|
+
|
|
68
|
+
const res = await fetch(url, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
method: req.method,
|
|
73
|
+
url: req.url,
|
|
74
|
+
headers: req.headers,
|
|
75
|
+
body: req.body
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await res.json();
|
|
80
|
+
alert(`Replay Status: ${res.status}\n\nResponse:\n${JSON.stringify(result, null, 2)}`);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
alert("Replay Failed: " + e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|