はじめに
こんにちは、Gakken LEAPインフラエンジニアの丹羽です。
今回はエラーログの通知を改良した件についてお話ししたいと思います。
きっかけ
弊社サービスのアプリケーションではログをCloudwatchログに出力しており、エラーログがあった場合は社内チャットツールに通知を飛ばしています。
通知自体にログ内容は含まれないので、AWSコンソールに実際のログを確認しに行く必要があるわけですが、今までは業務のルーチンに含めていたこともありそれほど気にしていませんでした。
しかし先日、アラートが多かった日にふと「これ、面倒だな…」と思ってしまいました。
プログラマーの三大美徳に「怠惰」があるように、面倒と思った気持ちを放置するのは良くありませんし、逆に効率化のチャンスにつながる可能性を秘めています。
というわけで、面倒さが軽減するよう通知を改良してみることにしました。
改良方針
既存の構成は下記の通りです。
Cloudwatchログに送信されたログをメトリクスフィルターで検索し、エラーと思われる文言があればカスタムメトリクスに記録します。このカスタムメトリクスを監視するCloudwatchアラームを別途設定し、メトリクスに動きがあればSNSに通知するわけです。
今回、面倒と感じた原因は、通知を見る=>実際のログを確認する、という2アクションが必要な点にあったので、単純に通知内容にログ内容を含めてしまおうと考えました。
Cloudwatchログにはメトリクスフィルターの他にサブスクリプションフィルターという機能があり、こちらはフィルタに該当したログ内容そのものを別サービス(Kinesis, Lambda, OpenSearchなど)に転送することができます。
このサブスクリプションフィルターを使って、改良した構成が下記です。
CloudwatchログとSNSトピックの中継ぎをCloudwatchアラームの代わりにLambda関数が担っています。Lambda関数の処理は下記の通りです。
sns = boto3.client('sns')
def lambda_handler(event, context):
# SNSトピックのARNを環境変数から取得
topic_arn = os.environ['SNS_TOPIC_ARN']
# CloudWatchログのデータをデコード&解凍
compressed_payload = base64.b64decode(event['awslogs']['data'])
uncompressed_payload = gzip.decompress(compressed_payload)
log_data = json.loads(uncompressed_payload)
# エラーログのメッセージを抽出して結合
error_messages = "\n".join([log_event['message'] for log_event in log_data['logEvents']])
error_log_group = log_data['logGroup']
error_log_stream = log_data['logStream']
subject = "{0}:{1}".format(error_log_group, error_log_stream)
# SNSに送信
response = sns.publish(
TopicArn=topic_arn,
Subject=subject,
Message=error_messages
)
ポイントとしては、サブスクリプションフィルターから送られてくるデータはbase64エンコード&gzip圧縮されているため、デコードと解凍を行ってやる必要があります。
また、エラーログ本文だけでなく参照元のロググループやログストリームの情報も含まれているので、詳細なログ解析などでAWSコンソールに見に行く時のために、これらの情報も通知に含めてあげると親切でしょう。
まとめ
エラーログ通知にログ本文が含まれるように改良し、AWSコンソールに見に行く手間が省けるようになりました。
実際のところ、これらの通知を一番よく見るのは開発チームなので、今後は開発チームの感想もうかがいながら実用的な通知を目指していきたいと思います。
また、メトリクスフィルター方式の通知はここ以外にもあるので、将来的には横展開してそちらの改良にも活用できればと期待しています。
エンジニア募集
Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!