lsh-framework 0.5.4
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/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job Registry - Comprehensive job execution history and analytics
|
|
3
|
+
* Tracks all job runs with detailed pass/failure history, output logs, and performance metrics
|
|
4
|
+
*
|
|
5
|
+
* REFACTORED: Now extends BaseJobManager for unified interface
|
|
6
|
+
* Note: This is a read-only tracker - startJob/stopJob only record events
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { BaseJobManager } from '../lib/base-job-manager.js';
|
|
13
|
+
import MemoryJobStorage from '../lib/job-storage-memory.js';
|
|
14
|
+
export class JobRegistry extends BaseJobManager {
|
|
15
|
+
config;
|
|
16
|
+
records = new Map(); // jobId -> execution records
|
|
17
|
+
index = new Map(); // tag -> jobIds
|
|
18
|
+
statistics = new Map(); // jobId -> stats
|
|
19
|
+
constructor(config) {
|
|
20
|
+
super(new MemoryJobStorage(), 'JobRegistry');
|
|
21
|
+
this.config = {
|
|
22
|
+
registryFile: '/tmp/lsh-job-registry.json',
|
|
23
|
+
maxRecordsPerJob: 1000,
|
|
24
|
+
maxTotalRecords: 50000,
|
|
25
|
+
compressionEnabled: true,
|
|
26
|
+
metricsRetentionDays: 90,
|
|
27
|
+
outputLogDir: '/tmp/lsh-job-logs',
|
|
28
|
+
indexingEnabled: true,
|
|
29
|
+
...config
|
|
30
|
+
};
|
|
31
|
+
this.ensureLogDirectory();
|
|
32
|
+
this.loadRegistry();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Record the start of a job execution
|
|
36
|
+
*/
|
|
37
|
+
recordJobStart(job, executionId) {
|
|
38
|
+
const record = {
|
|
39
|
+
executionId: executionId || this.generateExecutionId(),
|
|
40
|
+
jobId: job.id,
|
|
41
|
+
jobName: job.name,
|
|
42
|
+
command: job.command,
|
|
43
|
+
startTime: new Date(),
|
|
44
|
+
status: 'running',
|
|
45
|
+
stdout: '',
|
|
46
|
+
stderr: '',
|
|
47
|
+
outputSize: 0,
|
|
48
|
+
environment: { ...(job.env || {}) },
|
|
49
|
+
workingDirectory: job.cwd || process.cwd(),
|
|
50
|
+
user: job.user || process.env.USER || 'unknown',
|
|
51
|
+
hostname: os.hostname(),
|
|
52
|
+
tags: [...(job.tags || [])],
|
|
53
|
+
priority: job.priority || 5,
|
|
54
|
+
scheduled: job.type === 'scheduled',
|
|
55
|
+
retryCount: 0,
|
|
56
|
+
pid: job.pid,
|
|
57
|
+
ppid: job.ppid
|
|
58
|
+
};
|
|
59
|
+
// Store output logs in separate files for large outputs
|
|
60
|
+
if (this.config.outputLogDir) {
|
|
61
|
+
record.logFile = path.join(this.config.outputLogDir, `${record.executionId}.log`);
|
|
62
|
+
}
|
|
63
|
+
this.addRecord(record);
|
|
64
|
+
this.emit('executionStarted', record);
|
|
65
|
+
return record;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Record job output (stdout/stderr)
|
|
69
|
+
*/
|
|
70
|
+
recordJobOutput(executionId, type, data) {
|
|
71
|
+
const record = this.findRecordByExecutionId(executionId);
|
|
72
|
+
if (!record)
|
|
73
|
+
return;
|
|
74
|
+
if (type === 'stdout') {
|
|
75
|
+
record.stdout += data;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
record.stderr += data;
|
|
79
|
+
}
|
|
80
|
+
record.outputSize += data.length;
|
|
81
|
+
// Write to log file if configured
|
|
82
|
+
if (record.logFile) {
|
|
83
|
+
const logEntry = `[${new Date().toISOString()}] ${type.toUpperCase()}: ${data}`;
|
|
84
|
+
fs.appendFileSync(record.logFile, logEntry);
|
|
85
|
+
}
|
|
86
|
+
this.emit('outputRecorded', executionId, type, data);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Record job completion
|
|
90
|
+
*/
|
|
91
|
+
recordJobCompletion(executionId, status, exitCode, signal, error) {
|
|
92
|
+
const record = this.findRecordByExecutionId(executionId);
|
|
93
|
+
if (!record)
|
|
94
|
+
return;
|
|
95
|
+
record.endTime = new Date();
|
|
96
|
+
record.duration = record.endTime.getTime() - record.startTime.getTime();
|
|
97
|
+
record.status = status;
|
|
98
|
+
record.exitCode = exitCode;
|
|
99
|
+
record.signal = signal;
|
|
100
|
+
if (error) {
|
|
101
|
+
record.errorType = error.constructor.name;
|
|
102
|
+
record.errorMessage = error.message;
|
|
103
|
+
record.stackTrace = error.stack;
|
|
104
|
+
}
|
|
105
|
+
// Record resource usage
|
|
106
|
+
this.recordResourceUsage(record);
|
|
107
|
+
this.updateJobStatistics(record);
|
|
108
|
+
this.saveRegistry();
|
|
109
|
+
this.emit('executionCompleted', record);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get execution history for a job - overrides BaseJobManager
|
|
113
|
+
* Returns JobExecutionRecord[] which is compatible with BaseJobExecution[]
|
|
114
|
+
*/
|
|
115
|
+
async getJobHistory(jobId, limit = 50) {
|
|
116
|
+
const records = this.records.get(jobId) || [];
|
|
117
|
+
return limit ? records.slice(0, limit) : records;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get job statistics - overrides BaseJobManager
|
|
121
|
+
* Returns JobStatistics which is compatible with BaseJobStatistics
|
|
122
|
+
*/
|
|
123
|
+
async getJobStatistics(jobId) {
|
|
124
|
+
const stats = this.statistics.get(jobId);
|
|
125
|
+
if (!stats) {
|
|
126
|
+
throw new Error(`No statistics found for job ${jobId}`);
|
|
127
|
+
}
|
|
128
|
+
return stats;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get all job statistics
|
|
132
|
+
*/
|
|
133
|
+
getAllStatistics() {
|
|
134
|
+
return Array.from(this.statistics.values());
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Search job executions
|
|
138
|
+
*/
|
|
139
|
+
searchExecutions(criteria) {
|
|
140
|
+
let results = [];
|
|
141
|
+
// Collect all records
|
|
142
|
+
for (const records of this.records.values()) {
|
|
143
|
+
results.push(...records);
|
|
144
|
+
}
|
|
145
|
+
// Apply filters
|
|
146
|
+
if (criteria.jobId) {
|
|
147
|
+
results = results.filter(r => r.jobId === criteria.jobId);
|
|
148
|
+
}
|
|
149
|
+
if (criteria.status) {
|
|
150
|
+
results = results.filter(r => criteria.status.includes(r.status));
|
|
151
|
+
}
|
|
152
|
+
if (criteria.startTime) {
|
|
153
|
+
if (criteria.startTime.from) {
|
|
154
|
+
results = results.filter(r => r.startTime >= criteria.startTime.from);
|
|
155
|
+
}
|
|
156
|
+
if (criteria.startTime.to) {
|
|
157
|
+
results = results.filter(r => r.startTime <= criteria.startTime.to);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (criteria.duration) {
|
|
161
|
+
if (criteria.duration.min && criteria.duration.max) {
|
|
162
|
+
results = results.filter(r => r.duration &&
|
|
163
|
+
r.duration >= criteria.duration.min &&
|
|
164
|
+
r.duration <= criteria.duration.max);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (criteria.tags && criteria.tags.length > 0) {
|
|
168
|
+
results = results.filter(r => criteria.tags.some(tag => r.tags.includes(tag)));
|
|
169
|
+
}
|
|
170
|
+
if (criteria.user) {
|
|
171
|
+
results = results.filter(r => r.user === criteria.user);
|
|
172
|
+
}
|
|
173
|
+
if (criteria.command) {
|
|
174
|
+
results = results.filter(r => criteria.command.test(r.command));
|
|
175
|
+
}
|
|
176
|
+
if (criteria.exitCode) {
|
|
177
|
+
results = results.filter(r => r.exitCode !== undefined &&
|
|
178
|
+
criteria.exitCode.includes(r.exitCode));
|
|
179
|
+
}
|
|
180
|
+
// Sort by start time (newest first)
|
|
181
|
+
results.sort((a, b) => b.startTime.getTime() - a.startTime.getTime());
|
|
182
|
+
// Apply limit
|
|
183
|
+
if (criteria.limit) {
|
|
184
|
+
results = results.slice(0, criteria.limit);
|
|
185
|
+
}
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generate execution report
|
|
190
|
+
*/
|
|
191
|
+
async generateReport(options = {}) {
|
|
192
|
+
const { jobId, timeRange, format = 'text' } = options;
|
|
193
|
+
let records = [];
|
|
194
|
+
if (jobId) {
|
|
195
|
+
records = await this.getJobHistory(jobId);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
for (const jobRecords of this.records.values()) {
|
|
199
|
+
records.push(...jobRecords);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (timeRange) {
|
|
203
|
+
records = records.filter(r => r.startTime >= timeRange.from && r.startTime <= timeRange.to);
|
|
204
|
+
}
|
|
205
|
+
switch (format) {
|
|
206
|
+
case 'json':
|
|
207
|
+
return JSON.stringify(records, null, 2);
|
|
208
|
+
case 'csv':
|
|
209
|
+
return this.generateCSVReport(records);
|
|
210
|
+
case 'text':
|
|
211
|
+
default:
|
|
212
|
+
return this.generateTextReport(records);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Clean old records - overrides BaseJobManager
|
|
217
|
+
*/
|
|
218
|
+
async cleanup() {
|
|
219
|
+
const cutoffDate = new Date();
|
|
220
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.config.metricsRetentionDays);
|
|
221
|
+
let removedCount = 0;
|
|
222
|
+
for (const [jobId, records] of this.records) {
|
|
223
|
+
const filteredRecords = records.filter(r => r.startTime >= cutoffDate);
|
|
224
|
+
const removed = records.length - filteredRecords.length;
|
|
225
|
+
if (removed > 0) {
|
|
226
|
+
this.records.set(jobId, filteredRecords);
|
|
227
|
+
removedCount += removed;
|
|
228
|
+
// Clean up log files for removed records
|
|
229
|
+
records
|
|
230
|
+
.filter(r => r.startTime < cutoffDate)
|
|
231
|
+
.forEach(r => {
|
|
232
|
+
if (r.logFile && fs.existsSync(r.logFile)) {
|
|
233
|
+
fs.unlinkSync(r.logFile);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Limit records per job
|
|
238
|
+
if (filteredRecords.length > this.config.maxRecordsPerJob) {
|
|
239
|
+
const limited = filteredRecords.slice(0, this.config.maxRecordsPerJob);
|
|
240
|
+
this.records.set(jobId, limited);
|
|
241
|
+
removedCount += filteredRecords.length - limited.length;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
this.saveRegistry();
|
|
245
|
+
this.logger.info(`Cleaned up ${removedCount} old records`);
|
|
246
|
+
// Call base cleanup
|
|
247
|
+
await super.cleanup();
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Export registry data
|
|
251
|
+
*/
|
|
252
|
+
export(filePath, format = 'json') {
|
|
253
|
+
const allRecords = Array.from(this.records.values()).flat();
|
|
254
|
+
let content;
|
|
255
|
+
if (format === 'csv') {
|
|
256
|
+
content = this.generateCSVReport(allRecords);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
content = JSON.stringify({
|
|
260
|
+
records: allRecords,
|
|
261
|
+
statistics: Array.from(this.statistics.values()),
|
|
262
|
+
exportedAt: new Date().toISOString()
|
|
263
|
+
}, null, 2);
|
|
264
|
+
}
|
|
265
|
+
fs.writeFileSync(filePath, content);
|
|
266
|
+
}
|
|
267
|
+
addRecord(record) {
|
|
268
|
+
const jobRecords = this.records.get(record.jobId) || [];
|
|
269
|
+
jobRecords.unshift(record); // Add to beginning (newest first)
|
|
270
|
+
// Limit records per job
|
|
271
|
+
if (jobRecords.length > this.config.maxRecordsPerJob) {
|
|
272
|
+
const removed = jobRecords.splice(this.config.maxRecordsPerJob);
|
|
273
|
+
// Clean up log files for removed records
|
|
274
|
+
removed.forEach(r => {
|
|
275
|
+
if (r.logFile && fs.existsSync(r.logFile)) {
|
|
276
|
+
fs.unlinkSync(r.logFile);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
this.records.set(record.jobId, jobRecords);
|
|
281
|
+
// Update index
|
|
282
|
+
if (this.config.indexingEnabled) {
|
|
283
|
+
record.tags.forEach(tag => {
|
|
284
|
+
const jobIds = this.index.get(tag) || new Set();
|
|
285
|
+
jobIds.add(record.jobId);
|
|
286
|
+
this.index.set(tag, jobIds);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
findRecordByExecutionId(executionId) {
|
|
291
|
+
for (const records of this.records.values()) {
|
|
292
|
+
const record = records.find(r => r.executionId === executionId);
|
|
293
|
+
if (record)
|
|
294
|
+
return record;
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
updateJobStatistics(record) {
|
|
299
|
+
const stats = this.statistics.get(record.jobId) || this.createInitialStatistics(record);
|
|
300
|
+
stats.totalExecutions++;
|
|
301
|
+
switch (record.status) {
|
|
302
|
+
case 'completed':
|
|
303
|
+
stats.successfulExecutions++;
|
|
304
|
+
stats.lastSuccess = record.endTime;
|
|
305
|
+
break;
|
|
306
|
+
case 'failed':
|
|
307
|
+
stats.failedExecutions++;
|
|
308
|
+
stats.lastFailure = record.endTime;
|
|
309
|
+
this.updateFailureAnalysis(stats, record);
|
|
310
|
+
break;
|
|
311
|
+
case 'killed':
|
|
312
|
+
stats.killedExecutions++;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
stats.successRate = (stats.successfulExecutions / stats.totalExecutions) * 100;
|
|
316
|
+
stats.lastExecution = record.endTime;
|
|
317
|
+
// Update timing statistics
|
|
318
|
+
if (record.duration) {
|
|
319
|
+
stats.totalRuntime += record.duration;
|
|
320
|
+
stats.averageDuration = stats.totalRuntime / stats.totalExecutions;
|
|
321
|
+
if (stats.minDuration === 0 || record.duration < stats.minDuration) {
|
|
322
|
+
stats.minDuration = record.duration;
|
|
323
|
+
}
|
|
324
|
+
if (record.duration > stats.maxDuration) {
|
|
325
|
+
stats.maxDuration = record.duration;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Update resource statistics
|
|
329
|
+
if (record.maxMemory) {
|
|
330
|
+
stats.averageMemory = (stats.averageMemory * (stats.totalExecutions - 1) + record.maxMemory) / stats.totalExecutions;
|
|
331
|
+
if (record.maxMemory > stats.maxMemoryUsed) {
|
|
332
|
+
stats.maxMemoryUsed = record.maxMemory;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (record.avgCpu) {
|
|
336
|
+
stats.averageCpuUsage = (stats.averageCpuUsage * (stats.totalExecutions - 1) + record.avgCpu) / stats.totalExecutions;
|
|
337
|
+
}
|
|
338
|
+
// Determine trend
|
|
339
|
+
stats.recentTrend = this.calculateTrend(record.jobId);
|
|
340
|
+
this.statistics.set(record.jobId, stats);
|
|
341
|
+
}
|
|
342
|
+
createInitialStatistics(record) {
|
|
343
|
+
return {
|
|
344
|
+
jobId: record.jobId,
|
|
345
|
+
jobName: record.jobName,
|
|
346
|
+
totalExecutions: 0,
|
|
347
|
+
successfulExecutions: 0,
|
|
348
|
+
failedExecutions: 0,
|
|
349
|
+
killedExecutions: 0,
|
|
350
|
+
successRate: 0,
|
|
351
|
+
averageDuration: 0,
|
|
352
|
+
minDuration: 0,
|
|
353
|
+
maxDuration: 0,
|
|
354
|
+
totalRuntime: 0,
|
|
355
|
+
averageMemory: 0,
|
|
356
|
+
maxMemoryUsed: 0,
|
|
357
|
+
averageCpuUsage: 0,
|
|
358
|
+
recentTrend: 'stable',
|
|
359
|
+
commonFailures: []
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
updateFailureAnalysis(stats, record) {
|
|
363
|
+
if (!record.errorMessage)
|
|
364
|
+
return;
|
|
365
|
+
const existing = stats.commonFailures.find(f => f.error === record.errorMessage);
|
|
366
|
+
if (existing) {
|
|
367
|
+
existing.count++;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
stats.commonFailures.push({
|
|
371
|
+
error: record.errorMessage,
|
|
372
|
+
count: 1,
|
|
373
|
+
percentage: 0
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
// Update percentages
|
|
377
|
+
const totalFailures = stats.commonFailures.reduce((sum, f) => sum + f.count, 0);
|
|
378
|
+
stats.commonFailures.forEach(f => {
|
|
379
|
+
f.percentage = (f.count / totalFailures) * 100;
|
|
380
|
+
});
|
|
381
|
+
// Sort by frequency
|
|
382
|
+
stats.commonFailures.sort((a, b) => b.count - a.count);
|
|
383
|
+
// Keep only top 10 failure types
|
|
384
|
+
stats.commonFailures = stats.commonFailures.slice(0, 10);
|
|
385
|
+
}
|
|
386
|
+
calculateTrend(jobId) {
|
|
387
|
+
const records = this.records.get(jobId) || [];
|
|
388
|
+
if (records.length < 5)
|
|
389
|
+
return 'stable';
|
|
390
|
+
const recent = records.slice(0, 5);
|
|
391
|
+
const successCount = recent.filter(r => r.status === 'completed').length;
|
|
392
|
+
const _failureCount = recent.filter(r => r.status === 'failed').length;
|
|
393
|
+
const recentSuccessRate = successCount / recent.length;
|
|
394
|
+
const overallStats = this.statistics.get(jobId);
|
|
395
|
+
if (!overallStats)
|
|
396
|
+
return 'stable';
|
|
397
|
+
const overallSuccessRate = overallStats.successRate / 100;
|
|
398
|
+
if (recentSuccessRate > overallSuccessRate + 0.1) {
|
|
399
|
+
return 'improving';
|
|
400
|
+
}
|
|
401
|
+
else if (recentSuccessRate < overallSuccessRate - 0.1) {
|
|
402
|
+
return 'degrading';
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
return 'stable';
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
recordResourceUsage(record) {
|
|
409
|
+
if (!record.pid)
|
|
410
|
+
return;
|
|
411
|
+
try {
|
|
412
|
+
exec(`ps -p ${record.pid} -o %mem,%cpu`, (error, stdout) => {
|
|
413
|
+
if (!error && stdout) {
|
|
414
|
+
const lines = stdout.trim().split('\n');
|
|
415
|
+
if (lines.length > 1) {
|
|
416
|
+
const [mem, cpu] = lines[1].trim().split(/\s+/).map(parseFloat);
|
|
417
|
+
record.maxMemory = mem;
|
|
418
|
+
record.avgCpu = cpu;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (_error) {
|
|
424
|
+
// Ignore resource monitoring errors
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
generateExecutionId() {
|
|
428
|
+
return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
429
|
+
}
|
|
430
|
+
generateTextReport(records) {
|
|
431
|
+
let report = `Job Execution Report\n`;
|
|
432
|
+
report += `Generated: ${new Date().toISOString()}\n`;
|
|
433
|
+
report += `Total Executions: ${records.length}\n\n`;
|
|
434
|
+
const byStatus = records.reduce((acc, r) => {
|
|
435
|
+
acc[r.status] = (acc[r.status] || 0) + 1;
|
|
436
|
+
return acc;
|
|
437
|
+
}, {});
|
|
438
|
+
report += `Status Summary:\n`;
|
|
439
|
+
Object.entries(byStatus).forEach(([status, count]) => {
|
|
440
|
+
report += ` ${status}: ${count}\n`;
|
|
441
|
+
});
|
|
442
|
+
report += `\nRecent Executions:\n`;
|
|
443
|
+
records.slice(0, 20).forEach(r => {
|
|
444
|
+
report += `${r.startTime.toISOString()} | ${r.jobName} | ${r.status} | ${r.duration || 0}ms\n`;
|
|
445
|
+
});
|
|
446
|
+
return report;
|
|
447
|
+
}
|
|
448
|
+
generateCSVReport(records) {
|
|
449
|
+
const headers = [
|
|
450
|
+
'executionId', 'jobId', 'jobName', 'command', 'startTime', 'endTime',
|
|
451
|
+
'duration', 'status', 'exitCode', 'user', 'hostname', 'outputSize'
|
|
452
|
+
];
|
|
453
|
+
let csv = headers.join(',') + '\n';
|
|
454
|
+
records.forEach(r => {
|
|
455
|
+
const values = [
|
|
456
|
+
r.executionId,
|
|
457
|
+
r.jobId,
|
|
458
|
+
r.jobName,
|
|
459
|
+
`"${r.command}"`,
|
|
460
|
+
r.startTime.toISOString(),
|
|
461
|
+
r.endTime?.toISOString() || '',
|
|
462
|
+
r.duration || '',
|
|
463
|
+
r.status,
|
|
464
|
+
r.exitCode || '',
|
|
465
|
+
r.user,
|
|
466
|
+
r.hostname,
|
|
467
|
+
r.outputSize
|
|
468
|
+
];
|
|
469
|
+
csv += values.join(',') + '\n';
|
|
470
|
+
});
|
|
471
|
+
return csv;
|
|
472
|
+
}
|
|
473
|
+
ensureLogDirectory() {
|
|
474
|
+
if (!fs.existsSync(this.config.outputLogDir)) {
|
|
475
|
+
fs.mkdirSync(this.config.outputLogDir, { recursive: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
loadRegistry() {
|
|
479
|
+
try {
|
|
480
|
+
if (fs.existsSync(this.config.registryFile)) {
|
|
481
|
+
const data = JSON.parse(fs.readFileSync(this.config.registryFile, 'utf8'));
|
|
482
|
+
// Restore records
|
|
483
|
+
if (data.records) {
|
|
484
|
+
for (const [jobId, records] of Object.entries(data.records)) {
|
|
485
|
+
this.records.set(jobId, records.map(r => ({
|
|
486
|
+
...r,
|
|
487
|
+
startTime: new Date(r.startTime),
|
|
488
|
+
endTime: r.endTime ? new Date(r.endTime) : undefined
|
|
489
|
+
})));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Restore statistics
|
|
493
|
+
if (data.statistics) {
|
|
494
|
+
for (const [jobId, stats] of Object.entries(data.statistics)) {
|
|
495
|
+
const s = stats;
|
|
496
|
+
this.statistics.set(jobId, {
|
|
497
|
+
...s,
|
|
498
|
+
lastExecution: s.lastExecution ? new Date(s.lastExecution) : undefined,
|
|
499
|
+
lastSuccess: s.lastSuccess ? new Date(s.lastSuccess) : undefined,
|
|
500
|
+
lastFailure: s.lastFailure ? new Date(s.lastFailure) : undefined
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
this.logger.error('Failed to load job registry', error);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
saveRegistry() {
|
|
511
|
+
try {
|
|
512
|
+
const data = {
|
|
513
|
+
records: Object.fromEntries(this.records),
|
|
514
|
+
statistics: Object.fromEntries(this.statistics),
|
|
515
|
+
savedAt: new Date().toISOString()
|
|
516
|
+
};
|
|
517
|
+
fs.writeFileSync(this.config.registryFile, JSON.stringify(data, null, 2));
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
this.logger.error('Failed to save job registry', error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Start job - implements BaseJobManager abstract method
|
|
525
|
+
* JobRegistry is read-only, so this records the start event
|
|
526
|
+
*/
|
|
527
|
+
async startJob(jobId) {
|
|
528
|
+
const job = await this.getJob(jobId);
|
|
529
|
+
if (!job) {
|
|
530
|
+
throw new Error(`Job ${jobId} not found in registry`);
|
|
531
|
+
}
|
|
532
|
+
// Record execution start
|
|
533
|
+
this.recordJobStart(job);
|
|
534
|
+
// Update job status
|
|
535
|
+
return await this.updateJobStatus(jobId, 'running', {
|
|
536
|
+
startedAt: new Date(),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Stop job - implements BaseJobManager abstract method
|
|
541
|
+
* JobRegistry is read-only, so this records the stop event
|
|
542
|
+
*/
|
|
543
|
+
async stopJob(jobId, _signal) {
|
|
544
|
+
const job = await this.getJob(jobId);
|
|
545
|
+
if (!job) {
|
|
546
|
+
throw new Error(`Job ${jobId} not found in registry`);
|
|
547
|
+
}
|
|
548
|
+
// Update job status
|
|
549
|
+
return await this.updateJobStatus(jobId, 'stopped', {
|
|
550
|
+
completedAt: new Date(),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
export default JobRegistry;
|