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.

JavaScriptadvanced
15 min read

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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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 DecoratorCapturesBest ForOverhead
Structured loggingArgs, result, timingAPI servicesLow
Method-level classAll public methodsService classesLow-Medium
Performance trackingDuration, percentiles, memoryBottleneck detectionMedium
Audit trailUser, action, resource, statusCompliance, securityMedium
Conditional/samplingConfigurable subsetProduction at scaleVery Low
Rune AI

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
RunePowered by Rune AI

Frequently Asked Questions

How do I avoid logging sensitive data like passwords and tokens?

Use a sanitize function in your decorator that strips or masks sensitive fields before logging. Create an allowlist of safe fields rather than a blocklist of dangerous ones. Common patterns include replacing passwords with "***", truncating tokens to the first 4 characters, and omitting entire fields like `creditCard` or `ssn` from log output.

Should I log every function call in production?

No. Use sampling (log 1-10% of calls) for high-frequency operations. Log all calls for critical operations like payments, authentication, and data mutations. Use conditional strategies that only log when duration exceeds a threshold or when an error occurs. This keeps log volume manageable and storage costs controlled.

How do I correlate logs across multiple decorated functions in a request?

Pass a `requestId` (correlation ID) through function arguments or use AsyncLocalStorage in Node.js to propagate the ID automatically. Each decorated function includes the requestId in its log entries. This lets you filter all logs for a single request across all service layers.

Can logging decorators be composed with other decorators?

Yes. Place the logging decorator as the outermost wrapper so it captures the full execution time including other decorators. For example: `withLogging(withRetry(withCache(fetchUser)))`. The logging decorator sees retries and cache hits as part of the function's execution.

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.