bs-winstonを作ろうとする過程

1

winstonjsありますよね。jsのlogライブラリ。

github.com

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.hogelogger.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にしています。