はじめに
npmパッケージを作っていて、こんな悩みを抱えたことはないでしょうか。ライブラリの内部で何が起きているか知りたくてconsole.logを仕込んだものの、公開する段になって消し忘れがないか不安になる。かといってWinstonやPinoのような本格的なロギングライブラリを依存関係に加えるのは、軽量さを売りにしたいライブラリにとっては重すぎる。
LogTapeはこの悩みをまるごと解決するために作られたロギングライブラリです。最大の特徴は「アプリケーション側がconfigure()を呼ぶまで、ライブラリ内のログは完全に沈黙する」という設計にあります。ライブラリ作者は気兼ねなくログを仕込んでおき、それを使うアプリケーション開発者だけが必要なときにログを表示できる。しかもゼロ依存・5.3KB(min+gzip)という軽さで、Node.js・Deno・Bun・ブラウザ・Edge Runtimeのすべてで同じコードが動きます。
まずは実際に触ってみたほうが早いと思います。ログレベルの切り替えやカテゴリごとのフィルタリングがその場で確認できるので、先に挙動を見たい方はこちらからどうぞ。
LogTapeとは
LogTapeは、TypeScript製のロギングライブラリです。作者は非同期ライブラリ「Fedify」などでも知られるHong Minhee氏で、「ライブラリファースト」という思想のもとに設計されています。npmパッケージ名は@logtape/logtapeで、JSRにも公開されています。
主な特徴
- ライブラリファースト設計 -
configure()を呼ぶまでログは一切出力されない。ライブラリの内部に安心してログを仕込める - ゼロ依存・軽量 - 依存パッケージなし、5.3KB(min+gzip)。バンドルサイズへの影響が最小限
- オールランタイム対応 - Node.js・Deno・Bun・ブラウザ・Edge Runtimeで同じAPIが動く
- 階層的カテゴリシステム -
["app", "db"]のような配列でロガーを分類し、親カテゴリの設定が子に継承される - 構造化ログ - テンプレートリテラルのプレースホルダーにメタデータを渡すだけで構造化データとして記録できる
- 柔軟なSink/Filter/Formatter - Console・File・OpenTelemetry・Sentry・CloudWatch Logsなど出力先を選べ、フィルタや整形も自由に組める
インストール
npm install @logtape/logtape
pnpm add @logtape/logtape
yarn add @logtape/logtape
Deno環境ではjsr:@logtape/logtapeから、Bunでも上記のnpmコマンドがそのまま利用できます。
LogTapeのサンプルを動かす
下のサンプルでは、getLogger()で作成したロガーに対してconfigure()でログレベルを切り替えながら、ボタンを押した際の出力を確認できます。LogTapeではtrace・debug・info・warning・error・fatalの6段階のログレベルがあり、lowestLevelで指定したレベル未満のログは出力されません。
まずは要点だけを抜き出すと、以下のような形です。
import { configure, getConsoleSink, getLogger } from "@logtape/logtape";
// アプリ側の設定。これを呼ぶまでログは沈黙している
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: "my-app", lowestLevel: "info", sinks: ["console"] },
],
});
const logger = getLogger(["my-app"]);
logger.info("Hello, {name}!", { name: "LogTape" });
実際に動かせるものが下です。セレクトボックスでlowestLevelを切り替えてからボタンを押すと、そのレベル未満のログが除外される様子がその場で確認できます。
lowestLevelをerrorにしてから実行すると、debug・info・warningのログは出力先まで届かず、errorのログだけが画面に表示されます。これがLogTapeの「アプリ側が閾値を決めるまでライブラリは黙っている」という設計の核心部分です。
基本的な使い方
LogTapeの使い方は、大きく「ライブラリ側」と「アプリケーション側」の2つの視点に分かれます。
ライブラリ(あるいは自分のモジュール)の中では、getLogger()でカテゴリを指定してロガーを取得し、そのまま使うだけです。
import { getLogger } from "@logtape/logtape";
const logger = getLogger(["shopkit", "checkout"]);
export function charge(orderId: string, amount: number) {
logger.debug("charging order {orderId} for {amount}", { orderId, amount });
// ...実際の課金処理
}
この時点では、まだどこにもログは出力されません。configure()を一度も呼んでいないためです。アプリケーションのエントリーポイントで、初めて出力先とレベルを決めます。
import { configure, getConsoleSink } from "@logtape/logtape";
await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["shopkit"], lowestLevel: "debug", sinks: ["console"] },
{ category: ["logtape", "meta"], lowestLevel: "warning", sinks: ["console"] },
],
});
{orderId}のようなプレースホルダーは、第2引数のオブジェクトから値が埋め込まれるだけでなく、そのままメタデータとして構造化ログにも記録されます。文字列連結でログメッセージを組み立てる必要がありません。
実践的なユースケース
階層的カテゴリでモジュールごとの詳細度を制御する
大きめのアプリケーションでは、モジュールによって欲しいログの詳細度が異なります。LogTapeのカテゴリは文字列の配列で表現され、["app", "db"]は["app"]の子カテゴリとして扱われます。親カテゴリに設定したlowestLevelやSinkは、明示的に上書きしない限り子に継承されるため、モジュールを追加するたびに個別設定を書く必要がありません。
await configure({
sinks: { console: getConsoleSink() },
loggers: [
// appカテゴリ全体はinfo以上をconsoleへ
{ category: ["app"], lowestLevel: "info", sinks: ["console"] },
// dbサブカテゴリだけはdebugまで詳しく見たい
{ category: ["app", "db"], lowestLevel: "debug", sinks: ["console"] },
],
});
const dbLogger = getLogger(["app", "db"]);
dbLogger.debug("query executed in {ms}ms", { ms: 12 });
以下のサンプルでは、app・app.db・app.authという3つのカテゴリのロガーを用意し、カテゴリごとのlowestLevel設定によって出力される・されないが変わる様子をボタン操作で確認できます。
「app.db に debug ログ」ボタンはapp.dbのlowestLevel: "debug"により出力されますが、「app.auth に debug ログ」ボタンはapp.authの閾値がwarningのため画面に何も現れません。カテゴリ単位で詳細度を変えられることが分かります。
プレースホルダー構文で構造化ログを記録する
エラー調査のときに欲しいのは、人間が読む文章ではなく機械的に集計・検索できるデータです。LogTapeはテンプレートリテラルの{key}プレースホルダーに値を渡すだけで、メッセージ文字列と構造化データの両方を同時に作れます。文字列結合やJSON.stringifyを自分で書く必要はありません。
const logger = getLogger(["api"]);
logger.info("request {method} {path} completed in {ms}ms", {
method: "POST",
path: "/orders",
ms: 42,
});
// メッセージ: "request POST /orders completed in 42ms"
// 構造化データ: { method: "POST", path: "/orders", ms: 42 }
下のサンプルは、フォームに入力したユーザー名と操作内容をlogger.info()のプレースホルダーへ渡し、生成されたメッセージと構造化データの両方を画面に表示します。入力を変えて送信し直すと、その場で出力が更新されます。
user欄を空にして送信すると、メッセージ文中の該当箇所も構造化データのuserフィールドも同時に空文字になることが分かります。ログメッセージと検索用のメタデータが常に一致するため、後から集計基盤に取り込む際も値のズレに悩まされません。
フィルタでログを絞り込む
すべてのログをそのまま出力先へ流すと、ノイズが多くて本当に見たい情報が埋もれてしまいます。LogTapeはconfigure()のfiltersに、LogRecordを受け取ってboolean値を返す関数(あるいは組み込みのgetLevelFilter())を登録し、ロガーごとに適用できます。
import { configure, getLevelFilter } from "@logtape/logtape";
await configure({
filters: {
// amountが1万円以上のログだけ通す独自フィルタ
highAmount: (record) =>
typeof record.properties.amount === "number" &&
record.properties.amount >= 10000,
},
loggers: [
{ category: ["payment"], sinks: ["console"], filters: ["highAmount"] },
],
});
以下のサンプルでは、金額を入力してログを記録するたびに、カスタムフィルタ(amount >= 10000)を通過したものだけが一覧に残る様子を確認できます。
金額を5000にして記録するとhighAmountフィルタで弾かれ、一覧には何も追加されません。15000に戻すと再び表示されます。フィルタはSinkの手前で評価されるため、出力先の実装を変えずにノイズを削減できるのが利点です。
まとめ
LogTapeは「ライブラリの中では黙って待ち、アプリ側がconfigure()した瞬間だけ話し出す」という一貫した設計思想を持つロギングライブラリです。ゼロ依存で5.3KBという軽さながら、Node.js・Deno・Bun・ブラウザ・Edge Runtimeを横断して同じコードが動き、階層的カテゴリ・構造化ログ・柔軟なフィルタといった実運用に必要な機能も揃っています。
自作のnpmパッケージにロギングを仕込みたいときはもちろん、既存アプリのログ基盤を整理したいときにも候補に入れてみる価値があります。まずはgetLogger()とconfigure()だけの最小構成から試してみてください。
