tslog

TypeScript専用に設計されたロギングライブラリ(183,000週間ダウンロード、1,410 GitHub スター)。TypeScriptの型システムと完全統合し、美しいコンソール出力とブラウザサポートを提供。開発者体験を重視した設計。

ロギングライブラリTypeScriptJavaScriptユニバーサルトランスポートパフォーマンスブラウザNode.js

ロギングライブラリ

tslog

概要

tslogは、TypeScriptとJavaScript用のユニバーサルロギングライブラリです。ブラウザとNode.js両方で動作し、美しい出力形式高性能カスタマイズ可能なトランスポートを特徴とします。開発時のpretty出力から本番環境のJSON形式まで、用途に応じて出力形式を切り替えられ、rotating file streamsdatabase接続外部API連携など幅広い出力先をサポートします。

詳細

tslogは、現代のTypeScript/JavaScript開発における課題を解決するために設計されたロギングライブラリです。従来のconsole.logの代替として、型安全性構造化ログ柔軟な出力制御を提供します。AsyncLocalStorageとの統合によりリクエストIDの自動追跡が可能で、マイクロサービス分散システムでのトレーサビリティを向上させます。

技術的特徴

  • ユニバーサル: Node.js、ブラウザ、Deno、React Native対応
  • TypeScript完全サポート: 完全な型安全性と型推論
  • カスタマイズ可能な出力: pretty、json、hiddenの3つの基本形式
  • トランスポートシステム: カスタム出力先への柔軟な接続
  • AsyncLocalStorage統合: 自動的なコンテキストトラッキング
  • パフォーマンス最適化: 最小限のオーバーヘッドとメモリ使用量
  • ソースマップサポート: スタックトレースの正確な位置表示

出力形式

  • pretty: 開発時の見やすい色付き出力
  • json: 本番環境でのパースしやすいJSON形式
  • hidden: ログ出力を無効化(テスト環境など)

主要コンポーネント

  • Logger: メインのロギングクラス
  • ILogObj: ログオブジェクトのインターフェース
  • ITransportProvider: カスタムトランスポートの抽象化
  • ILogObjMeta: ログメタデータのインターフェース

メリット・デメリット

メリット

  • TypeScript完全対応: 型安全性と優れた開発体験
  • ユニバーサル: 複数の実行環境で統一的に使用可能
  • 高パフォーマンス: 最小限のオーバーヘッドと効率的な処理
  • 柔軟な出力: 開発から本番まで適切な出力形式を選択可能
  • トランスポート: 豊富な出力先オプションとカスタマイズ性
  • AsyncLocalStorage: 自動的なリクエストトラッキング
  • JSON出力: 構造化ログによる優れた検索性とパース性
  • 軽量: 追加依存関係が少なく導入が容易

デメリット

  • 学習コスト: 高度な機能の習得に時間が必要
  • 設定複雑性: 大規模な設定では複雑になりがち
  • エコシステム: Winston等と比べると周辺ツールが少ない
  • ドキュメント: 一部の高度な機能の説明が不足
  • 互換性: 他ライブラリからの移行で設定調整が必要
  • デバッグ: トランスポート機能での問題特定が困難な場合

参考ページ

書き方の例

基本的な使用方法

import { Logger } from "tslog";

// 基本的なロガーの作成
const logger = new Logger();

// 基本的なログ出力
logger.info("Hello World!");
logger.warn("警告メッセージ");
logger.error("エラーが発生しました");

// 構造化されたログデータ
logger.info("ユーザーログイン", {
  userId: 12345,
  username: "john_doe",
  timestamp: new Date(),
  sessionId: "abc123"
});

// 各ログレベルの使用例
logger.silly("詳細なデバッグ情報");
logger.trace("トレーシング情報");
logger.debug("デバッグ情報");
logger.info("一般的な情報");
logger.warn("警告メッセージ");
logger.error("エラー情報");
logger.fatal("致命的エラー");

設定とカスタマイズ

import { Logger, ILogObj } from "tslog";

// 開発環境用の設定
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"]
  }
});

// 本番環境用の設定
const prodLogger = new Logger({
  name: "MyApp-Production",
  minLevel: "info",
  type: "json",
  maskValuesOfKeys: ["password", "authorization", "cookie"],
  maskPlaceholder: "[HIDDEN]",
  exposeErrorCodeFrame: false
});

// 条件分岐での設定
const logger = process.env.NODE_ENV === "production" ? prodLogger : devLogger;

// 設定変更
logger.settings.minLevel = "warn";
logger.settings.type = "json";

トランスポート機能

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

// ファイル出力トランスポート
function logToFile(logObj: ILogObj) {
  const logFilePath = path.join(__dirname, 'logs', 'app.log');
  const logMessage = JSON.stringify(logObj) + '\n';
  
  fs.appendFileSync(logFilePath, logMessage);
}

// データベース出力トランスポート
function logToDatabase(logObj: ILogObj) {
  // データベース接続とログ保存
  const logEntry = {
    timestamp: logObj._meta.date,
    level: logObj._meta.logLevelName,
    message: logObj[0],
    data: logObj[1] || {},
    source: logObj._meta.path?.fileNameWithLine
  };
  
  // データベースへの保存処理
  // database.logs.insert(logEntry);
}

// HTTP API トランスポート
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
  };
  
  // 非同期でAPIに送信
  fetch('https://api.example.com/logs', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  }).catch(console.error);
}

// トランスポート付きロガーの作成
const logger = new Logger({
  name: "TransportLogger",
  type: "json"
});

// 複数のトランスポートを登録
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,
  }
]);

// ログ出力(自動的に全てのトランスポートに送信される)
logger.info("ユーザー登録", { userId: 123, email: "[email protected]" });
logger.error("データベース接続エラー", { error: "Connection timeout" });

AsyncLocalStorage との統合

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

// リクエストコンテキストの型定義
interface RequestContext {
  requestId: string;
  userId?: string;
  traceId: string;
}

// AsyncLocalStorage の設定
const requestContext = new AsyncLocalStorage<RequestContext>();

// コンテキスト情報を自動追加するトランスポート
function contextEnhancedTransport(logObj: ILogObj) {
  const context = requestContext.getStore();
  
  if (context) {
    // ログオブジェクトにコンテキスト情報を追加
    const enhancedLogObj = {
      ...logObj,
      requestId: context.requestId,
      userId: context.userId,
      traceId: context.traceId
    };
    
    console.log(JSON.stringify(enhancedLogObj));
  } else {
    console.log(JSON.stringify(logObj));
  }
}

// ロガーの設定
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 ミドルウェア
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("リクエスト開始", {
      method: req.method,
      url: req.url,
      userAgent: req.headers['user-agent']
    });
    
    next();
  });
});

// ルートハンドラー
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  
  // このログには自動的にrequestId、traceId、userIdが追加される
  logger.info("ユーザー情報取得", { targetUserId: userId });
  
  try {
    // ビジネスロジック
    const user = getUserById(userId);
    
    logger.info("ユーザー情報取得成功", { user: user.id });
    res.json(user);
  } catch (error) {
    logger.error("ユーザー情報取得エラー", { error: error.message });
    res.status(500).json({ error: "Internal Server Error" });
  }
});

ログのフィルタリングとフォーマッティング

import { Logger, ILogObj } from "tslog";

// カスタムログオブジェクトの型定義
interface CustomLogObj extends ILogObj {
  service?: string;
  version?: string;
  environment?: string;
}

// フィルタリング関数
function sensitiveDataFilter(logObj: ILogObj): ILogObj {
  const filtered = { ...logObj };
  
  // センシティブデータのマスキング
  if (filtered[1] && typeof filtered[1] === 'object') {
    const data = { ...filtered[1] };
    
    ['password', 'token', 'secret', 'key', 'authorization'].forEach(field => {
      if (data[field]) {
        data[field] = '[REDACTED]';
      }
    });
    
    // 深いネストのマスキング
    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;
}

// カスタムフォーマッター
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);
}

// ロガーの設定
const logger = new Logger({
  name: "StructuredLogger",
  type: "json",
  minLevel: "info"
});

// フィルタリングとフォーマットを適用したトランスポート
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);
  }
});

// 使用例
logger.info("ユーザー認証成功", {
  userId: 12345,
  username: "john_doe",
  password: "secret123",  // 自動的にマスキングされる
  authToken: "abc123",    // 自動的にマスキングされる
  loginTime: new Date()
});

テストとモッキング

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

// テスト用のロガーモック
class MockLogger extends Logger {
  public logs: ILogObj[] = [];
  
  constructor() {
    super({
      name: "MockLogger",
      type: "hidden" // ログ出力を無効化
    });
    
    // 全てのログを記録するトランスポート
    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' })
    });
  }
  
  // テスト用のヘルパーメソッド
  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];
  }
}

// テスト対象のサービス
class UserService {
  constructor(private logger: Logger) {}
  
  async createUser(userData: { name: string; email: string }) {
    this.logger.info("ユーザー作成開始", { userData });
    
    try {
      // ユーザー作成のロジック
      const user = { id: 123, ...userData };
      
      this.logger.info("ユーザー作成成功", { userId: user.id });
      return user;
    } catch (error) {
      this.logger.error("ユーザー作成エラー", { error: error.message });
      throw error;
    }
  }
}

// Jest テスト
describe('UserService', () => {
  let mockLogger: MockLogger;
  let userService: UserService;
  
  beforeEach(() => {
    mockLogger = new MockLogger();
    userService = new UserService(mockLogger);
  });
  
  afterEach(() => {
    mockLogger.clearLogs();
  });
  
  test('ユーザー作成時に適切なログが出力される', async () => {
    const userData = { name: 'John Doe', email: '[email protected]' };
    
    await userService.createUser(userData);
    
    // ログの確認
    const infoLogs = mockLogger.getLogsByLevel('info');
    expect(infoLogs).toHaveLength(2);
    
    // 開始ログの確認
    expect(infoLogs[0][0]).toContain('ユーザー作成開始');
    expect(infoLogs[0][1]).toEqual({ userData });
    
    // 成功ログの確認
    expect(infoLogs[1][0]).toContain('ユーザー作成成功');
    expect(infoLogs[1][1]).toEqual({ userId: 123 });
  });
  
  test('エラー時にエラーログが出力される', async () => {
    // エラーを発生させるためのモック
    const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
    
    try {
      // エラーを引き起こす処理
      await userService.createUser({ name: '', email: 'invalid' });
    } catch (error) {
      // エラーログの確認
      const errorLogs = mockLogger.getLogsByLevel('error');
      expect(errorLogs).toHaveLength(1);
      expect(errorLogs[0][0]).toContain('ユーザー作成エラー');
    }
    
    consoleSpy.mockRestore();
  });
});