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

こんにちは、皆さん。今回は、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の基本的な使い方から高度な機能まで、詳しく解説していきます。ぜひ最後までお付き合いください。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286030/0b96f492-e6fc-1824-07d3-b5be076eff77.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/286030/77774ebd-3818-273b-a11d-49b6f710f742.png)

[LogTape]: https://github.com/dahlia/logtape

## インストール

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

### Deno

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

```sh
deno add @logtape/logtape
```

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

### Node.js

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

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

### Bun

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

```sh
bun add @logtape/logtape
```

## クイックスタート

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

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

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

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

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

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

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

## ログの記録方法

ログレベルは全部で5つあります：`debug`、`info`、`warning`、`error`、`fatal`（詳細度の順）。以下の構文でメッセージをログに記録できます：

```typescript
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}.`;
```

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

```typescript
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 });
```

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

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

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

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

## カテゴリ

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

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

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

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

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

```typescript
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}.");
```

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

```typescript
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つのシンクを提供しています。ただし、独自のシンクを簡単に追加できます。シンクのシグネチャは以下の通りです：

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

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

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

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

### コンソールシンク

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

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

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

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

[`getConsoleSink()`]: https://jsr.io/@logtape/logtape/doc/~/getConsoleSink
[`ConsoleSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/ConsoleSinkOptions

### ストリームシンク

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

```typescript
// Deno:
await configure({
  sinks: {
    stream: getStreamSink(Deno.stderr.writable),
  },
  // 省略
});
```

```typescript
// Node.js:
import stream from "node:stream";

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

:::note warn
ここではWeb Streams APIの`WritableStream`を使用しています。Node.jsを使用している場合、`process.stderr`は`WritableStream`ではなく[`Writable`]（Node.jsストリーム）であるため、直接`getStreamSink`に渡すことはできません。[`Writable.toWeb()`]メソッドを使用して、Node.jsストリームを`WritableStream`に変換できます。
:::

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

[`WritableStream`]: https://developer.mozilla.org/en-US/docs/Web/API/WritableStream
[`Writable`]: https://nodejs.org/api/stream.html#class-streamwritable
[`Writable.toWeb()`]: https://nodejs.org/api/stream.html#streamwritabletowebstreamwritable
[`getStreamSink()`]: https://jsr.io/@logtape/logtape/doc/~/getStreamSink
[`StreamSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/StreamSinkOptions

### ファイルシンク

:::note warn
ファイルシンクはブラウザ環境では利用できません。
:::

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

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

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

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

:::note warn
Denoでは、ファイルシンクを使用するために`--allow-write`フラグと`--unstable-fs`フラグが必要です。
:::

[`getFileSink()`]: https://jsr.io/@logtape/logtape/doc/~/getFileSink
[`FileSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/FileSinkOptions

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

:::note warn
ローテーティングファイルシンクはブラウザ環境では利用できません。
:::

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

```typescript
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`]インターフェースを参照してください。

:::note warn
Denoでは、ローテーティングファイルシンクを使用するために`--allow-write`フラグと`--unstable-fs`フラグが必要です。
:::

[`getRotatingFileSink()`]: https://jsr.io/@logtape/logtape/doc/~/getRotatingFileSink
[`RotatingFileSinkOptions`]: https://jsr.io/@logtape/logtape/doc/~/RotatingFileSinkOptions

### テキストフォーマッタ

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

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

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

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

:::note
JSON Lines形式でフォーマットされたログメッセージをリアルタイムで読みやすく監視したい場合は、`tail`と[`jq`]コマンドを利用できます：

```sh
tail -f log.jsonl | jq .
```
:::

[JSON Lines]: https://jsonlines.org/
[`jq`]: https://jqlang.github.io/jq/

### ディスポーザブルシンク

:::note
ディスポーザブルの概念に慣れていない場合は [ECMAScript Explicit Resource Management]の提案も参照してください。
:::

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

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

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

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

[ECMAScript Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management

### 明示的な破棄

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

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

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

[`dispose()`]: https://jsr.io/@logtape/logtape/doc/~/dispose
[`ctx.waitUntil()`]: https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil

## フィルター

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

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

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

```typescript
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`未満のログメッセージを破棄します：

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

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

[`LogLevel`]: https://jsr.io/@logtape/logtape/doc/~/LogLevel

### シンクフィルター

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

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

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

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

:::note
`withFilter()`関数は、第2引数として[`LogLevel`]文字列を取ることもできます。この場合、指定されたログレベル未満のログメッセージは破棄されます。
:::

[`withFilter()`]: https://jsr.io/@logtape/logtape/doc/~/withFilter

## テスト

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

### 設定のリセット

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

```typescript
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は組み込みのバッファシンクを提供していませんが、簡単に実装できます：

```typescript
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

ハッピーロギング！