Why LogTape Should Be Your Go-To Logging Library for JavaScript/TypeScript

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

In the diverse and ever-evolving JavaScript ecosystem, logging remains a critical component for development, debugging, and monitoring applications. While numerous logging libraries exist, LogTape stands out with its unique combination of simplicity, flexibility, and cross-runtime compatibility. Let's explore why LogTape deserves consideration for your next JavaScript or TypeScript project—whether you're building an application or a library.

Zero Dependencies: A Lightweight Footprint

One of LogTape's most compelling features is its complete absence of dependencies. In an era where “dependency hell” plagues many JavaScript projects, LogTape offers a refreshing alternative:

// No additional packages to install beyond LogTape itself
import { configure, getConsoleSink, getLogger } from "@logtape/logtape";

This zero-dependency approach provides several advantages:

Reduced bundle size
No transitive dependencies means smaller packages
Enhanced stability
No risk of breaking changes from upstream dependencies
Simplified security
Fewer potential vulnerabilities from third-party code
Lower integration overhead
Particularly valuable for library authors who don't want to burden users with additional dependencies

Runtime Diversity: Write Once, Log Everywhere

While many popular logging libraries focus primarily on Node.js, LogTape provides seamless support across diverse JavaScript runtimes:

This runtime flexibility means you can use consistent logging patterns regardless of your deployment environment:

// Same API works seamlessly across all JavaScript runtimes
import { getLogger } from "@logtape/logtape";

const logger = getLogger(["my-service", "user-management"]);

// Works in Node.js, Deno, Bun, browsers, or edge functions
logger.info`User ${userId} logged in successfully`;

For teams working across multiple platforms or projects transitioning between runtimes, this consistency is invaluable. No need to learn different logging libraries or approaches—LogTape works the same way everywhere.

Hierarchical Categories: Fine-Grained Control

LogTape's hierarchical category system represents a standout feature that's surprisingly rare among JavaScript logging libraries. Categories allow you to organize logs in a tree-like structure:

// Parent category
const appLogger = getLogger(["my-app"]);

// Child category inherits settings from parent
const dbLogger = getLogger(["my-app", "database"]);

// Grandchild category
const queryLogger = getLogger(["my-app", "database", "queries"]);

// Alternative approach using getChild()
const userLogger = appLogger.getChild("users");
const authLogger = userLogger.getChild("auth");

This hierarchical approach offers powerful benefits:

Targeted filtering
Configure different log levels for different parts of your application
Inheritance
Child loggers inherit settings from parents, reducing configuration overhead
Organizational clarity
Logs naturally follow your application's module structure

Here's how you might configure logging levels for different categories:

await configure({
  sinks: {
    console: getConsoleSink(),
    file: getFileSink("app.log"),
  },
  loggers: [
    // Base configuration for all app logs
    { 
      category: ["my-app"], 
      lowestLevel: "info", 
      sinks: ["console", "file"] 
    },
    // More verbose logging just for database components
    { 
      category: ["my-app", "database"], 
      lowestLevel: "debug", 
      sinks: ["file"] 
    }
  ]
});

With this configuration, all application logs at "info" level and above go to both console and file, while database-specific logs include more detailed "debug" level information, but only in the log file.

Structured Logging: Beyond Simple Text

Modern logging goes beyond simple text strings. LogTape embraces structured logging, which treats log entries as data objects rather than plain text:

logger.info("User logged in", {
  userId: 123456,
  username: "johndoe",
  loginTime: new Date(),
  ipAddress: "192.168.1.1"
});

LogTape also supports placeholders in messages, connecting structured data with human-readable text:

logger.info("User {username} (ID: {userId}) logged in from {ipAddress}", {
  userId: 123456,
  username: "johndoe",
  ipAddress: "192.168.1.1"
});

Structured logging offers substantial benefits:

Improved searchability
Search for specific field values instead of parsing text
Better analysis
Perform data analysis on structured fields
Consistent format
Enforce standardized log formats
Machine-readable
Easier processing by log management systems

For performance-conscious applications, LogTape offers lazy evaluation of structured data:

logger.debug("Performance metrics", () => ({
  memoryUsage: process.memoryUsage(),
  cpuUsage: process.cpuUsage(),
  timestamp: performance.now()
}));

The function is only evaluated if the debug level is enabled, preventing unnecessary computation for suppressed log levels.

Extremely Simple Sinks and Filters: Minimal Boilerplate

LogTape's approach to extensibility is remarkably straightforward. Creating custom sinks (output destinations) and filters requires minimal boilerplate code.

Dead Simple Sinks

A sink in LogTape is just a function that receives a log record:

// Creating a custom sink is as simple as defining a function
const mySink = (record) => {
  const timestamp = new Date(record.timestamp).toISOString();
  const level = record.level.toUpperCase();
  const category = record.category.join('.');
  
  // Send to your custom destination
  myCustomLogService.send({
    time: timestamp,
    priority: level,
    component: category,
    message: record.message,
    ...record.properties
  });
};

// Use your custom sink in configuration
await configure({
  sinks: {
    console: getConsoleSink(),
    custom: mySink
  },
  loggers: [
    { category: ["my-app"], sinks: ["console", "custom"] }
  ]
});

Compare this with other libraries that require extending classes, implementing multiple methods, or following specific patterns. LogTape's approach is refreshingly straightforward.

Simple Filters

Similarly, filters in LogTape are just functions that return a Boolean:

// Filter that only passes high-priority or specific component logs
const importantLogsFilter = (record) => {
  // Always include errors
  if (record.level === "error" || record.level === "fatal") {
    return true;
  }
  
  // Always include payment-related logs
  if (record.category.includes("payments")) {
    return true;
  }
  
  // Filter out other logs
  return false;
};

await configure({
  // ...sinks configuration
  filters: {
    important: importantLogsFilter
  },
  loggers: [
    { 
      category: ["my-app"], 
      sinks: ["alertSystem"], 
      filters: ["important"] 
    }
  ]
});

LogTape also provides a convenient shorthand for level-based filtering:

await configure({
  // ...sinks configuration
  filters: {
    // This creates a filter for "warning" level and above
    warningAndAbove: "warning"
  },
  loggers: [
    { 
      category: ["my-app"], 
      sinks: ["console"], 
      filters: ["warningAndAbove"] 
    }
  ]
});

Perfect for Library Authors

LogTape is uniquely well-suited for library authors who want to incorporate logging without burdening their users. The core philosophy is simple:

  1. Libraries provide logging output points
  2. Applications configure how those logs are handled

Here's how a library might implement LogTape:

// my-awesome-lib/database.js
import { getLogger } from "@logtape/logtape";

export class Database {
  private logger = getLogger(["my-awesome-lib", "database"]);

  constructor(host, port, user) {
    this.host = host;
    this.port = port;
    this.user = user;
  }

  connect() {
    this.logger.info("Connecting to database", {
      host: this.host,
      port: this.port,
      user: this.user
    });
    
    // Connection logic...
    
    this.logger.debug("Connection established");
  }
  
  query(sql) {
    this.logger.debug("Executing query", { sql });
    // Query logic...
  }
}

The key point is that the library never calls configure(). Instead, it provides useful log output points with appropriate levels and contextual data.

Applications using the library can then decide exactly how to handle these logs:

// Application code
import { configure, getConsoleSink } from "@logtape/logtape";
import { Database } from "my-awesome-lib";

// Configure how logs should be handled
await configure({
  sinks: {
    console: getConsoleSink(),
    file: getFileSink("app.log")
  },
  loggers: [
    // Handle all library logs
    { 
      category: ["my-awesome-lib"], 
      lowestLevel: "info", 
      sinks: ["file"] 
    },
    // More verbose for database component during development
    { 
      category: ["my-awesome-lib", "database"], 
      lowestLevel: "debug", 
      sinks: ["console", "file"] 
    }
  ]
});

// Use the library
const db = new Database("localhost", 5432, "user");
db.connect();

This separation of concerns offers several benefits:

  1. Library users have complete control over log handling
  2. Libraries can provide rich logging without imposing implementation details
  3. No risk of conflict with application logging configurations
  4. Libraries can be "noisy" internally while allowing applications to filter as needed

Contexts for Richer Logging

LogTape provides context mechanisms for adding consistent properties across multiple log messages. This is particularly valuable for tracing requests through a system:

Explicit Contexts

const logger = getLogger(["my-app", "api"]);

// Create a logger with context
const requestLogger = logger.with({
  requestId: "abc-123",
  userId: 42,
  endpoint: "/users"
});

// All logs from this logger include the context properties
requestLogger.info("Processing request");
requestLogger.debug("Validating input");
requestLogger.info("Request completed", { durationMs: 120 });

Implicit Contexts (v0.7.0+)

For cases where you want context to apply across function calls without explicit passing:

import { getLogger, withContext } from "@logtape/logtape";

function handleRequest(req, res) {
  withContext({ 
    requestId: req.id, 
    userId: req.user?.id 
  }, () => {
    // All logs within this function and any functions it calls
    // will automatically include the context properties
    processRequest(req, res);
  });
}

function processRequest(req, res) {
  // No need to pass context - it's automatically available
  getLogger(["my-app", "processor"]).info("Processing data");
  
  // Call other functions that will also inherit the context
  validateInput(req.body);
}

function validateInput(data) {
  // This log also gets the requestId and userId
  getLogger(["my-app", "validator"]).debug("Validating input", { data });
}

This implicit context capability is invaluable for tracing requests through multiple layers of code without manually threading context through every function call.

When LogTape Might Not Be Your Best Choice

While LogTape offers compelling advantages for many use cases, it's not universally the best choice:

Extreme performance requirements
If your application logs tens of thousands of entries per second and raw performance is the top priority, specialized high-performance libraries like Pino may be more suitable with their focus on optimized logging throughput.
Extensive pre-built integrations
If you need immediate integration with numerous specific systems (Elasticsearch, Graylog, etc.) without writing any custom code, Winston's rich ecosystem of transports might provide a faster starting point.
Legacy systems with specific logging requirements
If you're maintaining systems built around specific logging patterns from Java or other environments, purpose-built libraries like Log4js might offer more familiar APIs.
Web browser-only applications with minimal logging needs
For extremely simple web browser-only logging needs where you just want basic console output with levels, even simpler libraries like loglevel might be sufficient.

Conclusion

LogTape stands out in the crowded JavaScript logging landscape by offering a unique combination of features that address real-world development challenges:

  • Zero dependencies for a lightweight, secure foundation
  • Runtime diversity supporting Node.js, Deno, Bun, browsers, and edge functions
  • Hierarchical categories for better log organization and filtering
  • Structured logging for improved analysis and searchability
  • Simple extension mechanisms with minimal boilerplate
  • Library-friendly design that respects separation of concerns

Whether you're building applications or libraries, working across multiple JavaScript runtimes, or simply seeking a clean, well-designed logging solution, LogTape deserves serious consideration. Its thoughtful design balances simplicity with powerful features, avoiding common pitfalls of JavaScript logging libraries.

For more information and detailed documentation, visit LogTape's official website.

6

2 comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0196e16d-24a5-719f-bdfe-091a2162cbd2 on your instance and reply to it.

1
1