Updated Logging System to Include Application Logs

This commit is contained in:
2026-06-08 12:25:47 -05:00
parent 15e93c1e45
commit 9a9f890494
13 changed files with 323 additions and 12 deletions

View File

@@ -28,6 +28,7 @@ const CAPABILITIES = [
{ key: 'admin.roles', label: 'Manage roles', group: 'Administration' },
{ key: 'admin.security', label: 'Manage password & lockout policy', group: 'Administration' },
{ key: 'admin.audit', label: 'View activity & audit trail', group: 'Administration' },
{ key: 'admin.logs', label: 'View application logs', group: 'Administration' },
{ key: 'admin.settings', label: 'Manage application settings', group: 'Administration' },
{ key: 'admin.integrations', label: 'Manage integrations (email, webhook, ServiceNow, Workday)', group: 'Administration' }
];

View File

@@ -0,0 +1,58 @@
const fs = require('fs');
const { LOG_FILE } = require('../logger');
// Read the application/event log (JSON lines written by the Winston file transport) for the admin
// Application Logs viewer. Reads the current log file, parses the most recent lines, and returns them
// newest-first with optional category/level/search filtering plus the available filter facets.
const SCAN_LINES = 8000; // how many trailing lines to parse (the file is size-rotated, ~5MB)
function readLogs(filters = {}, limit = 500) {
let lines = [];
try {
lines = fs.readFileSync(LOG_FILE, 'utf8').split('\n').filter(Boolean);
} catch {
return { logs: [], total: 0, categories: [], levels: [] };
}
// Parse the trailing window, newest first.
const parsed = [];
for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < SCAN_LINES; i -= 1, scanned += 1) {
try {
const obj = JSON.parse(lines[i]);
parsed.push({
timestamp: obj.timestamp || null,
level: obj.level || 'info',
category: obj.category || 'system',
message: obj.message || '',
meta: stripMeta(obj)
});
} catch {
// skip malformed lines
}
}
const categories = [...new Set(parsed.map((p) => p.category))].sort();
const levels = [...new Set(parsed.map((p) => p.level))].sort();
let filtered = parsed;
if (filters.category) filtered = filtered.filter((p) => p.category === filters.category);
if (filters.level) filtered = filtered.filter((p) => p.level === filters.level);
if (filters.q) {
const needle = String(filters.q).toLowerCase();
filtered = filtered.filter((p) => `${p.message} ${JSON.stringify(p.meta)}`.toLowerCase().includes(needle));
}
return { logs: filtered.slice(0, Math.min(2000, Number(limit) || 500)), total: filtered.length, categories, levels };
}
// Everything on a log line except the standard fields is "meta" (the structured detail).
function stripMeta(obj) {
const meta = {};
for (const [key, value] of Object.entries(obj)) {
if (!['timestamp', 'level', 'category', 'message'].includes(key)) meta[key] = value;
}
return meta;
}
module.exports = { readLogs };

View File

@@ -1,5 +1,6 @@
const nodemailer = require('nodemailer');
const { all, audit, now, run } = require('../db');
const { logEvent } = require('../logger');
function readSettings() {
const rows = all('SELECT key, value FROM app_settings');
@@ -95,13 +96,20 @@ async function sendMail({ to, subject, html, text }) {
throw Object.assign(new Error('No recipients are configured'), { status: 400 });
}
const transport = getTransport();
return transport.sendMail({
from: c.from || c.user,
to: recipients.join(', '),
subject,
html,
text
});
try {
const info = await transport.sendMail({
from: c.from || c.user,
to: recipients.join(', '),
subject,
html,
text
});
logEvent('smtp', `Email sent: "${subject}" to ${recipients.length} recipient(s)`, { subject, recipients: recipients.length, host: c.host, messageId: info.messageId });
return info;
} catch (error) {
logEvent('smtp', `Email send failed: "${subject}" — ${error.message}`, { subject, recipients: recipients.length, host: c.host }, 'error');
throw error;
}
}
async function sendTestEmail(to, userId) {

View File

@@ -3,6 +3,7 @@
// Office 365 connector ("MessageCard"). Best-effort delivery — failures are counted, never thrown,
// so a bad Teams endpoint can't break the alert cycle.
const { all, audit, now, run } = require('../db');
const { logEvent } = require('../logger');
const TIMEOUT_MS = 10000;
const FORMATS = ['adaptive', 'messagecard'];
@@ -161,8 +162,10 @@ async function deliverAlerts(alerts) {
if (!relevant.length) return { attempted: false, delivered: 0, failed: 0 };
try {
const result = await postPayload(c.webhookUrl, buildPayload(relevant, c));
logEvent('teams', `Posted ${relevant.length} alert(s) to Teams (HTTP ${result.status})`, { count: relevant.length, status: result.status, format: c.format }, result.ok ? 'info' : 'warn');
return { attempted: true, delivered: result.ok ? 1 : 0, failed: result.ok ? 0 : 1, status: result.status, count: relevant.length };
} catch {
} catch (error) {
logEvent('teams', `Teams alert post failed: ${error.message}`, { count: relevant.length }, 'error');
return { attempted: true, delivered: 0, failed: 1 };
}
}
@@ -177,8 +180,10 @@ async function sendTest(userId) {
try {
result = await postPayload(c.webhookUrl, payload);
} catch (error) {
logEvent('teams', `Test card request failed: ${error.message}`, {}, 'error');
throw Object.assign(new Error(`Teams request failed: ${error.message}`), { status: 400 });
}
logEvent('teams', `Test card sent (HTTP ${result.status})`, { status: result.status, format: c.format }, result.ok ? 'info' : 'warn');
audit(userId, 'send', 'teams_test', null, null, { status: result.status });
if (!result.ok) throw Object.assign(new Error(`Teams endpoint returned HTTP ${result.status}`), { status: 400 });
return { ok: true, status: result.status };

View File

@@ -4,6 +4,7 @@
const moment = require('moment');
const crypto = require('crypto');
const { audit } = require('../db');
const { logEvent } = require('../logger');
const { getConfig } = require('./notifications');
const TIMEOUT_MS = 10000;
@@ -74,11 +75,12 @@ async function deliverAlerts(alerts) {
try {
// Note: we build the payload for the alert using the buildPayload function, which creates a stable and flat JSON object with relevant information about the alert, and then we post it to the webhook URL using the postJson function; if the postJson function returns an OK response, we increment the delivered count, otherwise we increment the failed count, allowing us to track the success and failure of each delivery attempt.
const result = await postJson(c.webhookUrl, c.webhookSecret, buildPayload(alert));
if (result.ok) delivered += 1;
else failed += 1;
if (result.ok) { delivered += 1; logEvent('webhook', `Alert delivered: ${alert.title}`, { url: c.webhookUrl, status: result.status, alert_id: alert.id }); }
else { failed += 1; logEvent('webhook', `Alert delivery returned HTTP ${result.status}: ${alert.title}`, { url: c.webhookUrl, status: result.status, alert_id: alert.id }, 'warn'); }
} catch (error) {
console.log(`${moment().format()} ❌ Failed to deliver alert to webhook: ${error.message}`);
failed += 1;
logEvent('webhook', `Alert delivery failed: ${alert.title}${error.message}`, { url: c.webhookUrl, alert_id: alert.id }, 'error');
}
}
console.log(`${moment().format()} 🚚 Webhook delivery complete: ${delivered} delivered, ${failed} failed`);
@@ -108,9 +110,11 @@ async function sendTestWebhook(userId) {
result = await postJson(c.webhookUrl, c.webhookSecret, payload);
} catch (error) {
console.log(`${moment().format()} ❌ Test webhook failed: ${error.message}`);
logEvent('webhook', `Test webhook request failed: ${error.message}`, { url: c.webhookUrl }, 'error');
throw Object.assign(new Error(`Webhook request failed: ${error.message}`), { status: 400 });
}
console.log(`${moment().format()} 📡 Test webhook sent successfully to: ${c.webhookUrl}`) ;
logEvent('webhook', `Test webhook sent (HTTP ${result.status})`, { url: c.webhookUrl, status: result.status }, result.ok ? 'info' : 'warn');
audit(userId, 'send', 'test_webhook', null, null, { url: c.webhookUrl, status: result.status });
if (!result.ok) {
// Note: if the response from the webhook endpoint is not OK (i.e., not in the 200-299 range), we log an error message with the HTTP status code returned by the endpoint and throw an error indicating that the webhook endpoint returned a non-OK status; this allows us to provide clear feedback about why the test webhook failed and helps users understand that their webhook endpoint may be rejecting the request or encountering an error when processing it.