securenow 5.17.0 → 5.17.1
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/cli/config.js +9 -3
- package/cli/monitor.js +54 -9
- package/cli/ui.js +31 -3
- package/cli.js +2 -2
- package/package.json +1 -1
package/cli/config.js
CHANGED
|
@@ -62,12 +62,18 @@ function getAuthSource() {
|
|
|
62
62
|
function loadConfig() {
|
|
63
63
|
const global = loadJSON(CONFIG_FILE);
|
|
64
64
|
const local = fs.existsSync(LOCAL_CONFIG_FILE) ? loadJSON(LOCAL_CONFIG_FILE) : {};
|
|
65
|
+
if (hasLocalCredentials()) {
|
|
66
|
+
const { defaultApp: _ignored, ...globalWithoutAccountScoped } = global;
|
|
67
|
+
return { ...DEFAULTS, ...globalWithoutAccountScoped, ...local };
|
|
68
|
+
}
|
|
65
69
|
return { ...DEFAULTS, ...global, ...local };
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
function saveConfig(config) {
|
|
69
|
-
const
|
|
70
|
-
|
|
72
|
+
function saveConfig(config, { local } = {}) {
|
|
73
|
+
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
74
|
+
const targetFile = useLocal ? LOCAL_CONFIG_FILE : CONFIG_FILE;
|
|
75
|
+
const existing = loadJSON(targetFile);
|
|
76
|
+
saveJSON(targetFile, { ...existing, ...config });
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
function getConfigValue(key) {
|
package/cli/monitor.js
CHANGED
|
@@ -163,6 +163,24 @@ async function tracesAnalyze(args, flags) {
|
|
|
163
163
|
|
|
164
164
|
// ── Logs ──
|
|
165
165
|
|
|
166
|
+
// Parse a duration string like "6h", "30m", "2d", "90s" into minutes.
|
|
167
|
+
// Returns null on unparseable input so the caller can error out clearly
|
|
168
|
+
// instead of silently falling through to a default.
|
|
169
|
+
function parseDurationToMinutes(value) {
|
|
170
|
+
if (value == null || value === '') return null;
|
|
171
|
+
const m = String(value).trim().match(/^(\d+)\s*(s|m|h|d)?$/i);
|
|
172
|
+
if (!m) return null;
|
|
173
|
+
const n = parseInt(m[1], 10);
|
|
174
|
+
const unit = (m[2] || 'm').toLowerCase();
|
|
175
|
+
if (unit === 's') return Math.max(1, Math.round(n / 60));
|
|
176
|
+
if (unit === 'm') return n;
|
|
177
|
+
if (unit === 'h') return n * 60;
|
|
178
|
+
if (unit === 'd') return n * 60 * 24;
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'WARNING', 'ERROR', 'FATAL'];
|
|
183
|
+
|
|
166
184
|
async function logsList(args, flags) {
|
|
167
185
|
requireAuth();
|
|
168
186
|
const appKey = resolveApp(flags);
|
|
@@ -171,17 +189,42 @@ async function logsList(args, flags) {
|
|
|
171
189
|
process.exit(1);
|
|
172
190
|
}
|
|
173
191
|
|
|
192
|
+
let minutes = 60;
|
|
193
|
+
if (flags.since != null) {
|
|
194
|
+
const parsed = parseDurationToMinutes(flags.since);
|
|
195
|
+
if (parsed == null) {
|
|
196
|
+
ui.error(`Invalid --since value "${flags.since}". Expected e.g. 30m, 6h, 2d.`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
minutes = parsed;
|
|
200
|
+
} else if (flags.minutes != null) {
|
|
201
|
+
const n = parseInt(flags.minutes, 10);
|
|
202
|
+
if (isNaN(n) || n <= 0) {
|
|
203
|
+
ui.error(`Invalid --minutes value "${flags.minutes}".`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
minutes = n;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let severity = null;
|
|
210
|
+
if (flags.level) {
|
|
211
|
+
severity = String(flags.level).toUpperCase();
|
|
212
|
+
if (!LOG_LEVELS.includes(severity)) {
|
|
213
|
+
ui.error(`Invalid --level "${flags.level}". Expected one of: ${LOG_LEVELS.join(', ').toLowerCase()}.`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
174
218
|
const s = ui.spinner('Fetching logs');
|
|
175
219
|
try {
|
|
176
|
-
const minutes = parseInt(flags.minutes || '60', 10);
|
|
177
220
|
const now = Date.now();
|
|
178
221
|
const query = {
|
|
179
222
|
appKeys: appKey,
|
|
180
|
-
limit: flags.limit ||
|
|
223
|
+
limit: flags.limit || 200,
|
|
181
224
|
from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
|
|
182
225
|
to: flags.end || new Date(now).toISOString(),
|
|
183
226
|
};
|
|
184
|
-
if (
|
|
227
|
+
if (severity) query.severity = severity;
|
|
185
228
|
|
|
186
229
|
const data = await api.get('/logs', { query });
|
|
187
230
|
const logs = data.logs || [];
|
|
@@ -193,11 +236,12 @@ async function logsList(args, flags) {
|
|
|
193
236
|
for (const log of logs) {
|
|
194
237
|
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
195
238
|
const levelColor = {
|
|
196
|
-
ERROR: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
197
|
-
INFO: ui.c.cyan, DEBUG: ui.c.dim,
|
|
239
|
+
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
240
|
+
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
198
241
|
}[level] || ui.c.white;
|
|
199
242
|
|
|
200
|
-
const
|
|
243
|
+
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
244
|
+
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
201
245
|
const body = log.body || log.message || log.severityText || '';
|
|
202
246
|
|
|
203
247
|
console.log(` ${time} ${levelColor(level.padEnd(7))} ${body}`);
|
|
@@ -233,11 +277,12 @@ async function logsTrace(args, flags) {
|
|
|
233
277
|
for (const log of logs) {
|
|
234
278
|
const level = (log.severityText || log.level || 'INFO').toUpperCase();
|
|
235
279
|
const levelColor = {
|
|
236
|
-
ERROR: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
237
|
-
INFO: ui.c.cyan, DEBUG: ui.c.dim,
|
|
280
|
+
ERROR: ui.c.red, FATAL: ui.c.red, WARN: ui.c.yellow, WARNING: ui.c.yellow,
|
|
281
|
+
INFO: ui.c.cyan, DEBUG: ui.c.dim, TRACE: ui.c.dim,
|
|
238
282
|
}[level] || ui.c.white;
|
|
239
283
|
|
|
240
|
-
const
|
|
284
|
+
const parsedTime = ui.parseTimestamp(log.timestamp);
|
|
285
|
+
const time = ui.c.dim(parsedTime ? parsedTime.toLocaleTimeString() : '');
|
|
241
286
|
console.log(` ${time} ${levelColor(level.padEnd(7))} ${log.body || log.message || ''}`);
|
|
242
287
|
}
|
|
243
288
|
console.log('');
|
package/cli/ui.js
CHANGED
|
@@ -266,9 +266,36 @@ function hr() {
|
|
|
266
266
|
console.log(c.dim('─'.repeat(width)));
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// Parses timestamps the backend returns in multiple formats:
|
|
270
|
+
// - UInt64 nanoseconds as a string (signoz logs) → "1775856777891000000"
|
|
271
|
+
// - ClickHouse DateTime64 string (signoz traces) → "2026-04-10 21:34:51.133000000" (UTC, no Z)
|
|
272
|
+
// - ISO 8601 → "2026-04-10T21:34:51.133Z"
|
|
273
|
+
// - number (ms) / Date → passthrough
|
|
274
|
+
function parseTimestamp(ts) {
|
|
275
|
+
if (ts == null || ts === '') return null;
|
|
276
|
+
if (ts instanceof Date) return isNaN(ts.getTime()) ? null : ts;
|
|
277
|
+
if (typeof ts === 'number') {
|
|
278
|
+
const d = new Date(ts);
|
|
279
|
+
return isNaN(d.getTime()) ? null : d;
|
|
280
|
+
}
|
|
281
|
+
const s = String(ts).trim();
|
|
282
|
+
if (/^\d{16,}$/.test(s)) {
|
|
283
|
+
const d = new Date(Number(s) / 1e6);
|
|
284
|
+
return isNaN(d.getTime()) ? null : d;
|
|
285
|
+
}
|
|
286
|
+
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(s) && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) {
|
|
287
|
+
const isoish = s.replace(' ', 'T').replace(/\.(\d{3})\d*$/, '.$1') + 'Z';
|
|
288
|
+
const d = new Date(isoish);
|
|
289
|
+
if (!isNaN(d.getTime())) return d;
|
|
290
|
+
}
|
|
291
|
+
const d = new Date(s);
|
|
292
|
+
return isNaN(d.getTime()) ? null : d;
|
|
293
|
+
}
|
|
294
|
+
|
|
269
295
|
function timeAgo(dateStr) {
|
|
270
|
-
|
|
271
|
-
|
|
296
|
+
const date = parseTimestamp(dateStr);
|
|
297
|
+
if (!date) return '—';
|
|
298
|
+
const diff = Date.now() - date.getTime();
|
|
272
299
|
const seconds = Math.floor(diff / 1000);
|
|
273
300
|
if (seconds < 60) return 'just now';
|
|
274
301
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -277,7 +304,7 @@ function timeAgo(dateStr) {
|
|
|
277
304
|
if (hours < 24) return `${hours}h ago`;
|
|
278
305
|
const days = Math.floor(hours / 24);
|
|
279
306
|
if (days < 30) return `${days}d ago`;
|
|
280
|
-
return
|
|
307
|
+
return date.toLocaleDateString();
|
|
281
308
|
}
|
|
282
309
|
|
|
283
310
|
function truncate(str, len = 50) {
|
|
@@ -351,6 +378,7 @@ module.exports = {
|
|
|
351
378
|
json,
|
|
352
379
|
hr,
|
|
353
380
|
timeAgo,
|
|
381
|
+
parseTimestamp,
|
|
354
382
|
truncate,
|
|
355
383
|
statusBadge,
|
|
356
384
|
httpStatusColor,
|
package/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const ui = require('./cli/ui');
|
|
@@ -90,7 +90,7 @@ const COMMANDS = {
|
|
|
90
90
|
desc: 'View application logs',
|
|
91
91
|
usage: 'securenow logs [options]',
|
|
92
92
|
sub: {
|
|
93
|
-
list: { desc: 'List recent logs', flags: { app: 'App key', limit: 'Max results', minutes: 'Time window in minutes', level: 'Filter by level' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
|
|
93
|
+
list: { desc: 'List recent logs', flags: { app: 'App key', limit: 'Max results (default 200)', since: 'Time window (e.g. 30m, 6h, 2d)', minutes: 'Time window in minutes (alias for --since Nm)', level: 'Filter by level (error, warn, info, debug)', start: 'Start time (ISO 8601)', end: 'End time (ISO 8601)' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
|
|
94
94
|
trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
|
|
95
95
|
},
|
|
96
96
|
defaultSub: 'list',
|
package/package.json
CHANGED