Deno の console 出力を1行化したい

Deno Deploy でサービスを作っていくときにログが、REPLの表示のようにオブジェクトが展開されて複数行になるのが少々煩わしかった。
なので、対応を試みた。

参考

前提

例えば、こういう表示が有る。

1
2
3
4
5
6
7
8
9
10
11
> new Response
Response {
body: null,
bodyUsed: false,
headers: Headers {},
ok: true,
redirected: false,
status: 200,
statusText: "",
url: ""
}

親切ではあるが大量のログが流れる前提の時、複数行の改行が有るのは避けたくなります。

願わくば、こうしたい。

1
2
> new Response
Response { body: null, bodyUsed: false, headers: Headers {}, ok: true, redirected: false, status: 200, statusText: "", url: ""}

これに近しい動きを実現したい。
先の例示のように、REPLではこのように展開されるし、だいたい console.log を使っても同じように出てくる。

やってみる

「console.log の出力結果相当の文字列を変数に取り込んで文字列を加工できればいいなと」と始めてみたが、なかなか厳しい。
いろいろ試してみたが、例えばこうなる。

1
2
3
4
5
6
7
8
> {a:1}.toString()
"[object Object]"
> JSON.stringify({a:1})
'{"a":1}'
> (new Response).toString()
"[object Response]"
> JSON.stringify(new Response)
"{}"

Responseの中身がつぶれて [object Response] になるのがいただけない。

何かないかと調べてみると deno_std/fmt に、sprintf が定義されている。
これの内部実装を調べると、format指定ない(もしくは不適切な指定?)時、 Deno.inspect が使われている。

試してみると次のように。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> Deno.inspect({a:1})
"{ a: 1 }"
> Deno.inspect(new Response)
'Response {\n body: null,\n bodyUsed: false,\n headers: Headers {},\n ok: true,\n redirected: false,\n status: 200,\n statusText: "",\n url: ""\n}'
> console.log(Deno.inspect(new Response))
Response {
body: null,
bodyUsed: false,
headers: Headers {},
ok: true,
redirected: false,
status: 200,
statusText: "",
url: ""
}

良さそう。展開した内容は文字列で取れている。

(内部的に、console.log の中でも Deno.inspect が使われていそうだなと、ここで感じる。本当にそうなのか?は見てない)

改行と、インデントのためのスペースが邪魔なので、文字列操作して次のように。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> Deno.inspect(new Response).replace(/\n/g, "").replace(/  /g, " ")
'Response { body: null, bodyUsed: false, headers: Headers {}, ok: true, redirected: false, status: 200, statusText: "", url: ""}'
> console.log(Deno.inspect(new Response).replace(/\n/g, "").replace(/ /g, " "))
Response { body: null, bodyUsed: false, headers: Headers {}, ok: true, redirected: false, status: 200, statusText: "", url: ""}
undefined
> Deno.inspect({a:1}).replace(/\n/g, "").replace(/ /g, " ")
"{ a: 1 }"
> console.log(Deno.inspect({a:1}).replace(/\n/g, "").replace(/ /g, " "))
{ a: 1 }
undefined
> Deno.inspect("text").replace(/\n/g, "").replace(/ /g, " ")
'"text"'
> console.log(Deno.inspect("text").replace(/\n/g, "").replace(/ /g, " "))
"text"
undefined

なかなか良さげ。

この方向で、実装。

実装

こんな感じで実装。
(freshで1リクエスト当たりのログの追跡で、IDを振ってますが本質ではない)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { nanoid } from 'https://cdn.jsdelivr.net/npm/nanoid/nanoid.js'

class LogModule{
#id: string
constructor(){
this.#id = nanoid()
}

log(param: string){
console.log(this.#buildText(param, "LOG"))
}
info(param: string){
console.info(this.#buildText(param, "INFO"))
}
error(param: string){
console.error(this.#buildText(param, "ERROR"))
}
warn(param: string){
console.warn(this.#buildText(param, "WARN"))
}
#buildText(param:string, level: string){
return Deno.inspect({
log_id: this.#id,
level,
body: param
}).replace(/\n/g, "").replace(/ /g, " ")
}
}

これが呼び出せると、次のように動作できる。

1
2
3
4
5
6
7
8
9
10
11
> const logger = new LogModule()
undefined
> logger.log("text")
{ log_id: "DGCmtkWJrPSI5Ye0XUCcp", level: "LOG", body: "text" }
undefined
> logger.error({a:1})
{ log_id: "DGCmtkWJrPSI5Ye0XUCcp", level: "ERROR", body: { a: 1 } }
undefined
> logger.error(new Response)
{ log_id: "DGCmtkWJrPSI5Ye0XUCcp", level: "ERROR", body: Response { body: null, bodyUsed: false, headers: Headers {}, ok: true, redirected: false, status: 200, statusText: "", url: ""}}
undefined

イイ感じにできた。

Deno.inspect には、オプションがいくつかあるのでもう少しスッキリできる可能性はあるが、まぁ及第点であろう。


これを組み込んだ freshの拡張を近々公開することを検討中。
そのうちだけれど。

他でアウトプットは有ったんですが、最近バッタバタで気が付けば1カ月ぶりにブログを更新していました。
年末にかけてもうしばらくこんな感じになってしまいそうです。

ではでは。