tlc-claude-code 1.4.8 → 1.4.9
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/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incident Manager Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
createIncident,
|
|
7
|
+
generateTimeline,
|
|
8
|
+
linkAlerts,
|
|
9
|
+
updateStatus,
|
|
10
|
+
generatePostMortem,
|
|
11
|
+
calculateMttr,
|
|
12
|
+
INCIDENT_STATUS,
|
|
13
|
+
createIncidentManager,
|
|
14
|
+
} from './incident-manager.js';
|
|
15
|
+
|
|
16
|
+
describe('incident-manager', () => {
|
|
17
|
+
describe('INCIDENT_STATUS', () => {
|
|
18
|
+
it('defines status constants', () => {
|
|
19
|
+
expect(INCIDENT_STATUS.OPEN).toBe('open');
|
|
20
|
+
expect(INCIDENT_STATUS.INVESTIGATING).toBe('investigating');
|
|
21
|
+
expect(INCIDENT_STATUS.RESOLVED).toBe('resolved');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createIncident', () => {
|
|
26
|
+
it('creates incident from alert', () => {
|
|
27
|
+
const incident = createIncident({ alert: { id: 'a1', title: 'DB Down' } });
|
|
28
|
+
expect(incident.id).toBeDefined();
|
|
29
|
+
expect(incident.title).toBe('DB Down');
|
|
30
|
+
expect(incident.status).toBe('open');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('generateTimeline', () => {
|
|
35
|
+
it('generates timeline from events', () => {
|
|
36
|
+
const timeline = generateTimeline({
|
|
37
|
+
events: [
|
|
38
|
+
{ type: 'alert', timestamp: new Date('2024-01-01T10:00:00Z') },
|
|
39
|
+
{ type: 'acknowledge', timestamp: new Date('2024-01-01T10:05:00Z') },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
expect(timeline.length).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('linkAlerts', () => {
|
|
47
|
+
it('links related alerts to incident', () => {
|
|
48
|
+
const incident = createIncident({ title: 'Outage' });
|
|
49
|
+
const linked = linkAlerts(incident, [{ id: 'a1' }, { id: 'a2' }]);
|
|
50
|
+
expect(linked.alerts).toHaveLength(2);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('updateStatus', () => {
|
|
55
|
+
it('updates incident status', () => {
|
|
56
|
+
const incident = createIncident({ title: 'Test' });
|
|
57
|
+
const updated = updateStatus(incident, 'investigating');
|
|
58
|
+
expect(updated.status).toBe('investigating');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('records status history', () => {
|
|
62
|
+
const incident = createIncident({ title: 'Test' });
|
|
63
|
+
const updated = updateStatus(incident, 'resolved');
|
|
64
|
+
expect(updated.statusHistory.length).toBeGreaterThan(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('generatePostMortem', () => {
|
|
69
|
+
it('generates post-mortem template', () => {
|
|
70
|
+
const postMortem = generatePostMortem({
|
|
71
|
+
incident: { id: 'i1', title: 'Outage' },
|
|
72
|
+
});
|
|
73
|
+
expect(postMortem).toContain('# Post-Mortem');
|
|
74
|
+
expect(postMortem).toContain('Outage');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('calculateMttr', () => {
|
|
79
|
+
it('calculates mean time to resolve', () => {
|
|
80
|
+
const incidents = [
|
|
81
|
+
{ createdAt: new Date('2024-01-01T10:00:00Z'), resolvedAt: new Date('2024-01-01T11:00:00Z') },
|
|
82
|
+
{ createdAt: new Date('2024-01-02T10:00:00Z'), resolvedAt: new Date('2024-01-02T10:30:00Z') },
|
|
83
|
+
];
|
|
84
|
+
const mttr = calculateMttr(incidents);
|
|
85
|
+
expect(mttr).toBe(45 * 60 * 1000); // 45 minutes
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('createIncidentManager', () => {
|
|
90
|
+
it('creates manager with methods', () => {
|
|
91
|
+
const manager = createIncidentManager();
|
|
92
|
+
expect(manager.create).toBeDefined();
|
|
93
|
+
expect(manager.update).toBeDefined();
|
|
94
|
+
expect(manager.resolve).toBeDefined();
|
|
95
|
+
expect(manager.getMetrics).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Aggregator
|
|
3
|
+
* Collects and aggregates logs from multiple sources
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Log level constants
|
|
8
|
+
*/
|
|
9
|
+
export const LOG_LEVELS = {
|
|
10
|
+
ERROR: 'error',
|
|
11
|
+
WARN: 'warn',
|
|
12
|
+
INFO: 'info',
|
|
13
|
+
DEBUG: 'debug',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Structures a log entry as JSON with timestamp
|
|
18
|
+
* @param {Object} entry - Log entry with message and level
|
|
19
|
+
* @returns {Object} Structured log entry
|
|
20
|
+
*/
|
|
21
|
+
export function structureLog(entry) {
|
|
22
|
+
return {
|
|
23
|
+
...entry,
|
|
24
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Filters sensitive data from log entries
|
|
30
|
+
* @param {Object} log - Log entry to filter
|
|
31
|
+
* @returns {Object} Filtered log entry
|
|
32
|
+
*/
|
|
33
|
+
export function filterSensitiveData(log) {
|
|
34
|
+
const sensitivePatterns = [
|
|
35
|
+
/password\s*[=:]\s*\S+/gi,
|
|
36
|
+
/api_key\s*[=:]\s*\S+/gi,
|
|
37
|
+
/secret\s*[=:]\s*\S+/gi,
|
|
38
|
+
/token\s*[=:]\s*\S+/gi,
|
|
39
|
+
/authorization\s*[=:]\s*\S+/gi,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
let filteredMessage = log.message;
|
|
43
|
+
for (const pattern of sensitivePatterns) {
|
|
44
|
+
filteredMessage = filteredMessage.replace(pattern, (match) => {
|
|
45
|
+
const [key] = match.split(/[=:]/);
|
|
46
|
+
return `${key}=[REDACTED]`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...log,
|
|
52
|
+
message: filteredMessage,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Aggregates logs from multiple sources
|
|
58
|
+
* @param {Array} sources - Array of log sources with logs
|
|
59
|
+
* @returns {Array} Aggregated logs
|
|
60
|
+
*/
|
|
61
|
+
export function aggregateLogs(sources) {
|
|
62
|
+
const aggregated = [];
|
|
63
|
+
for (const source of sources) {
|
|
64
|
+
for (const log of source.logs) {
|
|
65
|
+
aggregated.push({
|
|
66
|
+
...log,
|
|
67
|
+
source: source.source,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return aggregated;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Determines if logs should be rotated
|
|
76
|
+
* @param {Object} options - Rotation options
|
|
77
|
+
* @param {number} options.maxSize - Maximum size in bytes
|
|
78
|
+
* @param {number} options.currentSize - Current size in bytes
|
|
79
|
+
* @param {number} options.maxAge - Maximum age in milliseconds
|
|
80
|
+
* @param {number} options.age - Current age in milliseconds
|
|
81
|
+
* @returns {Object} Rotation result
|
|
82
|
+
*/
|
|
83
|
+
export function rotateLogs(options) {
|
|
84
|
+
const { maxSize, currentSize, maxAge, age } = options;
|
|
85
|
+
|
|
86
|
+
const shouldRotateBySize = maxSize && currentSize && currentSize > maxSize;
|
|
87
|
+
const shouldRotateByAge = maxAge && age && age > maxAge;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
rotated: shouldRotateBySize || shouldRotateByAge,
|
|
91
|
+
reason: shouldRotateBySize ? 'size' : shouldRotateByAge ? 'age' : null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Exports logs to specified format
|
|
97
|
+
* @param {Array} logs - Logs to export
|
|
98
|
+
* @param {string} format - Output format (json or ndjson)
|
|
99
|
+
* @returns {string} Exported logs
|
|
100
|
+
*/
|
|
101
|
+
export function exportLogs(logs, format) {
|
|
102
|
+
if (format === 'ndjson') {
|
|
103
|
+
return logs.map(log => JSON.stringify(log)).join('\n');
|
|
104
|
+
}
|
|
105
|
+
return JSON.stringify(logs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a log aggregator instance
|
|
110
|
+
* @returns {Object} Log aggregator with methods
|
|
111
|
+
*/
|
|
112
|
+
export function createLogAggregator() {
|
|
113
|
+
const sources = [];
|
|
114
|
+
const logs = [];
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
/**
|
|
118
|
+
* Adds a log source
|
|
119
|
+
* @param {Object} source - Source configuration
|
|
120
|
+
*/
|
|
121
|
+
addSource(source) {
|
|
122
|
+
sources.push(source);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Collects logs from all sources
|
|
127
|
+
* @returns {Array} Collected logs
|
|
128
|
+
*/
|
|
129
|
+
collect() {
|
|
130
|
+
const collected = aggregateLogs(sources.map(s => ({
|
|
131
|
+
source: s.name,
|
|
132
|
+
logs: s.getLogs ? s.getLogs() : [],
|
|
133
|
+
})));
|
|
134
|
+
logs.push(...collected);
|
|
135
|
+
return collected;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Exports all collected logs
|
|
140
|
+
* @param {string} format - Export format
|
|
141
|
+
* @returns {string} Exported logs
|
|
142
|
+
*/
|
|
143
|
+
export(format = 'json') {
|
|
144
|
+
return exportLogs(logs, format);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Aggregator Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
aggregateLogs,
|
|
7
|
+
structureLog,
|
|
8
|
+
filterSensitiveData,
|
|
9
|
+
rotateLogs,
|
|
10
|
+
exportLogs,
|
|
11
|
+
LOG_LEVELS,
|
|
12
|
+
createLogAggregator,
|
|
13
|
+
} from './log-aggregator.js';
|
|
14
|
+
|
|
15
|
+
describe('log-aggregator', () => {
|
|
16
|
+
describe('LOG_LEVELS', () => {
|
|
17
|
+
it('defines level constants', () => {
|
|
18
|
+
expect(LOG_LEVELS.ERROR).toBe('error');
|
|
19
|
+
expect(LOG_LEVELS.WARN).toBe('warn');
|
|
20
|
+
expect(LOG_LEVELS.INFO).toBe('info');
|
|
21
|
+
expect(LOG_LEVELS.DEBUG).toBe('debug');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('structureLog', () => {
|
|
26
|
+
it('structures log as JSON', () => {
|
|
27
|
+
const log = structureLog({ message: 'Test', level: 'info' });
|
|
28
|
+
expect(log.message).toBe('Test');
|
|
29
|
+
expect(log.level).toBe('info');
|
|
30
|
+
expect(log.timestamp).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('filterSensitiveData', () => {
|
|
35
|
+
it('redacts passwords', () => {
|
|
36
|
+
const log = filterSensitiveData({ message: 'password=secret123' });
|
|
37
|
+
expect(log.message).not.toContain('secret123');
|
|
38
|
+
expect(log.message).toContain('[REDACTED]');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('redacts API keys', () => {
|
|
42
|
+
const log = filterSensitiveData({ message: 'api_key: sk-abc123' });
|
|
43
|
+
expect(log.message).not.toContain('sk-abc123');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('aggregateLogs', () => {
|
|
48
|
+
it('aggregates from multiple sources', () => {
|
|
49
|
+
const logs = aggregateLogs([
|
|
50
|
+
{ source: 'app', logs: [{ message: 'a' }] },
|
|
51
|
+
{ source: 'db', logs: [{ message: 'b' }] },
|
|
52
|
+
]);
|
|
53
|
+
expect(logs.length).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('rotateLogs', () => {
|
|
58
|
+
it('rotates by size', () => {
|
|
59
|
+
const result = rotateLogs({ maxSize: 1000, currentSize: 1500 });
|
|
60
|
+
expect(result.rotated).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rotates by time', () => {
|
|
64
|
+
const result = rotateLogs({ maxAge: 86400000, age: 90000000 });
|
|
65
|
+
expect(result.rotated).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('exportLogs', () => {
|
|
70
|
+
it('exports to JSON', () => {
|
|
71
|
+
const output = exportLogs([{ message: 'test' }], 'json');
|
|
72
|
+
expect(JSON.parse(output)).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('exports to NDJSON', () => {
|
|
76
|
+
const output = exportLogs([{ message: 'a' }, { message: 'b' }], 'ndjson');
|
|
77
|
+
expect(output.split('\n').filter(Boolean)).toHaveLength(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('createLogAggregator', () => {
|
|
82
|
+
it('creates aggregator with methods', () => {
|
|
83
|
+
const aggregator = createLogAggregator();
|
|
84
|
+
expect(aggregator.addSource).toBeDefined();
|
|
85
|
+
expect(aggregator.collect).toBeDefined();
|
|
86
|
+
expect(aggregator.export).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Collector
|
|
3
|
+
* Prometheus format metrics collection and formatting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const METRIC_TYPES = {
|
|
7
|
+
COUNTER: 'counter',
|
|
8
|
+
HISTOGRAM: 'histogram',
|
|
9
|
+
GAUGE: 'gauge',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a counter metric
|
|
14
|
+
* @param {string} name - Metric name
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
* @param {Array} options.labels - Label names
|
|
17
|
+
* @returns {Object} Counter with inc and get methods
|
|
18
|
+
*/
|
|
19
|
+
export function createCounter(name, options = {}) {
|
|
20
|
+
const { labels = [] } = options;
|
|
21
|
+
let value = 0;
|
|
22
|
+
const labeledValues = new Map();
|
|
23
|
+
|
|
24
|
+
function getLabelKey(labelValues) {
|
|
25
|
+
return JSON.stringify(labelValues);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
inc(amountOrLabels = 1) {
|
|
30
|
+
if (typeof amountOrLabels === 'number') {
|
|
31
|
+
value += amountOrLabels;
|
|
32
|
+
} else if (typeof amountOrLabels === 'object' && labels.length > 0) {
|
|
33
|
+
const key = getLabelKey(amountOrLabels);
|
|
34
|
+
labeledValues.set(key, (labeledValues.get(key) || 0) + 1);
|
|
35
|
+
} else {
|
|
36
|
+
value += 1;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
get(labelValues) {
|
|
41
|
+
if (labelValues && labels.length > 0) {
|
|
42
|
+
const key = getLabelKey(labelValues);
|
|
43
|
+
return labeledValues.get(key) || 0;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
getName() {
|
|
49
|
+
return name;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
getType() {
|
|
53
|
+
return METRIC_TYPES.COUNTER;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a histogram metric
|
|
60
|
+
* @param {string} name - Metric name
|
|
61
|
+
* @param {Object} options - Configuration options
|
|
62
|
+
* @param {Array} options.buckets - Bucket boundaries
|
|
63
|
+
* @returns {Object} Histogram with observe and get methods
|
|
64
|
+
*/
|
|
65
|
+
export function createHistogram(name, options = {}) {
|
|
66
|
+
const { buckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] } = options;
|
|
67
|
+
const observations = [];
|
|
68
|
+
let sum = 0;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
observe(value) {
|
|
72
|
+
observations.push(value);
|
|
73
|
+
sum += value;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
getCount() {
|
|
77
|
+
return observations.length;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
getSum() {
|
|
81
|
+
return sum;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
getPercentile(percentile) {
|
|
85
|
+
if (observations.length === 0) return 0;
|
|
86
|
+
|
|
87
|
+
const sorted = [...observations].sort((a, b) => a - b);
|
|
88
|
+
const index = Math.floor((percentile / 100) * sorted.length);
|
|
89
|
+
return sorted[Math.min(index, sorted.length - 1)];
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
getBuckets() {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const bucket of buckets) {
|
|
95
|
+
result[bucket] = observations.filter((v) => v <= bucket).length;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getName() {
|
|
101
|
+
return name;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
getType() {
|
|
105
|
+
return METRIC_TYPES.HISTOGRAM;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a gauge metric
|
|
112
|
+
* @param {string} name - Metric name
|
|
113
|
+
* @param {Object} options - Configuration options
|
|
114
|
+
* @returns {Object} Gauge with set, inc, dec, and get methods
|
|
115
|
+
*/
|
|
116
|
+
export function createGauge(name, options = {}) {
|
|
117
|
+
let value = 0;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
set(newValue) {
|
|
121
|
+
value = newValue;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
inc(amount = 1) {
|
|
125
|
+
value += amount;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
dec(amount = 1) {
|
|
129
|
+
value -= amount;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
get() {
|
|
133
|
+
return value;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
getName() {
|
|
137
|
+
return name;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
getType() {
|
|
141
|
+
return METRIC_TYPES.GAUGE;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collect system metrics
|
|
148
|
+
* @param {Object} options - Configuration options
|
|
149
|
+
* @param {boolean} options.cpu - Include CPU metrics
|
|
150
|
+
* @param {boolean} options.memory - Include memory metrics
|
|
151
|
+
* @param {boolean} options.eventLoop - Include event loop metrics
|
|
152
|
+
* @returns {Object} Collected metrics
|
|
153
|
+
*/
|
|
154
|
+
export async function collectMetrics(options = {}) {
|
|
155
|
+
const { cpu = false, memory = false, eventLoop = false } = options;
|
|
156
|
+
const metrics = {};
|
|
157
|
+
|
|
158
|
+
if (cpu) {
|
|
159
|
+
const cpuUsage = process.cpuUsage();
|
|
160
|
+
metrics.cpu = {
|
|
161
|
+
user: cpuUsage.user,
|
|
162
|
+
system: cpuUsage.system,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (memory) {
|
|
167
|
+
const memUsage = process.memoryUsage();
|
|
168
|
+
metrics.memory = {
|
|
169
|
+
heapUsed: memUsage.heapUsed,
|
|
170
|
+
heapTotal: memUsage.heapTotal,
|
|
171
|
+
external: memUsage.external,
|
|
172
|
+
rss: memUsage.rss,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (eventLoop) {
|
|
177
|
+
// Measure event loop lag
|
|
178
|
+
const start = Date.now();
|
|
179
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
180
|
+
const lag = Date.now() - start;
|
|
181
|
+
metrics.eventLoop = {
|
|
182
|
+
lag,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return metrics;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Format metric in Prometheus format
|
|
191
|
+
* @param {Object} metric - Metric to format
|
|
192
|
+
* @param {string} metric.name - Metric name
|
|
193
|
+
* @param {string} metric.type - Metric type
|
|
194
|
+
* @param {number} metric.value - Metric value (for counter/gauge)
|
|
195
|
+
* @param {Object} metric.buckets - Bucket values (for histogram)
|
|
196
|
+
* @param {number} metric.sum - Sum (for histogram)
|
|
197
|
+
* @param {number} metric.count - Count (for histogram)
|
|
198
|
+
* @param {Object} metric.labels - Labels
|
|
199
|
+
* @returns {string} Prometheus formatted string
|
|
200
|
+
*/
|
|
201
|
+
export function formatPrometheus(metric) {
|
|
202
|
+
const { name, type, value, buckets, sum, count, labels } = metric;
|
|
203
|
+
const lines = [];
|
|
204
|
+
|
|
205
|
+
// Type declaration
|
|
206
|
+
lines.push(`# TYPE ${name} ${type}`);
|
|
207
|
+
|
|
208
|
+
// Format labels
|
|
209
|
+
let labelStr = '';
|
|
210
|
+
if (labels && Object.keys(labels).length > 0) {
|
|
211
|
+
const labelParts = Object.entries(labels).map(([k, v]) => `${k}="${v}"`);
|
|
212
|
+
labelStr = `{${labelParts.join(',')}}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (type === 'histogram') {
|
|
216
|
+
// Histogram buckets
|
|
217
|
+
for (const [le, bucketCount] of Object.entries(buckets)) {
|
|
218
|
+
const bucketLabel = labelStr ? labelStr.replace('}', `,le="${le}"}`) : `{le="${le}"}`;
|
|
219
|
+
lines.push(`${name}_bucket${bucketLabel} ${bucketCount}`);
|
|
220
|
+
}
|
|
221
|
+
// Add +Inf bucket
|
|
222
|
+
const infLabel = labelStr ? labelStr.replace('}', ',le="+Inf"}') : '{le="+Inf"}';
|
|
223
|
+
lines.push(`${name}_bucket${infLabel} ${count}`);
|
|
224
|
+
lines.push(`${name}_sum${labelStr} ${sum}`);
|
|
225
|
+
lines.push(`${name}_count${labelStr} ${count}`);
|
|
226
|
+
} else {
|
|
227
|
+
// Counter or Gauge
|
|
228
|
+
lines.push(`${name}${labelStr} ${value}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create a metrics collector
|
|
236
|
+
* @param {Object} options - Configuration options
|
|
237
|
+
* @param {number} options.retentionMs - Retention period in milliseconds
|
|
238
|
+
* @returns {Object} Metrics collector
|
|
239
|
+
*/
|
|
240
|
+
export function createMetricsCollector(options = {}) {
|
|
241
|
+
const { retentionMs = 3600000 } = options;
|
|
242
|
+
|
|
243
|
+
const counters = new Map();
|
|
244
|
+
const histograms = new Map();
|
|
245
|
+
const gauges = new Map();
|
|
246
|
+
const requestMetrics = [];
|
|
247
|
+
|
|
248
|
+
// Create default request metrics
|
|
249
|
+
const requestCounter = createCounter('http_requests_total', { labels: ['method', 'path', 'status'] });
|
|
250
|
+
const requestDuration = createHistogram('http_request_duration_seconds');
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
counter(name, opts) {
|
|
254
|
+
if (!counters.has(name)) {
|
|
255
|
+
counters.set(name, createCounter(name, opts));
|
|
256
|
+
}
|
|
257
|
+
return counters.get(name);
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
histogram(name, opts) {
|
|
261
|
+
if (!histograms.has(name)) {
|
|
262
|
+
histograms.set(name, createHistogram(name, opts));
|
|
263
|
+
}
|
|
264
|
+
return histograms.get(name);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
gauge(name, opts) {
|
|
268
|
+
if (!gauges.has(name)) {
|
|
269
|
+
gauges.set(name, createGauge(name, opts));
|
|
270
|
+
}
|
|
271
|
+
return gauges.get(name);
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
trackRequest({ method, path, status, duration }) {
|
|
275
|
+
requestCounter.inc({ method, path, status: String(status) });
|
|
276
|
+
requestDuration.observe(duration);
|
|
277
|
+
requestMetrics.push({
|
|
278
|
+
method,
|
|
279
|
+
path,
|
|
280
|
+
status,
|
|
281
|
+
duration,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Clean up old metrics based on retention
|
|
286
|
+
const cutoff = Date.now() - retentionMs;
|
|
287
|
+
while (requestMetrics.length > 0 && requestMetrics[0].timestamp < cutoff) {
|
|
288
|
+
requestMetrics.shift();
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
collect() {
|
|
293
|
+
return {
|
|
294
|
+
counters: Object.fromEntries(counters),
|
|
295
|
+
histograms: Object.fromEntries(histograms),
|
|
296
|
+
gauges: Object.fromEntries(gauges),
|
|
297
|
+
requests: requestMetrics,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
format() {
|
|
302
|
+
const output = [];
|
|
303
|
+
|
|
304
|
+
for (const [name, counter] of counters) {
|
|
305
|
+
output.push(formatPrometheus({
|
|
306
|
+
name,
|
|
307
|
+
type: 'counter',
|
|
308
|
+
value: counter.get(),
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const [name, histogram] of histograms) {
|
|
313
|
+
output.push(formatPrometheus({
|
|
314
|
+
name,
|
|
315
|
+
type: 'histogram',
|
|
316
|
+
buckets: histogram.getBuckets(),
|
|
317
|
+
sum: histogram.getSum(),
|
|
318
|
+
count: histogram.getCount(),
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const [name, gauge] of gauges) {
|
|
323
|
+
output.push(formatPrometheus({
|
|
324
|
+
name,
|
|
325
|
+
type: 'gauge',
|
|
326
|
+
value: gauge.get(),
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return output.join('\n\n');
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
getRetention() {
|
|
334
|
+
return retentionMs;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|