@logboard/cli 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/.env.example +37 -0
- package/README.md +200 -0
- package/bin/logboard +536 -0
- package/client/logger.js +309 -0
- package/config/index.js +142 -0
- package/config.js +2 -0
- package/controllers/AnalyticsController.js +46 -0
- package/controllers/ApiAnalyticsController.js +129 -0
- package/controllers/ApiKeyController.js +58 -0
- package/controllers/AuthController.js +131 -0
- package/controllers/HealthController.js +56 -0
- package/controllers/LogController.js +197 -0
- package/controllers/OrgController.js +152 -0
- package/controllers/RoleConfigController.js +20 -0
- package/controllers/SettingsController.js +39 -0
- package/controllers/StreamController.js +55 -0
- package/controllers/UiController.js +789 -0
- package/controllers/UserController.js +79 -0
- package/lib/batchWriter.js +57 -0
- package/lib/cleanup.js +67 -0
- package/lib/ejs.js +103 -0
- package/lib/emitter.js +5 -0
- package/lib/healthMonitor.js +245 -0
- package/lib/logger.js +21 -0
- package/lib/streams.js +32 -0
- package/lib/theme.js +77 -0
- package/lib/userStore.js +13 -0
- package/lib/utils.js +44 -0
- package/middleware/apiKey.js +82 -0
- package/middleware/auth.js +55 -0
- package/middleware/ipWhitelist.js +59 -0
- package/middleware/org.js +85 -0
- package/middleware/pageAccess.js +20 -0
- package/middleware/rateLimit.js +29 -0
- package/middleware/roles.js +11 -0
- package/package.json +77 -0
- package/routes/alerts.js +18 -0
- package/routes/analytics.js +26 -0
- package/routes/api-analytics.js +30 -0
- package/routes/api-keys.js +12 -0
- package/routes/archive.js +91 -0
- package/routes/audit.js +50 -0
- package/routes/auth.js +22 -0
- package/routes/bookmarks.js +13 -0
- package/routes/health.js +11 -0
- package/routes/logs.js +88 -0
- package/routes/metrics.js +66 -0
- package/routes/notifications.js +14 -0
- package/routes/orgs.js +98 -0
- package/routes/registration.js +202 -0
- package/routes/role-config.js +97 -0
- package/routes/saved-searches.js +12 -0
- package/routes/server.js +151 -0
- package/routes/settings.js +28 -0
- package/routes/status.js +21 -0
- package/routes/stream.js +11 -0
- package/routes/super.js +129 -0
- package/routes/ui.js +120 -0
- package/routes/users.js +13 -0
- package/server.js +172 -0
- package/services/AlertRulesService.js +323 -0
- package/services/AnalyticsService.js +665 -0
- package/services/ApiAnalyticsService.js +471 -0
- package/services/ApiKeyService.js +166 -0
- package/services/AuditService.js +249 -0
- package/services/AuthService.js +234 -0
- package/services/BookmarkService.js +49 -0
- package/services/GlobalSettingsService.js +44 -0
- package/services/LogService.js +1066 -0
- package/services/MetricsService.js +116 -0
- package/services/NotificationService.js +70 -0
- package/services/OrgService.js +217 -0
- package/services/ReportService.js +247 -0
- package/services/RoleConfigService.js +201 -0
- package/services/SavedSearchService.js +63 -0
- package/services/SettingsService.js +220 -0
- package/services/UserService.js +121 -0
- package/setup.js +132 -0
- package/views/404.ejs +8 -0
- package/views/alerts.ejs +190 -0
- package/views/analytics.ejs +209 -0
- package/views/api-analytics.ejs +660 -0
- package/views/api-keys.ejs +150 -0
- package/views/archive.ejs +123 -0
- package/views/audit.ejs +314 -0
- package/views/bookmarks.ejs +54 -0
- package/views/custom-dashboard.ejs +162 -0
- package/views/dashboard.ejs +186 -0
- package/views/diff.ejs +98 -0
- package/views/health.ejs +269 -0
- package/views/heatmap.ejs +126 -0
- package/views/insights.ejs +334 -0
- package/views/invite.ejs +74 -0
- package/views/live.ejs +299 -0
- package/views/login.ejs +64 -0
- package/views/logo.png +0 -0
- package/views/logs.ejs +754 -0
- package/views/notifications.ejs +58 -0
- package/views/partials/head.ejs +282 -0
- package/views/partials/sidebar.ejs +168 -0
- package/views/register.ejs +100 -0
- package/views/roles.ejs +279 -0
- package/views/saved-searches.ejs +51 -0
- package/views/service-map.ejs +142 -0
- package/views/settings.ejs +1159 -0
- package/views/sidebar.ejs +129 -0
- package/views/status.ejs +100 -0
- package/views/super-admin-admins.ejs +58 -0
- package/views/super-admin-analytics.ejs +49 -0
- package/views/super-admin-orgs.ejs +310 -0
- package/views/super-admin-profile.ejs +77 -0
- package/views/super-admin-settings.ejs +108 -0
- package/views/super-admin-system.ejs +46 -0
- package/views/users.ejs +153 -0
package/client/logger.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* LogBoard Client Logger
|
|
4
|
+
* Drop-in structured logger that ships to LogBoard.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const logger = require('./client/logger');
|
|
8
|
+
* logger.configure({ appName:'my-api', remoteUrl:'http://logboard:9900/api/logs', apiKey:'blq_...' });
|
|
9
|
+
* app.use(logger.requestLogger()); // request metrics
|
|
10
|
+
* const log = logger.create({ service:'PaymentSvc' });
|
|
11
|
+
* log.info('done', { amount:99 });
|
|
12
|
+
*/
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
const CFG = {
|
|
17
|
+
appName: process.env.APP_NAME || 'app',
|
|
18
|
+
remoteUrl: process.env.LOG_REMOTE_URL || 'http://localhost:9900/api/logs',
|
|
19
|
+
apiKey: process.env.LOG_API_KEY || '',
|
|
20
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
21
|
+
prettyPrint: process.env.NODE_ENV !== 'production',
|
|
22
|
+
bufferSize: Number(process.env.LOG_BUFFER_SIZE) || 50,
|
|
23
|
+
flushInterval: Number(process.env.LOG_FLUSH_INTERVAL) || 100,
|
|
24
|
+
remoteTimeout: Number(process.env.LOG_REMOTE_TIMEOUT) || 200,
|
|
25
|
+
remoteRetries: Number(process.env.LOG_REMOTE_RETRIES) || 2,
|
|
26
|
+
interceptConsole: process.env.LOG_INTERCEPT === 'true',
|
|
27
|
+
skipPaths: (process.env.LOG_SKIP_PATHS || '/health,/ping,/favicon')
|
|
28
|
+
.split(',')
|
|
29
|
+
.map((s) => s.trim()),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40, fatal: 50 };
|
|
33
|
+
const MASK = [
|
|
34
|
+
'authorization',
|
|
35
|
+
'token',
|
|
36
|
+
'password',
|
|
37
|
+
'secret',
|
|
38
|
+
'apikey',
|
|
39
|
+
'key',
|
|
40
|
+
'auth',
|
|
41
|
+
];
|
|
42
|
+
const state = { buf: [], fp: null, installed: false, orig: null };
|
|
43
|
+
|
|
44
|
+
function shouldLog (l) {
|
|
45
|
+
return (LEVELS[l] || 0) >= (LEVELS[CFG.level] || 20);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeStr (o) {
|
|
49
|
+
const s = new WeakSet();
|
|
50
|
+
return JSON.stringify(o, (k, v) => {
|
|
51
|
+
if (typeof v === 'object' && v !== null) {
|
|
52
|
+
if (s.has(v)) {
|
|
53
|
+
return '[Circular]';
|
|
54
|
+
}
|
|
55
|
+
s.add(v);
|
|
56
|
+
}
|
|
57
|
+
if (k && MASK.some((m) => k.toLowerCase().includes(m))) {
|
|
58
|
+
return '***';
|
|
59
|
+
}
|
|
60
|
+
return v;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fmt (level, appName, ctx, msg, extras) {
|
|
65
|
+
const p = {
|
|
66
|
+
ts: new Date().toISOString(),
|
|
67
|
+
level: level.toUpperCase(),
|
|
68
|
+
appName: appName || CFG.appName,
|
|
69
|
+
host: os.hostname(),
|
|
70
|
+
pid: process.pid,
|
|
71
|
+
...ctx,
|
|
72
|
+
message: String(msg),
|
|
73
|
+
};
|
|
74
|
+
if (extras && extras.length) {
|
|
75
|
+
p.data = extras.map((x) =>
|
|
76
|
+
x instanceof Error ? { errorMessage: x.message, stack: x.stack } : x,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return CFG.prettyPrint ? JSON.stringify(p, null, 2) : safeStr(p);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function enq (line) {
|
|
83
|
+
state.buf.push(line);
|
|
84
|
+
if (state.buf.length >= CFG.bufferSize) {
|
|
85
|
+
flush();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function flush () {
|
|
90
|
+
if (state.fp) {
|
|
91
|
+
state.fp = state.fp.then(() => drain());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
state.fp = drain().finally(() => {
|
|
95
|
+
state.fp = null;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function drain () {
|
|
100
|
+
return new Promise((r) =>
|
|
101
|
+
setImmediate(async () => {
|
|
102
|
+
const logs = state.buf.splice(0);
|
|
103
|
+
if (!logs.length) {
|
|
104
|
+
r();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (CFG.prettyPrint) {
|
|
108
|
+
for (const l of logs) {
|
|
109
|
+
try {
|
|
110
|
+
process.stdout.write(`${l}\n`);
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (CFG.remoteUrl) {
|
|
115
|
+
send(logs).catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
r();
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function send (logs) {
|
|
123
|
+
let a = 0,
|
|
124
|
+
max = CFG.remoteRetries + 1;
|
|
125
|
+
while (a < max) {
|
|
126
|
+
try {
|
|
127
|
+
const r = await fetch(CFG.remoteUrl, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
...(CFG.apiKey ? { 'x-api-key': CFG.apiKey } : {}),
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({ appName: CFG.appName, logs }),
|
|
134
|
+
signal: AbortSignal.timeout(CFG.remoteTimeout),
|
|
135
|
+
});
|
|
136
|
+
if (r.ok) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`HTTP ${r.status}`);
|
|
140
|
+
} catch {
|
|
141
|
+
a++;
|
|
142
|
+
if (a >= max) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await new Promise((r) => setTimeout(r, 200 * Math.pow(2, a - 1)));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class Logger {
|
|
151
|
+
constructor (ctx = {}) {
|
|
152
|
+
this.ctx = ctx;
|
|
153
|
+
this.appName = ctx.appName || CFG.appName;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
child (e = {}) {
|
|
157
|
+
return new Logger({ ...this.ctx, ...e });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_l (l, m, p) {
|
|
161
|
+
if (!shouldLog(l)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
enq(fmt(l, this.appName, this.ctx, m, p));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
debug (m, ...p) {
|
|
168
|
+
this._l('debug', m, p);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
info (m, ...p) {
|
|
172
|
+
this._l('info', m, p);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
warn (m, ...p) {
|
|
176
|
+
this._l('warn', m, p);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
error (m, ...p) {
|
|
180
|
+
this._l('error', m, p);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fatal (m, ...p) {
|
|
184
|
+
if (!shouldLog('fatal')) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const f = fmt('fatal', this.appName, this.ctx, m, p);
|
|
188
|
+
try {
|
|
189
|
+
process.stderr.write(`${f}\n`);
|
|
190
|
+
} catch {}
|
|
191
|
+
if (CFG.remoteUrl) {
|
|
192
|
+
send([f]).catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const root = new Logger();
|
|
198
|
+
|
|
199
|
+
root.configure = function (opts = {}) {
|
|
200
|
+
Object.assign(CFG, opts);
|
|
201
|
+
if (opts.interceptConsole) {
|
|
202
|
+
root.install();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
root.create = function (ctx = {}) {
|
|
206
|
+
return new Logger(ctx);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
root.install = function () {
|
|
210
|
+
if (state.installed) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
state.installed = true;
|
|
214
|
+
state.orig = {
|
|
215
|
+
log: console.log.bind(console),
|
|
216
|
+
warn: console.warn.bind(console),
|
|
217
|
+
error: console.error.bind(console),
|
|
218
|
+
debug: console.debug.bind(console),
|
|
219
|
+
info: console.info.bind(console),
|
|
220
|
+
};
|
|
221
|
+
const mk = (l, o) =>
|
|
222
|
+
function (...a) {
|
|
223
|
+
o(...a);
|
|
224
|
+
const m = a
|
|
225
|
+
.map((x) => (typeof x === 'string' ? x : safeStr(x)))
|
|
226
|
+
.join(' ');
|
|
227
|
+
if (shouldLog(l)) {
|
|
228
|
+
enq(fmt(l, CFG.appName, {}, m, []));
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
console.log = mk('info', state.orig.log);
|
|
232
|
+
console.info = mk('info', state.orig.info);
|
|
233
|
+
console.warn = mk('warn', state.orig.warn);
|
|
234
|
+
console.error = mk('error', state.orig.error);
|
|
235
|
+
console.debug = mk('debug', state.orig.debug);
|
|
236
|
+
};
|
|
237
|
+
root.uninstall = function () {
|
|
238
|
+
if (!state.installed || !state.orig) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
Object.assign(console, state.orig);
|
|
242
|
+
state.installed = false;
|
|
243
|
+
};
|
|
244
|
+
root.console = {
|
|
245
|
+
log: (...a) => root.info(...a),
|
|
246
|
+
info: (...a) => root.info(...a),
|
|
247
|
+
warn: (...a) => root.warn(...a),
|
|
248
|
+
error: (...a) => root.error(...a),
|
|
249
|
+
debug: (...a) => root.debug(...a),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
root.requestLogger = function () {
|
|
253
|
+
return function (req, res, next) {
|
|
254
|
+
const raw = req.path || req.url?.split('?')[0] || '/';
|
|
255
|
+
if (CFG.skipPaths.some((p) => raw.startsWith(p))) {
|
|
256
|
+
return next();
|
|
257
|
+
}
|
|
258
|
+
const t = process.hrtime.bigint(),
|
|
259
|
+
rid = req.headers['x-request-id'] || crypto.randomUUID(),
|
|
260
|
+
rsz = parseInt(req.headers['content-length'] || '0', 10) || 0;
|
|
261
|
+
req.requestId = rid;
|
|
262
|
+
res.setHeader('X-Request-Id', rid);
|
|
263
|
+
let done = false;
|
|
264
|
+
const rec = () => {
|
|
265
|
+
if (done) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
done = true;
|
|
269
|
+
const ms = Math.round(Number(process.hrtime.bigint() - t) / 1e4) / 100,
|
|
270
|
+
sc = res.statusCode,
|
|
271
|
+
p = (req.baseUrl || '') + (req.route ? req.route.path : raw);
|
|
272
|
+
enq(
|
|
273
|
+
JSON.stringify({
|
|
274
|
+
ts: new Date().toISOString(),
|
|
275
|
+
level: sc >= 500 ? 'error' : sc >= 400 ? 'warn' : 'info',
|
|
276
|
+
appName: `${CFG.appName}-requests`,
|
|
277
|
+
type: 'api_request',
|
|
278
|
+
requestId: rid,
|
|
279
|
+
method: req.method,
|
|
280
|
+
path: p,
|
|
281
|
+
statusCode: sc,
|
|
282
|
+
durationMs: ms,
|
|
283
|
+
reqSizeBytes: rsz,
|
|
284
|
+
resSizeBytes:
|
|
285
|
+
parseInt(res.getHeader('content-length') || '0', 10) || 0,
|
|
286
|
+
message: `${req.method} ${p} ${sc} ${ms}ms`,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
res.once('finish', rec);
|
|
291
|
+
res.once('close', rec);
|
|
292
|
+
next();
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
setInterval(() => flush(), CFG.flushInterval).unref();
|
|
297
|
+
process.on('beforeExit', () => flush());
|
|
298
|
+
process.on('SIGINT', () => {
|
|
299
|
+
flush();
|
|
300
|
+
process.exit(0);
|
|
301
|
+
});
|
|
302
|
+
process.on('SIGTERM', () => {
|
|
303
|
+
flush();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
});
|
|
306
|
+
if (CFG.interceptConsole) {
|
|
307
|
+
root.install();
|
|
308
|
+
}
|
|
309
|
+
module.exports = root;
|
package/config/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ROOT = path.join(__dirname, '..');
|
|
4
|
+
try {
|
|
5
|
+
require('dotenv').config({ path: path.join(ROOT, '.env') });
|
|
6
|
+
} catch (_) {}
|
|
7
|
+
|
|
8
|
+
function str (n, f) {
|
|
9
|
+
return process.env[n] || f;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function int (n, f) {
|
|
13
|
+
const v = process.env[n];
|
|
14
|
+
if (!v) {
|
|
15
|
+
return f;
|
|
16
|
+
}
|
|
17
|
+
const i = parseInt(v, 10);
|
|
18
|
+
if (isNaN(i)) {
|
|
19
|
+
throw new Error(`[Config] ${n} must be integer`);
|
|
20
|
+
}
|
|
21
|
+
return i;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bool (n, f) {
|
|
25
|
+
const v = process.env[n];
|
|
26
|
+
if (!v) {
|
|
27
|
+
return f;
|
|
28
|
+
}
|
|
29
|
+
return v === 'true' || v === '1';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const JWT_SECRET = str('JWT_SECRET', '');
|
|
33
|
+
const NODE_ENV = str('NODE_ENV', 'development');
|
|
34
|
+
|
|
35
|
+
if (NODE_ENV === 'production') {
|
|
36
|
+
if (!JWT_SECRET || JWT_SECRET.length < 32) {
|
|
37
|
+
throw new Error('[Config] JWT_SECRET must be ≥32 chars in production');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DATA_DIR = path.resolve(str('DATA_DIR', path.join(ROOT, 'data')));
|
|
42
|
+
const LOG_BASE_DIR = path.resolve(str('LOG_BASE_DIR', path.join(ROOT, 'logs')));
|
|
43
|
+
|
|
44
|
+
const cfg = {
|
|
45
|
+
NODE_ENV,
|
|
46
|
+
PORT: int('PORT', 9900),
|
|
47
|
+
LOG_BASE_DIR,
|
|
48
|
+
DATA_DIR,
|
|
49
|
+
RETENTION_DAYS: int('RETENTION_DAYS', 7),
|
|
50
|
+
CLEANUP_INTERVAL: int('CLEANUP_INTERVAL', 86400000),
|
|
51
|
+
BATCH_SIZE: int('BATCH_SIZE', 20),
|
|
52
|
+
BATCH_TIMEOUT: int('BATCH_TIMEOUT', 200),
|
|
53
|
+
JWT_SECRET: JWT_SECRET || 'dev-secret-change-in-production',
|
|
54
|
+
JWT_EXPIRES_IN: str('JWT_EXPIRES_IN', '24h'),
|
|
55
|
+
SESSION_NAME: str('SESSION_NAME', 'logboard_session'),
|
|
56
|
+
MAX_BODY_SIZE: str('MAX_BODY_SIZE', '10mb'),
|
|
57
|
+
ENABLE_STREAM: bool('ENABLE_STREAM', true),
|
|
58
|
+
CORS_ORIGINS: str('CORS_ORIGINS', 'http://localhost:9900')
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((s) => s.trim()),
|
|
61
|
+
WEBHOOK_URL: str('WEBHOOK_URL', ''),
|
|
62
|
+
TENANT_MODE: bool('TENANT_MODE', true), // multi-tenancy enabled by default now
|
|
63
|
+
|
|
64
|
+
// ── Legacy flat paths (used for super-admin + backward compat) ────────────
|
|
65
|
+
USERS_FILE: path.join(DATA_DIR, 'users.json'),
|
|
66
|
+
SETTINGS_FILE: path.join(DATA_DIR, 'settings.json'),
|
|
67
|
+
ROLES_FILE: path.join(DATA_DIR, 'role-config.json'),
|
|
68
|
+
API_KEYS_FILE: path.join(DATA_DIR, 'api-keys.json'),
|
|
69
|
+
AUDIT_FILE: path.join(DATA_DIR, 'audit.ndjson'),
|
|
70
|
+
ALERTS_FILE: path.join(DATA_DIR, 'alerts.json'),
|
|
71
|
+
ARCHIVE_DIR: path.join(LOG_BASE_DIR, '_archive'),
|
|
72
|
+
TENANTS_FILE: path.join(DATA_DIR, 'tenants.json'),
|
|
73
|
+
BOOKMARKS_FILE: path.join(DATA_DIR, 'bookmarks.json'),
|
|
74
|
+
SAVED_SEARCHES_FILE: path.join(DATA_DIR, 'saved-searches.json'),
|
|
75
|
+
NOTIFICATIONS_FILE: path.join(DATA_DIR, 'notifications.json'),
|
|
76
|
+
TEAMS_FILE: path.join(DATA_DIR, 'teams.json'),
|
|
77
|
+
DASHBOARDS_FILE: path.join(DATA_DIR, 'dashboards.json'),
|
|
78
|
+
|
|
79
|
+
// ── Org-aware path helpers ────────────────────────────────────────────────
|
|
80
|
+
// Returns the data dir for an org (or global if no org)
|
|
81
|
+
orgDataDir (orgSlug) {
|
|
82
|
+
return orgSlug ? path.join(DATA_DIR, 'orgs', orgSlug) : DATA_DIR;
|
|
83
|
+
},
|
|
84
|
+
orgLogsDir (orgSlug) {
|
|
85
|
+
return orgSlug ? path.join(LOG_BASE_DIR, orgSlug) : LOG_BASE_DIR;
|
|
86
|
+
},
|
|
87
|
+
orgUsersFile (s) {
|
|
88
|
+
return path.join(cfg.orgDataDir(s), 'users.json');
|
|
89
|
+
},
|
|
90
|
+
orgSettingsFile (s) {
|
|
91
|
+
return path.join(cfg.orgDataDir(s), 'settings.json');
|
|
92
|
+
},
|
|
93
|
+
orgRolesFile (s) {
|
|
94
|
+
return path.join(cfg.orgDataDir(s), 'role-config.json');
|
|
95
|
+
},
|
|
96
|
+
orgApiKeysFile (s) {
|
|
97
|
+
return path.join(cfg.orgDataDir(s), 'api-keys.json');
|
|
98
|
+
},
|
|
99
|
+
orgAuditFile (s) {
|
|
100
|
+
return path.join(cfg.orgDataDir(s), 'audit.ndjson');
|
|
101
|
+
},
|
|
102
|
+
orgAlertsFile (s) {
|
|
103
|
+
return path.join(cfg.orgDataDir(s), 'alerts.json');
|
|
104
|
+
},
|
|
105
|
+
orgBookmarksFile (s) {
|
|
106
|
+
return path.join(cfg.orgDataDir(s), 'bookmarks.json');
|
|
107
|
+
},
|
|
108
|
+
orgSavedSearchesFile (s) {
|
|
109
|
+
return path.join(cfg.orgDataDir(s), 'saved-searches.json');
|
|
110
|
+
},
|
|
111
|
+
orgNotificationsFile (s) {
|
|
112
|
+
return path.join(cfg.orgDataDir(s), 'notifications.json');
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Branding
|
|
116
|
+
APP_NAME: str('APP_NAME', 'LogBoard'),
|
|
117
|
+
APP_LOGO_URL: str('APP_LOGO_URL', '/public/logo.png'),
|
|
118
|
+
|
|
119
|
+
// Health thresholds
|
|
120
|
+
ALERT_RAM_PCT: int('ALERT_RAM_PCT', 85),
|
|
121
|
+
ALERT_CPU_PCT: int('ALERT_CPU_PCT', 90),
|
|
122
|
+
ALERT_DISK_PCT: int('ALERT_DISK_PCT', 90),
|
|
123
|
+
|
|
124
|
+
// OAuth
|
|
125
|
+
GITHUB_CLIENT_ID: str('GITHUB_CLIENT_ID', ''),
|
|
126
|
+
GITHUB_CLIENT_SECRET: str('GITHUB_CLIENT_SECRET', ''),
|
|
127
|
+
GOOGLE_CLIENT_ID: str('GOOGLE_CLIENT_ID', ''),
|
|
128
|
+
GOOGLE_CLIENT_SECRET: str('GOOGLE_CLIENT_SECRET', ''),
|
|
129
|
+
OAUTH_CALLBACK_BASE: str('OAUTH_CALLBACK_BASE', 'http://localhost:9900'),
|
|
130
|
+
|
|
131
|
+
// Email
|
|
132
|
+
SMTP_HOST: str('SMTP_HOST', ''),
|
|
133
|
+
SMTP_PORT: int('SMTP_PORT', 587),
|
|
134
|
+
SMTP_USER: str('SMTP_USER', ''),
|
|
135
|
+
SMTP_PASS: str('SMTP_PASS', ''),
|
|
136
|
+
SMTP_FROM: str('SMTP_FROM', 'logboard@example.com'),
|
|
137
|
+
SENDGRID_KEY: str('SENDGRID_API_KEY', ''),
|
|
138
|
+
PAGERDUTY_KEY: str('PAGERDUTY_ROUTING_KEY', ''),
|
|
139
|
+
API_KEY: str('API_KEY', 'change-me-secret-key'),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
module.exports = cfg;
|
package/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class AnalyticsController {
|
|
4
|
+
constructor (analyticsService) { this.svc = analyticsService; }
|
|
5
|
+
|
|
6
|
+
async overview (req, res) {
|
|
7
|
+
try { res.json(await this.svc.getOverview()); } catch (err) { res.status(500).json({ error: err.message }); }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async hourly (req, res) {
|
|
11
|
+
try {
|
|
12
|
+
const { service, date } = req.query;
|
|
13
|
+
res.json(await this.svc.getHourlyVolume(service, date));
|
|
14
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async levels (req, res) {
|
|
18
|
+
try {
|
|
19
|
+
const { service, date } = req.query;
|
|
20
|
+
res.json(await this.svc.getLevelBreakdown(service, date));
|
|
21
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async topServices (req, res) {
|
|
25
|
+
try {
|
|
26
|
+
const { date, top } = req.query;
|
|
27
|
+
res.json(await this.svc.getTopServices(date, parseInt(top, 10) || 10));
|
|
28
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async recentErrors (req, res) {
|
|
32
|
+
try {
|
|
33
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
|
|
34
|
+
res.json(await this.svc.getRecentErrors(limit));
|
|
35
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async trend (req, res) {
|
|
39
|
+
try {
|
|
40
|
+
const { service, days } = req.query;
|
|
41
|
+
res.json(await this.svc.getDailyTrend(service, parseInt(days, 10) || 7));
|
|
42
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = AnalyticsController;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class ApiAnalyticsController {
|
|
4
|
+
constructor (apiAnalyticsService) { this.svc = apiAnalyticsService; }
|
|
5
|
+
|
|
6
|
+
async services (req, res) {
|
|
7
|
+
try { res.json(await this.svc.getApiServices()); } catch (err) { res.status(500).json({ error: err.message }); }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async overview (req, res) {
|
|
11
|
+
try {
|
|
12
|
+
const { service, date } = req.query;
|
|
13
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
14
|
+
res.json(await this.svc.getOverview(service, date));
|
|
15
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async hourly (req, res) {
|
|
19
|
+
try {
|
|
20
|
+
const { service, date } = req.query;
|
|
21
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
22
|
+
res.json(await this.svc.getHourlyVolume(service, date));
|
|
23
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async endpoints (req, res) {
|
|
27
|
+
try {
|
|
28
|
+
const { service, date } = req.query;
|
|
29
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
30
|
+
res.json(await this.svc.getEndpointStats(service, date));
|
|
31
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async slowest (req, res) {
|
|
35
|
+
try {
|
|
36
|
+
const { service, date } = req.query;
|
|
37
|
+
const topN = Math.min(parseInt(req.query.top, 10) || 10, 50);
|
|
38
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
39
|
+
res.json(await this.svc.getTopSlowest(service, date, topN));
|
|
40
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async topErrors (req, res) {
|
|
44
|
+
try {
|
|
45
|
+
const { service, date } = req.query;
|
|
46
|
+
const topN = Math.min(parseInt(req.query.top, 10) || 10, 50);
|
|
47
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
48
|
+
res.json(await this.svc.getTopErrors(service, date, topN));
|
|
49
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async statusDist (req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const { service, date } = req.query;
|
|
55
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
56
|
+
res.json(await this.svc.getStatusDistribution(service, date));
|
|
57
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async apdex (req, res) {
|
|
61
|
+
try {
|
|
62
|
+
const { service, date } = req.query;
|
|
63
|
+
const tMs = parseInt(req.query.t, 10) || 200;
|
|
64
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
65
|
+
res.json(await this.svc.getApdex(service, date, tMs));
|
|
66
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async trend (req, res) {
|
|
70
|
+
try {
|
|
71
|
+
const { service } = req.query;
|
|
72
|
+
const days = Math.min(parseInt(req.query.days, 10) || 7, 30);
|
|
73
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
74
|
+
res.json(await this.svc.getDailyTrend(service, days));
|
|
75
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async heaviest (req, res) {
|
|
79
|
+
try {
|
|
80
|
+
const { service, date } = req.query;
|
|
81
|
+
const topN = Math.min(parseInt(req.query.top, 10) || 10, 50);
|
|
82
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
83
|
+
res.json(await this.svc.getTopHeaviest(service, date, topN));
|
|
84
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async individualSlowest (req, res) {
|
|
88
|
+
try {
|
|
89
|
+
const { service, date } = req.query;
|
|
90
|
+
const topN = Math.min(parseInt(req.query.top, 10) || 20, 100);
|
|
91
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
92
|
+
res.json(await this.svc.getIndividualSlowest(service, date, topN));
|
|
93
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async peakRpm (req, res) {
|
|
97
|
+
try {
|
|
98
|
+
const { service, date } = req.query;
|
|
99
|
+
if (!service) { return res.status(400).json({ error: 'service required' }); }
|
|
100
|
+
res.json(await this.svc.getPeakRpm(service, date));
|
|
101
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async slowTrend (req, res) {
|
|
105
|
+
try {
|
|
106
|
+
const { service, start, end } = req.query;
|
|
107
|
+
if (!service || !start || !end) { return res.status(400).json({ error: 'service, start, end required' }); }
|
|
108
|
+
res.json(await this.svc.getSlowTrend(service, start, end));
|
|
109
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async errorTrend (req, res) {
|
|
113
|
+
try {
|
|
114
|
+
const { service, start, end } = req.query;
|
|
115
|
+
if (!service || !start || !end) { return res.status(400).json({ error: 'service, start, end required' }); }
|
|
116
|
+
res.json(await this.svc.getErrorTrend(service, start, end));
|
|
117
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async hourlyPattern (req, res) {
|
|
121
|
+
try {
|
|
122
|
+
const { service, start, end } = req.query;
|
|
123
|
+
if (!service || !start || !end) { return res.status(400).json({ error: 'service, start, end required' }); }
|
|
124
|
+
res.json(await this.svc.getHourlySlowPattern(service, start, end));
|
|
125
|
+
} catch (err) { res.status(500).json({ error: err.message }); }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = ApiAnalyticsController;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const audit = require('../services/AuditService');
|
|
2
|
+
('use strict');
|
|
3
|
+
const ApiKeyService = require('../services/ApiKeyService');
|
|
4
|
+
const svc = new ApiKeyService();
|
|
5
|
+
class ApiKeyController {
|
|
6
|
+
async list (req, res) {
|
|
7
|
+
try {
|
|
8
|
+
res.json(await svc.list());
|
|
9
|
+
} catch (e) {
|
|
10
|
+
res.status(500).json({ error: e.message });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async create (req, res) {
|
|
15
|
+
try {
|
|
16
|
+
const { name, scopes, expiresAt } = req.body;
|
|
17
|
+
res
|
|
18
|
+
.status(201)
|
|
19
|
+
.json(
|
|
20
|
+
await svc.create({
|
|
21
|
+
name,
|
|
22
|
+
scopes,
|
|
23
|
+
expiresAt,
|
|
24
|
+
createdBy: req.user.username,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async update (req, res) {
|
|
33
|
+
try {
|
|
34
|
+
res.json(await svc.update(req.params.id, req.body, req.user.username));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async revoke (req, res) {
|
|
41
|
+
try {
|
|
42
|
+
await svc.revoke(req.params.id, req.user.username);
|
|
43
|
+
res.json({ success: true });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async remove (req, res) {
|
|
50
|
+
try {
|
|
51
|
+
await svc.remove(req.params.id, req.user.username);
|
|
52
|
+
res.json({ success: true });
|
|
53
|
+
} catch (e) {
|
|
54
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
module.exports = ApiKeyController;
|