@thomkjel/logger
Version:
Security-focused event logging library for Next.js applications (Work in Progress)
236 lines (235 loc) • 10 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Logger = void 0;
class Logger {
constructor(config) {
this.environment = (config?.environment || process.env.NODE_ENV) || 'development';
this.sourceToken = config?.sourceToken || process.env.BETTERSTACK_SOURCE_TOKEN;
this.betterStackEndpoint = config?.betterStackEndpoint || process.env.BETTERSTACK_ENDPOINT || 'https://s1285049.eu-nbg-2.betterstackdata.com';
this.config = {
environment: this.environment,
sourceToken: this.sourceToken,
betterStackEndpoint: this.betterStackEndpoint,
rateLimitPerMinute: 1000, // Default rate limit for future SaaS
enableMetrics: true,
...config
};
this.metrics = {
totalLogs: 0,
logsByLevel: {
INFO: 0,
WARN: 0,
ERROR: 0,
CRITICAL: 0
},
logsByCategory: {
AUTHN: 0,
AUTHZ: 0,
SESSION: 0,
UPLOAD: 0,
INPUT: 0,
MALICIOUS: 0,
PRIVILEGE: 0,
DATA: 0,
SEQUENCE: 0,
SYS: 0,
USER: 0,
EXCESS: 0,
GENERAL: 0
},
lastLogTimestamp: new Date().toISOString(),
apiKeyUsage: {}
};
}
static getInstance(config) {
if (!Logger.instance) {
Logger.instance = new Logger(config);
}
return Logger.instance;
}
async log(level, eventType, metadata = {}) {
const category = this.getCategoryFromEvent(eventType);
// Update metrics if enabled
if (this.config.enableMetrics) {
this.updateMetrics(level, category);
}
// Future SaaS feature: API key validation and rate limiting
if (this.config.apiKey) {
const rateLimitCheck = this.checkRateLimit(this.config.apiKey);
if (!rateLimitCheck) {
console.warn('Rate limit exceeded for API key');
return;
}
}
const logData = {
dt: new Date().toISOString(),
level: level.toLowerCase(),
event: eventType,
category: category,
message: this.formatMessage(eventType, metadata),
context: {
environment: this.config.environment || this.environment,
...(this.config.apiKey && { apiKey: this.config.apiKey }),
...metadata,
},
};
// In development, log to console
if (this.config.environment === 'development') {
console.log(JSON.stringify(logData, null, 2));
}
// In production, send to Better Stack - but don't block
if (this.config.sourceToken) {
this.sendToBetterStack(logData).catch((error) => {
console.error('Logging error:', error.message);
});
}
}
async sendToBetterStack(logData) {
const response = await fetch(this.config.betterStackEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.sourceToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(logData),
});
if (!response.ok) {
throw new Error(`Better Stack logging failed: ${response.statusText}`);
}
}
updateMetrics(level, category) {
this.metrics.totalLogs++;
this.metrics.logsByLevel[level]++;
this.metrics.logsByCategory[category]++;
this.metrics.lastLogTimestamp = new Date().toISOString();
// Update API key usage metrics if applicable
if (this.config.apiKey && this.metrics.apiKeyUsage) {
this.metrics.apiKeyUsage[this.config.apiKey] =
(this.metrics.apiKeyUsage[this.config.apiKey] || 0) + 1;
}
}
checkRateLimit(apiKey) {
// Future SaaS feature: implement actual rate limiting logic
// For now, always return true
return true;
}
getCategoryFromEvent(eventType) {
if (eventType.startsWith('authn_'))
return 'AUTHN';
if (eventType.startsWith('authz_'))
return 'AUTHZ';
if (eventType.startsWith('session_'))
return 'SESSION';
if (eventType.startsWith('upload_'))
return 'UPLOAD';
if (eventType.startsWith('input_'))
return 'INPUT';
if (eventType.startsWith('malicious_'))
return 'MALICIOUS';
if (eventType.startsWith('privilege_'))
return 'PRIVILEGE';
if (eventType.startsWith('sensitive_'))
return 'DATA';
if (eventType.startsWith('sequence_'))
return 'SEQUENCE';
if (eventType.startsWith('sys_'))
return 'SYS';
if (eventType.startsWith('user_'))
return 'USER';
if (eventType.startsWith('excess_'))
return 'EXCESS';
return 'GENERAL';
}
formatMessage(eventType, metadata) {
switch (eventType) {
// Authentication events
case 'authn_login_success':
return `User ${metadata.userid} logged in successfully`;
case 'authn_login_successafterfail':
return `User ${metadata.userid} logged in successfully after ${metadata.retries || 'multiple'} failed attempts`;
case 'authn_login_fail':
return `User ${metadata.userid} login failed`;
case 'authn_login_fail_max':
return `User ${metadata.userid} reached the login fail limit of ${metadata.maxlimit}`;
case 'authn_login_lock':
return `User ${metadata.userid} login locked because ${metadata.reason || 'unknown reason'}`;
case 'authn_password_change':
return `User ${metadata.userid} has successfully changed their password`;
case 'authn_password_change_fail':
return `User ${metadata.userid} failed to change their password`;
case 'authn_impossible_travel':
return `User ${metadata.userid} has accessed the application in two distant regions: ${metadata.region1 || 'unknown'} and ${metadata.region2 || 'unknown'}`;
case 'authn_token_created':
return `A token has been created for ${metadata.userid} with ${metadata.entitlement || 'unknown'} entitlements`;
case 'authn_token_revoked':
return `Token ID: ${metadata.tokenid || 'unknown'} was revoked for user ${metadata.userid}`;
case 'authn_token_reuse':
return `User ${metadata.userid} attempted to use token ID: ${metadata.tokenid || 'unknown'} which was previously revoked`;
case 'authn_token_delete':
return `The token for ${metadata.appid || metadata.userid || 'unknown'} has been deleted`;
// Session events
case 'session_created':
return `User ${metadata.userid} has started a new session`;
case 'session_renewed':
return `User ${metadata.userid} was warned of expiring session and extended it`;
case 'session_expired':
return `User ${metadata.userid} session expired due to ${metadata.reason || 'timeout'}`;
case 'session_use_after_expire':
return `User ${metadata.userid} attempted access after session expired`;
// Authorization events
case 'authz_fail':
return `User ${metadata.userid} attempted to access ${metadata.resource || 'a resource'} without entitlement`;
case 'authz_change':
return `User ${metadata.userid} access was changed from ${metadata.from || 'previous role'} to ${metadata.to || 'new role'}`;
case 'authz_admin':
return `Administrator ${metadata.userid} has performed ${metadata.event || 'an administrative action'}`;
// User management events
case 'user_created':
return `User ${metadata.userid} created ${metadata.newuserid} with ${metadata.attributes || 'default'} privilege attributes`;
case 'user_updated':
return `User ${metadata.userid} updated ${metadata.onuserid} with attributes ${metadata.attributes || 'unknown'}`;
case 'user_archived':
return `User ${metadata.userid} archived ${metadata.onuserid}`;
case 'user_deleted':
return `User ${metadata.userid} has deleted ${metadata.onuserid}`;
// Default case for events without specific formatting
default:
return eventType;
}
}
// Basic logging methods
info(eventType, metadata) {
this.log('INFO', eventType, metadata);
}
warn(eventType, metadata) {
this.log('WARN', eventType, metadata);
}
error(eventType, metadata) {
this.log('ERROR', eventType, metadata);
}
critical(eventType, metadata) {
this.log('CRITICAL', eventType, metadata);
}
// Future SaaS features
getMetrics() {
return { ...this.metrics };
}
configure(config) {
this.config = { ...this.config, ...config };
}
// Reset metrics (useful for testing or periodic resets)
resetMetrics() {
this.metrics = {
totalLogs: 0,
logsByLevel: { INFO: 0, WARN: 0, ERROR: 0, CRITICAL: 0 },
logsByCategory: {
AUTHN: 0, AUTHZ: 0, SESSION: 0, UPLOAD: 0, INPUT: 0,
MALICIOUS: 0, PRIVILEGE: 0, DATA: 0, SEQUENCE: 0,
SYS: 0, USER: 0, EXCESS: 0, GENERAL: 0
},
lastLogTimestamp: new Date().toISOString(),
apiKeyUsage: {}
};
}
}
exports.Logger = Logger;