ログのクラスタリング分析の処理をGoで実装し、Cloud RunとPub/Subを使ってサーバレスで処理できる仕組みを作る

先日、Firebase + GAEを使って実装したサービス「[LogCrow](https://logcrow.firebaseapp.com/)」に新しい機能として、ログの登録を簡略化するために、ログファイルをアップロードして、ログの各行をクラスタリングし、類似ログ同士をまとめあげた上で、ログ登録できる機能を追加しました。

LogCrowのAnalyzeメニューにアクセスすると、ファイルをアップロードできるフォームがあります。ここに分析にかけたいログファイルをアップロードして実行すると分析処理がサーバサイドで走ります。

![AnalyzePage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45851/7fce6520-87bb-4b1f-483d-9805afca1a6d.png)

![AnalyzePage2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45851/8c3eac53-6dbd-7f0e-21a1-9ab26e642cf5.png)

![AnalyzePage3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45851/0c8c5b6b-3d30-4478-723e-e8da9366a2a5.png)

分析には多少時間がかかります。
分析が完了すると、分析結果を以下のような感じで確認できます。

![AnalyzePage4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45851/ae17163a-0575-cd7c-a6b9-558c52df534a.png)

類似ログがまとめ上げられているので、その内容を眺め、ログの原因と対策の情報を記録しておきたいものを選択し、「Add log」を実行することで、このログメッセージをベースにした新規ログ登録画面に遷移します。

前置きが長くなりましたが、この機能の裏側でどのようなことが行われているのかを基盤のアーキテクチャの視点と分析処理の視点で紹介します。

## この仕組みの裏側の流れ(基盤アーキテクチャ)

まずはこの処理を実行する基盤側の流れです。基本的に全てサーバレスで実装です。
裏側の仕組みとしては[こちら](https://qiita.com/ike_dai/items/0ffd6f6d472761b1e603)の記事で紹介したような感じで以下図の流れになります。

![CloudRun_CloudPubSub.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45851/1547a123-d0b4-72c6-2e9b-ee54c8737261.png)

1. フロントエンドのVue.jsからファイルをアップロード
2. バックエンドのGAEのAPI側でファイルを受け取りCloud Storage上に一時的にファイルをアップロード
3. Storageにファイルアップロードが完了したことをトリガーとしてCloud Functionsを実行
4. Cloud Functionsの関数処理の中でCloud Pub/SubにメッセージをPush
5. Pub/SubのSubscriberとして登録されているCloud Runを実行
6. Cloud Runでは、Cloud Storageにアップロードされているファイルを読み込み分析処理を実行
7. 実行結果はFirestoreに保存
8. フロントエンドからは7.で登録された結果を確認

### Cloud Runを採用した理由

当初はCloud Functionsで実行しようと考えていました。しかし、後述のログのクラスタリング分析処理をGo言語で実装しており、かつGoの1.12のバージョン以上でないと動かないライブラリに依存した実装にしていたため、Cloud FunctionsのGoランタイムの言語制約の1.11のバージョンに合致せず、コンテナベースで自由に動かせるCloud Runで実行することにしました。


### Cloud Functions -> Pub/Sub -> Cloud Runの流れを採用した理由

Cloud Functionsの場合は様々なトリガーに対応しているのですが、Cloud Runは実行トリガーが以下のみ対応となります。
- HTTPSリクエストによる実行
- Pub/Subからのメッセージによる実行
- Cloud Schedulerによるスケジュール実行
- Cloud Tasksを使った実行
- Webhookによる呼び出しによる実行

Storageへのアップロード完了を持って実行できるようにすることが最もリアルタイムに処理を後続にすすめることができると考えたので、Storageへの書き込み完了をトリガーとしてキックできるCloud Functions側の処理を間に挟むことにしました。Cloud Functionsからは上記のいずれかの方法で実行すれば良いので直接HTTPSリクエストをCloud Run宛に投げても実行できるのですが、認証情報の引き渡しのことや、連携失敗時の処理再実行のことなどを考慮し、Pub/Subを間に挟み、サービス間連携させるようにしました。
Cloud Runの呼び出しを認証済みの内部のサービスからのみ許可する仕組みにしたかったため、Cloud FunctionsからPub/Subを経由して呼び出すようにしました。


## この仕組みの裏側の流れ(分析アルゴリズム)

次にCloud Runで実行させている分析処理の中身です。
具体的には[こちらのリポジトリ](https://github.com/ike-dai/log-cluster)で公開しているようなクラスタリング処理を行っています。

1. ログに含まれる時刻表記や単純な数字のみの単語などの不要なキーワードを除去
2. ログ文をGoの[prose](https://github.com/jdkato/prose)というライブラリを使って形態素解析を行い品詞タグ付け
3. 品詞タグのCD(数字、序数など)、SYM(記号)などの不要なキーワード以外のものを重要キーワードとしてピックアップ
4. ピックアップされた単語を[wego](https://github.com/ynqa/wego)というライブラリに含まれるWord2Vecでベクトル化
5. 1行のログに含まれる各単語のベクトルを加算平均し、ログ1行のベクトルを算出
6. [goClustering](https://github.com/cipepser/goClustering)というライブラリを使い、Ward法を用いて各行のログをクラスタリング分類

こんな流れになります。
最後のクラスタリング分類の方法としては、K-meansとかも考えたのですが、K-meansだと予め決まった数のクラスタ数に分割するというトップダウンのアプローチになってしまい、今回の要件のように件数がわからない、場合によってはそれぞれ全く別のクラスタに分類されるケースもあり得るような場合に分割しづらかったため、ボトムアップのアプローチであるWard法を採用しています。

![out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45851/a04cf46a-9fb1-907f-6707-f1db5b79bc2c.png)

イメージとしては上記図のような感じで、ログの各行が近いもの同士が並んでおり、どのレベルの距離以上でクラスタを区切るかを指定すればある程度の類似度以上の近しいものを同一クラスタとみなして分割することができます。上の図の場合、0.008以上の距離で分割すると、2つのクラスタに、0.004～0.007ぐらいの距離で分割すると3クラスタに分割されることになります。

## まとめ

このような処理もサーバレスで実装できてしまいます。ある程度制約がある中でどのような構成が良いかを検討して組めばサーバレスでも十分に要件を満たせる実装は可能かと思います。
特にCloud Runはコンテナベースで非常に柔軟に処理実行組み込めるので非常に便利です。
LogCrowもぜひお試しいただければと思います。

https://logcrow.firebaseapp.com/