ts-gn-log は、Cloud Run 上の Node.js サーバから Cloud Logging 向けの構造化ログを出すための TypeScript ライブラリです。py-gn-log の TypeScript 版で、出力の契約 (キー名、trace の運び方、fingerprint の規則) を py-gn-log と共有します。設計は docs/designs/0001-ts-gn-log-design.md を参照してください。
対象は Node.js で動くサーバ側コード (当面は Next.js の Route Handler、runtime = 'nodejs') です。ブラウザ側と Next.js middleware (edge) は対象外です。
py-gn-log と同じく、ts-gn-log の直下は provider (Google Cloud / AWS 等) を知らない共通部で、provider ごとの実装はサブパスにあります。利用側は使う provider のサブパスを明示的に import し、そこを入口にします。サブパスと役割の正本は設計案 §6で、下の表は実装の状態を添えた要約です。
| サブパス | 役割 | 状態 |
|---|---|---|
ts-gn-log |
共通部の再輸出。google/* は読み込まない |
context / fingerprint / level / output / trace を再輸出 |
ts-gn-log/context |
リクエスト / タスク単位の文脈を全ログ行に付ける (AsyncLocalStorage) | 実装済み |
ts-gn-log/fingerprint |
ERROR の dedup 用 fingerprint の正規化とハッシュ | 実装済み |
ts-gn-log/level |
ログレベルの変換、LOG_LEVEL の読み取り |
実装済み |
ts-gn-log/output |
出力形式の決定 (GNLOG_FORMAT)、text 整形、stdout / stderr への書き出し、Logger の核 |
実装済み |
ts-gn-log/trace |
W3C Trace Context (traceparent) の解釈・組み立てと、現在の trace の保持 |
実装済み |
ts-gn-log/google/cloud-run |
Cloud Run 向けの入口 createLogger() と isCloudRun() |
実装済み |
ts-gn-log/google/cloud-logging |
Cloud Logging 向けの JSON 整形 (severity / labels / stack_trace / fingerprint) | 実装済み (fingerprint は未実装) |
ts-gn-log/google/cloud-trace |
X-Cloud-Trace-Context の解釈と Cloud Logging の特殊フィールド (logging.googleapis.com/trace 等)、withRequestTrace |
実装済み |
npm には公開していません。GitHub の public リポジトリを git 参照で使います。tag か SHA で版を固定してください。
npm install github:tengine/ts-gn-log#v0.1.0
package.json に書く場合:
{
"dependencies": {
"ts-gn-log": "github:tengine/ts-gn-log#v0.1.0"
}
}ビルド済みの dist/ (ESM と .d.ts) をリポジトリにコミットしているので、利用側に TypeScript は要りません。ランタイム依存はありません。Node 24 以上が必要です。配布方針 (npm に公開しない理由、dist/ をコミットする理由) の正本は設計案 §4.3、依存の方針は §4.1、Node の版は §4.2 です。
入口は ts-gn-log/google/cloud-run の createLogger() です。プロセスで 1 回呼び、返ったロガーを使います (py-gn-log の setup_logging() に相当)。Cloud Run 上では Cloud Logging 向けの JSON 行、ローカルでは人が読む text 形式を、console を経由せず stdout / stderr に書きます (severity が ERROR 以上なら stderr、それ以外は stdout)。
import { createLogger } from "ts-gn-log/google/cloud-run";
const log = createLogger({
name: "bff", // 全行の name
labels: { service: "frontend" }, // logging.googleapis.com/labels (JSON 形式のみ)
// level: "INFO", // 省略時は LOG_LEVEL、無ければ INFO
// json: true, // 省略時は GNLOG_FORMAT、無ければ Cloud Run 上なら JSON
// projectId: "my-project", // logging.googleapis.com/trace の組み立てに使う。省略時は GOOGLE_CLOUD_PROJECT
// fields: { app: "bff" }, // 全行に付く固定フィールド (child() の固定フィールドと同じ扱い)
});
log.info("task accepted", { site: "site-a", operation: "POST /api/v1/things" });
log.error("save failed", { err, error_type: "infra", operation: "save_result" });
const siteLog = log.child({ site: "site-a" }); // 固定フィールドを持つ子ロガー
siteLog.warn("retrying");Cloud Run 上の出力 (1 行の JSON):
{"site":"site-a","operation":"POST /api/v1/things","severity":"INFO","message":"task accepted","timestamp":"2026-09-09T01:23:45.678Z","name":"bff","logging.googleapis.com/labels":{"service":"frontend"}}- ログの呼び出しは例外を投げません (Python の
loggingと同じ)。フィールドに循環参照や BigInt があっても行は出ます (循環は"[Circular]"、BigInt は文字列)。それでも直列化できない値 (投げるtoJSONなど) があるときは、フィールドを落としてfields_errorに理由を入れ、severity/messageなどは必ず出します。整形そのものが失敗した場合 (固定キーも直列化できない等) は、severity/message/nameとformat_errorだけの固定の行を出し、書き出し (stdout / stderr) が失敗しても呼び出し元には伝播させません - 呼び出し時のフィールドはそのまま JSON のキーになります。キー名は py-gn-log の
extraと同じく snake_case を推奨します。severity/message/timestamp/name/logging.googleapis.com/labelsと同名のフィールドは固定の値が勝ちます errだけは特別で、severity が ERROR 以上ならstack_trace(Error Reporting が認識するフィールド) に、WARNING 以下ならerrorに、その文字列 (Error ならstack) が入ります。errは Error でなくても構いません (文字列などはそのまま入り、messageは変わりません)。child({ err })の固定フィールドに渡したerrも同じ扱いです- ローカルの text 形式は
2026-09-09T01:23:45.678Z INFO bff task accepted {"site":"site-a"}の 1 行で、errがあれば次の行以降に stack が続きます。書式は固定です
1 つのリクエストや 1 つのタスクの処理中に出るログ行を、あとから 1 つの ID で串刺しにしたい場面のために、ts-gn-log/context を用意しています (py-gn-log の gnlog.context と対)。node:async_hooks の AsyncLocalStorage に置いた値を、ロガーが出力時にフィールドとして混ぜます。Next.js の Route Handler は Node の非同期文脈をそのまま通すので、await 先まで届きます。
import { runWithContext, setContext, clearContext, getContext } from "ts-gn-log/context";
// fn の間だけ付ける (抜けると元に戻る。入れ子にでき、内側の値が勝つ)
await runWithContext({ request_id: "4bf92f35", site: "tokyo" }, async () => {
log.info("処理開始"); // {"request_id":"4bf92f35","site":"tokyo","message":"処理開始",...}
await doWork(); // この中のログにも付く
});
// いちばん内側の runWithContext の範囲を更新する (その範囲が終わるまで残る)。
// リクエストの途中で分かった値 (本文から取り出した ID など) を後から足す使い方
await runWithContext({}, async () => {
const body = await req.json();
setContext({ request_id: body.id }); // コールバックや await 先で呼んでも、同じ範囲なので残る
log.info("...");
clearContext(); // この範囲の文脈を空にする (外側の範囲は消さない)
});setContext / clearContext は runWithContext の外では使えません (Error になります)。runWithContext の中で始めて await し忘れた処理から、範囲が終わった後に setContext した場合は、エラーにはならず、その書き込みは誰にも読まれません (その処理自身のログには、範囲が生きていた頃の文脈が付きます)。文脈の更新は、その範囲を待っている処理の中で行ってください。runWithContext は fn が返した Promise をそのまま返し、観測しません — await / catch しなかった reject は通常どおり unhandled rejection になります。範囲の中でしか更新できないようにしている理由 (enterWith を使わず run で張る) は src/context.ts の docstring を参照してください。Next.js の Route Handler は withRequestTrace (Cloud Trace の節) で囲う前提です。
- キー名は自由ですが、
severity/message/timestamp/name/logging.googleapis.com/labels/logging.googleapis.com/trace/logging.googleapis.com/spanId/logging.googleapis.com/trace_sampled/stack_trace/error/fields_error/format_error/errは出力の固定キーと同名なので置けません (Errorになります)。py-gn-log のextraと同じく snake_case を推奨します traceは予約キーで、出力には混ぜません (ts-gn-log/traceとts-gn-log/google/cloud-traceが使います)- 値が
null/undefinedのキーは出力されません (入れ子で外側の値を一時的に外すのに使えます) - 優先順位は 呼び出し時のフィールド >
child()の固定フィールド > 文脈 です - text 形式でも同じくフィールドとして末尾の JSON に出ます
Cloud Run はリクエストごとに X-Cloud-Trace-Context ヘッダ (と W3C の traceparent) を付けます。ts-gn-log/google/cloud-trace の withRequestTrace で Route Handler を包むと、受信ヘッダの trace を文脈に置き、全ログ行に logging.googleapis.com/trace / spanId / trace_sampled が付いて Cloud Logging でリクエスト単位に紐付きます (py-gn-log の gnlog.google.cloud_trace と対)。
import { createLogger } from "ts-gn-log/google/cloud-run";
import { currentTrace, traceHeaders, withRequestTrace } from "ts-gn-log/google/cloud-trace";
const log = createLogger({ name: "bff" }); // projectId は省略時 GOOGLE_CLOUD_PROJECT
export const POST = withRequestTrace(async (req: Request) => {
log.info("received"); // {"logging.googleapis.com/trace":"projects/<PROJECT_ID>/traces/<TRACE_ID>", ...}
await fetch(url, { headers: traceHeaders() }); // 下流に trace を引き継ぐ (traceparent + X-Cloud-Trace-Context)
return Response.json({ trace_id: currentTrace()?.traceId }); // 応答 body に載せる trace id は Cloud Trace の trace id
});- 受信ヘッダは
traceparentを優先し、無ければX-Cloud-Trace-Contextを見ます (py-gn-log と同じ順)。どちらも無ければ新しい trace id (32 桁の 16 進) を生成するので、包まれた処理の中ではcurrentTrace()が常に返ります (生成した trace はspanId/sampledが不明で、下流にはX-Cloud-Trace-Contextだけを送ります) logging.googleapis.com/traceの組み立てにはプロジェクト ID が要ります。createLogger({ projectId })か環境変数GOOGLE_CLOUD_PROJECTで指定し、どちらも無ければ trace のフィールドは付きません (既定値を持たない)。その場合でもcurrentTrace()とtraceHeaders()は動くので、下流への引き継ぎはできますwithRequestTraceはrunWithContextで囲むので、その中でsetContextが使えます。fieldsオプションで、trace とあわせて置くフィールド (リクエストから組み立てる関数でもよい) を渡せます。py-gn-log のcloud_trace.bind(trace, log_fields(...))と違い、logging.googleapis.com/traceなどの trace のフィールドはfieldsに渡しません (予約キーtraceからjsonFormatが自動で組みます。渡すと型エラーになります)。newTrace/fieldsの関数が投げてもリクエストは落とさず、既定 (生成し直す / fields 無し) に倒します- HTTP 以外の経路 (Pub/Sub の属性、タスクのペイロード) では、発行側で
traceHeaders()の値を属性に載せ、受信側でtraceFromHeaders(attributes)(名前と値の Record を受けます) で取り出してrunWithTrace(trace, undefined, fn)で囲みます
ts-gn-log/fingerprint は、メッセージの可変部 (ID、件数、引用文字列) を置き換えて正規化し、分類と組み合わせた短いハッシュを作ります (py-gn-log の gnlog.fingerprint と対)。Error Reporting が自動でグループ化できないエラー (例外を伴わない ERROR など) の dedup に使います。createLogger の errorEvent オプションが ERROR 以上のログに自動で付けるようにする予定です (PR 7 で実装)。
import { normalizeMessage, buildFingerprint } from "ts-gn-log/fingerprint";
normalizeMessage('order 123 for "alice" not found');
// => 'order <num> for <str> not found'
buildFingerprint("worker", "orders.create", "validation", "order 123 missing");
// => sha1("worker|orders.create|validation|order <num> missing") の先頭 16 文字規則は py-gn-log との契約です。この節は ts-gn-log の利用者に向けた挙動・制限・注意の正本です (正規化とハッシュの規則そのものは src/fingerprint.ts の docstring が正本)。py-gn-log の挙動の正本は py-gn-log の README「fingerprint の規則 (他言語の実装との契約)」です。 2 つは独立したライブラリなので、どちらか一方を唯一の正本にはしません。規則を変えるときは、片方だけを変えて食い違ったままにするか、揃えるかを先に決めてください。 揃えるなら両方に同時に PR を出すか、片方に同一性を確保する Issue を立てます。食い違いを選んだなら、互いの参照を消します。
単一引用符には制限があります。引用の区切りとアポストロフィを同じ文字で兼ねるため、アポストロフィで始まる語 ('cause、'90s) の先頭や、対になっていない単一引用符が、開き引用符とみなされることがあります。その後ろに次の単一引用符が現れると、それが所有格のアポストロフィであっても閉じ引用符として働き、そこまでが <str> にまとまって、別種のエラーが同じ fingerprint になります ('bob's file は <str>s file になります)。後ろに単一引用符が現れなければ置き換えは起きません ('cause it broke はそのままです)。厳密さが必要なメッセージでは二重引用符を使ってください。
このほかに、両言語で値が一致しない条件 (孤立サロゲート) と、入力の大きさについての注意があります。
- 孤立サロゲート (両言語で値が一致しない条件): 対にならないサロゲート (JS 側で文字列を UTF-16 単位に切り詰めた結果、たとえば
"boom 😀".slice(0, 6)に現れます) は UTF-8 にエンコードできません。ts 側は Node の既定に従って U+FFFD に置き換えて値を返します (ログの呼び出しは投げない方針に合わせています)。py-gn-log と値が一致するかは、孤立サロゲートが正規化と切り詰めの後にも残る位置にあるかで決まります。切り詰めで削られる位置にあれば影響せず、両言語の値は一致します (2026-09-14 にbuildFingerprint("w", "op", "t", "x".repeat(400) + "\uD83D")と、py-gn-log の同じ引数のbuild_fingerprintを実行し、どちらもba925d5a90b03deeになることを確認しました。値は surface / operation / error_type によって変わります。この一致を検査する関門はありません)。残る位置にあると、py 側は UTF-8 にエンコードできない文字列を扱うことになり、ts と同じ fingerprint が得られるとは限りません。py 側で何が起きるか (例外になるか、ログの行が残るか) は前提にしないでください (py-gn-log #34)。呼び出す前に ts 側でメッセージを切り詰めるなら、コードポイント単位で行ってください (UTF-16 コード単位で切ると孤立サロゲートを作ります)。 - 入力の大きさ (呼び出し側の責務): 置換は入力の全体に走るので、
normalizeMessage/buildFingerprintは入力の大きさに比例したメモリを使います。落ちるかどうかはヒープの大きさと置換でどれだけ膨らむか次第で、実測では、ヒープを 512MB に制限した環境で 40MB の数値を多く含むメッセージがtry/catchで受けられない fatal OOM になり、既定のヒープ (約 4GB) では 40MB は通って 250MB 相当で落ちました。上限は呼び出し側の責務です (py-gn-log #35)。ログの経路での上限は設計案 §2.5 fingerprint を付けない入力の上限を参照してください。
両言語で同じ値になることは、py-gn-log の Python 実装から生成したゴールデンベクタ (test/fixtures/fingerprint-golden.json) で検証しています。軸は、正規化、maxLength を振った切り詰め (負の値と 0 を含む)、fingerprint です。規則を変えたときは py-gn-log の環境で再生成します:
cd ../py-gn-log && uv run python ../ts-gn-log/test/fixtures/generate-fingerprint-golden.py "$(git rev-parse --short HEAD)" > ../ts-gn-log/test/fixtures/fingerprint-golden.json
py-gn-log と同じ名前と意味です。
| 環境変数 | 意味 | 未設定のとき |
|---|---|---|
LOG_LEVEL |
出力するレベルの下限。DEBUG / INFO / WARN / WARNING / ERROR / CRITICAL (大文字小文字を問わない) |
INFO。未知の値も INFO に倒す (エラーにしない) |
GNLOG_FORMAT |
出力形式を明示的に指定する。json (Cloud Logging 向けの JSON 行) か text (人が読む形式)。それ以外の値は createLogger() がエラーを投げる |
Cloud Run 上 (K_SERVICE などがある) なら json、それ以外なら text |
K_SERVICE / CLOUD_RUN_JOB / CLOUD_RUN_WORKER_POOL |
Cloud Run が自動設定する。存在すれば Cloud Run 上と判定する | — |
GOOGLE_CLOUD_PROJECT |
logging.googleapis.com/trace (projects/<PROJECT_ID>/traces/<TRACE_ID>) の組み立てに使うプロジェクト ID。createLogger({ projectId }) が優先 |
trace のフィールドを付けない (既定値を持たない) |
createLogger({ level, json }) の引数は環境変数より優先します。json を引数で指定した場合は GNLOG_FORMAT を読まないので、不正な値があってもエラーになりません。
py-gn-log にある LOG_FILE_PATH (テキスト形式のファイル出力) と LOG_FORMAT (テキスト形式の書式文字列) は、ts-gn-log にはありません。Cloud Run では stdout / stderr が標準の経路で、ローカルの text 形式は固定です。
Cloud Run (Service / Job / Worker Pool) 上かどうかは、Cloud Run が自動設定する環境変数 K_SERVICE / CLOUD_RUN_JOB / CLOUD_RUN_WORKER_POOL のいずれかが存在するかで判定します (py-gn-log の is_cloud_run() と同じ 3 変数、同じ判定)。判定だけを使う場合は isCloudRun() を呼びます。
import { isCloudRun } from "ts-gn-log/google/cloud-run";
if (isCloudRun()) {
// Cloud Run 上
}- Node 24 (
.tool-versionsで指定。asdf などで合わせてください) - npm
npm ci
| コマンド | 内容 |
|---|---|
npm run build |
tsc で src/ を dist/ にビルドする (ESM + .d.ts) |
npm run check:dist |
dist/ を空にしてビルドし直し、コミット済みの dist/ と一致することを検査する (git status --porcelain dist/ が空なら成功) |
npm run typecheck |
src/ と test/ と vitest.config.ts を tsc で型検査する (出力なし)。npm run build は src/ しか見ないので、テストの型はこちらで検査する |
npm test |
Vitest でテストを実行する |
npm run test:cov |
カバレッジ付きでテストを実行する |
npm run lint |
Biome で lint と書式を検査する |
npm run format |
Biome で書式を整える |
規則の正本は CLAUDE.md の「文書とテストの題に書く断定」です。
規律の正本は設計案 §4.3です。手順だけを書きます。
src/を変えた PR では、最後にnpm run buildを実行してdist/を再生成し、ソースの変更とは別のコミットとして含める- PR を出す前に
npm run check:distを実行し、コミット済みのdist/が現在のソースから生成されるものと一致することを確かめる。CI (.github/workflows/ci.yml) も同じ検査を PR ごとに走らせ、ずれていれば失敗する prepareスクリプトは足さない (理由は §4.3)