bs-winstonを作ろうとする過程
1
winstonjsありますよね。jsのlogライブラリ。
BuckleScriptから使いたくなりません?
というわけで、作りながら書いていきます。
今回は作る過程を描く感じで完成してpublishはしないです。
2
README
を読めばぼんやり使い方はわかると思うので、まずはtsの型定義から見ていきます。
https://github.com/winstonjs/winston/blob/master/index.d.ts
createLogger
に色々オプションを渡すとLogger
が出てくる感じです。
今回注目して欲しいのがlevels
optionで、これがかなり胆になってきます。
型的にはこれですね
interface AbstractConfigSetLevels { [key: string]: number; }
objectで表現されたMap<string,number>
型なんですが、winstonはこれのkey情報を使ってmeta的にLoggerにmethodを生やしてきます。
つまり
levels: { hoge: 0, fuga: 1, }
というオプションを渡せばlogger.hoge
やlogger.fuga
みたいなmethodが生えるわけですが、それに対して型をつけるのは厳しいのでその部分は諦めます
ただ、どのlevelがlogに使えるかやどのlevel以上のlogを出力するかというオプションに関わってくるので上のlevelsオプションは作る必要があります。
2
logに使うlevelを文字列で渡す方法がありますが、そんな安全じゃない方法は取りたくないので今回はそれにうまく制約をつけていきます。
イメージとして、 log `Alert "message"
みたいな感じなれば嬉しいですね
ただ、どのようなlevelが使えるかというところはカスタマイズできるように作れるようにします。
Variantからstringにする部分を自動生成するppxが最初から入っているのでこれを使っていきます
一旦Syslogに対応するということで
type t = [ | `Emerg [@bs.as "emerg"] | `Alert [@bs.as "alert"] | `Crit [@bs.as "crit"] | `Err [@bs.as "error"] | `Warn [@bs.as "warning"] | `Notice [@bs.as "notice"] | `Info [@bs.as "info"] | `Debug [@bs.as "debug"] ] [@@bs.deriving jsConverter]
こういう感じにしていきます。
上記から本来はlevels
オプションを生成したいわけですがそれはmetaプロになっちゃってきついのです。あとlevelの順序を定義する必要もあります。
なので一旦どのlevelが使えるかというenabled
という概念を用意してt list
にします。
let enabled = [
Emerg; Alert;
Crit; Err;
Warn; Notice;
Info; Debug]
そしてここからlevelsを生成できます。
let levels = enabled |> List.mapi (fun i l -> (Level.string_of_t l, i)) |> Js.Dict.fromList
結果は
{ emerg: 0, alert: 1, crit: 2, error: 3, warning: 4, notice: 5, info: 6, debug: 7 }
こうなります。
3
そういえばFFIの部分を書いてなかったので書いていきます。
winston_internal.ml
type transport (* ここのオプションは諸事情で一部省略 *) type mk_console_transport_option = { eol: string [@bs.optional] } [@@bs.deriving abstract] external console_transport: mk_console_transport_option -> transport = "Console" [@@bs.new][@@bs.module "winston/lib/winston/transports/index"] (* winston本体 *) type mk_option = { levels: int Js.Dict.t; level: string; transports: transport array; } [@@bs.deriving abstract] type winston external create_logger: mk_option -> winston = "createLogger" [@@bs.module "winston"] type mk_log_entry = { level: string; message: string; } [@@bs.deriving abstract] external log: winston -> mk_log_entry -> unit = "" [@@bs.send]
これはもうこんな感じとしかいえないですね。
4
あとは組み合わせると一旦は動くのですが、levelのカスタマイズができません。
そのためにcreate_logger
をFunctorにします
module type LogLevel = sig type t val string_of_t : t -> string val enabled : t list end module Make(Level: LogLevel)(Conf: sig val transports: Winston_internal.transport list val level: Level.t end) = struct type t = Level.t type log_entry = { message: string; level: Level.t; } let _levels = Level.enabled |> List.mapi (fun i l -> (Level.string_of_t l, i)) |> Js.Dict.fromList let w = Winston_internal.(create_logger @@ mk_option ~levels: _levels ~level: (Level.string_of_t Conf.level) ~transports: (Conf.transports |> Array.of_list) ) let log level message = let entry = Winston_internal.mk_log_entry ~level: (Level.string_of_t level) ~message in Winston_internal.log w entry end
こうすることでLogLevel
を実装すればいい感じのLoggerモジュールができる設計になります。
Syslogモジュールは
module Syslog = struct type t = [ | `Emerg [@bs.as "emerg"] | `Alert [@bs.as "alert"] | `Crit [@bs.as "crit"] | `Err [@bs.as "error"] | `Warn [@bs.as "warning"] | `Notice [@bs.as "notice"] | `Info [@bs.as "info"] | `Debug [@bs.as "debug"] ] [@@bs.deriving jsConverter] let enabled = [`Emerg; `Alert; `Crit; `Err; `Warn; `Notice; `Info; `Debug] let string_of_t x = tToJs x end
という風に定義できるので、
module SyslogLogger = Make(Syslog)(struct let transports = Winston_internal.([console_transport @@ mk_console_transport_option ();]) let level = `Debug end)
SyslogLogger
はこのように生成できます。
使用例
let () = SyslogLogger.log `Emerg "yabai";;
LogLevelさえ満たせばいいのでNpmLogLevelのようなものを作れば以下のようなnpm log levelにも対応することできます。 https://github.com/winstonjs/triple-beam/blob/master/config/npm.js
5
実はwinston-transport
(上記のconsole_tranport)に対してもlog levelが設定できるのですが一旦TODOにしました。
またwinstonのformat optionのFFIを書くのがちょっと骨が折れるのでこれもTODOにしています。