TypescriptでLog4jsを組み込んでロギングしまくる

# TypescriptでもLog4jみたいなことをしたい！

ありますよ、 ``Log4js`` が。

## この記事で書くこと

* Log4jsってなにそれおいしいの？
* configってなにそれおいしいの？
* 巷にあるLog4jsの設定は当てにならない！？
* Typescriptに組み込むならば……

## Log4jsとはなんぞ

[Log4js](https://www.npmjs.com/package/log4js)

Java使いであれば超定番のロガーライブラリLog4JのJavascript版です。
ログ処理を設定ファイルであれこれ制御できるので便利。
Typescript(Javascriptでもですが)で設定ファイルを記載するとなると便利になるのがconfigというモジュールです。

## configとはなんぞ

[config](https://www.npmjs.com/package/config)

NODE_ENV環境変数に応じて読み込む設定ファイルを変更してくれる、かつ、簡単にソースから設定を取得できるモジュール。

## 今回お話する内容でのディレクトリ構成

なお、Node.jsアプリの構成です。

* node-root/ ( ``npm init`` したところ)
    * bin/
        * app/
            * config/ -> ../../config
            * hoge.js
            * hoge.js.map
        * test/
            * hoge.test.js
            * hoge.test.js.map
    * src/
        * app/
        * hoge.ts
        * test/
        * hoge.test.ts
    * config/
        * development.json

※configはnode-rootの直下のディレクトリにないとtscコンパイルがうまく行かなかったです。
かつ、トランスパイル後のファイルからも読める必要があるので、node-root/bin/app配下にシンボリックリンクでconfigを用意しています。
なお、NODE_ENVはdevelopmentに設定しています。

### Log4jsとconfigのバージョン

| library| version |
|---:|:---:|
|Log4js|2.3.3|
|config|1.26.2|

## まずはconfigを使えるようにする

まずはnpmでインストール。

```bash:console
npm install config
npm install @types/config
```

んで、config/${NODE_ENV}.jsonを用意。

一旦はコレでconfigの下地は完成。あとはコレを使うLog4jsを導入。

## いざLog4js……？

こちらもまずはインストール。

```bash:console
npm install log4js
npm install @types/log4js
```

各モジュールをインストールした後は、

1. configの設定を記述
1. ソースからconfigを読み込むコードを実装
1. configから取得したJSONを元にLog4jsを初期化
1. ログを吐きまくる

という流れです。

では早速、configを書き始めます。 ___ワタシはここでハマった___

先人たちのナレッジを参考に設定するわけですねまずは。

[log4jsの覚え書き - log4.jsを使ってみた -](http://qiita.com/_daisuke/items/865cf929a403cc9eec53)

よくある設定ですと、こんな感じになります。

```javascript:development.json.よくあるやつ
{
    "log4js": {
        "appenders": [
            {
                "category": "system",
                "type":     "dateFile",
                "filename": "/var/log/app/system.log"
            },
            {
                "category": "access",
                "type":     "dateFile",
                "filename": "/var/logs/app/access.log"
            },
            {
                "category": "error",
                "type":     "dateFile",
                "filename": "/var/logs/app/error.log"
            }
    }
}
```

そして、実装の中でこう使うんですね。

```typescript:code
import * as Config from "config";
import * as Path from "path";

var configure = Config.util.loadFileConfigs(Path.join(__dirname,"config")).log4js;
```

……
……
……あれ、エラーする。
__「typeがObjectであるプロパティappendersが見つからない」__

appendersプロパティは設定しているのに……

ん？
__「typeがObjectであるプロパティ」……__

[Migrating from log4js versions older than 2.x](https://nomiddlename.github.io/log4js-node/migration-guide.html)

> The main changes are a need for you to name your appenders, and you also have to define the default category.

__Log4js v1.Xではappendersは配列だったけれど、Log4js v2.Xではappendersはオブジェクト__
という大きな仕様変更があったみたいです。

ドキュメントを整理をすると、

* log4js v2ではappenndersプロパティ、categoriesプロパティが必須のJSONで設定を行う。
* appendersプロパティはオブジェクトとして、 ``設定名：{出力設定}`` というプロパティの列挙で各出力設定を用意する。
* categoriesプロパティはオブジェクトとして、 ``カテゴリ名:{適用設定,出力ログレベル}`` というプロパティの列挙でログ定義をする。
* categories::defaultプロパティは必須。

コレを上の設定ファイルに当てはめると以下のようになりました。

```javascript:development.json.うごくやつ
{
    "log4js": {
        "appenders": {
            "access": {
                "type":     "dateFile",
                "filename": "/var/log/app/access.log"
            },
            "error": {
                "type":     "dateFile",
                "filename": "/var/logs/app/error.log"
            },
            "system": {
                "type":     "dateFile",
                "filename": "/var/log/app/system.log"
            },
            "console": {
                "type": "console"
            },
            "stdout": {
              "type": "stdout"
            }
        },
        "categories": {
            "default": {
                "appenders": [
                    "access"
                    ,"console"
                    ,"stdout"
                ]
                ,"level": "INFO"
            },
            "access": {
                "appenders": [
                    "access"
                    ,"console"
                    ,"stdout"
                ]
                ,"level": "INFO"
            },
            "system": {
                "appenders": [
                    "system"
                    ,"console"
                    ,"stdout"
                ]
                ,"level": "ALL"
            },
            "error": {
                "appenders": [
                    "error"
                    ,"console"
                    ,"stdout"
                ]
                ,"level": "WARN"
            }
        }
    }
}
```

この設定では、出力設定としては

* access
* error
* system
* console
* stdout

の5つを用意していて、ログ設定としては

* access
  * 出力設定→access,console,stdout
* error
  * 出力設定→error,console,stdout
* system
  * 出力設定→system,console,stdout

の３つを用意しています。この時、アプリケーションから使えるログはaccess,error,systemです。
categoriesプロパティで設定する項目がログ項目となるわけです。

この設定をして、

```code
import * as Config from "config";
import * as Path from "path";

var configure = Config.util.loadFileConfigs(Path.join(__dirname,"config")).log4js;
```

が機能するようになりました。

## いざ本当にLog4js

Log4jsを利用する流れは大きく２段階です。

1. 初期化
1. ロギングしまくる

### 初期化

```typescript:code
import * as Config from "config";
import * as Path from "path";
import * as Log4js from "log4js";


var configure = Config.util.loadFileConfigs(Path.join(__dirname,"config")).log4js;
Log4js.configure(<Log4js.IConfig>configure);
```

@types/configで定義されている``util.loadFileCOnfigs(path: string).<<定義キー>>``で必要なJSONを取得します。
今回、log4jsをキーとしているため、``util.loadFileCOnfigs(path: string).log4js``としてます。
そして、取得したJSONオブジェクトを@types/log4jsで定義されている``configure(config: IConfig): void``を実行し設定を行います。
``Log4js.configure(<Log4js.IConfig>configure);``で強制的な型変換を行っているのは、``util.loadFileCOnfigs(path: string).<<定義キー>>``で取得できるオブジェクトはTypescript上any型だからです。
普通に行うと``configure(config: IConfig): void``ではなく``configure(path: string): void``に向かってしまい、正しく動いてくれないのです。

### ロギングしまくる

初期化を終えたらあとは気が向くままにロギングするだけです。

```typescript:code
import * as Config from "config";
import * as Path from "path";
import * as Log4js from "log4js";

let message = "hogehoge"

let logger = Log4js.getLogger("access");
logger.info(message);
```

``getLogger(logname: string): Logger``メソッドで対応するロガーインスタンスを取得します。``logname``は設定ファイルのcategoriesで用意した名称を指定します。
取得したLoggerインスタンスに対して、``インスタンス.<<ログレベル>>(message: string)``でロギングします。
<<ログレベル>>はLog4jsのv1と変わらない感じなので、ここは他の紹介ページを参考にして問題ないです。
上記コードでは、ログ名称accessのLoggerインスタンスを取得して、infoレベルのメッセージをロギングしています。

## 最終的な実装なこんな感じ

Typescriptに組み込んだ時、このような実装になりました。

```typescript:logger.ts
import * as Path from "path";
import * as Config from "config";
import * as Log4js from "log4js";


export class Logger {

    public static initialize() {
        let configure = Config.util.loadFileConfigs(Path.join(__dirname,"config")).log4js;
        Log4js.configure(configure as Log4js.Configuration);
    }

    public static LogAccessInfo(message: string): void {

        let logger = Log4js.getLogger("access");
        logger.info(message);
    }

    public static LogAccessWarning(message: string): void {

        let logger = Log4js.getLogger("access");
        logger.warn(message);
    }

    public static LogAccessError(message: string): void {

        let logger = Log4js.getLogger("access");
        logger.error(message);
    }

    public static LogSystemInfo(message: string): void {

        let logger = Log4js.getLogger("system");
        logger.info(message);
    }

    public static LogSystemWarning(message: string): void {

        let logger = Log4js.getLogger("system");
        logger.warn(message);
    }

    public static LogSystemError(message: string): void {

        let logger = Log4js.getLogger("system");
        logger.error(message);
    }

    public static LogError(message: string): void {

        let logger = Log4js.getLogger("error");
        logger.error(message);
    }

}
```

```typescript:呼び出し元コード.ts
import {Logger} from "./logger";


public main() {
    Logger.initialize();
    execute();
}

private execute() {
    Logger.LogAccessInfo("accessログのINFOメッセージ");
    Logger.LogSystemError("systemログのERRORメッセージ");
}

main()
```

エントリポイントとなる箇所で``Logger.initialize(): void``を実行して初期化を行い、必要に応じて``Logger.LogXXXX``を実行する実装です。