◆ モジュール用のロガー

ロガーを使うとき 最初にインスタンスを作ってそこにフォーマットやら出力先やらを設定してそれを使ってログをするのが一般的だと思います
今作ってるアプリケーション用のログならそれでいいのですが ライブラリなど汎用的なモジュールになるとちょっと困ります
ライブラリ内でロガーを作って出力してしまうと使う側で意図しないファイルに出力されたり 標準出力に混ざったりします
かと言ってライブラリを使うときにライブラリ用のロガーを外部から受け取るのもなんか違う感じがします
わざわざロガーを渡さないといけないのも使いづらいですし

なので 普通のログとは別の考え方で インスタンスを個々に作らずグローバルなものにしてエントリポイントとなるアプリケーション側で設定するものを作ってみました

ライブラリ部分

[glog.js]
const config_default = Symbol("config-default")
const config_error = Symbol("config-error")

const defaultLog = (ns, ...values) => console.log(`[${ns}]`, ...values)
const errorLog = (msg, err) => console.error(`[glog error] ${msg};`, err)

const options = {}
const names = new Map()

const namespace = (ns) => {
const n = ~~names.get(ns)
names.set(ns, n + 1)
if (n) {
return `${ns} (${n})`
}
return ns
}

const fnOr = (...fns) => fns.find(f => typeof f === "function")

const onLoggingError = err => {
const efn = fnOr(options[config_error], errorLog)

try {
const maybe_promise = efn("logging error", err)
if (maybe_promise instanceof Promise) {
return maybe_promise.catch(err => errorLog("finally", err))
}
} catch(err) {
errorLog("finally", err)
return
}
}

const logger = ns => {
ns = namespace(ns)
return (...values) => {
const fn = fnOr(options[ns], options[config_default], defaultLog)

try {
const maybe_promise = fn(ns, ...values)
if (maybe_promise instanceof Promise) {
return maybe_promise.catch(onLoggingError)
}
} catch(err) {
return onLoggingError(err)
}
}
}

export default {
logger,
config(opt) {
Object.assign(options, opt)
},
config_default,
config_error,
}

export { logger }

モジュール

[module1.js]
import { logger } from "./glog.js"
const log = logger("foo")

export default () => {
log("aa")
log(new Error("bb"))
log("aa", 12, "23", false, null)
}

メイン

[index.html]
<script type="module">
import glog from "./glog.js"
import m1 from "./module1.js"

m1()

glog.config({
[glog.config_default]: (ns, ...values) => { console.log("DEFAULT, NS:", ns, "VALUES:", values) },
})

m1()

glog.config({
foo: (ns, ...values) => { console.log("NS:", ns, "VALUES:", values) },
})

m1()

glog.logger("foo")(1)
</script>

結果

14:19:20.457 glog.js:4 [foo] aa
14:19:20.457 glog.js:4 [foo] Error: bb
at default (module1.js:6)
at (index):5
14:19:20.457 glog.js:4 [foo] aa 12 23 false null
14:19:20.457 (index):8 DEFAULT, NS: foo VALUES: ["aa"]
14:19:20.458 (index):8 DEFAULT, NS: foo VALUES: [Error: bb
at default (http://localhost:8000/module1.js:6:6)
at http://localhost:8000/:11:1]
14:19:20.458 (index):8 DEFAULT, NS: foo VALUES: (5) ["aa", 12, "23", false, null]
14:19:20.459 (index):14 NS: foo VALUES: ["aa"]
14:19:20.459 (index):14 NS: foo VALUES: [Error: bb
at default (http://localhost:8000/module1.js:6:6)
at http://localhost:8000/:17:1]
14:19:20.459 (index):14 NS: foo VALUES: (5) ["aa", 12, "23", false, null]
14:19:20.460 (index):8 DEFAULT, NS: foo (1) VALUES: [1]

各モジュールでログするには logger 関数にネームスペースの文字列を入れると返ってくる関数を呼び出します
グローバルだとどこのログかわからなくなるので パッケージ名やモジュールを名を入れることを想定してます
グローバルになると それでも重複があるかもしれないので同じネームスペースだと 2 つ目以降には (1) のような番号を自動で割り振ります
そういう事情のため ログするたびに logger 関数を呼び出してはいけないです

// ダメな例
const ns = "foo"

logger(ns)("message1")
logger(ns)("message2")
logger(ns)("message3")

// 良い例
const log = logger("bar")

log("message1")
log("message2")
log("message3")

メイン部分では glog.config で全体の設定を変更します
引数にはオブジェクトを渡して そのキーがネームスペースに対応します
バリューにはログ関数を設定します
引数はネームスペースが最初で残りにログ関数で渡した値です
glog.config_default をキーに設定すれば全体のデフォルトを設定できます
ネームスペースに設定がない場合のデフォルト値なので ネームスペースに直接設定したほうが優先されます

今はデフォルトが console.log になってますが 基本モジュールでログするのはデバッグ用途だと思うので デフォルトは何もしないでよかったかもしれません

debug

作っては見たものの npm の debug モジュールが近いことできそうなのでそっちのほうが良い気がしました
ダウンロード数が 5000 万以上とか すごい人気です
基本 debug モジュールのほうが機能は多いですが 同じネームスペースを連番にしてくれないのと カスタムログ関数周りは自分で作ったほうが好みです
debug モジュールのカスタムログ機能は 関数を設定できるのですが 渡される情報が加工済みになってました
% を使ったフォーマットや色を付けるためのエスケープシーケンス付きの文字列なので これを受け取ってもここからできることは少なめです
ドキュメントの通り stdout か stderr のどっちに流すか くらいの設定のようです
Node.js だと標準出力で文字列になるのであんまり困らなそうですが ブラウザだと生のオブジェクトを console.log したほうが見やすく表示されたりするので 不便なところもありそうです