Angular6 で俺式 KOAN スタック Webアプリを構築する、ほぼ全手順（4）

## 概要
Angular6で「俺式 KOAN スタック」で Web アプリ構築するための個人的な備忘録記事。
概要については「[こちら](https://qiita.com/tomy0610/items/9e948154f9b8f56f28cf)」を参照。
事前に「[ほぼ全手順（3）](https://qiita.com/tomy0610/items/15241a615d107ec99613)」が完了していることが前提。

今回の内容は「ロギング機構」をつくる。

## ログの出力先を設定ファイルに追記

config ディレクトリ配下にある、node-config の各環境用の設定ファイル（default.yaml, development.yaml, production.yaml）の全てに、以下のとおりログ出力のファイルパスを設定する。

```yaml:configディレクトリの各ファイル
...
  log:
    access:
      filename: 'logs/access/access.log'
    app:
      filename: 'logs/app/app.log'
```

## ログ出力を Git 管理対象外にする

出力されたログは、バージョン管理の対象にする必要がないので、`.gitignore` にルートディレクトリ配下のログ出力のファイルは全て対象外にするように、以下を追記する。
（本来は、マシンのルート配下の log 等にログ出力すべきだが、今回の開発ではアプリのルート配下にログ出力するように設定するので対象外設定が必要）

```bash
...

# log
/logs

...
```

## ログ出力方式設定

ログレベルや出力先を集約した設定を行うクラスを作成

```ts:server/server/log.manager.ts
import * as config from 'config';
import * as Log4js from 'log4js'; // 初期構築の段階ですでに自動インストール済み（のはず）

/**
 * ログ出力管理クラス
 */
export class LogManager {

  public static ACCESS = 'access';
  public static APP = 'app';

  public serve(): void {

    // ログ設定
    const conf: Log4js.Configuration = {
      // 出力設定
      appenders: {
        access: {
          type: 'dateFile',
          filename: config.get('log.access.filename'),
          pattern: '_yyyy_MM_dd',
          layout: {
            type: 'pattern',
            pattern: '[%d{yyyy-MM-dd hh:mm:ss.SSS}] [%p] %m'
          }
        },
        app: {
          type: 'dateFile',
          filename: config.get('log.app.filename'),
          pattern: '_yyyy_MM_dd',
          layout: {
            type: 'pattern',
            pattern: '[%d{yyyy-MM-dd hh:mm:ss.SSS}] [%p] %m'
          }
        },
        console: {
            type: 'console'
        },
        stdout: {
          type: 'stdout'
        }
      },
      // getLogger で指定するログカテゴリ (プロパティ default は必須)
      categories: {
        default: {
          appenders: ['access'],
          level: 'ALL'
        },
        error: {
          appenders: ['console', 'stdout'],
          level: 'WARN'
        }
      }
    };

    // ログ追加設定
    conf.categories[LogManager.ACCESS] = {appenders: ['access'], level: 'ALL'};
    conf.categories[LogManager.APP] = {appenders: ['app'], level: 'ALL'};

    // ログ設定の読み込み
    Log4js.configure(conf);
  }
}
```

## web-server.ts にログ設定クラスを組み込む

今作ったログ設定を、アプリ起動時に読み込むように組み込む。

```ts:server/web-server.ts
...

import * as Koa from 'koa';
import * as koaStatic from 'koa-static';
import * as bodyParser from 'koa-bodyparser';
import * as config from 'config';
import { join } from 'path';
import { MiddlewareRouterManager } from './managers/middleware-router.manager';
import { LogManager } from './managers/log.manager'; // 追加

const app = new Koa();
const PORT = process.env.PORT || config.get('port');
const clientSrcPath = join(process.cwd(), 'dist/client');

// ログ管理の提供
new LogManager().serve(); // 追加

app.use(bodyParser({
  extendTypes: {
    json: ['application/x-javascript']
  }
}));

...
```

## ロガーの作成

実際にログ出力を行うロガークラスを作成。

```ts:server/logger.ts
import * as Log4js from 'log4js';

import { LogManager } from './managers/log.manager';

/**
 * ロガー
 */
export class Logger {

  /** アクセス ログ */
  public static access(message: any, otherDatas?: any[]): void {
    if (otherDatas) {
      Log4js.getLogger(LogManager.ACCESS).info(message, otherDatas);
    } else {
      Log4js.getLogger(LogManager.ACCESS).info(message);
    }
  }

  /** アプリケーション ログ (INFO) */
  public static info(message: any, otherDatas?: any[]): void {
    if (otherDatas) {
      Log4js.getLogger(LogManager.APP).info(message, otherDatas);
    } else {
      Log4js.getLogger(LogManager.APP).info(message);
    }
  }

  /** アプリケーション ログ (ERROR) */
  public static error(message: any, otherDatas?: any[]): void {
    if (otherDatas) {
      Log4js.getLogger(LogManager.APP).error(message, otherDatas);
    } else {
      Log4js.getLogger(LogManager.APP).error(message);
    }
  }

  /** アプリケーション ログ (DEBUG) */
  public static debug(message: any, otherDatas?: any[]): void {
    if (process.env.NODE_ENV !== 'production') {
      if (otherDatas) {
        Log4js.getLogger(LogManager.APP).debug(message, otherDatas);
      } else {
        Log4js.getLogger(LogManager.APP).debug(message);
      }
    }
  }

}

```

ここまでで、やっとログ出力を行う準備が整った。
あとは、好きな箇所でログ出力するだけ。

ただし、バックエンドでのみロガーは使用可能で、フロントではログファイルの生成ができない（っていうか、仮にできたとしてもする必要がない）ので、フロントでは使わない。

## アプリの初回アクセス時にアクセスログを出力させる

本来なら、バックエンド側に初回アクセスログを仕込むべきだが、今回は、Koa のルーティングに初期遷移のルートを作成していないので、Angular 側に初期遷移時の API 呼出処理を書き、その API を Koa のルーティングで処理するフローとする。

### フロントエンドにアクセスログ API 呼出サービスを作成

`client/app` 直下に以下のとおり、サービスを作成する。

```ts:client/app/init-access.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';

/**
 * アプリ初期遷移 サービス
 */
@Injectable({
  providedIn: 'root'
})
export class InitAccessService {

  constructor(protected http: HttpClient) { }

  /** アプリ初期遷移時処理を行う */
  public async initAccess(): Promise<void> {
    const httpParams: HttpParams = new HttpParams();
    const url = '/api/init';

    try {
      const aa = await this.http.get(url, {
        headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
        params: httpParams
      }).toPromise();
    } catch (error) {
      console.log('[error]: ', error);
    }
  }
}
```

`@Injectable({ providedIn: 'root' })` のおかげで、app.module.ts にサービスを追記しなくても勝手に追加されたことになっている。

#### app.module.ts に HTTP 通信用モジュールを追加

```ts:client/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; // 追加

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeModule } from './modules/home/home.module';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule, // 追加
    HomeModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

### 最初に呼ばれるコンポーネントにアクセスログ呼出サービスを追加

#### app.component.ts でサービスを利用

```ts:client/app/app.component.ts
import { Component, OnInit } from '@angular/core'; // OnInit を追加

import { InitAccessService } from './init-access.service'; // 追加

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit { //  implements OnInit を追加
  title = 'demo-app';

  constructor(private service: InitAccessService) { } // 追加

  // メソッド追加
  ngOnInit() {
    this.service.initAccess();
  }

}
```

### バックエンドの API ルーティングに、アクセスログ専用のルートを作成する

```ts:server/managers/middleware-router.manager.ts
import * as KoaRouter from 'koa-router';

import { Logger } from '../logger'; // 追加
import { ApiPathManager } from './api-path.manager';

...

  public routes(): KoaRouter.IMiddleware {
    const koaRouter = new KoaRouter();

    // 以下のルート処理を追加
    koaRouter.get('/api/init', (ctx) => {
      const userAgent = ctx.header['user-agent'];
      Logger.access(`IP: [${ctx.request.ip}], ${userAgent}`);
      ctx.status = 200;
    });

    ...

    return koaRouter.routes();
  }

...
```

### ログを確認

- アプリを起動し、 Web ブラウザから `http://localhost:3000` にアクセス
- アプリにアクセスすると、ルートディレクトリ配下に logs/ というディレクトリが生成されている。
- アクセスログを確認してみると、以下の様なログが記録されているはず

```text:logs/access/access.log
[2018-12-30 12:58:58.639] [INFO] IP: [::1], Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
```

上記の様に、ログが出力できればOK. ローカルマシンで実行しているためわかりにくいが、`[::1]` の部分は、サーバ上で起動したアプリにアクセスすれば、きちんと IP アドレスも出る。

尚、ここでは割愛するが、バックエンドのルーティング処理内で生成した `userAgent` 変数をうまく解析するロジックを追加すれば、アクセスしてきたデバイスが何なのか、OS は何なのかをわかりやすく出すことができる。

## バックエンドのコントローラにアプリログ出力を入れてみる

バックエンドのサンプルコントローラを以下のとおりにしてみる。

```ts:server/controllers/sample.controller.ts
import { Logger } from '../logger';

/** サンプル コントローラ */
export class SampleContoroller {

  public async test(request: any): Promise<any> {
    Logger.info(`[SampleContoroller] #test, ${JSON.stringify(request)}`);

    const response = {
      statusCode: 200,
      body: { result: `Your Request is\n${JSON.stringify(request)}` }
    };

    Logger.info(`[SampleContoroller] #test, ${JSON.stringify(response)}`);

    return response;
  }

}
```

メソッドの最初でコントローラの箇所とリクエスト内容がわかるようにログ出力。
レスポンス前にコントローラの箇所とレスポンス内容がわかるように出力。
これで、バックエンドのどの処理を通り、最終的に何を返しているのかわかる。

### 確認

#### アプリログ確認

- アプリを起動し、 Web ブラウザから `http://localhost:3000` にアクセス
- 画面の「GET TEST」ボタンを押下する
- アプリログを確認してみると、以下の様なログが記録されているはず

```text:logs/app/app.log
[2018-12-30 13:27:42.022] [INFO] [SampleContoroller] #test, {"testKey":"aaa","otherKey":"bbb"}
[2018-12-30 13:27:42.022] [INFO] [SampleContoroller] #test, {"result":"Your Request is\n{\"testKey\":\"aaa\",\"otherKey\":\"bbb\"}"}
```

上記の様に、ログが出力できればOK.
これで、ログ出力の機構はできた。あとは、例外時にエラーログ吐くなり、ロギング方式に合わせて埋め込んでいくだけです。
なるべく解析しやすいログ出力、かつ、ログファイルが膨らみ過ぎないシンプルな方式にすると良い。


## この後やること

セッション管理を行う

→ 「[Angular6 で俺式 KOAN スタック Webアプリを構築する、ほぼ全手順（5）](https://qiita.com/tomy0610/items/3ef7d6d27ea534146c9a)」に執筆