初めまして。アドテクdivのswfzです。
少し前に流行った fluentd + elasticsearch + kibanaでアクセスログを可視化、チームでもやっていましたが集計したグラフはみれるしめちゃいい!けど実際の生データを検索かけるのはちょっと操作が面倒ですよね。
今回はangular2を使ってelasticsearchの生ログを検索できるSPAを作成してみました。
今回は作ったSPAの実際の動作と実装を軽く紹介させていただきます。
※データはダミーデータ生成用のgemがあるのでそれを使わせてもらいました。
動機
- アクセスログの量が重いと調査でgrepとかする際時間がかかりストレスがマッハ(ノ`Д´)ノ.:・┻┻)
- ログをelasticsearchに突っ込めばファイルから検索するより早く検索できる
- 集計したグラフも見れて二度おいしい
- ただ、検索クエリ覚えるのつらい
- ディレクターや非エンジニアの方でもログ調査くらいなら出来るようになればお互いハッピー
- エンジニアは作業を中断しなくて良く余計なコミュニケーションコストがなくなる
- ディレクターはわざわざエンジニアに調査依頼せず自分で調査できる
elasticsearchにはRESTAPIがあるので直接リクエストを投げて問い合わせることで簡単にデータを検索することができます。
ということで、angular2を使ってelasticsearchのRESTAPIを叩くSPAを作ってみました。
angular2を使ったのは今後業務で使っていくので勉強し始めたからといった理由です。
ページ数が少なく簡単な作りになると思うのであえてangular2でやる必要はない気がしますが今回は気にしないことにします。
動作
それでは、実際の動作を見てみます。
各項目に対して検索条件を入力します。
色々入力して検索すると
こんな感じで結果が出てきます
クエリパラメータも確認することもあるなと思ったのでセルをクリックすることでクエリパラメータの一覧を見れるようにしました。
大まかな流れはこんな感じです。
実装
次は実際に処理の流れを追っていきます。
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表示ができるライブラリを使用します。
有料版もあるのですが、無料の分だけでも十分使えると思います。
公式にサンプルが沢山あるので導入方法やグリッド部分の実装に関しては割愛させていただきます。
コンポーネントの実装
今回は二つのタイプのログを検索できるようにしたかったので検索条件フォームのコンポーネントは二つ用意します
コンポーネント構成は図のようにしました
実際のファイル構成は下記にしました
|-- 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