angular2でelasticsearchの生ログ検索ページを作る

初めまして。アドテクdivのswfzです。

少し前に流行った fluentd + elasticsearch + kibanaでアクセスログを可視化、チームでもやっていましたが集計したグラフはみれるしめちゃいい!けど実際の生データを検索かけるのはちょっと操作が面倒ですよね。

f:id:AdwaysEngineerBlog:20170324155659p:plain

今回はangular2を使ってelasticsearchの生ログを検索できるSPAを作成してみました。

今回は作ったSPAの実際の動作と実装を軽く紹介させていただきます。

※データはダミーデータ生成用のgemがあるのでそれを使わせてもらいました。

動機

  • アクセスログの量が重いと調査でgrepとかする際時間がかかりストレスがマッハ(ノ`Д´)ノ.:・┻┻)
  • ログをelasticsearchに突っ込めばファイルから検索するより早く検索できる
    • 集計したグラフも見れて二度おいしい
    • ただ、検索クエリ覚えるのつらい
  • ディレクターや非エンジニアの方でもログ調査くらいなら出来るようになればお互いハッピー
    • エンジニアは作業を中断しなくて良く余計なコミュニケーションコストがなくなる
    • ディレクターはわざわざエンジニアに調査依頼せず自分で調査できる

elasticsearchにはRESTAPIがあるので直接リクエストを投げて問い合わせることで簡単にデータを検索することができます。

ということで、angular2を使ってelasticsearchのRESTAPIを叩くSPAを作ってみました。

angular2を使ったのは今後業務で使っていくので勉強し始めたからといった理由です。

ページ数が少なく簡単な作りになると思うのであえてangular2でやる必要はない気がしますが今回は気にしないことにします。

動作

それでは、実際の動作を見てみます。

各項目に対して検索条件を入力します。

f:id:AdwaysEngineerBlog:20170324155840p:plain

色々入力して検索すると

f:id:AdwaysEngineerBlog:20170324155849p:plain

こんな感じで結果が出てきます

クエリパラメータも確認することもあるなと思ったのでセルをクリックすることでクエリパラメータの一覧を見れるようにしました。

f:id:AdwaysEngineerBlog:20170324155906p:plain

大まかな流れはこんな感じです。

実装

次は実際に処理の流れを追っていきます。

angular-cliで雛形の作成

angular-cliを使ってアプリケーションの雛形を作成します。

  • インストール
npm install -g angular-cli rxjs
  • プロジェクトの作成
ng new ngapp --style=scss
cd ngapp
ng serve --host=0.0.0.0

これだけで動くものが作れてしまいます。簡単ですね。

  • 構成

下記のファイルたちが生成されました。

angular-cli.json
package.json
tslint.json
protractor.conf.js
src
|-- app
|   |-- app.component.html
|   |-- app.component.scss
|   |-- app.component.spec.ts
|   |-- app.component.ts
|   `-- app.module.ts
|-- assets
|-- environments
|   |-- environment.prod.ts
|   `-- environment.ts
|-- favicon.ico
|-- index.html
|-- main.ts
|-- polyfills.ts
|-- styles.scss
|-- test.ts
`-- tsconfig.json

ag-gridの導入

検索結果にgrid表示ができるライブラリを使用します。

有料版もあるのですが、無料の分だけでも十分使えると思います。

公式にサンプルが沢山あるので導入方法やグリッド部分の実装に関しては割愛させていただきます。

Javascript Datagrid

www.ag-grid.com

Angular Datagrid

www.ag-grid.com

コンポーネントの実装

今回は二つのタイプのログを検索できるようにしたかったので検索条件フォームのコンポーネントは二つ用意します

コンポーネント構成は図のようにしました

f:id:AdwaysEngineerBlog:20170324155934p:plain

実際のファイル構成は下記にしました

|-- components
|   | # 検索条件を入力するフォーム(access log)
|   |-- access-log
|   |   |-- access-log.component.html
|   |   |-- access-log.component.scss
|   |   |-- access-log.component.ts
|   |   `-- access-log.service.ts
|   |-- ag-grid
|   |   |-- ag-grid-cell
|   |   |   | # ag-gridのセル用(jsonデータ整形)
|   |   |   |-- ag-grid-cell-json-data
|   |   |   |   |-- ag-grid-cell-json-data.component.html
|   |   |   |   |-- ag-grid-cell-json-data.component.scss
|   |   |   |   `-- ag-grid-cell-json-data.component.ts
|   |   |   | # ag-gridのセル用(リクエストパラメータ整形)
|   |   |   |-- ag-grid-cell-search-params
|   |   |   |   |-- ag-grid-cell-search-params.component.html
|   |   |   |   |-- ag-grid-cell-search-params.component.scss
|   |   |   |   `-- ag-grid-cell-search-params.component.ts
|   |   |   `-- index.ts
|   |   | # 検索結果表示用
|   |   |-- ag-grid.component.html
|   |   |-- ag-grid.component.scss
|   |   `-- ag-grid.component.ts
|   | # 検索条件を入力するフォーム(twitter api log)
|   `-- twitter-api
|       |-- twitter-api.component.html
|       |-- twitter-api.component.scss
|       |-- twitter-api.component.ts
|       `-- twitter-api.service.ts

データバインド

検索条件フォームのコンポーネントから検索結果表示用のコンポーネントへ片方向のデータバインドを行っています。

まず子コンポーネントを呼び出す側から見ていきます。

  • src/app/components/access-log/access-log.component.html(検索条件フォーム)
<div class="container">
  <app-ag-grid [searchedData]="searchedData"
               [isSearchingToggle]="isSearchingToggle"
               [columnDefs]="columnDefs">
  </app-ag-grid>
</div>

app-ag-gridタグで子コンポーネントを呼び出します。

左辺が子コンポーネントで扱う値、右辺が親コンポーネントで持っている値です。

子コンポーネントは下記のようなコードになっています。先ほどのapp-ag-gridタグはselectorで定義された値です。

このselectorを他のhtmlで記述すればこのコンポーネントを呼び出すことが出来ます。

  • src/app/components/ag-grid/ag-grid.component.ts
import {Component, Input, OnChanges,AfterViewInit,SimpleChanges} from '@angular/core';
import {GridOptions} from "ag-grid";

@Component({
  selector: 'app-ag-grid',
  templateUrl: './ag-grid.component.html',
  styleUrls: ['./ag-grid.component.scss']
})
export class AgGridComponent implements OnChanges, AfterViewInit{
  @Input() searchedData: any;
  @Input() isSearchingToggle: boolean;
  @Input() columnDefs: any;

  private gridOptions: GridOptions;

  constructor(
  ) {
    this.gridOptions = <GridOptions>{
      enableSorting :true,
      enableFilter :true,
      enableColResize :true,
      rowHeight :50,
      enableCellChangeFlash :true
    };
    this.gridOptions.columnDefs = this.columnDefs;
    this.gridOptions.rowData = [];
  }

  ngAfterViewInit() {
    this.gridOptions.api.setColumnDefs(this.columnDefs);
  }

  ngOnChanges(changes: any) {
    if ( this.gridOptions.api ) {
      if ( changes.searchedData ) {
        this.gridOptions.api.hideOverlay();
        this.gridOptions.api.setRowData(this.searchedData);
      }

      if ( changes.isSearchingToggle ) {
        this.gridOptions.api.showLoadingOverlay();
      }
    }
  }
}

検索結果表示用のコンポーネントでは親コンポーネントから@Inputでデータを受け取ります。

受け取ったデータに変更があったらngOnChangesライフサイクルフックを用いてグリッドのデータを更新します。

また、初期のカラム定義this.columnDefsを親コンポーネントから受け取るようにしたためag-gridのapiを使ってカラム定義を更新する必要があります。

しかしag-gridのapithis.gridOptions.apiがviewを生成してからでないと使うことが出来ないためngAfterViewInitライフサイクルフックを用いてビューの生成後にカラム定義を行うようにしています。

またthis.columnDefsの定義次第でag-gridの各セルに対してもコンポーネントを用意することで自由に表現することが出来ますが、ここでは説明を割愛します。

elasticsearchへのクエリ組み立て

elasticsearchへのリクエストを送る部分は共通で使えるのでserviceディレクトリを作成しそこに実装します。

このファイルではelasticsearchへクエリを投げる処理のみを行います。

`-- services
    `-- es-search.service.ts

実際のクエリの中身は呼び出し元の親コンポーネントのディレクトリでservice.tsを作成しそこで対象のログに合わせた関数を書きます。

  • src/app/components/access-log/access-log.service.ts
.....
.....
.....
  buildRequestBody(params: any): any {
    let bodyParams = { "size": params.size };

    let paramsCount = Object.keys(params).filter(k => params[k].length > 0).length;
    if (paramsCount > 1) {
      bodyParams["query"] = {"bool": {"filter": []}};
    }

    if (params.code) {
      if (params.not_code){
        bodyParams["query"]["bool"]["filter"].push(
          {
            "bool": {
              "must_not": {
                "terms": {
                  "code": params.code.split(',')
                }
              }
            }
          }
        )
      }else{
        bodyParams["query"]["bool"]["filter"].push(
          {
            "terms":
              { "code": params.code.split(',') }
          }
        );
      }
    }
.....
.....
.....
    return bodyParams;
  }

boolクエリやfilterクエリを用いて複数の条件を入力してもよしなに検索してくれるようにjsonを組み立てます。

多分この作業が一番大変な気がします…

検索処理

最後に検索ボタンを押した際に呼ばれる関数searchを実装します。

accessLogServiceからリクエストに必要なパラメータを取得してきてesSearchServiceでelasticsearchへクエリを投げます。

返ってきたデータをインスタンス変数に格納し、view側で表示させます。

データバインドの項で紹介したように、検索結果表示用のコンポーネントにデータバインドしているので親コンポーネントのデータが変わったらその都度子コンポーネント側で更新してくれるような実装になっています。

  • src/app/components/access-log/access-log.component.ts
.....
.....
.....
  private columnDefs: any;
  private searchedData: any = [];
  private totalCount: number;
  private displayCount: number;
  private isSearchingToggle: boolean = false;

  constructor(
    private esSearchService: EsSearchService,
    private accessLogService: AccessLogService
  ) { }

  ngOnInit() {
    this.columnDefs = this.accessLogService.columnDefs;
  }

  search(params: any): void {
    this.isSearchingToggle =  ( this.isSearchingToggle ) ? false : true;

    let pathName  = this.accessLogService.getPath(params);
    let jsonQuery = this.accessLogService.buildRequestBody(params);
    this.esSearchService.search(pathName,jsonQuery).subscribe(
      data => {
        this.searchedData = data.hits.hits.map(row => row._source);
        this.totalCount   = data.hits.total;
        this.displayCount = data.hits.hits.length;
      },
      error => {
        console.log('search error');
      }
    );
  }
}

以上の流れでelasticsearchへクエリを投げてデータを表示するといった処理を実装することが出来ました。

まとめ

初めてのangular2ということで、入門書片手にangular2を触ってみました。

今までフロントエンドに対して避けてきた部分があったのですが、angular2はてとても楽しく実装を進めることができました。

感覚的にですが、サーバサイドのコードを書く感覚で書くことができるからかなと思います。

また機会があれば何か書きたいと思います。

下記サンプルです

swfz/angular2-es-raw-search: raw log search from Elasticsearch

github.com