@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.
Files changed (114) hide show
  1. package/.env.example +37 -0
  2. package/README.md +200 -0
  3. package/bin/logboard +536 -0
  4. package/client/logger.js +309 -0
  5. package/config/index.js +142 -0
  6. package/config.js +2 -0
  7. package/controllers/AnalyticsController.js +46 -0
  8. package/controllers/ApiAnalyticsController.js +129 -0
  9. package/controllers/ApiKeyController.js +58 -0
  10. package/controllers/AuthController.js +131 -0
  11. package/controllers/HealthController.js +56 -0
  12. package/controllers/LogController.js +197 -0
  13. package/controllers/OrgController.js +152 -0
  14. package/controllers/RoleConfigController.js +20 -0
  15. package/controllers/SettingsController.js +39 -0
  16. package/controllers/StreamController.js +55 -0
  17. package/controllers/UiController.js +789 -0
  18. package/controllers/UserController.js +79 -0
  19. package/lib/batchWriter.js +57 -0
  20. package/lib/cleanup.js +67 -0
  21. package/lib/ejs.js +103 -0
  22. package/lib/emitter.js +5 -0
  23. package/lib/healthMonitor.js +245 -0
  24. package/lib/logger.js +21 -0
  25. package/lib/streams.js +32 -0
  26. package/lib/theme.js +77 -0
  27. package/lib/userStore.js +13 -0
  28. package/lib/utils.js +44 -0
  29. package/middleware/apiKey.js +82 -0
  30. package/middleware/auth.js +55 -0
  31. package/middleware/ipWhitelist.js +59 -0
  32. package/middleware/org.js +85 -0
  33. package/middleware/pageAccess.js +20 -0
  34. package/middleware/rateLimit.js +29 -0
  35. package/middleware/roles.js +11 -0
  36. package/package.json +77 -0
  37. package/routes/alerts.js +18 -0
  38. package/routes/analytics.js +26 -0
  39. package/routes/api-analytics.js +30 -0
  40. package/routes/api-keys.js +12 -0
  41. package/routes/archive.js +91 -0
  42. package/routes/audit.js +50 -0
  43. package/routes/auth.js +22 -0
  44. package/routes/bookmarks.js +13 -0
  45. package/routes/health.js +11 -0
  46. package/routes/logs.js +88 -0
  47. package/routes/metrics.js +66 -0
  48. package/routes/notifications.js +14 -0
  49. package/routes/orgs.js +98 -0
  50. package/routes/registration.js +202 -0
  51. package/routes/role-config.js +97 -0
  52. package/routes/saved-searches.js +12 -0
  53. package/routes/server.js +151 -0
  54. package/routes/settings.js +28 -0
  55. package/routes/status.js +21 -0
  56. package/routes/stream.js +11 -0
  57. package/routes/super.js +129 -0
  58. package/routes/ui.js +120 -0
  59. package/routes/users.js +13 -0
  60. package/server.js +172 -0
  61. package/services/AlertRulesService.js +323 -0
  62. package/services/AnalyticsService.js +665 -0
  63. package/services/ApiAnalyticsService.js +471 -0
  64. package/services/ApiKeyService.js +166 -0
  65. package/services/AuditService.js +249 -0
  66. package/services/AuthService.js +234 -0
  67. package/services/BookmarkService.js +49 -0
  68. package/services/GlobalSettingsService.js +44 -0
  69. package/services/LogService.js +1066 -0
  70. package/services/MetricsService.js +116 -0
  71. package/services/NotificationService.js +70 -0
  72. package/services/OrgService.js +217 -0
  73. package/services/ReportService.js +247 -0
  74. package/services/RoleConfigService.js +201 -0
  75. package/services/SavedSearchService.js +63 -0
  76. package/services/SettingsService.js +220 -0
  77. package/services/UserService.js +121 -0
  78. package/setup.js +132 -0
  79. package/views/404.ejs +8 -0
  80. package/views/alerts.ejs +190 -0
  81. package/views/analytics.ejs +209 -0
  82. package/views/api-analytics.ejs +660 -0
  83. package/views/api-keys.ejs +150 -0
  84. package/views/archive.ejs +123 -0
  85. package/views/audit.ejs +314 -0
  86. package/views/bookmarks.ejs +54 -0
  87. package/views/custom-dashboard.ejs +162 -0
  88. package/views/dashboard.ejs +186 -0
  89. package/views/diff.ejs +98 -0
  90. package/views/health.ejs +269 -0
  91. package/views/heatmap.ejs +126 -0
  92. package/views/insights.ejs +334 -0
  93. package/views/invite.ejs +74 -0
  94. package/views/live.ejs +299 -0
  95. package/views/login.ejs +64 -0
  96. package/views/logo.png +0 -0
  97. package/views/logs.ejs +754 -0
  98. package/views/notifications.ejs +58 -0
  99. package/views/partials/head.ejs +282 -0
  100. package/views/partials/sidebar.ejs +168 -0
  101. package/views/register.ejs +100 -0
  102. package/views/roles.ejs +279 -0
  103. package/views/saved-searches.ejs +51 -0
  104. package/views/service-map.ejs +142 -0
  105. package/views/settings.ejs +1159 -0
  106. package/views/sidebar.ejs +129 -0
  107. package/views/status.ejs +100 -0
  108. package/views/super-admin-admins.ejs +58 -0
  109. package/views/super-admin-analytics.ejs +49 -0
  110. package/views/super-admin-orgs.ejs +310 -0
  111. package/views/super-admin-profile.ejs +77 -0
  112. package/views/super-admin-settings.ejs +108 -0
  113. package/views/super-admin-system.ejs +46 -0
  114. package/views/users.ejs +153 -0
@@ -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;
@@ -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,2 @@
1
+ // Backward-compat shim — all config now lives in config/index.js
2
+ module.exports = require('./config/index');
@@ -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;