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 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 existing = loadJSON(CONFIG_FILE);
70
- saveJSON(CONFIG_FILE, { ...existing, ...config });
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 || 50,
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 (flags.level) query.severity = flags.level;
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 time = ui.c.dim(log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : '');
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 time = ui.c.dim(log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : '');
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
- if (!dateStr) return '—';
271
- const diff = Date.now() - new Date(dateStr).getTime();
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 new Date(dateStr).toLocaleDateString();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.17.0",
3
+ "version": "5.17.1",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",