michimani.net

AWS CLI で CloudWatch Logs にログを送信する

2019-09-03

CloudWatch Logs には AWS の各サービスからあらゆるログが送信されるようになっていますが、SDK や CLI を使えば外部のアプリケーションからもログを送信することができます。今回は AWS CLI を使ってログを送信してみました。

CloudWatch Logs とは

Amazon CloudWatch Logs を使用して、Amazon Elastic Compute Cloud (Amazon EC2) インスタンス、AWS CloudTrail、Route 53、およびその他のソースのログファイルの監視、保存、アクセスができます。

AWS のリソースに限らず、アプリケーションのログも溜めておくことができます。サーバレスアーキテクチャだったりスケーリングを意識したアーキテクチャだと、ログの置き場所に困りますが、そういう構成ではとりあえず CloudWatch Logs に投げるみたいな構成になっていると思います。

CloudWatch Logs の構成要素

CloudWatch Logs では ロググループログストリーム という要素があるので、それぞれについてざっと確認します。

ロググループ

ロググループは、保持、監視、アクセス制御について同じ設定を共有するログストリームのグループです。ロググループを定義して、各グループに入れるストリームを指定することができます。1 つのロググループに属することができるログストリームの数に制限はありません。

つまり、アプリケーション単位や画面単位といったくくりでログをグループ分けしている要素です。例えば Lambda の実行ログに関しては、関数単位でロググループが作成されています。

ログストリーム

ログストリームは、同じソースを共有する一連のログイベントです。CloudWatch Logs へのログの各ソースによって、個別のログストリームが構成されます。

ロググループ内でさらにログを分割している要素がログストリームです。同じアプリケーションであっても、別のバージョンや別日付などでストリームを分けることになると思います。Lambda の実行ログを例にすると、関数のバージョン単位、一定時間単位でストリームが分割されています。

ロググループ内には数の制限なくログストリームを作成することができます。一つだけでもいいです。ただし、最低でも日付ごととかで分割してあると、あとでログを閲覧するときに見つけやすいです。
また、ストリームを分けたとしても、マネジメントコンソール上ではストリームを横断したログの検索が可能なので、心配ありません。

ログイベント

いわゆるログのことです。あとで書きますが、 AWS CLI では put-log-events というサブコマンドを使ってログを送信します。

AWS CLI での操作

では、実際に AWS CLI で CloudWatch Logs を操作してみます。
CloudWatch Logs のコマンドは aws logs です。これに続けてサブコマンドを足していく感じです。

コマンドの詳細については公式のドキュメントを参照してください。

ログの送信 (準備)

ログの送信には put-log-events サブコマンドを使うと書きましたが、実行時には下記のオプションが必要になります。

つまり、ログの送信するにはロググループとログストリームが既に存在している必要があります。なので、今回はそれらも AWS CLI で作成してみます。

ロググループ、ログストリームの作成

まずは ロググループです。

$ aws logs create-log-group --log-group-name michimani-test-group

出力は特にありません。本当に作成されたのか確認してみます。

$ aws logs describe-log-groups --log-group-name-prefix michimani
{
    "logGroups": [
        {
            "logGroupName": "michimani-test-group",
            "creationTime": 1567477587999,
            "metricFilterCount": 0,
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXXXXXX:log-group:michimani-test-group:*",
            "storedBytes": 0
        }
    ]
}

もちろんですが、既にグループが存在している場合はエラーが出力されます。

$ aws logs create-log-group --log-group-name michimani-test-group

An error occurred (ResourceAlreadyExistsException) when calling the CreateLogGroup operation: The specified log group already exists

続いてログストリームを作成します。

$ aws logs create-log-stream \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001

これも出力は特に何もないので、作成されているのか確認してみます。

$ aws logs describe-log-streams \
--log-group-name michimani-test-group \
--log-stream-name-prefix test-stream-001
{
    "logStreams": [
        {
            "logStreamName": "test-stream-001",
            "creationTime": 1567478248995,
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXXXXXX:log-group:michimani-test-group:log-stream:test-stream-001",
            "storedBytes": 0
        }
    ]
}

これでログを入れる箱ができたので、ログを送信していきます。

ログの送信

ログの送信 (準備) で書いた通り、ログの送信にはロググループとログストリーム、そしてログのイベントデータを指定する必要があります。 イベントデータは --log-events オプションで指定しますが、方法が Shorthand SyntaxJSON Syntax の 2 通りあるのでそれぞれ見ていきます。
例として、Hello CloudWatch Logs というログを送信するとします。

Shorthand Syntax

ワンラインでタイムスタンプとメッセージを指定する方法です。

$ aws logs put-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--log-events timestamp=1567478448995,message="Hello CloudWatch Logs"

JSON Syntax

JSON 形式で指定する方法です。この方法だと、複数のログイベントを一度に送信することができます。

[
  {
    "timestamp": 1567478448995,
    "message" : "Hello CloudWatch Logs"
  }
]

この方法の場合は、 JSON をファイルとして保存しておいて、そのファイルを --log-events で指定する形になります。

$ aws logs put-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--log-events file://events.json

どちらの方式でも timestampmessage が必要になります。また、 timestamp13 桁 である必要があります。

では実際に JSON Syntax の方式でログを送信してみます。
送信するイベントデータとして、下記のような JSON を準備します。

[
  {
    "timestamp": 1567478448995,
    "message" : "Good morning CloudWatch Logs"
  },
  {
    "timestamp": 1567478548995,
    "message" : "Good afternoon CloudWatch Logs"
  },
  {
    "timestamp": 1567478648995,
    "message" : "God night CloudWatch Logs"
  }
]
aws logs put-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--log-events file://events.json
{
    "nextSequenceToken": "49596402802751870934231393163316499531521111741320675602"
}

結果として、 nextSequenceToken という値が出力されました。この値は、 引き続き同じログストリームにログを送信する際に必要 になります。仮に同じコマンドをもう一度実行してみると

$ aws logs put-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--log-events file://events.json

An error occurred (DataAlreadyAcceptedException) when calling the PutLogEvents operation: The given batch of log events has already been accepted. The next batch can be sent with sequenceToken: 49596402802751870934231393163316499531521111741320675602

エラーとともに、先ほどと同じ sequenceToken の値が記されています。ということで、 sequenceToken を指定して送信してみます。

aws logs put-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--log-events file://events.json \
--sequence-token 49596402802751870934231393163316499531521111741320675602
{
    "nextSequenceToken": "49596402802751870934231393650155762793610763307610816786"
}

別の sequenceToken が返ってきました。つまり、同一のログストリームにログイベントを送信する場合は、 2 回目以降はその一つ前に送信した際に返ってきた sequenceToken--sequence-token オプションで指定する必要があります。

ログの確認

では、上で送信したログを確認してみます。

$ aws logs get-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
{
    "events": [
        {
            "timestamp": 1567478448995,
            "message": "Good morning CloudWatch Logs",
            "ingestionTime": 1567483678013
        },
        {
            "timestamp": 1567478448995,
            "message": "Good morning CloudWatch Logs",
            "ingestionTime": 1567484033671
        },
        {
            "timestamp": 1567478548995,
            "message": "Good afternoon CloudWatch Logs",
            "ingestionTime": 1567483678013
        },
        {
            "timestamp": 1567478548995,
            "message": "Good afternoon CloudWatch Logs",
            "ingestionTime": 1567484033671
        },
        {
            "timestamp": 1567478648995,
            "message": "God night CloudWatch Logs",
            "ingestionTime": 1567483678013
        },
        {
            "timestamp": 1567478648995,
            "message": "God night CloudWatch Logs",
            "ingestionTime": 1567484033671
        }
    ],
    "nextForwardToken": "f/34955941955374514222924862267686763373673893786318209026",
    "nextBackwardToken": "b/34955937495225474516800233530579142916825806445936443392"
}

同じ JSON を 2 回送信したので、それぞれ 2 つずつ同じログイベントが取得できました。
一緒に帰ってきている nextForwardTokennextBackwardToken は何でしょうか。

AWS CLI で取得できるログイベントは、データサイズは 1 MB または イベント数が 10,000 件 が上限となっています。なので、それ以降 (Forward) もしくは それ以前 (Backward) のイベントを取得する際には --next-token で値を指定します。
また、取得するログイベント数の上限は --limit オプションでも指定できます。 --limit--next-token の挙動を試してみます。

$ aws logs get-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--limit 2
{
    "events": [
        {
            "timestamp": 1567478648995,
            "message": "God night CloudWatch Logs",
            "ingestionTime": 1567483678013
        },
        {
            "timestamp": 1567478648995,
            "message": "God night CloudWatch Logs",
            "ingestionTime": 1567484033671
        }
    ],
    "nextForwardToken": "f/34955941955374514222924862267686763373673893786318209026",
    "nextBackwardToken": "b/34955941955374514222924861837722797446498107642019643394"
}

直近の 2 件が取得できました。それ以前の 2 件を取得するために --next-tokennextBackwardToken の値を指定してみます。

$ aws logs get-log-events \
--log-group-name michimani-test-group \
--log-stream-name test-stream-001 \
--limit 2 \
--next-token b/34955941955374514222924861837722797446498107642019643394
{
    "events": [
        {
            "timestamp": 1567478548995,
            "message": "Good afternoon CloudWatch Logs",
            "ingestionTime": 1567483678013
        },
        {
            "timestamp": 1567478548995,
            "message": "Good afternoon CloudWatch Logs",
            "ingestionTime": 1567484033671
        }
    ],
    "nextForwardToken": "f/34955939725299994369862548114114936108837743188276609025",
    "nextBackwardToken": "b/34955939725299994369862547684150970181661957043978043393"
}

次の 2 件が取得できました。
こんな感じで、前後のイベントを取得できます。

ログの検索

filter-log-events サブコマンドで、任意の文字列でログイベントを検索することができます。
filter-log-events で必須のオプションは --log-group-name のみで、 --log-stream-name は任意です。つまり、冒頭にも書きましたがログストリームを横断してロググループ全体で検索することが可能です。これは AWS CLI だけでなく、マネジメントコンソール上でも可能です。

では実際に試してみます。

$ aws logs filter-log-events \
--log-group-name michimani-test-group \
--filter-pattern "Good morning"
{
    "events": [
        {
            "logStreamName": "test-stream-001",
            "timestamp": 1567478448995,
            "message": "Good morning CloudWatch Logs",
            "ingestionTime": 1567483678013,
            "eventId": "34955937495225474516800233530579142916825806445936443392"
        },
        {
            "logStreamName": "test-stream-001",
            "timestamp": 1567478448995,
            "message": "Good morning CloudWatch Logs",
            "ingestionTime": 1567484033671,
            "eventId": "34955937495225474516800233960543108844001592590235009024"
        }
    ],
    "searchedLogStreams": [
        {
            "logStreamName": "test-stream-001",
            "searchedCompletely": true
        }
    ]
}

ログイベントの形式

上の例で送ったログを、マネジメントコンソールで確認してみます。

CloudWatch Logs Management console

まあ、普通です。
ログの内容がシンプルなのでこれでもいいかもしれませんが、実際のログにはいろんな情報が含まれています。なので、それらの情報をマネジメントコンソール上で見やすくなるような形でログを送信してみます。

方法としては、ログイベントの messageJSON 文字列にする です。具体的には下記のような JSON を準備して、送信します。

[
    {
        "timestamp": 1567478748995,
        "message": "{\"name\":\"Ken\",\"message\":\"Good morning\"}"
    },
    {
        "timestamp": 1567478848995,
        "message": "{\"name\":\"Kumi\",\"message\":\"Good afternoon\"}"
    }
]

マネジメントコンソールで確認してみると、下のキャプチャのようにログイベントを展開したときに JSON が整形されて表示されます。

CloudWatch Logs JSON

この JSON 形式にしておくと視認性が良くなるというメリットもありますが、ログの検索時に JSON のキーと値を指定して検索することもできます。例えば nameKen のログであれば、 {$.name = "Ken"} というフィルタで検索できます。

CloudWatch Logs JSON filter

もちろんこれは AWS CLI で操作するときにも使えます。

$ aws logs filter-log-events \
--log-group-name michimani-test-group \
--filter-pattern '{$.name = "Ken"}'
{
    "events": [
        {
            "logStreamName": "test-stream-001",
            "timestamp": 1567478748995,
            "message": "{\"name\":\"Ken\",\"message\":\"Good morning\"}",
            "ingestionTime": 1567486685224,
            "eventId": "34955944185449034075987179626789594095612148516425498624"
        }
    ],
    "searchedLogStreams": [
        {
            "logStreamName": "test-stream-001",
            "searchedCompletely": true
        }
    ]
}

ということなので、送信した後のログイベントのその後の扱い方を考えると、 JSON 形式で送信しておいた方が便利なような気がします。

まとめ

AWS CLI での CloudWatch Logs の操作してみた話でした。
実際にアプリケーションからログを送信する際には、適度な粒度でログストリームを作成したりする必要があります。今回は CLI で試しましたが、各言語の SDK で実装する場合にも、基本的にはこんな感じの操作になるかと思います。
やっぱり CLI で操作する楽しさって何か特別なものがありますよね。


comments powered by Disqus