LogTape:JavaScript/TypeScript向けのシンプルで柔軟なロギングライブラリの紹介と使い方

2024/09/08に公開

こんにちは、皆さん。今回は、JavaScriptとTypeScript向けの新しいロギングライブラリ「LogTape」をご紹介します。

https://github.com/dahlia/logtape

ログ出力は開発者にとって欠かせない作業ですが、適切なツールがないと面倒で時間がかかることがあります。LogTapeは、この問題を解決するために設計された、シンプルでありながら強力なロギングライブラリです。

LogTapeの主な特徴は以下の通りです:

  • 依存関係なし:LogTapeには依存関係がありません。LogTapeの依存関係を心配することなく使用できます。

  • ライブラリサポート:LogTapeはアプリケーションだけでなく、ライブラリでも使用できるように設計されています。ライブラリにログ機能を提供するためにLogTapeを使用できます。

  • ランタイム多様性:LogTapeは、Deno、Node.js、Bun、エッジ関数、ブラウザをサポートしています。コードを変更することなく、さまざまな環境でLogTapeを使用できます。

  • 構造化ログ:構造化データでメッセージをログに記録できます。

  • 階層的カテゴリ:LogTapeは階層的なカテゴリシステムを使用してロガーを管理します。カテゴリ階層の異なるレベルでログレベルを設定することで、ログメッセージの詳細度を制御できます。

  • テンプレートリテラル:LogTapeはログメッセージにテンプレートリテラルをサポートしています。プレースホルダーと値を使用してメッセージをログに記録できます。

  • シンプルなシンク:LogTapeに独自のシンクを簡単に追加できます。現在、LogTapeは限られたシンクしか提供していませんが、独自のシンクを簡単に追加できます

本記事では、LogTapeの基本的な使い方から高度な機能まで、詳しく解説していきます。ぜひ最後までお付き合いください。


インストール

LogTapeは様々な JavaScript/TypeScript 実行環境をサポートしています。以下に、主要な環境でのインストール方法を示します。

Deno

Denoは、セキュリティとパフォーマンスに重点を置いた最新の JavaScript/TypeScript ランタイムです。Denoでは、以下のコマンドを使用してLogTapeを追加できます:

deno add @logtape/logtape

このコマンドは、プロジェクトのdeno.jsonにLogTapeの依存関係を追加します。

Node.js

Node.jsでは、お好みのパッケージマネージャーを使用してLogTapeをインストールできます。npm、pnpm、yarnのいずれかを使用して以下のようにインストールしてください:

npm add @logtape/logtape
# または
pnpm add @logtape/logtape
# または
yarn add @logtape/logtape

Bun

Bunは、高速で全機能を備えたJavaScript/TypeScriptツールキットです。BunでLogTapeをインストールするには、以下のコマンドを使用します:

bun add @logtape/logtape

クイックスタート

アプリケーションのエントリーポイントでLogTapeを設定します(ライブラリを作成している場合は、ライブラリ自体でLogTapeを設定しないでください。LogTapeの設定はアプリケーション側の責任です):

import { configure, getConsoleSink } from "@logtape/logtape";

await configure({
  sinks: { console: getConsoleSink() },
  filters: {},
  loggers: [
    { category: "my-app", level: "debug", sinks: ["console"] }
  ]
});

そして、アプリケーションやライブラリでLogTapeを使用できます:

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

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

export function myFunc(value: number): void {
  logger.debug `Hello, ${value}!`;
}

ログの記録方法

ログレベルは全部で5つあります:debuginfowarningerrorfatal(詳細度の順)。以下の構文でメッセージをログに記録できます:

logger.debug `This is a debug message with ${value}.`;
logger.info  `This is an info message with ${value}.`;
logger.warn  `This is a warning message with ${value}.`;
logger.error `This is an error message with ${value}.`;
logger.fatal `This is a fatal message with ${value}.`;

関数呼び出しを使用してメッセージをログに記録することもできます。この場合、ログメッセージは構造化データになります:

logger.debug("This is a debug message with {value}.", { value });
logger.info("This is an info message with {value}.", { value });
logger.warn("This is a warning message with {value}.", { value });
logger.error("This is an error message with {value}.", { value });
logger.fatal("This is a fatal message with {value}.", { value });

ログに記録される値の計算が高コストな場合があります。そのような場合、関数を使用して計算を遅延させ、ログメッセージが実際に記録されるときにのみ計算されるようにできます:

logger.debug(l => l`This is a debug message with ${computeValue()}.`);
logger.debug("Or you can use a function call: {value}.", () => {
  return { value: computeValue() };
});

関数呼び出しを使用する場合、単一の中括弧 { をログに記録するには、中括弧を二重にします {{

logger.debug("This logs {{single}} curly braces.");

カテゴリ

LogTapeはロガーを管理するために階層的なカテゴリシステムを使用します。カテゴリは文字列のリストです。例えば、["my-app", "my-module"]はカテゴリです。

メッセージをログに記録すると、そのロガーのカテゴリのプレフィックスを持つすべてのロガーにディスパッチされます。例えば、カテゴリ["my-app", "my-module", "my-submodule"]でメッセージをログに記録すると、カテゴリが["my-app", "my-module"]["my-app"]のロガーにディスパッチされます。

この動作により、カテゴリ階層の異なるレベルでログレベルを設定することで、ログメッセージの詳細度を制御できます。

以下は、異なるカテゴリにログレベルを設定する例です:

import { configure, getConsoleSink } from "@logtape/logtape";

await configure({
  sinks: {
    console: getConsoleSink(),
  },
  filters: {},
  loggers: [
    { category: ["my-app"], level: "info", sinks: ["console"] },
    { category: ["my-app", "my-module"], level: "debug", sinks: ["console"] },
  ],
})

コンテキスト

コンテキストはLogTape 0.5.0以降で利用可能です。

LogTapeは、ログメッセージ間で同じプロパティを再利用するためのコンテキストシステムを提供します。コンテキストはキーと値のマップです。ロガーにコンテキストを設定し、そのコンテキストでメッセージをログに記録できます。以下は、ロガーにコンテキストを設定する例です:

const logger = getLogger(["my-app", "my-module"]);
const ctx = logger.with({ userId: 1234, requestId: "abc" });
ctx.info `This log message will have the context (userId & requestId).`;
ctx.warn("Context can be used inside message template: {userId}, {requestId}.");

コンテキストは子ロガーに継承されます。以下は、親ロガーにコンテキストを設定し、子ロガーでメッセージをログに記録する例です:

const logger = getLogger(["my-app"]);
const parentCtx = logger.with({ userId: 1234, requestId: "abc" });
const childCtx = parentCtx.getLogger(["my-module"]);
childCtx.debug("This log message will have the context: {userId} {requestId}.");

コンテキストは、構造化ログを行いたい場合に特に有用です。

シンク

シンクはログメッセージの送信先です。LogTapeは現在、コンソールとストリームの2つのシンクを提供しています。ただし、独自のシンクを簡単に追加できます。シンクのシグネチャは以下の通りです:

export type Sink = (record: LogRecord) => void;

以下は、ログメッセージをコンソールに書き込む単純なシンクの例です:

import { configure } from "@logtape/logtape";

await configure({
  sinks: {
    console(record) {
      console.log(record.message);
    }
  },
  // 省略
});

コンソールシンク

もちろん、独自のコンソールシンクを実装する必要はありません。LogTapeはコンソールシンクを提供しています:

import { configure, getConsoleSink } from "@logtape/logtape";

await configure({
  sinks: {
    console: getConsoleSink(),
  },
  // 省略
});

詳細については、APIリファレンスのgetConsoleSink()関数とConsoleSinkOptionsインターフェースを参照してください。

ストリームシンク

もう1つの組み込みシンクはストリームシンクです。これはWritableStreamにログメッセージを書き込みます。以下は、標準エラーにログメッセージを書き込むストリームシンクの例です:

// Deno:
await configure({
  sinks: {
    stream: getStreamSink(Deno.stderr.writable),
  },
  // 省略
});
// Node.js:
import stream from "node:stream";

await configure({
  sinks: {
    stream: getStreamSink(stream.Writable.toWeb(process.stderr)),
  },
  // 省略
});

詳細については、APIリファレンスのgetStreamSink()関数とStreamSinkOptionsインターフェースを参照してください。

ファイルシンク

LogTapeはファイルシンクも提供しています。以下は、ログメッセージをファイルに書き込むファイルシンクの例です:

import { getFileSink } from "@logtape/logtape";

await configure({
  sinks: {
    file: getFileSink("my-app.log"),
  },
  // 省略
});

詳細については、APIリファレンスのgetFileSink()関数とFileSinkOptionsインターフェースを参照してください。

ローテーティングファイルシンク

ローテーティングファイルシンクは、ログファイルをローテーションするファイルシンクです。現在のログファイルが一定のサイズに達すると、新しいログファイルを作成します。以下は、ログメッセージをファイルに書き込むローテーティングファイルシンクの例です:

import { getRotatingFileSink } from "@logtape/logtape";

await configure({
  sinks: {
    file: getRotatingFileSink("my-app.log", {
      maxFileSize: 1024 * 1024,  // 1 MiB
      maxFiles: 5,
    }),
  },
  // 省略
});

ローテーションされたログファイルには、.1.2.3などのサフィックスが付けられます。

詳細については、APIリファレンスのgetRotatingFileSink()関数とRotatingFileSinkOptionsインターフェースを参照してください。

テキストフォーマッタ

ストリームシンクとファイルシンクは、プレーンテキスト形式でログメッセージを書き込みます。テキストフォーマッタを提供することで、フォーマットをカスタマイズできます。テキストフォーマッタの型は以下の通りです:

export type TextFormatter = (record: LogRecord) => string;

以下は、JSON Lines形式でログメッセージを書き込むテキストフォーマッタの例です:

await configure({
  sinks: {
    stream: getFileSink("log.jsonl", {
      formatter(log) {
        return JSON.stringify(log) + "\n",
      }
    }),
  },
  // 省略
})

ディスポーザブルシンク

ディスポーザブルシンクは、破棄できるシンクです。これらは、設定がリセットされるかプログラムが終了すると自動的に破棄されます。ディスポーザブルシンクの型は:Sink & Disposableです。[Symbol.dispose]メソッドを定義することで、ディスポーザブルシンクを作成できます:

const disposableSink: Sink & Disposable = (record: LogRecord) => {
  console.log(record.message);
};
disposableSink[Symbol.dispose] = () => {
  console.log("Disposed!");
};

シンクを非同期に破棄することもできます。非同期ディスポーザブルシンクの型は:Sink & AsyncDisposableです。[Symbol.asyncDispose]メソッドを定義することで、非同期ディスポーザブルシンクを作成できます:

const asyncDisposableSink: Sink & AsyncDisposable = (record: LogRecord) => {
  console.log(record.message);
};
asyncDisposableSink[Symbol.asyncDispose] = async () => {
  console.log("Disposed!");
};

明示的な破棄

dispose()メソッドを呼び出すことで、シンクを明示的に破棄できます。これは、エッジ関数でレスポンスの返却をブロックせずにシンクのバッファをフラッシュしたい場合に便利です。以下は、Cloudflare Workersでctx.waitUntil()dispose()を使用する例です:

import { configure, dispose } from "@logtape/logtape";

export default {
  async fetch(request, env, ctx) {
    await configure({ /* ... */ });
    // ...
    ctx.waitUntil(dispose());
  }
}

フィルター

フィルターは、ログメッセージをフィルタリングする関数です。フィルターはログレコードを受け取り、ブール値を返します。フィルターがtrueを返す場合、ログレコードはシンクに渡されます。そうでない場合、ログレコードは破棄されます。そのシグネチャは以下の通りです:

export type Filter = (record: LogRecord) => boolean;

例えば、以下のフィルターは、プロパティelapsedが100ミリ秒未満のログメッセージを破棄します:

import { configure, type LogRecord } from "@logtape/logtape";

await configure({
  // 省略
  filters: {
    tooSlow(record: LogRecord) {
      return "elapsed" in record.properties && record.properties.elapsed >= 100;
    },
  },
  loggers: [
    {
      category: ["my-app", "database"],
      level: "debug",
      sinks: ["console"],
      filters: ["tooSlow"],
    }
  ]
});

レベルフィルター

LogTapeは組み込みのレベルフィルターを提供しています。レベルフィルターを使用して、ログレベルでログメッセージをフィルタリングできます。レベルフィルターファクトリはLogLevel文字列を受け取り、レベルフィルターを返します。例えば、以下のレベルフィルターは、ログレベルがinfo未満のログメッセージを破棄します:

import { getLevelFilter } from "@logtape/logtape";

await configure({
  filters: {
    infoOrHigher: getLevelFilter("info");
  },
  // 省略
});

シンクフィルター

シンクフィルターは、特定のシンクに適用されるフィルターです。withFilter()でシンクを装飾することで、シンクにシンクフィルターを追加できます:

import { getConsoleSink, withFilter } from "@logtape/logtape";

await configure({
  sinks: {
    filteredConsole: withFilter(
      getConsoleSink(),
      log => "elapsed" in log.properties && log.properties.elapsed >= 100,
    ),
  },
  // 省略
});

filteredConsoleSinkは、プロパティelapsedが100ミリ秒以上のメッセージのみをコンソールにログ出力します。

テスト

以下は、LogTapeを使用してアプリケーションやライブラリをテストするためのいくつかのヒントです。

設定のリセット

LogTapeの設定を初期状態にリセットできます。これは、テスト間で設定をリセットしたい場合に便利です。例えば、以下のコードは、Denoでテスト後(テストが成功するかどうかにかかわらず)に設定をリセットする方法を示しています:

import { configure, reset } from "@logtape/logtape";

Deno.test("my test", async (t) => {
  await t.step("set up", async () => {
    await configure({ /* ... */ });
  });

  await t.step("run test", () => {
    // テストを実行
  });

  await t.step("tear down", async () => {
    await reset();
  });
});

バッファシンク

テスト目的で、ログメッセージをメモリに収集したい場合があります。LogTapeは組み込みのバッファシンクを提供していませんが、簡単に実装できます:

import { type LogRecord, configure } from "@logtape/logtape";

const buffer: LogRecord[] = [];

await configure({
  sinks: {
    buffer: buffer.push.bind(buffer),
  },
  // 省略
});

結語

いかがでしたでしょうか。LogTapeを使うことで、JavaScriptやTypeScriptプロジェクトでのログ管理がより簡単で効率的になることがおわかりいただけたと思います。

LogTapeの特徴をまとめると:

  • シンプルな設定と使い方
  • 多様な環境での動作
  • 柔軟なカテゴリシステムとフィルタリング
  • 構造化ログのサポート
  • カスタマイズ可能なシンク

これらの機能により、小規模なプロジェクトから大規模なアプリケーションまで、幅広い用途に対応できます。
LogTapeは現在も活発に開発が続けられており、今後さらに機能が拡張されることが期待されます。ぜひ一度お試しいただき、皆さんのプロジェクトでのログ管理をより効率的に行ってみてはいかがでしょうか。

最後に、この記事が皆さんのログ管理の改善に役立つことを願っています。質問やフィードバックがありましたら、ぜひコメントでお知らせください。また、GitHubでのスター、イシューの報告、プルリクエストなども大歓迎です。

LogTapeのGitHubリポジトリ:

https://github.com/dahlia/logtape

ハッピーロギング!

Discussion