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
A structured log decorator wraps any function to automatically emit JSON log entries on invocation, completion, and failure. Each entry includes a timestamp, namespace, log level, request ID, duration, and the function arguments/result. The decorator also accepts a sanitize function so you can strip sensitive fields like passwords before they reach the log output.
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
Instead of decorating functions one at a time, this decorator instruments every public method on a class instance in a single call. It walks the prototype, finds all functions (excluding the constructor), and replaces each with a logged wrapper. You can pass include and exclude arrays to control which methods get logging, so internal helpers stay clean while public API methods emit structured entries.
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
This decorator records execution time and memory usage for every call, stores the measurements in a rolling buffer, and warns when a call exceeds a configurable slow threshold. The wrapped function exposes a getStats() method that returns aggregate metrics: call count, success rate, average duration, min, max, P95, P99, and the number of slow calls. It gives you production-grade observability without pulling in an external metrics library.
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
An audit trail decorator records who called a function, what arguments were passed, whether it succeeded or failed, and when it happened. Each entry gets a UUID and is pushed into a storage array that you can later persist to a database or ship to a log aggregation service. This is useful for compliance requirements where you need a tamper-evident record of every data mutation.
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
Not every function call needs to be logged, especially in production with high request volume. This decorator lets you define named logging strategies (default, production, sampling) that control when and how logs are emitted. A shouldLog predicate gates whether logging happens at all, so you can log only critical functions, only slow calls, or a random 10% sample to keep costs down.
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.