Google Cloud Storage APIで格納されているログを集計してslackに転送してみた

MoriKensukePosted by

こんにちは!サーバーサイドエンジニアの森と申します。
今回はGoogle Cloud Storage(以下GCS)に保存されているログデータを定期的にslackに送るjenkinsのジョブを作ったのでそれについてお話します。

動機

私が所属するプロジェクトではアプリケーションのログをGCSに保存しているのですが、定期的にエラーログのサマリーを見たいという需要があったためです。
定期的にGCPコンソールからログを見に行けばいいのですが、ログの量が多いので結構大変です。

実装

流れは以下のようになります。

  1. GCSからデータを取得
  2. データを整形する
  3. slackに転送する
  4. 1〜3の処理をjenkinsで定期的に実行する

GCSからデータを取得

私が所属するプロジェクトのログは、(PROJECT_NAME)-log-storageバケットの中の(PROJECT_NAME).app/(年)/(月)/(日)
フォルダの下に(時):00:00_(時):59:59.jsonの形で1時間ごとのログが分割して保存されています。
例えば、2019年5月23日のログは0時から1時までのログは
(PROJECT_NAME).app/2019/05/23/00:00:00_00:59:59.jsonに保存されます。
なので2019年5月23日のログを集計する場合は(PROJECT_NAME).app/2019/05/23ディレクトリの下の24個のjsonファイルが必要になります。
ログはjson形式の文字列で保存されています。ログの内容を一部抜粋すると以下のようになります。

{
 jsonPayload: {
  deviceType:  "(デバイスの種類)"   
  message:  "(ログの内容)"   
  stackTrace:  "(スタックトレース)"       
  uriString:  "(ログが吐かれたコントローラとアクション)"
 }
 severity:  "(ログレベル)"
 timestamp:  "(ログの入った時間)"  
}

はじめにcomposerを使ってGCSのAPIライブラリをインストールします。
言語はPHPを使用します。

composer require google/cloud-storage

以下のようにしてphpでライブラリが読み込めます。

require 'vendor/autoload.php';
use Google\Cloud\Storage\StorageClient;

GCSにライブラリを使ってアクセスする場合にクレデンシャルを作る必要があります。弊社ではSREチームがGCP関連の管理を行なっているため、クレデンシャルを発行してもらいました。発行手順はこちらをご覧ください。

以下のようにして目的のログファイルに対応するオブジェクトを全て取得することができます。

// StorageClientオブジェクト作成
$storage = new StorageClient([
  'keyFile' => json_decode(file_get_contents($gcp_credential_path), true)
]);
$date = date("Y/m/d");

// (PROJECT_NAME)-log-storageのbucketオブジェクト取得
$bucket = $storage->bucket('(PROJECT_NAME)-log-storage');

// bucket内の「(PROJECT_NAME).app/' . $date」で始まる名前のファイルオブジェクトを全て取得
$objects = $bucket->objects([
  'prefix' => '(PROJECT_NAME).app/' . $date,
]);

バケットのファイルを全部取得すると重いので、目的のファイルのみをファイル名のprefixで指定して取得します。

データの整形

以下のようにして取得したデータをslackに送るために整形します。ここで不要なログのフィルターも行なっています。

$result_map = [];
foreach ($objects as $object){
  // ファイルのテキストを取得
  $log_string = $object->downloadAsString();
  
  // 改行でログを分割
  $log_strings = explode("\n", $log_string);
  foreach ($log_strings as $line){
    // jsonにする
    $log_map = json_decode($line, TRUE);
    // コントローラがデバッグのものはカウントしない
    if (strpos($log_map['jsonPayload']['uriString'], 'debug') !== FALSE){
      continue;
    }
    
    // ログレベルがwarnかerrorの場合のみ取得
    switch ($log_map['severity']){
    case 'WARN':
    case 'ERROR':
     // convert_message_to_key関数は見にくいログを見やすく整形している
      $msg = convert_message_to_key($log_map['jsonPayload']['message']);
      // ログのメッセージをキーにして件数をカウントする
      $result_map[$msg] = ($result_map[$msg] ?? 0) + 1;
      break;
    }
  }
}

これで$result_map変数にログのメッセージをキーに、そのメッセージのログの件数を値にした連想配列が作られます。
これをslackに転送するための配列に作り直します。

 // ログの件数の大きさで降順ソート
arsort($result_map);

// slackのattachmentの形に整形
$attachments = [];
foreach ($result_map as $err_msg => $err_count){
  $attachments[] = [
    "fallback" => sprintf("%d件: %s", $err_count, $err_msg),
    "color" => "#D00000",
    "fields" => [
      [
        "title" => $err_msg,
        "value" => $err_count . '件',
      ]
    ],
  ];
}

これでslackにデータを送る準備が完了です。

slackにデータを転送

上記で作成したデータをslackのIncoming Webhooks APIを使って転送します。
見やすいようにattachmentsの形式を採用しました。
@hereで通知するようにしていますが、<!here>で記述しないといけないといけないことに注意が必要です。

function send_to_slack($content, $attachments = []){
  header('Content-type: appliation/json; charset=utf-8');

  $url = (WebhookのURL);

  $json = [
    "text" => $content,
    "attachments" => $attachments,
  ];
  $text = json_encode($json);

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $text);

  $response = curl_exec($ch);
  curl_close($ch);
}

jenkinsで定期的に実行

上記のスクリプトをjenkinsで定期的に回します。

平日の17:00に実行したいので、「ビルド・トリガ」の「定期的に実行」で0 17 * * 1-5に設定しています(平日の17:00に設定した理由は、平日の午後にゲームのアップデートを行うことが多いのですが、その際にバグがもし発生していたら早急にキャッチしたいためです)。

クレデンシャルはコード管理したくないため、jenkinsのバインディング機能で設定し、この変数(以下ではGCP_CREDENTIAL)を上記のphpスクリプトに渡します($gcp_credential_pathに代入する)。

結果

これで以下のように平日の17:00になるとslackにログが通知されるようになりました。黒塗り部分多くてすみません。

終わりに

slackにGCSのログのサマリーを転送することで、ログ確認の時間を短縮することができました。簡単にできますので、ぜひ皆さんも試してみてください!