Using Decorators for Logging in JS Architecture
Learn to use the decorator pattern for logging in JavaScript architecture. Covers method-level logging, structured log decorators, performance tracking, request/response logging, audit trail decorators, and conditional logging strategies.
Logging is a cross-cutting concern that should not clutter business logic. The decorator pattern extracts logging into reusable wrappers that can be applied to any function, method, or class without modifying their source code. This guide builds production-grade logging decorators.
For the core decorator pattern techniques, see JavaScript Decorator Pattern: Complete Guide.
Structured Log Decorator
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
let currentLevel = "info";
function createLogger(namespace) {
function formatEntry(level, message, meta) {
return {
timestamp: new Date().toISOString(),
level,
namespace,
message,
...meta,
};
}
return {
debug(message, meta) {
if (LOG_LEVELS[currentLevel] <= LOG_LEVELS.debug) {
console.debug(JSON.stringify(formatEntry("debug", message, meta)));
}
},
info(message, meta) {
if (LOG_LEVELS[currentLevel] <= LOG_LEVELS.info) {
console.info(JSON.stringify(formatEntry("info", message, meta)));
}
},
warn(message, meta) {
if (LOG_LEVELS[currentLevel] <= LOG_LEVELS.warn) {
console.warn(JSON.stringify(formatEntry("warn", message, meta)));
}
},
error(message, meta) {
console.error(JSON.stringify(formatEntry("error", message, meta)));
},
setLevel(level) {
currentLevel = level;
},
};
}
function withStructuredLogging(fn, options = {}) {
const {
namespace = fn.name || "anonymous",
level = "info",
logArgs = true,
logResult = true,
sanitize = (v) => v,
} = options;
const logger = createLogger(namespace);
return async function (...args) {
const requestId = crypto.randomUUID().slice(0, 8);
const sanitizedArgs = logArgs ? sanitize(args) : "[redacted]";
logger[level]("invoked", { requestId, args: sanitizedArgs });
const start = performance.now();
try {
const result = await fn.apply(this, args);
const duration = performance.now() - start;
logger[level]("completed", {
requestId,
durationMs: duration.toFixed(2),
result: logResult ? sanitize(result) : "[redacted]",
});
return result;
} catch (error) {
const duration = performance.now() - start;
logger.error("failed", {
requestId,
durationMs: duration.toFixed(2),
error: error.message,
stack: error.stack,
});
throw error;
}
};
}
// Usage
const getUser = withStructuredLogging(
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
},
{
namespace: "UserService",
sanitize: (data) => {
if (Array.isArray(data)) return data;
if (data?.password) return { ...data, password: "***" };
return data;
},
}
);Method-Level Class Logging
function logMethods(target, options = {}) {
const {
include = null,
exclude = [],
logLevel = "info",
} = options;
const logger = createLogger(target.constructor?.name || "Object");
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(target))
.filter((name) => name !== "constructor")
.filter((name) => typeof target[name] === "function")
.filter((name) => !exclude.includes(name))
.filter((name) => (include ? include.includes(name) : true));
for (const method of methods) {
const original = target[method].bind(target);
target[method] = async function (...args) {
const callId = crypto.randomUUID().slice(0, 8);
logger[logLevel](`${method}() called`, { callId, argCount: args.length });
const start = performance.now();
try {
const result = await original(...args);
logger[logLevel](`${method}() completed`, {
callId,
durationMs: (performance.now() - start).toFixed(2),
});
return result;
} catch (error) {
logger.error(`${method}() failed`, {
callId,
durationMs: (performance.now() - start).toFixed(2),
error: error.message,
});
throw error;
}
};
}
return target;
}
// Usage
class OrderService {
async createOrder(items, userId) {
return { id: `ORD-${Date.now()}`, items, userId, status: "pending" };
}
async cancelOrder(orderId) {
return { id: orderId, status: "cancelled" };
}
async getOrderHistory(userId) {
return [];
}
#internalHelper() {
// Not decorated (private)
}
}
const orderService = logMethods(new OrderService(), {
exclude: ["getOrderHistory"],
});
await orderService.createOrder([{ sku: "A1", qty: 2 }], "user_123");Performance Tracking Decorator
function withPerformanceTracking(fn, options = {}) {
const { name = fn.name, slowThreshold = 1000, historySize = 100 } = options;
const measurements = [];
const wrapped = async function (...args) {
const start = performance.now();
const memBefore = typeof process !== "undefined"
? process.memoryUsage().heapUsed
: 0;
try {
const result = await fn.apply(this, args);
const duration = performance.now() - start;
const memAfter = typeof process !== "undefined"
? process.memoryUsage().heapUsed
: 0;
const measurement = {
name,
duration,
memoryDelta: memAfter - memBefore,
timestamp: Date.now(),
success: true,
};
measurements.push(measurement);
if (measurements.length > historySize) measurements.shift();
if (duration > slowThreshold) {
console.warn(`SLOW: ${name} took ${duration.toFixed(2)}ms (threshold: ${slowThreshold}ms)`);
}
return result;
} catch (error) {
const duration = performance.now() - start;
measurements.push({
name,
duration,
timestamp: Date.now(),
success: false,
error: error.message,
});
throw error;
}
};
wrapped.getStats = function () {
if (measurements.length === 0) return null;
const durations = measurements.map((m) => m.duration);
const successful = measurements.filter((m) => m.success);
const sorted = [...durations].sort((a, b) => a - b);
return {
name,
calls: measurements.length,
successRate: ((successful.length / measurements.length) * 100).toFixed(1) + "%",
avg: (durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2) + "ms",
min: sorted[0].toFixed(2) + "ms",
max: sorted[sorted.length - 1].toFixed(2) + "ms",
p95: sorted[Math.floor(sorted.length * 0.95)].toFixed(2) + "ms",
p99: sorted[Math.floor(sorted.length * 0.99)].toFixed(2) + "ms",
slowCalls: measurements.filter((m) => m.duration > slowThreshold).length,
};
};
wrapped.resetStats = function () {
measurements.length = 0;
};
return wrapped;
}
// Usage
const processOrder = withPerformanceTracking(
async function processOrder(order) {
await new Promise((r) => setTimeout(r, Math.random() * 200));
return { ...order, processed: true };
},
{ slowThreshold: 150 }
);
for (let i = 0; i < 20; i++) {
await processOrder({ id: i, total: 99 });
}
console.table(processOrder.getStats());Audit Trail Decorator
function withAuditTrail(fn, options = {}) {
const {
action = fn.name,
resource = "unknown",
getCurrentUser = () => ({ id: "system", role: "system" }),
storage = [],
maxEntries = 10000,
} = options;
return async function (...args) {
const user = getCurrentUser();
const entry = {
id: crypto.randomUUID(),
action,
resource,
userId: user.id,
userRole: user.role,
timestamp: new Date().toISOString(),
args: args.map((a) =>
typeof a === "object" ? JSON.stringify(a).slice(0, 200) : String(a)
),
ip: typeof globalThis.request !== "undefined" ? "server" : "client",
};
try {
const result = await fn.apply(this, args);
entry.status = "success";
entry.resultSummary =
typeof result === "object"
? `${Object.keys(result).length} fields`
: String(result).slice(0, 100);
return result;
} catch (error) {
entry.status = "failure";
entry.error = error.message;
throw error;
} finally {
storage.push(entry);
if (storage.length > maxEntries) storage.shift();
}
};
}
// Usage
const auditLog = [];
const deleteUser = withAuditTrail(
async function deleteUser(userId) {
console.log(`Deleting user: ${userId}`);
return { deleted: true, userId };
},
{
action: "DELETE_USER",
resource: "users",
getCurrentUser: () => ({ id: "admin_01", role: "admin" }),
storage: auditLog,
}
);
await deleteUser("user_456");
console.log(auditLog);
// [{
// id: "...", action: "DELETE_USER", resource: "users",
// userId: "admin_01", userRole: "admin", status: "success", ...
// }]Conditional Logging Strategy
function createConditionalLogger(strategies) {
return function withConditionalLogging(fn, strategyName = "default") {
const strategy = strategies[strategyName];
if (!strategy) throw new Error(`Unknown logging strategy: ${strategyName}`);
return async function (...args) {
const context = {
functionName: fn.name,
args,
timestamp: Date.now(),
};
if (!strategy.shouldLog(context)) {
return fn.apply(this, args);
}
strategy.before?.(context);
const start = performance.now();
try {
const result = await fn.apply(this, args);
context.duration = performance.now() - start;
context.result = result;
strategy.after?.(context);
return result;
} catch (error) {
context.duration = performance.now() - start;
context.error = error;
strategy.onError?.(context);
throw error;
}
};
};
}
const conditionalLog = createConditionalLogger({
default: {
shouldLog: () => true,
before: (ctx) => console.log(`Calling ${ctx.functionName}`),
after: (ctx) => console.log(`${ctx.functionName} took ${ctx.duration.toFixed(1)}ms`),
onError: (ctx) => console.error(`${ctx.functionName} failed: ${ctx.error.message}`),
},
production: {
shouldLog: (ctx) => ctx.functionName.startsWith("critical_"),
before: () => {},
after: (ctx) => {
if (ctx.duration > 5000) {
console.warn(`SLOW: ${ctx.functionName} - ${ctx.duration.toFixed(0)}ms`);
}
},
onError: (ctx) =>
console.error(JSON.stringify({
fn: ctx.functionName,
error: ctx.error.message,
duration: ctx.duration,
})),
},
sampling: {
shouldLog: () => Math.random() < 0.1, // Log 10% of calls
before: (ctx) => console.log(`[SAMPLE] ${ctx.functionName}`),
after: (ctx) => console.log(`[SAMPLE] ${ctx.functionName}: ${ctx.duration.toFixed(1)}ms`),
onError: (ctx) => console.error(`[SAMPLE] ${ctx.functionName}: ${ctx.error.message}`),
},
});
// Usage
const fetchProducts = conditionalLog(
async function fetchProducts(category) {
return [{ id: 1, category }];
},
"production"
);| Logging Decorator | Captures | Best For | Overhead |
|---|---|---|---|
| Structured logging | Args, result, timing | API services | Low |
| Method-level class | All public methods | Service classes | Low-Medium |
| Performance tracking | Duration, percentiles, memory | Bottleneck detection | Medium |
| Audit trail | User, action, resource, status | Compliance, security | Medium |
| Conditional/sampling | Configurable subset | Production at scale | Very Low |
Rune AI
Key Insights
- Structured log decorators produce consistent, machine-parseable JSON entries: Every log includes timestamp, namespace, level, and context for reliable filtering and aggregation
- Method-level decorators instrument entire classes without modifying method bodies: Apply logging to all public methods with include/exclude lists for selective coverage
- Performance tracking decorators capture percentiles and alert on slow operations: P95, P99, and threshold-based warnings identify bottlenecks automatically
- Audit trail decorators record user, action, and result for compliance: Every mutation is traceable with user identity, timestamp, and outcome
- Conditional and sampling strategies control log volume in production: Log only errors, slow calls, or a random percentage to balance observability with resource costs
Frequently Asked Questions
How do I avoid logging sensitive data like passwords and tokens?
Should I log every function call in production?
How do I correlate logs across multiple decorated functions in a request?
Can logging decorators be composed with other decorators?
Conclusion
Decorator-based logging separates cross-cutting concerns from business logic. Structured log decorators produce machine-readable JSON. Method-level decorators instrument entire service classes. Performance tracking captures percentiles and slow-call alerts. Audit trail decorators record who did what and when. For the core decorator pattern foundations, see JavaScript Decorator Pattern: Complete Guide. For event-driven architectures that complement logging, review The JavaScript Pub/Sub Pattern: Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.