tslog

Logging library designed specifically for TypeScript (183,000 weekly downloads, 1,410 GitHub stars). Fully integrated with TypeScript type system, provides beautiful console output and browser support. Design emphasizing developer experience.

logging libraryTypeScriptJavaScriptuniversaltransportperformancebrowserNode.js

Logging Library

tslog

Overview

tslog is a universal logging library for TypeScript and JavaScript. It works in both browser and Node.js environments and features beautiful output formats, high performance, and customizable transports. From development-friendly pretty output to production-ready JSON format, output formats can be switched according to usage, supporting a wide range of output destinations including rotating file streams, database connections, and external API integration.

Details

tslog was designed to solve challenges in modern TypeScript/JavaScript development. As a replacement for traditional console.log, it provides type safety, structured logging, and flexible output control. Through integration with AsyncLocalStorage, automatic request ID tracking is possible, improving traceability in microservices and distributed systems.

Technical Features

  • Universal: Supports Node.js, browser, Deno, React Native
  • Full TypeScript Support: Complete type safety and type inference
  • Customizable Output: Three basic formats: pretty, json, hidden
  • Transport System: Flexible connection to custom output destinations
  • AsyncLocalStorage Integration: Automatic context tracking
  • Performance Optimization: Minimal overhead and memory usage
  • Source Map Support: Accurate location display in stack traces

Output Formats

  • pretty: Visually appealing colored output for development
  • json: Easily parseable JSON format for production environments
  • hidden: Disables log output (for testing environments)

Main Components

  • Logger: Main logging class
  • ILogObj: Log object interface
  • ITransportProvider: Custom transport abstraction
  • ILogObjMeta: Log metadata interface

Pros and Cons

Pros

  • Full TypeScript Support: Type safety and excellent development experience
  • Universal: Unified usage across multiple runtime environments
  • High Performance: Minimal overhead and efficient processing
  • Flexible Output: Ability to choose appropriate output formats from development to production
  • Transport: Rich output destination options and customizability
  • AsyncLocalStorage: Automatic request tracking
  • JSON Output: Excellent searchability and parseability through structured logging
  • Lightweight: Easy adoption with few additional dependencies

Cons

  • Learning Curve: Time required to master advanced features
  • Configuration Complexity: Can become complex with large-scale configurations
  • Ecosystem: Fewer surrounding tools compared to Winston, etc.
  • Documentation: Insufficient explanation for some advanced features
  • Compatibility: Configuration adjustments required when migrating from other libraries
  • Debugging: Difficult to identify issues with transport functionality in some cases

Reference Links

Usage Examples

Basic Usage

import { Logger } from "tslog";

// Create basic logger
const logger = new Logger();

// Basic log output
logger.info("Hello World!");
logger.warn("Warning message");
logger.error("An error occurred");

// Structured log data
logger.info("User login", {
  userId: 12345,
  username: "john_doe",
  timestamp: new Date(),
  sessionId: "abc123"
});

// Usage examples for each log level
logger.silly("Detailed debug information");
logger.trace("Tracing information");
logger.debug("Debug information");
logger.info("General information");
logger.warn("Warning message");
logger.error("Error information");
logger.fatal("Fatal error");

Configuration and Customization

import { Logger, ILogObj } from "tslog";

// Development environment configuration
const devLogger = new Logger({
  name: "MyApp",
  minLevel: "debug",
  type: "pretty",
  prettyLogTemplate: "{{yyyy}}.{{mm}}.{{dd}} {{hh}}:{{MM}}:{{ss}}:{{ms}}\t{{logLevelName}}\t[{{name}}]\t",
  prettyErrorTemplate: "\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}",
  prettyErrorStackTemplate: "  • {{fileName}}:{{lineNumber}}:{{columnNumber}}\t{{functionName}}()",
  prettyErrorParentNamesSeparator: ":",
  prettyErrorLoggerNameDelimiter: "\t",
  stylePrettyLogs: true,
  prettyLogStyles: {
    logLevelName: {
      "*": ["bold", "black", "bgWhiteBright", "dim"],
      FATAL: ["bold", "red"],
      ERROR: ["bold", "red"],
      WARN: ["bold", "yellow"],
      INFO: ["bold", "white"],
      DEBUG: ["bold", "green"],
      TRACE: ["bold", "whiteBright"],
      SILLY: ["bold", "white"]
    },
    dateIsoStr: "white",
    filePathWithLine: "white",
    name: ["white", "bold"],
    nameWithDelimiterPrefix: ["white", "bold"],
    nameWithDelimiterSuffix: ["white", "bold"],
    errorName: ["bold", "bgRedBright", "whiteBright"],
    fileName: ["yellow"]
  }
});

// Production environment configuration
const prodLogger = new Logger({
  name: "MyApp-Production",
  minLevel: "info",
  type: "json",
  maskValuesOfKeys: ["password", "authorization", "cookie"],
  maskPlaceholder: "[HIDDEN]",
  exposeErrorCodeFrame: false
});

// Conditional configuration
const logger = process.env.NODE_ENV === "production" ? prodLogger : devLogger;

// Configuration changes
logger.settings.minLevel = "warn";
logger.settings.type = "json";

Transport Functionality

import { Logger, ILogObj } from "tslog";
import fs from "fs";
import path from "path";

// File output transport
function logToFile(logObj: ILogObj) {
  const logFilePath = path.join(__dirname, 'logs', 'app.log');
  const logMessage = JSON.stringify(logObj) + '\n';
  
  fs.appendFileSync(logFilePath, logMessage);
}

// Database output transport
function logToDatabase(logObj: ILogObj) {
  // Database connection and log storage
  const logEntry = {
    timestamp: logObj._meta.date,
    level: logObj._meta.logLevelName,
    message: logObj[0],
    data: logObj[1] || {},
    source: logObj._meta.path?.fileNameWithLine
  };
  
  // Database storage processing
  // database.logs.insert(logEntry);
}

// HTTP API transport
function logToAPI(logObj: ILogObj) {
  const payload = {
    level: logObj._meta.logLevelName,
    timestamp: logObj._meta.date.toISOString(),
    message: logObj[0],
    metadata: logObj[1] || {},
    service: "my-app",
    environment: process.env.NODE_ENV
  };
  
  // Send to API asynchronously
  fetch('https://api.example.com/logs', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  }).catch(console.error);
}

// Create logger with transports
const logger = new Logger({
  name: "TransportLogger",
  type: "json"
});

// Register multiple transports
logger.attachTransport([
  {
    silly: logToFile,
    debug: logToFile,
    trace: logToFile,
    info: logToFile,
    warn: logToFile,
    error: logToFile,
    fatal: logToFile,
  },
  {
    error: logToDatabase,
    fatal: logToDatabase,
  },
  {
    warn: logToAPI,
    error: logToAPI,
    fatal: logToAPI,
  }
]);

// Log output (automatically sent to all transports)
logger.info("User registration", { userId: 123, email: "[email protected]" });
logger.error("Database connection error", { error: "Connection timeout" });

AsyncLocalStorage Integration

import { Logger, ILogObj } from "tslog";
import { AsyncLocalStorage } from "async_hooks";
import express from "express";

// Request context type definition
interface RequestContext {
  requestId: string;
  userId?: string;
  traceId: string;
}

// AsyncLocalStorage setup
const requestContext = new AsyncLocalStorage<RequestContext>();

// Transport that automatically adds context information
function contextEnhancedTransport(logObj: ILogObj) {
  const context = requestContext.getStore();
  
  if (context) {
    // Add context information to log object
    const enhancedLogObj = {
      ...logObj,
      requestId: context.requestId,
      userId: context.userId,
      traceId: context.traceId
    };
    
    console.log(JSON.stringify(enhancedLogObj));
  } else {
    console.log(JSON.stringify(logObj));
  }
}

// Logger configuration
const logger = new Logger({
  name: "ContextLogger",
  type: "json"
});

logger.attachTransport({
  silly: contextEnhancedTransport,
  debug: contextEnhancedTransport,
  trace: contextEnhancedTransport,
  info: contextEnhancedTransport,
  warn: contextEnhancedTransport,
  error: contextEnhancedTransport,
  fatal: contextEnhancedTransport
});

// Express middleware
const app = express();

app.use((req, res, next) => {
  const context: RequestContext = {
    requestId: `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
    traceId: req.headers['x-trace-id'] as string || `trace_${Date.now()}`,
    userId: req.headers['x-user-id'] as string
  };
  
  requestContext.run(context, () => {
    logger.info("Request started", {
      method: req.method,
      url: req.url,
      userAgent: req.headers['user-agent']
    });
    
    next();
  });
});

// Route handler
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  
  // This log automatically includes requestId, traceId, userId
  logger.info("User data retrieval", { targetUserId: userId });
  
  try {
    // Business logic
    const user = getUserById(userId);
    
    logger.info("User data retrieval successful", { user: user.id });
    res.json(user);
  } catch (error) {
    logger.error("User data retrieval error", { error: error.message });
    res.status(500).json({ error: "Internal Server Error" });
  }
});

Log Filtering and Formatting

import { Logger, ILogObj } from "tslog";

// Custom log object type definition
interface CustomLogObj extends ILogObj {
  service?: string;
  version?: string;
  environment?: string;
}

// Filtering function
function sensitiveDataFilter(logObj: ILogObj): ILogObj {
  const filtered = { ...logObj };
  
  // Masking sensitive data
  if (filtered[1] && typeof filtered[1] === 'object') {
    const data = { ...filtered[1] };
    
    ['password', 'token', 'secret', 'key', 'authorization'].forEach(field => {
      if (data[field]) {
        data[field] = '[REDACTED]';
      }
    });
    
    // Deep nested masking
    Object.keys(data).forEach(key => {
      if (typeof data[key] === 'object' && data[key] !== null) {
        data[key] = maskSensitiveFields(data[key]);
      }
    });
    
    filtered[1] = data;
  }
  
  return filtered;
}

function maskSensitiveFields(obj: any): any {
  if (typeof obj !== 'object' || obj === null) return obj;
  
  const masked = { ...obj };
  
  ['password', 'token', 'secret', 'key', 'authorization'].forEach(field => {
    if (masked[field]) {
      masked[field] = '[REDACTED]';
    }
  });
  
  return masked;
}

// Custom formatter
function structuredLogFormatter(logObj: ILogObj) {
  const structured = {
    '@timestamp': logObj._meta.date.toISOString(),
    '@version': '1',
    level: logObj._meta.logLevelName,
    logger: logObj._meta.name,
    thread: `main`,
    message: logObj[0],
    fields: logObj[1] || {},
    location: {
      file: logObj._meta.path?.fileName,
      line: logObj._meta.path?.lineNumber,
      method: logObj._meta.path?.functionName
    },
    service: {
      name: process.env.SERVICE_NAME || 'unknown',
      version: process.env.SERVICE_VERSION || '1.0.0',
      environment: process.env.NODE_ENV || 'development'
    }
  };
  
  return JSON.stringify(structured);
}

// Logger configuration
const logger = new Logger({
  name: "StructuredLogger",
  type: "json",
  minLevel: "info"
});

// Transport with filtering and formatting applied
logger.attachTransport({
  silly: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.log(formatted);
  },
  debug: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.log(formatted);
  },
  trace: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.log(formatted);
  },
  info: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.log(formatted);
  },
  warn: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.warn(formatted);
  },
  error: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.error(formatted);
  },
  fatal: (logObj) => {
    const filtered = sensitiveDataFilter(logObj);
    const formatted = structuredLogFormatter(filtered);
    console.error(formatted);
  }
});

// Usage example
logger.info("User authentication successful", {
  userId: 12345,
  username: "john_doe",
  password: "secret123",  // Automatically masked
  authToken: "abc123",    // Automatically masked
  loginTime: new Date()
});

Testing and Mocking

import { Logger, ILogObj } from "tslog";
import { jest } from '@jest/globals';

// Mock logger for testing
class MockLogger extends Logger {
  public logs: ILogObj[] = [];
  
  constructor() {
    super({
      name: "MockLogger",
      type: "hidden" // Disable log output
    });
    
    // Transport that records all logs
    this.attachTransport({
      silly: (logObj) => this.logs.push({ ...logObj, level: 'silly' }),
      debug: (logObj) => this.logs.push({ ...logObj, level: 'debug' }),
      trace: (logObj) => this.logs.push({ ...logObj, level: 'trace' }),
      info: (logObj) => this.logs.push({ ...logObj, level: 'info' }),
      warn: (logObj) => this.logs.push({ ...logObj, level: 'warn' }),
      error: (logObj) => this.logs.push({ ...logObj, level: 'error' }),
      fatal: (logObj) => this.logs.push({ ...logObj, level: 'fatal' })
    });
  }
  
  // Helper methods for testing
  getLogsByLevel(level: string): ILogObj[] {
    return this.logs.filter(log => log.level === level);
  }
  
  getLogsByMessage(message: string): ILogObj[] {
    return this.logs.filter(log => log[0]?.includes(message));
  }
  
  clearLogs(): void {
    this.logs = [];
  }
  
  getLastLog(): ILogObj | undefined {
    return this.logs[this.logs.length - 1];
  }
}

// Service under test
class UserService {
  constructor(private logger: Logger) {}
  
  async createUser(userData: { name: string; email: string }) {
    this.logger.info("User creation started", { userData });
    
    try {
      // User creation logic
      const user = { id: 123, ...userData };
      
      this.logger.info("User creation successful", { userId: user.id });
      return user;
    } catch (error) {
      this.logger.error("User creation error", { error: error.message });
      throw error;
    }
  }
}

// Jest tests
describe('UserService', () => {
  let mockLogger: MockLogger;
  let userService: UserService;
  
  beforeEach(() => {
    mockLogger = new MockLogger();
    userService = new UserService(mockLogger);
  });
  
  afterEach(() => {
    mockLogger.clearLogs();
  });
  
  test('should output appropriate logs when creating user', async () => {
    const userData = { name: 'John Doe', email: '[email protected]' };
    
    await userService.createUser(userData);
    
    // Verify logs
    const infoLogs = mockLogger.getLogsByLevel('info');
    expect(infoLogs).toHaveLength(2);
    
    // Verify start log
    expect(infoLogs[0][0]).toContain('User creation started');
    expect(infoLogs[0][1]).toEqual({ userData });
    
    // Verify success log
    expect(infoLogs[1][0]).toContain('User creation successful');
    expect(infoLogs[1][1]).toEqual({ userId: 123 });
  });
  
  test('should output error log on error', async () => {
    // Mock to cause error
    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
    
    try {
      // Process that causes error
      await userService.createUser({ name: '', email: 'invalid' });
    } catch (error) {
      // Verify error log
      const errorLogs = mockLogger.getLogsByLevel('error');
      expect(errorLogs).toHaveLength(1);
      expect(errorLogs[0][0]).toContain('User creation error');
    }
    
    consoleSpy.mockRestore();
  });
});