JS, TS用のロギングライブラリLogTapeを使ってみる

JS, TS用のロギングライブラリLogTapeを使ってみる

Cloudflare Workersでエラーログが正しく出力されず困っていたので、LogTapeというロギングライブラリを試してみました。その使い方や特徴についてまとめます。
2026.09.10

こんばんは、情報システム室の夏目です。

個人的にCloudflare Workersを使っているのですが、エラーログの出力でちょっと困ったことになったので、LogTapeというロギングのライブラリを試してみました。

今回はその内容を共有します。

困ったこと

code
console.error(new Error("test", {cause: new Error("inner error")}));

上記コードをCloudflare Workersで実行したとき次のようなログが出力されました。

log(メタデータは除外)
{
  "level": "error",
  "message": "    at Object.fetch (index.js:4:19)"
}

Stack Traceは表示されるものの、エラーメッセージは表示されずcauseの中身も表示されませんでした。

運用していく上では、障害調査で困ってしまいます。

LogTape

今回は LogTape というLoggerを使用することで対応します。

https://logtape.org/

$ npm install @logtape/logtape
code
import { configure, getConsoleSink, jsonLinesFormatter, getLogger } from "@logtape/logtape";

await configure({
  sinks: { console: getConsoleSink({ formatter: jsonLinesFormatter }) },
  loggers: [{ category: ["app"], sinks: ["console"], lowestLevel: "info" }],
});

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const logger = getLogger(["app"]);

    logger.error(new Error("test", { cause: new Error("inner error") }));

    return new Response("Hello World!");
  },
} satisfies ExportedHandler<Env>;

上記コマンドでインストールし実装し、実行すると次のようになります。

{
  "level": "ERROR",
  "@timestamp": "2026-09-10T06:21:22.261Z",
  "message": "\"test\"",
  "logger": "app",
  "properties": {
    "error": {
      "name": "Error",
      "message": "test",
      "stack": "Error: test\n    at Object.fetch (src/index.ts:12:18)",
      "cause": {
        "name": "Error",
        "message": "inner error",
        "stack": "Error: inner error\n    at Object.fetch (src/index.ts:12:45)"
      }
    }
  }
}

messageの内容が少し気になるところですが、この内容なら問題なさそうです。

LogTapeとは

https://logtape.org/intro

LogTapeは、JavaScriptおよびTypeScript向けのロギングライブラリで、ライブラリファーストの設計思想に基づいて開発されました。従来のロガーとは異なり、LogTapeは干渉性が低く、ライブラリ側は設定なしで安全にログを記録できる一方、アプリケーション側では完全な制御が可能です。依存関係がゼロで、あらゆるランタイム環境で動作するため、Deno、Node.js、Bun、ブラウザ、エッジ関数など、幅広い環境でシームレスに使用できます。

JavaScript, TypeScript用のロギングライブラリです。

特に重要だと私が思った特徴は以下の通りです。

  • Zero dependencies
    • LogTape本体は依存ライブラリを持ちません
    • 依存関係でセキュリティ上問題になるパッケージが含まれるということはありません
  • Library support
    • アプリケーションの開発だけではなく、ライブラリの開発にも使用することができます
    • ライブラリ側が使用していたとしても、アプリケーション側で出力設定を完全行うことができます
  • Runtime diversity
    • Node.js, Deno, ブラウザ, Cloudflare Workersなどのエッジ関数に対応しており、コードの変更なくそれぞれの環境で使用できます

上記リンクには他の特徴も記載されています。

また、既存のロギングライブラリとの違いについては下記ページにまとまっています。
(自己の優位性を示すためのページではありますが、他のライブラリとの違いはわかりやすい)

https://logtape.org/comparison

基本的な使い方 - 設定

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

await configure({
  sinks: { console: getConsoleSink({ formatter: jsonLinesFormatter }) },
  loggers: [
    { category: ["logtape", "meta"], sinks: ["console"], lowestLevel: "warning" },
    { category: ["app"], sinks: ["console"], lowestLevel: "info" },
  ],
});

LogTapeではトップレベルで await configure() を実行してロガーの実行を行います。

設定するものは大きく分けると二つ。

  • sinks
    • ログの出力先に対する設定(sink)をKey-Value形式で行う
    • 上記コードでは console.info などを使って標準出力にログ出力するように設定している
    • 構造化ログにするするためにformatter jsonLinesFormatter の指定などもsink側で行う
  • loggers
    • ロガーの設定を行う
    • category を使って階層化して、異なる階層ごとにログレベルなどを簡単に制御できるようになっている
    • sinks で出力先を指定する (複数指定することもできる)

基本的な使い方 - ログ出力

先ほどの設定のもとにログ出力を行います。

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

const logger = getLogger(["app"]);

const value = "LogTape";

logger.info `This is an info message with ${value}`;
logger.info("This is an info message with {value}", { value });
出力
{
  "@timestamp": "2026-09-10T07:05:39.826Z",
  "level": "INFO",
  "message": "This is an info message with \"LogTape\"",
  "logger": "app",
  "properties": {}
}
{
  "@timestamp": "2026-09-10T07:05:39.832Z",
  "level": "INFO",
  "message": "This is an info message with \"LogTape\"",
  "logger": "app",
  "properties": {
    "value": "LogTape"
  }
}

(出力は見やすいように一行になっていたJSONを開いている)

message はどちらも同じです。

後者は propertiesvalue というKey-Valueがあります。
後者では properties を使ってロガーがメッセージを組み立てています。

ちなみに message の組み立ては必ずしも行う必要はありません。

code
logger.info("information!", { value: 1 });
出力
{
  "@timestamp": "2026-09-10T07:18:51.331Z",
  "level": "INFO",
  "message": "information!",
  "logger": "app",
  "properties": {
    "value": 1
  }
}

これを使えば任意の情報をログに埋め込むことができそうです。
(jsonLinesFormatter を使う場合なのですが)

categoryによるロガーの階層構造

LogTapeでは category を使った階層構造でロガーを区別しています。
ロガーを生成する際に第一引数でcategoryを指定します。

https://github.com/dahlia/logtape/blob/main/packages/logtape/src/logger.ts#L1526-L1528

階層構造がどう影響するのか試すとこうなります。

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

await configure({
  sinks: { console: getConsoleSink({ formatter: jsonLinesFormatter }) },
  loggers: [
    { category: ["logtape", "meta"], sinks: ["console"], lowestLevel: "warning" },
    { category: ["app"], sinks: ["console"], lowestLevel: "info" },
    { category: ["app", "db"], sinks: ["console"], lowestLevel: "debug" },
  ],
});

const loggerApp = getLogger(["app"]);
const loggerDB = getLogger(["app", "db"]);
const loggerOther = getLogger(["app", "other"]);
const loggerCloud = getLogger(["cloud"]);

loggerApp.info("app: info");
loggerApp.debug("app: debug");
loggerDB.info("app.db: info");
loggerDB.debug("app.db: debug");
loggerOther.info("app.other: info");
loggerOther.debug("app.other: debug");
loggerCloud.info("cloud: info");
loggerCloud.debug("cloud: debug");
出力
{"@timestamp":"2026-09-10T08:04:11.115Z","level":"INFO","message":"app: info","logger":"app","properties":{}}
{"@timestamp":"2026-09-10T08:04:11.119Z","level":"INFO","message":"app.db: info","logger":"app.db","properties":{}}
{"@timestamp":"2026-09-10T08:04:11.119Z","level":"INFO","message":"app.db: info","logger":"app.db","properties":{}}
{"@timestamp":"2026-09-10T08:04:11.119Z","level":"DEBUG","message":"app.db: debug","logger":"app.db","properties":{}}
{"@timestamp":"2026-09-10T08:04:11.120Z","level":"INFO","message":"app.other: info","logger":"app.other","properties":{}}

configure() では、 category: ["logtape", "meta"], lowestLevel: "warning", category: ["app"], lowestLevel: "info", category: ["app", "db"], lowestLevel: "debug" の3つを定義しています。

対して loggerApp = getLogger(["app"]), loggerDB = getLogger(["app", "db"]), loggerOther = getLogger(["app", "other"]), loggerCloud = getLogger(["cloud"]) の4つのロガーを生成しています。

出力の内容をまとめると次のようになります。

  • loggerApp = getLogger(["app"])
    • "level":"INFO","message":"app: info"
  • loggerDB = getLogger(["app", "db"])
    • "level":"INFO","message":"app.db: info"
    • "level":"INFO","message":"app.db: info"
    • "level":"DEBUG","message":"app.db: debug"
  • loggerOther = getLogger(["app", "other"])
    • "level":"INFO","message":"app.other: info"
  • loggerCloud = getLogger(["cloud"])
    • ログ出力なし

なぜこうなったかというと階層構造と継承ルールによるものです。

  • loggerApp = getLogger(["app"])
    • categoryの完全一致によって category: ["app"], lowestLevel: "info" のsinkが使われる
    • lowestLevel: "info" であるためdebugのログは出力されない
  • loggerDB = getLogger(["app", "db"])
    • categoryの完全一致によって category: ["app", "db"], lowestLevel: "debug" のsinkが使われる
    • ["app", "db"]["app"] の配下であるため category: ["app"], lowestLevel: "info" のsinkも平行して使われる
    • infoのログでは二つのsinkが対象になるため2つログレコードが出力される
    • debugのログでは対象となるsinkが1つであるためログレコードは1つ出力される
  • loggerOther = getLogger(["app", "other"])
    • ["app", "other"]["app"] の配下であるため category: ["app"], lowestLevel: "info" のsinkが使われる
    • lowestLevel: "info" であるためdebugのログは出力されない
  • loggerCloud = getLogger(["cloud"])
    • ["cloud"] のcategoryは定義されていないため使用するsinkはない
    • 故にログ出力は行われない

今回のcategoryの関係性を図示すると次のようになります。

bd2a206c-843e-4e7d-bd86-242f75349802

categoryで使用するロガーが決定し、そのsinkを使うという構造になっています。
デフォルトでは下位のcategoryは上位のロガーのsinkを引き継ぎます。
そのため configure() に記載していないcategoryであっても、上位のcategoryが定義されていればそのsinkでログ出力が行われます。

しかし下位のcategoryを設定している場合では、自身のsinkと上位のsink両方を使用します。
これを防ぐためには次のように書きます。

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

await configure({
  sinks: { console: getConsoleSink({ formatter: jsonLinesFormatter }) },
  loggers: [
    { category: ["logtape", "meta"], sinks: ["console"], lowestLevel: "warning" },
    { category: ["app"], sinks: ["console"], lowestLevel: "info" },
    { category: ["app", "db"], sinks: ["console"], lowestLevel: "debug", parentSinks: "override" },
  ],
});

const logger = getLogger(["app", "db"]);

logger.info("info");
logger.debug("debug");
{"@timestamp":"2026-09-10T08:38:24.450Z","level":"INFO","message":"info","logger":"app.db","properties":{}}
{"@timestamp":"2026-09-10T08:38:24.453Z","level":"DEBUG","message":"debug","logger":"app.db","properties":{}}

configure()parentSinks: "override" と設定したロガーでは上位のsinkを引き継ぐのでなく上書きとなり使わないようになります。

category: ["logtape", "meta"]

category: ["logtape", "meta"] はLogTape本体の内部で使用しているメタロガーのcategoryです。

メタロガーではLogTapeのエラーや重要なイベントを出力するのに使われます。

configure() を実行する際にメタロガーを指定していなくても自動で有効化され、次のinfoログが出力されます。

02:42:45.731 INF logtape·meta LogTape loggers are configured. Note that LogTape itself uses the meta logger, which has category [ 'logtape', 'meta', [length]: 2 ]. The meta logger is used to log internal diagnostics such as sink exceptions. It's recommended to configure the meta logger with a separate sink so that you can easily notice if logging itself fails or is misconfigured. To turn off this message, configure the meta logger with higher log levels than 'info'. See also <https://logtape.org/manual/categories#meta-logger>.

02:42:45.731 INF logtape·meta LogTapeロガーが設定されました。なお、LogTape自体はメタロガーを使用しており、そのカテゴリは [ 'logtape', 'meta', [length]: 2 ] です。メタロガーは、出力先(シンク)のエラーなどの内部診断を記録するために使用されます。ロギング自体の失敗や設定ミスに気づきやすくするため、メタロガーは別の出力先に設定することをお勧めします。このメッセージを非表示にするには、メタロガーのログレベルを「info」よりも高く設定してください。詳しくは https://logtape.org/manual/categories#meta-logger をご覧ください。

メタロガーのログ出力を無効化したいときにはsinkに空配列を指定します。

await configure({
  sinks: { console: getConsoleSink({ formatter: jsonLinesFormatter }) },
  loggers: [
    { category: ["logtape", "meta"], sinks: [], lowestLevel: "warning" }
  ],
});

third party libraryとの連携

https://logtape.org/manual/integrations

LogTapeは、ExpressやHonoなど主要なWeb FrameworkやDrizzle ORMなどのORMに対して連携させることができます。

例えばHonoと連携させるには次のようにします。

$ npm install hono @logtape/logtape @logtape/hono
code
import { honoLogger } from "@logtape/hono";
import { configure, getConsoleSink, jsonLinesFormatter } from "@logtape/logtape";
import { Hono } from "hono";

await configure({
  sinks: { console: getConsoleSink({ formatter: jsonLinesFormatter }) },
  loggers: [
    { category: ["logtape", "meta"], sinks: ["console"], lowestLevel: "warning" },
    { category: ["hono"], sinks: ["console"], lowestLevel: "info" },
  ],
});

const app = new Hono();

app.use(honoLogger());

export default app;

まとめ

以上、JS, TSのロギングライブラリLogTapeの使い方でした。

何かのお役に立てたら幸いです。

この記事をシェアする

関連記事