JavaScript/TypeScript 向けのロギングライブラリとして LogTape を選ぶべき理由

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

多様で常に進化する JavaScript エコシステムにおいて、ロギングは開発、デバッグ、アプリケーションの監視のための重要なコンポーネントであり続けています。数多くのロギングライブラリが存在する中、LogTape はシンプルさ、柔軟性、クロスランタイム互換性の独自の組み合わせで際立っています。アプリケーションを構築しているか、ライブラリを開発しているかに関わらず、次の JavaScript または TypeScript プロジェクトで LogTape を検討すべき理由を探ってみましょう。

依存関係ゼロ:軽量なフットプリント

LogTape の最も魅力的な機能の一つは、依存関係が完全に存在しないことです。「依存関係地獄」が多くの JavaScript プロジェクトを悩ませている時代に、LogTape は爽やかな選択肢を提供します:

// LogTape 自体以外に追加パッケージのインストールは不要
import { configure, getConsoleSink, getLogger } from "@logtape/logtape";

この依存関係ゼロのアプローチには、いくつかの利点があります:

バンドルサイズの削減
推移的依存関係がないため、パッケージが小さくなる
安定性の向上
上流の依存関係からの破壊的変更のリスクがない
セキュリティの簡素化
サードパーティコードからの潜在的な脆弱性が少ない
統合オーバーヘッドの低減
特にユーザーに追加の依存関係を負担させたくないライブラリ作者にとって価値がある

ランタイムの多様性:一度書いて、どこでもログを取る

多くの人気のあるロギングライブラリが主に Node.js に焦点を当てている一方、LogTape は多様な JavaScript ランタイム間でシームレスなサポートを提供します:

このランタイムの柔軟性は、デプロイ環境に関係なく一貫したロギングパターンを使用できることを意味します:

// 同じ API がすべての JavaScript ランタイムでシームレスに動作
import { getLogger } from "@logtape/logtape";

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

// Node.js、Deno、Bun、ブラウザ、またはエッジ関数で動作
logger.info`User ${userId} logged in successfully`;

複数のプラットフォームにまたがって作業するチームや、ランタイム間を移行するプロジェクトにとって、この一貫性は非常に価値があります。異なるロギングライブラリやアプローチを学ぶ必要はなく、LogTape はどこでも同じように動作します。

階層的カテゴリ:きめ細かな制御

LogTape の階層的カテゴリシステムは、JavaScript ロギングライブラリの中では驚くほど珍しい際立った機能です。カテゴリを使用すると、ログをツリーのような構造で整理できます:

// 親カテゴリ
const appLogger = getLogger(["my-app"]);

// 子カテゴリは親から設定を継承
const dbLogger = getLogger(["my-app", "database"]);

// 孫カテゴリ
const queryLogger = getLogger(["my-app", "database", "queries"]);

// getChild() を使用した代替アプローチ
const userLogger = appLogger.getChild("users");
const authLogger = userLogger.getChild("auth");

この階層的アプローチは強力な利点を提供します:

ターゲットを絞ったフィルタリング
アプリケーションの異なる部分に対して異なるログレベルを設定
継承
子ロガーは親から設定を継承し、設定のオーバーヘッドを削減
組織的な明確さ
ログは自然にアプリケーションのモジュール構造に従う

異なるカテゴリに対してログレベルを設定する方法の例:

await configure({
  sinks: {
    console: getConsoleSink(),
    file: getFileSink("app.log"),
  },
  loggers: [
    // すべてのアプリログの基本設定
    { 
      category: ["my-app"], 
      lowestLevel: "info", 
      sinks: ["console", "file"] 
    },
    // データベースコンポーネントのみのより詳細なロギング
    { 
      category: ["my-app", "database"], 
      lowestLevel: "debug", 
      sinks: ["file"] 
    }
  ]
});

この設定により、「info」レベル以上のすべてのアプリケーションログはコンソールとファイルの両方に送られますが、データベース固有のログには「debug」レベルのより詳細な情報が含まれ、ログファイルにのみ記録されます。

構造化ロギング:単純なテキストを超えて

現代のロギングは単純なテキスト文字列を超えています。LogTape は構造化ロギングを採用しており、ログエントリをプレーンテキストではなくデータオブジェクトとして扱います:

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

LogTape はメッセージ内のプレースホルダもサポートし、構造化データと人間が読みやすいテキストを接続します:

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

構造化ロギングは大きな利点を提供します:

検索性の向上
テキストを解析する代わりに特定のフィールド値を検索
より良い分析
構造化されたフィールドでデータ分析を実行
一貫したフォーマット
標準化されたログフォーマットを強制
機械可読性
ログ管理システムによる処理が容易

パフォーマンスを意識したアプリケーションのために、LogTape は構造化データの遅延評価を提供します:

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

この関数は、デバッグレベルが有効な場合にのみ評価され、抑制されたログレベルに対する不要な計算を防ぎます。

非常にシンプルなシンクとフィルター:最小限のボイラープレート

LogTape の拡張性へのアプローチは驚くほど簡単です。カスタムシンク(出力先)とフィルターの作成には最小限のボイラープレートコードが必要です。

超シンプルなシンク

LogTape におけるシンクは、ログレコードを受け取る関数に過ぎません:

// カスタムシンクの作成は関数を定義するだけでとても簡単
const mySink = (record) => {
  const timestamp = new Date(record.timestamp).toISOString();
  const level = record.level.toUpperCase();
  const category = record.category.join('.');
  
  // カスタム送信先に送信
  myCustomLogService.send({
    time: timestamp,
    priority: level,
    component: category,
    message: record.message,
    ...record.properties
  });
};

// 設定でカスタムシンクを使用
await configure({
  sinks: {
    console: getConsoleSink(),
    custom: mySink
  },
  loggers: [
    { category: ["my-app"], sinks: ["console", "custom"] }
  ]
});

これを、クラスの拡張、複数のメソッドの実装、または特定のパターンに従うことを要求する他のライブラリと比較してみてください。LogTape のアプローチは爽やかなほど簡潔です。

シンプルなフィルター

同様に、LogTape のフィルターは単にブール値を返す関数です:

// 高優先度または特定のコンポーネントログのみを通過させるフィルター
const importantLogsFilter = (record) => {
  // エラーは常に含める
  if (record.level === "error" || record.level === "fatal") {
    return true;
  }
  
  // 支払い関連のログは常に含める
  if (record.category.includes("payments")) {
    return true;
  }
  
  // その他のログはフィルタリング
  return false;
};

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

LogTape はレベルベースのフィルタリングのための便利な省略形も提供しています:

await configure({
  // ...シンク設定
  filters: {
    // これは「warning」レベル以上のフィルターを作成します
    warningAndAbove: "warning"
  },
  loggers: [
    { 
      category: ["my-app"], 
      sinks: ["console"], 
      filters: ["warningAndAbove"] 
    }
  ]
});

ライブラリ作者に最適

LogTape は、ユーザーに負担をかけずにロギングを組み込みたいライブラリ作者にとって特に適しています。その中核的な考え方はシンプルです:

  1. ライブラリはロギング出力ポイントを提供する
  2. アプリケーションはそれらのログの処理方法を設定する

ライブラリが 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
    });
    
    // 接続ロジック...
    
    this.logger.debug("Connection established");
  }
  
  query(sql) {
    this.logger.debug("Executing query", { sql });
    // クエリロジック...
  }
}

重要なポイントは、ライブラリが*configure()を呼び出さない*ことです。代わりに、適切なレベルとコンテキストデータを持つ有用なログ出力ポイントを提供します。

ライブラリを使用するアプリケーションは、これらのログをどのように処理するかを正確に決定できます:

// アプリケーションコード
import { configure, getConsoleSink } from "@logtape/logtape";
import { Database } from "my-awesome-lib";

// ログの処理方法を設定
await configure({
  sinks: {
    console: getConsoleSink(),
    file: getFileSink("app.log")
  },
  loggers: [
    // すべてのライブラリログを処理
    { 
      category: ["my-awesome-lib"], 
      lowestLevel: "info", 
      sinks: ["file"] 
    },
    // 開発中はデータベースコンポーネントをより詳細に
    { 
      category: ["my-awesome-lib", "database"], 
      lowestLevel: "debug", 
      sinks: ["console", "file"] 
    }
  ]
});

// ライブラリを使用
const db = new Database("localhost", 5432, "user");
db.connect();

この関心の分離は、いくつかの利点を提供します:

  1. ライブラリユーザーはログ処理を完全に制御できる
  2. ライブラリは実装の詳細を強制せずに豊富なロギングを提供できる
  3. アプリケーションのロギング設定との競合リスクがない
  4. ライブラリは内部的に「ノイズが多い」状態でも、アプリケーションが必要に応じてフィルタリングできる

より豊かなロギングのためのコンテキスト

LogTape は複数のログメッセージ間で一貫したプロパティを追加するためのコンテキストメカニズムを提供します。これはシステム全体でリクエストを追跡するのに特に価値があります:

明示的なコンテキスト

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

// コンテキスト付きのロガーを作成
const requestLogger = logger.with({
  requestId: "abc-123",
  userId: 42,
  endpoint: "/users"
});

// このロガーからのすべてのログにはコンテキストプロパティが含まれる
requestLogger.info("Processing request");
requestLogger.debug("Validating input");
requestLogger.info("Request completed", { durationMs: 120 });

暗黙的なコンテキスト(v0.7.0以降)

関数呼び出し間で明示的な受け渡しなしにコンテキストを適用したい場合:

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

function handleRequest(req, res) {
  withContext({ 
    requestId: req.id, 
    userId: req.user?.id 
  }, () => {
    // この関数内およびそれが呼び出す任意の関数内のすべてのログには
    // 自動的にコンテキストプロパティが含まれる
    processRequest(req, res);
  });
}

function processRequest(req, res) {
  // コンテキストを渡す必要はない - 自動的に利用可能
  getLogger(["my-app", "processor"]).info("Processing data");
  
  // コンテキストを継承する他の関数を呼び出す
  validateInput(req.body);
}

function validateInput(data) {
  // このログにも requestId と userId が含まれる
  getLogger(["my-app", "validator"]).debug("Validating input", { data });
}

この暗黙的なコンテキスト機能は、すべての関数呼び出しを通じて手動でコンテキストを渡すことなく、コードの複数の層を通じてリクエストを追跡するのに非常に価値があります。

LogTape が最適な選択ではない場合

LogTape は多くのユースケースで魅力的な利点を提供しますが、普遍的に最良の選択ではありません:

極端なパフォーマンス要件
アプリケーションが毎秒数万のエントリをログに記録し、生のパフォーマンスが最優先事項である場合、最適化されたロギングスループットに焦点を当てた Pino のような特化した高性能ライブラリがより適しているかもしれません。
広範な既製の統合
カスタムコードを書かずに多数の特定のシステム(Elasticsearch、Graylog など)との即時統合が必要な場合、Winston の豊富なトランスポートエコシステムがより速いスタート地点を提供するかもしれません。
特定のロギング要件を持つレガシーシステム
Java や他の環境からの特定のロギングパターンを中心に構築されたシステムを維持している場合、Log4js のような目的に合わせて構築されたライブラリがより馴染みのある API を提供するかもしれません。
最小限のロギングニーズを持つウェブブラウザのみのアプリケーション
レベル付きの基本的なコンソール出力だけが必要な非常にシンプルなウェブブラウザのみのロギングニーズの場合、loglevel のようなさらにシンプルなライブラリで十分かもしれません。

結論

LogTape は、実世界の開発課題に対応する独自の機能の組み合わせを提供することで、混雑した JavaScript ロギングの風景の中で際立っています:

  • 依存関係ゼロで軽量かつ安全な基盤
  • Node.js、Deno、Bun、ブラウザ、エッジ関数をサポートするランタイムの多様性
  • より良いログ整理とフィルタリングのための階層的カテゴリ
  • 分析と検索性を向上させる構造化ロギング
  • 最小限のボイラープレートによるシンプルな拡張メカニズム
  • 関心の分離を尊重するライブラリフレンドリーな設計

アプリケーションやライブラリを構築している場合でも、複数の JavaScript ランタイムにまたがって作業している場合でも、あるいは単にクリーンで設計の良いロギングソリューションを求めている場合でも、LogTape は真剣な検討に値します。その思慮深い設計はシンプルさと強力な機能のバランスを取り、JavaScript ロギングライブラリの一般的な落とし穴を回避しています。

詳細情報と詳細なドキュメントについては、LogTape の公式ウェブサイトをご覧ください。

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