qa360 1.0.4 → 1.1.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/dist/commands/history.js +1 -1
- package/dist/commands/pack.js +1 -1
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/serve.js +1 -1
- package/dist/commands/verify.js +1 -1
- package/dist/core/adapters/gitleaks-secrets.d.ts +115 -0
- package/dist/core/adapters/gitleaks-secrets.d.ts.map +1 -0
- package/dist/core/adapters/gitleaks-secrets.js +410 -0
- package/dist/core/adapters/k6-perf.d.ts +86 -0
- package/dist/core/adapters/k6-perf.d.ts.map +1 -0
- package/dist/core/adapters/k6-perf.js +398 -0
- package/dist/core/adapters/osv-deps.d.ts +124 -0
- package/dist/core/adapters/osv-deps.d.ts.map +1 -0
- package/dist/core/adapters/osv-deps.js +372 -0
- package/dist/core/adapters/playwright-api.d.ts +82 -0
- package/dist/core/adapters/playwright-api.d.ts.map +1 -0
- package/dist/core/adapters/playwright-api.js +252 -0
- package/dist/core/adapters/playwright-ui.d.ts +115 -0
- package/dist/core/adapters/playwright-ui.d.ts.map +1 -0
- package/dist/core/adapters/playwright-ui.js +346 -0
- package/dist/core/adapters/semgrep-sast.d.ts +100 -0
- package/dist/core/adapters/semgrep-sast.d.ts.map +1 -0
- package/dist/core/adapters/semgrep-sast.js +322 -0
- package/dist/core/adapters/zap-dast.d.ts +134 -0
- package/dist/core/adapters/zap-dast.d.ts.map +1 -0
- package/dist/core/adapters/zap-dast.js +424 -0
- package/dist/core/hooks/compose.d.ts +62 -0
- package/dist/core/hooks/compose.d.ts.map +1 -0
- package/dist/core/hooks/compose.js +225 -0
- package/dist/core/hooks/runner.d.ts +69 -0
- package/dist/core/hooks/runner.d.ts.map +1 -0
- package/dist/core/hooks/runner.js +303 -0
- package/dist/core/index.d.ts +74 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +39 -0
- package/dist/core/pack/migrator.d.ts +52 -0
- package/dist/core/pack/migrator.d.ts.map +1 -0
- package/dist/core/pack/migrator.js +304 -0
- package/dist/core/pack/validator.d.ts +43 -0
- package/dist/core/pack/validator.d.ts.map +1 -0
- package/dist/core/pack/validator.js +292 -0
- package/dist/core/proof/bundle.d.ts +138 -0
- package/dist/core/proof/bundle.d.ts.map +1 -0
- package/dist/core/proof/bundle.js +160 -0
- package/dist/core/proof/canonicalize.d.ts +48 -0
- package/dist/core/proof/canonicalize.d.ts.map +1 -0
- package/dist/core/proof/canonicalize.js +105 -0
- package/dist/core/proof/index.d.ts +14 -0
- package/dist/core/proof/index.d.ts.map +1 -0
- package/dist/core/proof/index.js +18 -0
- package/dist/core/proof/schema.d.ts +218 -0
- package/dist/core/proof/schema.d.ts.map +1 -0
- package/dist/core/proof/schema.js +263 -0
- package/dist/core/proof/signer.d.ts +112 -0
- package/dist/core/proof/signer.d.ts.map +1 -0
- package/dist/core/proof/signer.js +226 -0
- package/dist/core/proof/verifier.d.ts +98 -0
- package/dist/core/proof/verifier.d.ts.map +1 -0
- package/dist/core/proof/verifier.js +302 -0
- package/dist/core/runner/phase3-runner.d.ts +102 -0
- package/dist/core/runner/phase3-runner.d.ts.map +1 -0
- package/dist/core/runner/phase3-runner.js +471 -0
- package/dist/core/secrets/crypto.d.ts +76 -0
- package/dist/core/secrets/crypto.d.ts.map +1 -0
- package/dist/core/secrets/crypto.js +225 -0
- package/dist/core/secrets/manager.d.ts +77 -0
- package/dist/core/secrets/manager.d.ts.map +1 -0
- package/dist/core/secrets/manager.js +219 -0
- package/dist/core/security/redaction-patterns-extended.d.ts +28 -0
- package/dist/core/security/redaction-patterns-extended.d.ts.map +1 -0
- package/dist/core/security/redaction-patterns-extended.js +247 -0
- package/dist/core/security/redactor.d.ts +72 -0
- package/dist/core/security/redactor.d.ts.map +1 -0
- package/dist/core/security/redactor.js +279 -0
- package/dist/core/serve/diagnostics-collector.d.ts +33 -0
- package/dist/core/serve/diagnostics-collector.d.ts.map +1 -0
- package/dist/core/serve/diagnostics-collector.js +149 -0
- package/dist/core/serve/health-checker.d.ts +45 -0
- package/dist/core/serve/health-checker.d.ts.map +1 -0
- package/dist/core/serve/health-checker.js +219 -0
- package/dist/core/serve/index.d.ts +9 -0
- package/dist/core/serve/index.d.ts.map +1 -0
- package/dist/core/serve/index.js +8 -0
- package/dist/core/serve/metrics-collector.d.ts +25 -0
- package/dist/core/serve/metrics-collector.d.ts.map +1 -0
- package/dist/core/serve/metrics-collector.js +322 -0
- package/dist/core/serve/process-manager.d.ts +37 -0
- package/dist/core/serve/process-manager.d.ts.map +1 -0
- package/dist/core/serve/process-manager.js +213 -0
- package/dist/core/serve/server.d.ts +37 -0
- package/dist/core/serve/server.d.ts.map +1 -0
- package/dist/core/serve/server.js +191 -0
- package/dist/core/types/pack-v1.d.ts +162 -0
- package/dist/core/types/pack-v1.d.ts.map +1 -0
- package/dist/core/types/pack-v1.js +5 -0
- package/dist/core/types/trust-score.d.ts +70 -0
- package/dist/core/types/trust-score.d.ts.map +1 -0
- package/dist/core/types/trust-score.js +191 -0
- package/dist/core/vault/cas.d.ts +87 -0
- package/dist/core/vault/cas.d.ts.map +1 -0
- package/dist/core/vault/cas.js +255 -0
- package/dist/core/vault/index.d.ts +205 -0
- package/dist/core/vault/index.d.ts.map +1 -0
- package/dist/core/vault/index.js +631 -0
- package/package.json +12 -6
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Metrics Collector
|
|
3
|
+
* Génère des métriques au format OpenMetrics/Prometheus
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
export class MetricsCollector {
|
|
8
|
+
startTime;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.startTime = Date.now();
|
|
11
|
+
}
|
|
12
|
+
async collect() {
|
|
13
|
+
const metrics = await this.gatherMetrics();
|
|
14
|
+
return this.formatOpenMetrics(metrics);
|
|
15
|
+
}
|
|
16
|
+
async gatherMetrics() {
|
|
17
|
+
const metrics = [];
|
|
18
|
+
// System metrics
|
|
19
|
+
metrics.push({
|
|
20
|
+
name: 'qa360_uptime_seconds',
|
|
21
|
+
value: Math.floor((Date.now() - this.startTime) / 1000),
|
|
22
|
+
help: 'QA360 server uptime in seconds',
|
|
23
|
+
type: 'counter'
|
|
24
|
+
});
|
|
25
|
+
// Version info
|
|
26
|
+
const version = this.getVersion();
|
|
27
|
+
metrics.push({
|
|
28
|
+
name: 'qa360_info',
|
|
29
|
+
value: 1,
|
|
30
|
+
labels: { version },
|
|
31
|
+
help: 'QA360 version information',
|
|
32
|
+
type: 'gauge'
|
|
33
|
+
});
|
|
34
|
+
// Run metrics from history
|
|
35
|
+
const runMetrics = await this.collectRunMetrics();
|
|
36
|
+
metrics.push(...runMetrics);
|
|
37
|
+
// Gate metrics from last run
|
|
38
|
+
const gateMetrics = await this.collectGateMetrics();
|
|
39
|
+
metrics.push(...gateMetrics);
|
|
40
|
+
// Security metrics from last run
|
|
41
|
+
const securityMetrics = await this.collectSecurityMetrics();
|
|
42
|
+
metrics.push(...securityMetrics);
|
|
43
|
+
// Performance metrics from last run
|
|
44
|
+
const perfMetrics = await this.collectPerformanceMetrics();
|
|
45
|
+
metrics.push(...perfMetrics);
|
|
46
|
+
return metrics;
|
|
47
|
+
}
|
|
48
|
+
async collectRunMetrics() {
|
|
49
|
+
const metrics = [];
|
|
50
|
+
try {
|
|
51
|
+
const runsDir = join(process.cwd(), '.qa360', 'runs');
|
|
52
|
+
if (!existsSync(runsDir)) {
|
|
53
|
+
return [
|
|
54
|
+
{ name: 'qa360_runs_total', value: 0, help: 'Total number of QA360 runs', type: 'counter' },
|
|
55
|
+
{ name: 'qa360_last_run_trust_score', value: 0, help: 'Trust score of last run', type: 'gauge' }
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const runFiles = fs.readdirSync(runsDir).filter((f) => f.endsWith('.json'));
|
|
60
|
+
metrics.push({
|
|
61
|
+
name: 'qa360_runs_total',
|
|
62
|
+
value: runFiles.length,
|
|
63
|
+
help: 'Total number of QA360 runs',
|
|
64
|
+
type: 'counter'
|
|
65
|
+
});
|
|
66
|
+
// Get last run metrics
|
|
67
|
+
if (runFiles.length > 0) {
|
|
68
|
+
const lastRunFile = runFiles.sort().pop();
|
|
69
|
+
const lastRunPath = join(runsDir, lastRunFile);
|
|
70
|
+
const lastRun = JSON.parse(fs.readFileSync(lastRunPath, 'utf8'));
|
|
71
|
+
metrics.push({
|
|
72
|
+
name: 'qa360_last_run_trust_score',
|
|
73
|
+
value: lastRun.trustScore || lastRun.trust_score || 0,
|
|
74
|
+
help: 'Trust score of last run',
|
|
75
|
+
type: 'gauge'
|
|
76
|
+
});
|
|
77
|
+
metrics.push({
|
|
78
|
+
name: 'qa360_last_run_timestamp',
|
|
79
|
+
value: new Date(lastRun.timestamp || lastRun.ts || 0).getTime() / 1000,
|
|
80
|
+
help: 'Timestamp of last run',
|
|
81
|
+
type: 'gauge'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
metrics.push({
|
|
86
|
+
name: 'qa360_last_run_trust_score',
|
|
87
|
+
value: 0,
|
|
88
|
+
help: 'Trust score of last run',
|
|
89
|
+
type: 'gauge'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
metrics.push({
|
|
95
|
+
name: 'qa360_runs_total',
|
|
96
|
+
value: 0,
|
|
97
|
+
help: 'Total number of QA360 runs',
|
|
98
|
+
type: 'counter'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return metrics;
|
|
102
|
+
}
|
|
103
|
+
async collectGateMetrics() {
|
|
104
|
+
const metrics = [];
|
|
105
|
+
try {
|
|
106
|
+
const lastRun = await this.getLastRunData();
|
|
107
|
+
if (!lastRun)
|
|
108
|
+
return metrics;
|
|
109
|
+
// Gate duration metrics
|
|
110
|
+
const gates = ['api', 'ui', 'perf', 'a11y', 'sast', 'deps', 'secrets', 'dast'];
|
|
111
|
+
for (const gate of gates) {
|
|
112
|
+
const gateData = lastRun[gate] || lastRun.gates?.[gate];
|
|
113
|
+
if (gateData) {
|
|
114
|
+
const duration = gateData.duration_ms || gateData.duration || 0;
|
|
115
|
+
metrics.push({
|
|
116
|
+
name: 'qa360_gate_duration_ms',
|
|
117
|
+
value: duration,
|
|
118
|
+
labels: { gate },
|
|
119
|
+
help: 'Duration of gate execution in milliseconds',
|
|
120
|
+
type: 'gauge'
|
|
121
|
+
});
|
|
122
|
+
const failures = gateData.failures || (gateData.success === false ? 1 : 0);
|
|
123
|
+
metrics.push({
|
|
124
|
+
name: 'qa360_gate_failures_total',
|
|
125
|
+
value: failures,
|
|
126
|
+
labels: { gate },
|
|
127
|
+
help: 'Number of gate failures',
|
|
128
|
+
type: 'counter'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Return empty metrics on error
|
|
135
|
+
}
|
|
136
|
+
return metrics;
|
|
137
|
+
}
|
|
138
|
+
async collectSecurityMetrics() {
|
|
139
|
+
const metrics = [];
|
|
140
|
+
try {
|
|
141
|
+
const lastRun = await this.getLastRunData();
|
|
142
|
+
if (!lastRun?.security)
|
|
143
|
+
return metrics;
|
|
144
|
+
// SAST findings
|
|
145
|
+
if (lastRun.security.sast) {
|
|
146
|
+
const sastData = lastRun.security.sast;
|
|
147
|
+
const highFindings = sastData.max_high?.actual || 0;
|
|
148
|
+
const criticalFindings = sastData.max_critical?.actual || 0;
|
|
149
|
+
const mediumFindings = sastData.max_medium?.actual || 0;
|
|
150
|
+
metrics.push({
|
|
151
|
+
name: 'qa360_security_findings',
|
|
152
|
+
value: highFindings,
|
|
153
|
+
labels: { kind: 'sast_high' },
|
|
154
|
+
help: 'Number of security findings by type',
|
|
155
|
+
type: 'gauge'
|
|
156
|
+
});
|
|
157
|
+
metrics.push({
|
|
158
|
+
name: 'qa360_security_findings',
|
|
159
|
+
value: criticalFindings,
|
|
160
|
+
labels: { kind: 'sast_critical' },
|
|
161
|
+
help: 'Number of security findings by type',
|
|
162
|
+
type: 'gauge'
|
|
163
|
+
});
|
|
164
|
+
metrics.push({
|
|
165
|
+
name: 'qa360_security_findings',
|
|
166
|
+
value: mediumFindings,
|
|
167
|
+
labels: { kind: 'sast_medium' },
|
|
168
|
+
help: 'Number of security findings by type',
|
|
169
|
+
type: 'gauge'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// DAST findings
|
|
173
|
+
if (lastRun.security.dast) {
|
|
174
|
+
const dastData = lastRun.security.dast;
|
|
175
|
+
const dastHigh = dastData.max_high?.actual || 0;
|
|
176
|
+
metrics.push({
|
|
177
|
+
name: 'qa360_security_findings',
|
|
178
|
+
value: dastHigh,
|
|
179
|
+
labels: { kind: 'dast_high' },
|
|
180
|
+
help: 'Number of security findings by type',
|
|
181
|
+
type: 'gauge'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Dependencies vulnerabilities
|
|
185
|
+
if (lastRun.security.deps) {
|
|
186
|
+
const depsData = lastRun.security.deps;
|
|
187
|
+
const depsHigh = depsData.max_high?.actual || 0;
|
|
188
|
+
metrics.push({
|
|
189
|
+
name: 'qa360_security_findings',
|
|
190
|
+
value: depsHigh,
|
|
191
|
+
labels: { kind: 'deps_high' },
|
|
192
|
+
help: 'Number of security findings by type',
|
|
193
|
+
type: 'gauge'
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Secrets findings
|
|
197
|
+
if (lastRun.security.secrets) {
|
|
198
|
+
const secretsData = lastRun.security.secrets;
|
|
199
|
+
const secretsFindings = secretsData.max_findings?.actual || 0;
|
|
200
|
+
metrics.push({
|
|
201
|
+
name: 'qa360_security_findings',
|
|
202
|
+
value: secretsFindings,
|
|
203
|
+
labels: { kind: 'secrets' },
|
|
204
|
+
help: 'Number of security findings by type',
|
|
205
|
+
type: 'gauge'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Return empty metrics on error
|
|
211
|
+
}
|
|
212
|
+
return metrics;
|
|
213
|
+
}
|
|
214
|
+
async collectPerformanceMetrics() {
|
|
215
|
+
const metrics = [];
|
|
216
|
+
try {
|
|
217
|
+
const lastRun = await this.getLastRunData();
|
|
218
|
+
if (!lastRun?.perf && !lastRun?.performance)
|
|
219
|
+
return metrics;
|
|
220
|
+
const perfData = lastRun.perf || lastRun.performance;
|
|
221
|
+
// Performance percentiles
|
|
222
|
+
const p95 = perfData.p95_ms?.actual || perfData.p95 || 0;
|
|
223
|
+
const p90 = perfData.p90_ms?.actual || perfData.p90 || 0;
|
|
224
|
+
const p50 = perfData.p50_ms?.actual || perfData.p50 || 0;
|
|
225
|
+
metrics.push({
|
|
226
|
+
name: 'qa360_perf_p95_ms',
|
|
227
|
+
value: p95,
|
|
228
|
+
help: 'Performance P95 latency in milliseconds',
|
|
229
|
+
type: 'gauge'
|
|
230
|
+
});
|
|
231
|
+
metrics.push({
|
|
232
|
+
name: 'qa360_perf_p90_ms',
|
|
233
|
+
value: p90,
|
|
234
|
+
help: 'Performance P90 latency in milliseconds',
|
|
235
|
+
type: 'gauge'
|
|
236
|
+
});
|
|
237
|
+
metrics.push({
|
|
238
|
+
name: 'qa360_perf_p50_ms',
|
|
239
|
+
value: p50,
|
|
240
|
+
help: 'Performance P50 latency in milliseconds',
|
|
241
|
+
type: 'gauge'
|
|
242
|
+
});
|
|
243
|
+
// Error rate
|
|
244
|
+
const errorRate = perfData.max_errors_rate?.actual || perfData.error_rate || 0;
|
|
245
|
+
metrics.push({
|
|
246
|
+
name: 'qa360_perf_error_rate',
|
|
247
|
+
value: errorRate,
|
|
248
|
+
help: 'Performance error rate',
|
|
249
|
+
type: 'gauge'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Return empty metrics on error
|
|
254
|
+
}
|
|
255
|
+
return metrics;
|
|
256
|
+
}
|
|
257
|
+
async getLastRunData() {
|
|
258
|
+
try {
|
|
259
|
+
const runsDir = join(process.cwd(), '.qa360', 'runs');
|
|
260
|
+
if (!existsSync(runsDir))
|
|
261
|
+
return null;
|
|
262
|
+
const fs = require('fs');
|
|
263
|
+
const runFiles = fs.readdirSync(runsDir)
|
|
264
|
+
.filter((f) => f.endsWith('.json'))
|
|
265
|
+
.sort();
|
|
266
|
+
if (runFiles.length === 0)
|
|
267
|
+
return null;
|
|
268
|
+
const lastRunFile = runFiles.pop();
|
|
269
|
+
const lastRunPath = join(runsDir, lastRunFile);
|
|
270
|
+
return JSON.parse(fs.readFileSync(lastRunPath, 'utf8'));
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
formatOpenMetrics(metrics) {
|
|
277
|
+
const lines = [];
|
|
278
|
+
// Group metrics by name for proper formatting
|
|
279
|
+
const metricGroups = new Map();
|
|
280
|
+
for (const metric of metrics) {
|
|
281
|
+
if (!metricGroups.has(metric.name)) {
|
|
282
|
+
metricGroups.set(metric.name, []);
|
|
283
|
+
}
|
|
284
|
+
metricGroups.get(metric.name).push(metric);
|
|
285
|
+
}
|
|
286
|
+
// Format each metric group
|
|
287
|
+
for (const [name, metricList] of metricGroups) {
|
|
288
|
+
const firstMetric = metricList[0];
|
|
289
|
+
// Add help comment
|
|
290
|
+
if (firstMetric.help) {
|
|
291
|
+
lines.push(`# HELP ${name} ${firstMetric.help}`);
|
|
292
|
+
}
|
|
293
|
+
// Add type comment
|
|
294
|
+
if (firstMetric.type) {
|
|
295
|
+
lines.push(`# TYPE ${name} ${firstMetric.type}`);
|
|
296
|
+
}
|
|
297
|
+
// Add metric lines
|
|
298
|
+
for (const metric of metricList) {
|
|
299
|
+
let line = name;
|
|
300
|
+
if (metric.labels && Object.keys(metric.labels).length > 0) {
|
|
301
|
+
const labelPairs = Object.entries(metric.labels)
|
|
302
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
303
|
+
.join(',');
|
|
304
|
+
line += `{${labelPairs}}`;
|
|
305
|
+
}
|
|
306
|
+
line += ` ${metric.value}`;
|
|
307
|
+
lines.push(line);
|
|
308
|
+
}
|
|
309
|
+
lines.push(''); // Empty line between metric groups
|
|
310
|
+
}
|
|
311
|
+
return lines.join('\n');
|
|
312
|
+
}
|
|
313
|
+
getVersion() {
|
|
314
|
+
try {
|
|
315
|
+
const packageInfo = require('../../package.json');
|
|
316
|
+
return packageInfo.version || '0.9.0-core';
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return '0.9.0-core';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Process Manager
|
|
3
|
+
* Gestion graceful shutdown et cancel safe des processus
|
|
4
|
+
*/
|
|
5
|
+
import { ChildProcess } from 'child_process';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
export interface RunProcess {
|
|
8
|
+
runId: string;
|
|
9
|
+
pid: number;
|
|
10
|
+
startTime: number;
|
|
11
|
+
command: string;
|
|
12
|
+
status: 'running' | 'cancelled' | 'completed' | 'failed';
|
|
13
|
+
children: ChildProcess[];
|
|
14
|
+
}
|
|
15
|
+
export interface CancelResult {
|
|
16
|
+
cancelled: boolean;
|
|
17
|
+
runId: string;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class ProcessManager extends EventEmitter {
|
|
21
|
+
private activeRuns;
|
|
22
|
+
private gracefulTimeout;
|
|
23
|
+
constructor();
|
|
24
|
+
registerRun(runId: string, command: string, mainProcess?: ChildProcess): void;
|
|
25
|
+
addChildProcess(runId: string, child: ChildProcess): void;
|
|
26
|
+
cancelRun(runId: string): Promise<CancelResult>;
|
|
27
|
+
cancelAllRuns(): Promise<CancelResult[]>;
|
|
28
|
+
getActiveRuns(): RunProcess[];
|
|
29
|
+
getRunStatus(runId: string): RunProcess | null;
|
|
30
|
+
private gracefulShutdown;
|
|
31
|
+
private terminateProcess;
|
|
32
|
+
private cleanupDockerContainers;
|
|
33
|
+
private completeRun;
|
|
34
|
+
private setupSignalHandlers;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=process-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../../../src/core/serve/process-manager.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAS,YAAY,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAC;IACzD,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,UAAU,CAAiC;IACnD,OAAO,CAAC,eAAe,CAAS;;IAWhC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,YAAY,GAAG,IAAI;IAqB7E,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;IAOnD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAsC/C,aAAa,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAW9C,aAAa,IAAI,UAAU,EAAE;IAI7B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;YAIhC,gBAAgB;YAiBhB,gBAAgB;YA2ChB,uBAAuB;IAsCrC,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,mBAAmB;IA+B3B,OAAO;CAMR"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Process Manager
|
|
3
|
+
* Gestion graceful shutdown et cancel safe des processus
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
export class ProcessManager extends EventEmitter {
|
|
8
|
+
activeRuns = new Map();
|
|
9
|
+
gracefulTimeout = 10000; // 10 seconds
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
// Augmenter la limite des listeners pour les tests
|
|
13
|
+
if (process.env.NODE_ENV === 'test') {
|
|
14
|
+
process.setMaxListeners(50);
|
|
15
|
+
}
|
|
16
|
+
this.setupSignalHandlers();
|
|
17
|
+
}
|
|
18
|
+
registerRun(runId, command, mainProcess) {
|
|
19
|
+
const runProcess = {
|
|
20
|
+
runId,
|
|
21
|
+
pid: mainProcess?.pid || process.pid,
|
|
22
|
+
startTime: Date.now(),
|
|
23
|
+
command,
|
|
24
|
+
status: 'running',
|
|
25
|
+
children: mainProcess ? [mainProcess] : []
|
|
26
|
+
};
|
|
27
|
+
this.activeRuns.set(runId, runProcess);
|
|
28
|
+
this.emit('run-registered', runProcess);
|
|
29
|
+
// Auto-cleanup on process exit
|
|
30
|
+
if (mainProcess) {
|
|
31
|
+
mainProcess.on('exit', () => {
|
|
32
|
+
this.completeRun(runId);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
addChildProcess(runId, child) {
|
|
37
|
+
const run = this.activeRuns.get(runId);
|
|
38
|
+
if (run) {
|
|
39
|
+
run.children.push(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async cancelRun(runId) {
|
|
43
|
+
const run = this.activeRuns.get(runId);
|
|
44
|
+
if (!run) {
|
|
45
|
+
return {
|
|
46
|
+
cancelled: false,
|
|
47
|
+
runId,
|
|
48
|
+
reason: 'Run not found'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (run.status !== 'running') {
|
|
52
|
+
return {
|
|
53
|
+
cancelled: false,
|
|
54
|
+
runId,
|
|
55
|
+
reason: `Run already ${run.status}`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await this.gracefulShutdown(run);
|
|
60
|
+
run.status = 'cancelled';
|
|
61
|
+
this.emit('run-cancelled', run);
|
|
62
|
+
return {
|
|
63
|
+
cancelled: true,
|
|
64
|
+
runId
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
69
|
+
return {
|
|
70
|
+
cancelled: false,
|
|
71
|
+
runId,
|
|
72
|
+
reason: `Failed to cancel: ${errorMessage}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async cancelAllRuns() {
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const runId of this.activeRuns.keys()) {
|
|
79
|
+
const result = await this.cancelRun(runId);
|
|
80
|
+
results.push(result);
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
getActiveRuns() {
|
|
85
|
+
return Array.from(this.activeRuns.values());
|
|
86
|
+
}
|
|
87
|
+
getRunStatus(runId) {
|
|
88
|
+
return this.activeRuns.get(runId) || null;
|
|
89
|
+
}
|
|
90
|
+
async gracefulShutdown(run) {
|
|
91
|
+
const promises = [];
|
|
92
|
+
// Send SIGTERM to all child processes
|
|
93
|
+
for (const child of run.children) {
|
|
94
|
+
if (child.pid && !child.killed) {
|
|
95
|
+
promises.push(this.terminateProcess(child));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Wait for all processes to terminate
|
|
99
|
+
await Promise.all(promises);
|
|
100
|
+
// Clean up Docker containers if any
|
|
101
|
+
await this.cleanupDockerContainers(run);
|
|
102
|
+
}
|
|
103
|
+
async terminateProcess(process) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
if (!process.pid || process.killed) {
|
|
106
|
+
resolve();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let terminated = false;
|
|
110
|
+
// Set up exit handler
|
|
111
|
+
const onExit = () => {
|
|
112
|
+
if (!terminated) {
|
|
113
|
+
terminated = true;
|
|
114
|
+
resolve();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
process.on('exit', onExit);
|
|
118
|
+
process.on('error', onExit);
|
|
119
|
+
// Send SIGTERM
|
|
120
|
+
try {
|
|
121
|
+
process.kill('SIGTERM');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Process might already be dead
|
|
125
|
+
onExit();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Force kill after timeout
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
if (!terminated && process.pid && !process.killed) {
|
|
131
|
+
try {
|
|
132
|
+
process.kill('SIGKILL');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Process might already be dead
|
|
136
|
+
}
|
|
137
|
+
onExit();
|
|
138
|
+
}
|
|
139
|
+
}, this.gracefulTimeout);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async cleanupDockerContainers(run) {
|
|
143
|
+
try {
|
|
144
|
+
// Check if docker-compose was used
|
|
145
|
+
if (run.command.includes('docker-compose') || run.command.includes('docker compose')) {
|
|
146
|
+
const composeDown = spawn('docker', ['compose', 'down'], {
|
|
147
|
+
stdio: 'pipe',
|
|
148
|
+
timeout: 30000
|
|
149
|
+
});
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
composeDown.on('exit', () => resolve());
|
|
152
|
+
composeDown.on('error', () => resolve()); // Continue even if cleanup fails
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
composeDown.kill();
|
|
155
|
+
resolve();
|
|
156
|
+
}, 30000);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Clean up any QA360-related containers
|
|
160
|
+
const cleanup = spawn('docker', [
|
|
161
|
+
'ps', '-q', '--filter', 'label=qa360.run-id=' + run.runId
|
|
162
|
+
], { stdio: 'pipe' });
|
|
163
|
+
cleanup.stdout.on('data', (data) => {
|
|
164
|
+
const containerIds = data.toString().trim().split('\n').filter(Boolean);
|
|
165
|
+
for (const containerId of containerIds) {
|
|
166
|
+
spawn('docker', ['stop', containerId], { stdio: 'ignore' });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Docker cleanup is best effort
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
completeRun(runId) {
|
|
175
|
+
const run = this.activeRuns.get(runId);
|
|
176
|
+
if (run && run.status === 'running') {
|
|
177
|
+
run.status = 'completed';
|
|
178
|
+
this.emit('run-completed', run);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
setupSignalHandlers() {
|
|
182
|
+
const handleShutdown = async (signal) => {
|
|
183
|
+
console.log(`\nReceived ${signal}. Gracefully shutting down...`);
|
|
184
|
+
try {
|
|
185
|
+
await this.cancelAllRuns();
|
|
186
|
+
console.log('All runs cancelled successfully');
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error('Error during shutdown:', error);
|
|
190
|
+
}
|
|
191
|
+
process.exit(0);
|
|
192
|
+
};
|
|
193
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
194
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
195
|
+
// Handle uncaught exceptions
|
|
196
|
+
process.on('uncaughtException', async (error) => {
|
|
197
|
+
console.error('Uncaught exception:', error);
|
|
198
|
+
await this.cancelAllRuns();
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
201
|
+
process.on('unhandledRejection', async (reason) => {
|
|
202
|
+
console.error('Unhandled rejection:', reason);
|
|
203
|
+
await this.cancelAllRuns();
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
dispose() {
|
|
208
|
+
process.removeAllListeners('SIGINT');
|
|
209
|
+
process.removeAllListeners('SIGTERM');
|
|
210
|
+
process.removeAllListeners('uncaughtException');
|
|
211
|
+
process.removeAllListeners('unhandledRejection');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Core Observability Server
|
|
3
|
+
* Endpoints: /health, /diag, /metrics, /cancel
|
|
4
|
+
*/
|
|
5
|
+
export interface ServeConfig {
|
|
6
|
+
port: number;
|
|
7
|
+
host?: string;
|
|
8
|
+
metrics?: boolean;
|
|
9
|
+
readinessCheck?: boolean;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ServeResponse {
|
|
13
|
+
status: number;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
body: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class QA360Server {
|
|
18
|
+
private config;
|
|
19
|
+
private server?;
|
|
20
|
+
private healthChecker;
|
|
21
|
+
private diagnosticsCollector;
|
|
22
|
+
private metricsCollector;
|
|
23
|
+
private processManager;
|
|
24
|
+
private redactor;
|
|
25
|
+
private startTime;
|
|
26
|
+
constructor(config: ServeConfig);
|
|
27
|
+
start(): Promise<void>;
|
|
28
|
+
stop(): Promise<void>;
|
|
29
|
+
private handleRequest;
|
|
30
|
+
private routeRequest;
|
|
31
|
+
private handleHealth;
|
|
32
|
+
private handleDiagnostics;
|
|
33
|
+
private handleMetrics;
|
|
34
|
+
private handleCancel;
|
|
35
|
+
private log;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/core/serve/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,WAAW;IASV,OAAO,CAAC,MAAM;IAR1B,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,SAAS,CAAS;gBAEN,MAAM,EAAE,WAAW;IASjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAab,aAAa;YAmCb,YAAY;YAkCZ,YAAY;YAcZ,iBAAiB;YAUjB,aAAa;YAkBb,YAAY;IAsB1B,OAAO,CAAC,GAAG;CAaZ"}
|