bs9 1.0.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/LICENSE +21 -0
- package/README.md +532 -0
- package/bin/bs9 +97 -0
- package/package.json +48 -0
- package/src/.gitkeep +0 -0
- package/src/alerting/config.ts +194 -0
- package/src/commands/alert.ts +98 -0
- package/src/commands/export.ts +69 -0
- package/src/commands/logs.ts +22 -0
- package/src/commands/monit.ts +248 -0
- package/src/commands/restart.ts +13 -0
- package/src/commands/start.ts +207 -0
- package/src/commands/status.ts +162 -0
- package/src/commands/stop.ts +13 -0
- package/src/commands/web.ts +49 -0
- package/src/docker/Dockerfile +44 -0
- package/src/injectors/otel.ts +66 -0
- package/src/k8s/bs9-deployment.yaml +197 -0
- package/src/storage/metrics.ts +204 -0
- package/src/web/dashboard.ts +286 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
|
|
7
|
+
interface AlertConfig {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
webhookUrl?: string;
|
|
10
|
+
thresholds: {
|
|
11
|
+
cpu: number; // percentage
|
|
12
|
+
memory: number; // percentage
|
|
13
|
+
errorRate: number; // percentage
|
|
14
|
+
uptime: number; // percentage
|
|
15
|
+
};
|
|
16
|
+
cooldown: number; // seconds between alerts
|
|
17
|
+
services: {
|
|
18
|
+
[serviceName: string]: {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
customThresholds?: Partial<AlertConfig['thresholds']>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class AlertManager {
|
|
26
|
+
private configPath: string;
|
|
27
|
+
private config: AlertConfig;
|
|
28
|
+
private lastAlerts: Map<string, number> = new Map();
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
this.configPath = join(homedir(), ".config", "bs9", "alerts.json");
|
|
32
|
+
this.config = this.loadConfig();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private loadConfig(): AlertConfig {
|
|
36
|
+
const defaultConfig: AlertConfig = {
|
|
37
|
+
enabled: true,
|
|
38
|
+
thresholds: {
|
|
39
|
+
cpu: 80,
|
|
40
|
+
memory: 85,
|
|
41
|
+
errorRate: 5,
|
|
42
|
+
uptime: 95,
|
|
43
|
+
},
|
|
44
|
+
cooldown: 300, // 5 minutes
|
|
45
|
+
services: {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (existsSync(this.configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(this.configPath, 'utf-8');
|
|
51
|
+
return { ...defaultConfig, ...JSON.parse(content) };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Failed to load alert config, using defaults:', error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create default config file
|
|
58
|
+
this.saveConfig(defaultConfig);
|
|
59
|
+
return defaultConfig;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private saveConfig(config: AlertConfig): void {
|
|
63
|
+
try {
|
|
64
|
+
const configDir = join(homedir(), ".config", "bs9");
|
|
65
|
+
if (!existsSync(configDir)) {
|
|
66
|
+
mkdirSync(configDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
writeFileSync(this.configPath, JSON.stringify(config, null, 2));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to save alert config:', error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateConfig(updates: Partial<AlertConfig>): void {
|
|
75
|
+
this.config = { ...this.config, ...updates };
|
|
76
|
+
this.saveConfig(this.config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setServiceAlert(serviceName: string, enabled: boolean, customThresholds?: Partial<AlertConfig['thresholds']>): void {
|
|
80
|
+
this.config.services[serviceName] = {
|
|
81
|
+
enabled,
|
|
82
|
+
customThresholds,
|
|
83
|
+
};
|
|
84
|
+
this.saveConfig(this.config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async checkAlerts(serviceName: string, metrics: {
|
|
88
|
+
cpu: number;
|
|
89
|
+
memory: number;
|
|
90
|
+
health: 'healthy' | 'unhealthy' | 'unknown';
|
|
91
|
+
uptime: number;
|
|
92
|
+
}): Promise<void> {
|
|
93
|
+
if (!this.config.enabled) return;
|
|
94
|
+
|
|
95
|
+
const serviceConfig = this.config.services[serviceName];
|
|
96
|
+
if (serviceConfig && !serviceConfig.enabled) return;
|
|
97
|
+
|
|
98
|
+
const thresholds = {
|
|
99
|
+
...this.config.thresholds,
|
|
100
|
+
...serviceConfig?.customThresholds,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const alerts: string[] = [];
|
|
104
|
+
|
|
105
|
+
// Check CPU threshold
|
|
106
|
+
if (metrics.cpu > thresholds.cpu) {
|
|
107
|
+
alerts.push(`CPU usage (${metrics.cpu}%) exceeds threshold (${thresholds.cpu}%)`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check Memory threshold
|
|
111
|
+
if (metrics.memory > thresholds.memory) {
|
|
112
|
+
alerts.push(`Memory usage (${metrics.memory}%) exceeds threshold (${thresholds.memory}%)`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check Uptime threshold
|
|
116
|
+
if (metrics.uptime < thresholds.uptime) {
|
|
117
|
+
alerts.push(`Uptime (${metrics.uptime}%) below threshold (${thresholds.uptime}%)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check health
|
|
121
|
+
if (metrics.health === 'unhealthy') {
|
|
122
|
+
alerts.push('Service health check failed');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (alerts.length === 0) return;
|
|
126
|
+
|
|
127
|
+
// Check cooldown
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const lastAlert = this.lastAlerts.get(serviceName) || 0;
|
|
130
|
+
|
|
131
|
+
if (now - lastAlert < this.config.cooldown * 1000) {
|
|
132
|
+
return; // Still in cooldown period
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Send alert
|
|
136
|
+
await this.sendAlert(serviceName, alerts);
|
|
137
|
+
this.lastAlerts.set(serviceName, now);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async sendAlert(serviceName: string, alerts: string[]): Promise<void> {
|
|
141
|
+
const message = `šØ BS9 Alert for ${serviceName}:\n${alerts.join('\n')}`;
|
|
142
|
+
|
|
143
|
+
console.error(message);
|
|
144
|
+
|
|
145
|
+
if (this.config.webhookUrl) {
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
service: serviceName,
|
|
154
|
+
alerts,
|
|
155
|
+
timestamp: new Date().toISOString(),
|
|
156
|
+
severity: 'warning',
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
console.error(`Failed to send webhook alert: ${response.statusText}`);
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Failed to send webhook alert:', error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getConfig(): AlertConfig {
|
|
170
|
+
return { ...this.config };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
testWebhook(): Promise<boolean> {
|
|
174
|
+
if (!this.config.webhookUrl) {
|
|
175
|
+
return Promise.resolve(false);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return fetch(this.config.webhookUrl, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
test: true,
|
|
185
|
+
message: 'BS9 Alert System Test',
|
|
186
|
+
timestamp: new Date().toISOString(),
|
|
187
|
+
}),
|
|
188
|
+
})
|
|
189
|
+
.then(response => response.ok)
|
|
190
|
+
.catch(() => false);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { AlertManager, AlertConfig };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { AlertManager } from "../alerting/config.js";
|
|
4
|
+
|
|
5
|
+
interface AlertOptions {
|
|
6
|
+
enable?: boolean;
|
|
7
|
+
disable?: boolean;
|
|
8
|
+
webhook?: string;
|
|
9
|
+
cpu?: string;
|
|
10
|
+
memory?: string;
|
|
11
|
+
errorRate?: string;
|
|
12
|
+
uptime?: string;
|
|
13
|
+
cooldown?: string;
|
|
14
|
+
service?: string;
|
|
15
|
+
list?: boolean;
|
|
16
|
+
test?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function alertCommand(options: AlertOptions): Promise<void> {
|
|
20
|
+
const alertManager = new AlertManager();
|
|
21
|
+
|
|
22
|
+
if (options.list) {
|
|
23
|
+
const config = alertManager.getConfig();
|
|
24
|
+
console.log('š BS9 Alert Configuration');
|
|
25
|
+
console.log('='.repeat(40));
|
|
26
|
+
console.log(`Enabled: ${config.enabled}`);
|
|
27
|
+
if (config.webhookUrl) {
|
|
28
|
+
console.log(`Webhook: ${config.webhookUrl}`);
|
|
29
|
+
}
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('Thresholds:');
|
|
32
|
+
console.log(` CPU: ${config.thresholds.cpu}%`);
|
|
33
|
+
console.log(` Memory: ${config.thresholds.memory}%`);
|
|
34
|
+
console.log(` Error Rate: ${config.thresholds.errorRate}%`);
|
|
35
|
+
console.log(` Uptime: ${config.thresholds.uptime}%`);
|
|
36
|
+
console.log(` Cooldown: ${config.cooldown}s`);
|
|
37
|
+
console.log('');
|
|
38
|
+
|
|
39
|
+
if (Object.keys(config.services).length > 0) {
|
|
40
|
+
console.log('Service-specific configs:');
|
|
41
|
+
for (const [serviceName, serviceConfig] of Object.entries(config.services)) {
|
|
42
|
+
console.log(` ${serviceName}:`);
|
|
43
|
+
console.log(` Enabled: ${serviceConfig.enabled}`);
|
|
44
|
+
if (serviceConfig.customThresholds) {
|
|
45
|
+
console.log(` Custom thresholds:`, serviceConfig.customThresholds);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.test) {
|
|
53
|
+
console.log('š§Ŗ Testing alert webhook...');
|
|
54
|
+
const success = await alertManager.testWebhook();
|
|
55
|
+
if (success) {
|
|
56
|
+
console.log('ā
Webhook test successful');
|
|
57
|
+
} else {
|
|
58
|
+
console.log('ā Webhook test failed');
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const updates: any = {};
|
|
64
|
+
|
|
65
|
+
if (options.enable !== undefined) {
|
|
66
|
+
updates.enabled = options.enable;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (options.webhook) {
|
|
70
|
+
updates.webhookUrl = options.webhook;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const thresholdUpdates: any = {};
|
|
74
|
+
if (options.cpu) thresholdUpdates.cpu = Number(options.cpu);
|
|
75
|
+
if (options.memory) thresholdUpdates.memory = Number(options.memory);
|
|
76
|
+
if (options.errorRate) thresholdUpdates.errorRate = Number(options.errorRate);
|
|
77
|
+
if (options.uptime) thresholdUpdates.uptime = Number(options.uptime);
|
|
78
|
+
if (options.cooldown) thresholdUpdates.cooldown = Number(options.cooldown);
|
|
79
|
+
|
|
80
|
+
if (Object.keys(thresholdUpdates).length > 0) {
|
|
81
|
+
updates.thresholds = {
|
|
82
|
+
...alertManager.getConfig().thresholds,
|
|
83
|
+
...thresholdUpdates,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (options.service) {
|
|
88
|
+
const serviceName = options.service;
|
|
89
|
+
const enabled = options.enable !== false;
|
|
90
|
+
alertManager.setServiceAlert(serviceName, enabled, Object.keys(thresholdUpdates).length > 0 ? thresholdUpdates : undefined);
|
|
91
|
+
console.log(`ā
Updated alert config for service: ${serviceName}`);
|
|
92
|
+
} else if (Object.keys(updates).length > 0) {
|
|
93
|
+
alertManager.updateConfig(updates);
|
|
94
|
+
console.log('ā
Updated global alert configuration');
|
|
95
|
+
} else {
|
|
96
|
+
console.log('ā¹ļø No changes specified. Use --help for options.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { MetricsStorage } from "../storage/metrics.js";
|
|
4
|
+
import { writeFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
interface ExportOptions {
|
|
7
|
+
format?: string;
|
|
8
|
+
hours?: string;
|
|
9
|
+
output?: string;
|
|
10
|
+
service?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function exportCommand(options: ExportOptions): Promise<void> {
|
|
14
|
+
const storage = new MetricsStorage();
|
|
15
|
+
const format = options.format || 'json';
|
|
16
|
+
const hours = Number(options.hours) || 24;
|
|
17
|
+
const serviceName = options.service;
|
|
18
|
+
|
|
19
|
+
console.log(`š Exporting BS9 metrics (${format.toUpperCase()} format)`);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
let data: string;
|
|
23
|
+
|
|
24
|
+
if (serviceName) {
|
|
25
|
+
// Export specific service metrics
|
|
26
|
+
const serviceMetrics = storage.getServiceMetrics(serviceName, hours);
|
|
27
|
+
data = format === 'csv' ? serviceMetricsToCsv(serviceMetrics, serviceName) : JSON.stringify(serviceMetrics, null, 2);
|
|
28
|
+
console.log(`š Exporting metrics for service: ${serviceName}`);
|
|
29
|
+
} else {
|
|
30
|
+
// Export all metrics
|
|
31
|
+
data = storage.exportData(format as 'json' | 'csv');
|
|
32
|
+
console.log(`š Exporting all metrics for last ${hours} hours`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const outputFile = options.output || `bs9-metrics-${Date.now()}.${format}`;
|
|
36
|
+
writeFileSync(outputFile, data);
|
|
37
|
+
|
|
38
|
+
console.log(`ā
Metrics exported to: ${outputFile}`);
|
|
39
|
+
console.log(` Size: ${(data.length / 1024).toFixed(2)} KB`);
|
|
40
|
+
console.log(` Records: ${serviceName ? storage.getServiceMetrics(serviceName, hours).length : 'all services'}`);
|
|
41
|
+
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`ā Failed to export metrics: ${error}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function serviceMetricsToCsv(metrics: any[], serviceName: string): string {
|
|
49
|
+
const headers = ['timestamp', 'service_name', 'cpu_ms', 'memory_bytes', 'uptime', 'tasks', 'health', 'state'];
|
|
50
|
+
const rows = [headers.join(',')];
|
|
51
|
+
|
|
52
|
+
for (const metric of metrics) {
|
|
53
|
+
const cpuMatch = metric.cpu.match(/([\d.]+)ms/);
|
|
54
|
+
const cpuMs = cpuMatch ? cpuMatch[1] : '0';
|
|
55
|
+
|
|
56
|
+
rows.push([
|
|
57
|
+
new Date().toISOString(),
|
|
58
|
+
serviceName,
|
|
59
|
+
cpuMs,
|
|
60
|
+
metric.memory.toString(),
|
|
61
|
+
metric.uptime,
|
|
62
|
+
metric.tasks.toString(),
|
|
63
|
+
metric.health,
|
|
64
|
+
metric.state,
|
|
65
|
+
].join(','));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return rows.join('\n');
|
|
69
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
interface LogsOptions {
|
|
6
|
+
follow?: boolean;
|
|
7
|
+
lines?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function logsCommand(name: string, options: LogsOptions): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const args = ["--no-pager"];
|
|
13
|
+
if (options.follow) args.push("-f");
|
|
14
|
+
if (options.lines) args.push("-n", options.lines);
|
|
15
|
+
|
|
16
|
+
const cmd = `journalctl --user ${args.join(" ")} -u ${name}.service`;
|
|
17
|
+
execSync(cmd, { stdio: "inherit" });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error(`ā Failed to fetch logs for user service '${name}': ${err}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
interface MonitOptions {
|
|
7
|
+
refresh?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ServiceMetrics {
|
|
11
|
+
name: string;
|
|
12
|
+
loaded: string;
|
|
13
|
+
active: string;
|
|
14
|
+
sub: string;
|
|
15
|
+
state: string;
|
|
16
|
+
cpu: string;
|
|
17
|
+
memory: string;
|
|
18
|
+
uptime: string;
|
|
19
|
+
tasks: string;
|
|
20
|
+
description: string;
|
|
21
|
+
health?: string;
|
|
22
|
+
lastError?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function monitCommand(options: MonitOptions): Promise<void> {
|
|
26
|
+
const refreshInterval = Number(options.refresh) || 2;
|
|
27
|
+
|
|
28
|
+
// Clear screen and setup
|
|
29
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
30
|
+
console.log('š BS9 Real-time Monitoring Dashboard');
|
|
31
|
+
console.log('='.repeat(80));
|
|
32
|
+
console.log(`Refresh: ${refreshInterval}s | Press Ctrl+C to exit`);
|
|
33
|
+
console.log('');
|
|
34
|
+
|
|
35
|
+
const getMetrics = (): ServiceMetrics[] => {
|
|
36
|
+
try {
|
|
37
|
+
const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
|
|
38
|
+
const lines = listOutput.split("\n").filter(line => line.includes(".service"));
|
|
39
|
+
|
|
40
|
+
const services: ServiceMetrics[] = [];
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (!line.trim()) continue;
|
|
44
|
+
|
|
45
|
+
const match = line.match(/^(?:\s*([ā\sā]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
|
|
46
|
+
if (!match) continue;
|
|
47
|
+
|
|
48
|
+
const [, statusIndicator, name, loaded, active, sub, description] = match;
|
|
49
|
+
|
|
50
|
+
if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) continue;
|
|
51
|
+
|
|
52
|
+
const service: ServiceMetrics = {
|
|
53
|
+
name,
|
|
54
|
+
loaded,
|
|
55
|
+
active,
|
|
56
|
+
sub,
|
|
57
|
+
state: `${active}/${sub}`,
|
|
58
|
+
description,
|
|
59
|
+
cpu: '-',
|
|
60
|
+
memory: '-',
|
|
61
|
+
uptime: '-',
|
|
62
|
+
tasks: '-',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Get additional metrics
|
|
66
|
+
try {
|
|
67
|
+
const showOutput = execSync(`systemctl --user show ${name} -p CPUUsageNSec MemoryCurrent ActiveEnterTimestamp TasksCurrent State`, { encoding: "utf-8" });
|
|
68
|
+
const cpuMatch = showOutput.match(/CPUUsageNSec=(\d+)/);
|
|
69
|
+
const memMatch = showOutput.match(/MemoryCurrent=(\d+)/);
|
|
70
|
+
const timeMatch = showOutput.match(/ActiveEnterTimestamp=(.+)/);
|
|
71
|
+
const tasksMatch = showOutput.match(/TasksCurrent=(\d+)/);
|
|
72
|
+
const stateMatch = showOutput.match(/State=(.+)/);
|
|
73
|
+
|
|
74
|
+
if (cpuMatch) {
|
|
75
|
+
const cpuNs = Number(cpuMatch[1]);
|
|
76
|
+
service.cpu = `${(cpuNs / 1000000).toFixed(1)}ms`;
|
|
77
|
+
}
|
|
78
|
+
if (memMatch) {
|
|
79
|
+
const memBytes = Number(memMatch[1]);
|
|
80
|
+
service.memory = formatMemory(memBytes);
|
|
81
|
+
}
|
|
82
|
+
if (timeMatch) {
|
|
83
|
+
service.uptime = formatUptime(timeMatch[1]);
|
|
84
|
+
}
|
|
85
|
+
if (tasksMatch) {
|
|
86
|
+
service.tasks = tasksMatch[1];
|
|
87
|
+
}
|
|
88
|
+
if (stateMatch) {
|
|
89
|
+
service.state = stateMatch[1].trim();
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore metrics errors
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check health endpoint
|
|
96
|
+
try {
|
|
97
|
+
const portMatch = description.match(/port[=:]?\s*(\d+)/i);
|
|
98
|
+
if (portMatch) {
|
|
99
|
+
const port = portMatch[1];
|
|
100
|
+
const healthCheck = execSync(`curl -s -o /dev/null -w "%{http_code}" http://localhost:${port}/healthz`, { encoding: "utf-8", timeout: 1000 });
|
|
101
|
+
service.health = healthCheck === "200" ? "ā
OK" : "ā FAIL";
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
service.health = "ā ļø UNKNOWN";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
services.push(service);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return services;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error fetching metrics:', error);
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const formatMemory = (bytes: number): string => {
|
|
118
|
+
if (bytes === 0) return '0B';
|
|
119
|
+
const k = 1024;
|
|
120
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
121
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
122
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const formatUptime = (timestamp: string): string => {
|
|
126
|
+
try {
|
|
127
|
+
const date = new Date(timestamp);
|
|
128
|
+
const now = new Date();
|
|
129
|
+
const diff = now.getTime() - date.getTime();
|
|
130
|
+
|
|
131
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
132
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
133
|
+
|
|
134
|
+
if (hours > 0) {
|
|
135
|
+
return `${hours}h ${minutes}m`;
|
|
136
|
+
}
|
|
137
|
+
return `${minutes}m`;
|
|
138
|
+
} catch {
|
|
139
|
+
return '-';
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const renderDashboard = () => {
|
|
144
|
+
// Clear screen
|
|
145
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
146
|
+
|
|
147
|
+
// Header
|
|
148
|
+
console.log('š BS9 Real-time Monitoring Dashboard');
|
|
149
|
+
console.log('='.repeat(120));
|
|
150
|
+
console.log(`Refresh: ${refreshInterval}s | Last update: ${new Date().toLocaleTimeString()} | Press Ctrl+C to exit`);
|
|
151
|
+
console.log('');
|
|
152
|
+
|
|
153
|
+
const services = getMetrics();
|
|
154
|
+
|
|
155
|
+
if (services.length === 0) {
|
|
156
|
+
console.log('No BS9-managed services running.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Table header
|
|
161
|
+
console.log('SERVICE'.padEnd(20) +
|
|
162
|
+
'STATE'.padEnd(15) +
|
|
163
|
+
'HEALTH'.padEnd(10) +
|
|
164
|
+
'CPU'.padEnd(10) +
|
|
165
|
+
'MEMORY'.padEnd(12) +
|
|
166
|
+
'UPTIME'.padEnd(12) +
|
|
167
|
+
'TASKS'.padEnd(8) +
|
|
168
|
+
'DESCRIPTION');
|
|
169
|
+
console.log('-'.repeat(120));
|
|
170
|
+
|
|
171
|
+
// Service rows
|
|
172
|
+
for (const service of services) {
|
|
173
|
+
const stateColor = service.active === 'active' ? '\x1b[32m' : '\x1b[31m';
|
|
174
|
+
const resetColor = '\x1b[0m';
|
|
175
|
+
|
|
176
|
+
const state = `${stateColor}${service.sub}${resetColor}`;
|
|
177
|
+
const health = service.health || '-';
|
|
178
|
+
|
|
179
|
+
console.log(
|
|
180
|
+
service.name.padEnd(20) +
|
|
181
|
+
state.padEnd(15) +
|
|
182
|
+
health.padEnd(10) +
|
|
183
|
+
service.cpu.padEnd(10) +
|
|
184
|
+
service.memory.padEnd(12) +
|
|
185
|
+
service.uptime.padEnd(12) +
|
|
186
|
+
service.tasks.padEnd(8) +
|
|
187
|
+
service.description
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Summary
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log('='.repeat(120));
|
|
194
|
+
|
|
195
|
+
const running = services.filter(s => s.active === 'active').length;
|
|
196
|
+
const totalMemory = services.reduce((sum, s) => {
|
|
197
|
+
if (s.memory !== '-') {
|
|
198
|
+
const match = s.memory.match(/([\d.]+)(B|KB|MB|GB)/);
|
|
199
|
+
if (match) {
|
|
200
|
+
const [, value, unit] = match;
|
|
201
|
+
const bytes = Number(value) * Math.pow(1024, ['B', 'KB', 'MB', 'GB'].indexOf(unit));
|
|
202
|
+
return sum + bytes;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return sum;
|
|
206
|
+
}, 0);
|
|
207
|
+
|
|
208
|
+
console.log(`š Summary: ${running}/${services.length} services running | Total Memory: ${formatMemory(totalMemory)} | Services: ${services.length}`);
|
|
209
|
+
|
|
210
|
+
// Alerts
|
|
211
|
+
const failed = services.filter(s => s.active !== 'active');
|
|
212
|
+
const unhealthy = services.filter(s => s.health === 'ā FAIL');
|
|
213
|
+
|
|
214
|
+
if (failed.length > 0 || unhealthy.length > 0) {
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log('ā ļø ALERTS:');
|
|
217
|
+
if (failed.length > 0) {
|
|
218
|
+
console.log(` Failed services: ${failed.map(s => s.name).join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
if (unhealthy.length > 0) {
|
|
221
|
+
console.log(` Unhealthy services: ${unhealthy.map(s => s.name).join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Initial render
|
|
227
|
+
renderDashboard();
|
|
228
|
+
|
|
229
|
+
// Setup refresh loop
|
|
230
|
+
const refresh = async () => {
|
|
231
|
+
while (true) {
|
|
232
|
+
await setTimeout(refreshInterval * 1000);
|
|
233
|
+
renderDashboard();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Handle Ctrl+C gracefully
|
|
238
|
+
process.on('SIGINT', () => {
|
|
239
|
+
console.log('\nš Monitoring stopped');
|
|
240
|
+
process.exit(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Start monitoring
|
|
244
|
+
refresh().catch(error => {
|
|
245
|
+
console.error('Monitoring error:', error);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export async function restartCommand(name: string): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`systemctl --user restart ${name}`, { stdio: "inherit" });
|
|
8
|
+
console.log(`š User service '${name}' restarted`);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
console.error(`ā Failed to restart user service '${name}': ${err}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
}
|