🗒️
任意の CloudWatch Logs ログメッセージを、一発で表示できるURLを組み立てたい
直面した問題
プロダクトにて、Subscription Filter & Lambda 構成でエラーログを捕捉しています。
- コンピューティングリソースが、CloudWatch Logs へログを配信する
- CloudWatch Logs の Subscription Filter にて、配信されたログが、特定の文字列を含むか監視する
- 特定の文字列を含むログを捕捉したら、
EventLogConsumer
Lambda関数を呼び出す - Lambda関数を使用して、ユーザー(Slack)へ通知する
Slackへ投下するテキストは、以下の情報を含んでいます。
- エラーログに記録されたメッセージ
- 該当メッセージを含む CloudWatch Logs ログストリームへのリンク
リンクからログストリームの画面は表示できるのですが、そこからエラーログを頑張って探している状況でした。
このエラーログを探す作業がなかなか面倒で、運用作業で度々消耗していました。
やりたいこと
エラーログを探す手間を省きたい。
より具体的には、
特定の、
CloudWatch Logs のログメッセージを、
一発で表示できるURLを組み立てたい。
結論
以下のようにURLを構成します。
// URLの組み立てに必要な情報
log_group="ロググループ名"
log_stream="ログストリーム名"
log_timestamp_ISO_format="yyyy-MM-ddTHH:mm:ss.fffZ"
log_id="ログメッセージに割り当てられるID情報"
// 組み立て
cwl_url = f"https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:log-groups/log-group/{log_group}/log-events/{log_stream}?start={log_timestamp_ISO_format}&refEventId={log_id}"
ポイント
-
start=
で、表示するログメッセージの先頭時刻を絞り込む- => 該当ログメッセージの表示時刻を設定することで、先頭に表示されるようになる
-
refEventId=
の値を設定することで、該当メッセージが色付けされ、少し目立つようになる - 以下は、全て Lambda の
event
変数から取得できる- URLの組み立てに必要な情報
- Subscription Filter に引っかかったログメッセージ
event
変数から、URL組み立てに必要な情報を取り出す
Lambda 関数 の Lambda関数の event
変数から、ログデータを取り出し、jsonへ変換します。
def handler(event, context):
text = gzip.decompress(base64.b64decode(event['awslogs']['data']))
text_json = json.loads(text)
jsonへ変換したログデータは、以下のフォーマットで定義されています。
{
"owner": "123456789012",
"logGroup": "CloudTrail",
"logStream": "123456789012_CloudTrail_us-east-1",
"subscriptionFilters": [
"Destination"
],
"messageType": "DATA_MESSAGE",
"logEvents": [
{
"id": "31953106606966983378809025079804211143289615424298221568",
"timestamp": 1432826855000,
"message": "{\"eventVersion\":\"1.03\",\"userIdentity\":{\"type\":\"Root\"}"
},
{
"id": "31953106606966983378809025079804211143289615424298221569",
"timestamp": 1432826855000,
"message": "{\"eventVersion\":\"1.03\",\"userIdentity\":{\"type\":\"Root\"}"
},
{
"id": "31953106606966983378809025079804211143289615424298221570",
"timestamp": 1432826855000,
"message": "{\"eventVersion\":\"1.03\",\"userIdentity\":{\"type\":\"Root\"}"
}
]
}
-
"logGroup"
... ロググループ名 -
"logStream"
... ログストリーム名 -
"logEvents"
... Subscription Filter に引っかかった、ログメッセージ情報-
"id"
... ログメッセージに割り当てられるID情報 -
"timestamp"
... ログ配信時刻。エポック時間がミリ秒単位で格納されている -
"message"
... Subscription Filter に引っかかったログメッセージ
-
URLの組み立てに必要なデータを、取り出します。
log_group = text_json['logGroup']
log_stream = text_json['logStream']
log_id = text_json['logEvents'][0]['id']
log_timestamp_msec = text_json['logEvents'][0]['timestamp']
今回は logEvents の先頭要素だけ欲しいので、配列の先頭要素を直で参照しています。
データを取り出すにあたって、以下の点に注意が必要です。
-
/
記号を事前にURLエンコーディング - timestamp 情報をISOフォーマットへ変換
/
記号を事前にURLエンコーディング
注意点1 : ロググループ名およびログストリーム名は、名前に /
が入る可能性があります。
そのままURLを組み立ててしまうと、ブラウザが /
をディレクトリ階層と勘違いし、意図しない画面表示に繋がります。
そのため、事前にURLエンコーディングしておくと安全です。
encoded_log_group = urllib.parse.quote(log_group).replace('/', '%252F')
encoded_log_stream = urllib.parse.quote(log_stream).replace('/', '%252F')
注意点2 : timestamp 情報をISOフォーマットへ変換
url に必要な時刻情報は ISOフォーマットです。
ログ情報はエポック時間がミリ秒単位で格納されているため、変換が必要です。
dt = datetime(1970, 1, 1) + timedelta(milliseconds=timestamp_msec)
encoded_timestamp_ISO_format = "{}Z".format(dt.isoformat(timespec='milliseconds'))
該当ログメッセージに直飛びする URL を組み立てる
取り出したデータを使って、 結論 に記載した通りに、URLを組み立てます。
cwl_url = "https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:log-groups/log-group/{}/log-events/{}?start={}&refEventId={}".format(
encoded_log_group, encoded_log_stream, encoded_timestamp_ISO_format, log_id)
以上。
さいごに
やればできるものですね。
付録に、pythonで記述した実装のサンプルを掲載しています。
必要に応じて、こちらも併せてご確認ください。
参考文献
- https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/ValidateLogEventFlow.html
- https://dev.classmethod.jp/articles/cwl-lambda-sns-publish/
付録
実装例(python)
import json
import base64
import gzip
import urllib
from datetime import datetime, timedelta
def _msec_to_isoformat(timestamp_msec: int):
dt = datetime(1970, 1, 1) + timedelta(milliseconds=timestamp_msec)
return "{}Z".format(dt.isoformat(timespec='milliseconds'))
def handler(event, context):
text = gzip.decompress(base64.b64decode(event['awslogs']['data']))
text_json = json.loads(text)
# https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/ValidateLogEventFlow.html
log_group = text_json['logGroup']
log_stream = text_json['logStream']
log_id = text_json['logEvents'][0]['id']
log_timestamp_msec = text_json['logEvents'][0]['timestamp']
log_msg = text_json['logEvents'][0]['message']
encoded_log_group = urllib.parse.quote(log_group).replace('/', '%252F')
encoded_log_stream = urllib.parse.quote(log_stream).replace('/', '%252F')
encoded_timestamp_ISO_format = urllib.parse.quote(
_msec_to_isoformat(log_timestamp_msec))
cwl_url = "https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:log-groups/log-group/{}/log-events/{}?start={}&refEventId={}".format(
encoded_log_group, encoded_log_stream, encoded_timestamp_ISO_format, log_id)
# 以下、必要に応じて、Slackへ投下したり他のLambdaを呼び出したり。
print(cwl_url)
return {
'statusCode': 200,
'body': {
'log_msg':log_msg,
'url':cwl_url
}
}
Discussion