vague memory

うろ覚えを無くしていこうともがき苦しむ人の備忘録

AWS Lambda で CloudWatch Logs のログ本文をSlack通知(2)

f:id:htnosm:20160505000213p:plain

前回 AWS Lambda で CloudWatch Logs のログ本文をSlack通知(1) の続きです。
SNSを介さず、Lambdaから直接Slackへ投稿する方法です。

f:id:htnosm:20160504231804p:plain

(2) Stream to AWS Lambda でSlack通知

各種設定

Blueprints冒頭に記載されている Slack Integration や KMS の設定については割愛します。

Lambda Function 作成

  • [Services] -> [Lambda] -> [Create a Lambda Function]

f:id:htnosm:20160504231806p:plain

cloudwatchlog-alarm-to-slack.py

今回使用するFunctionです。 Blueprintと同様に ENCRYPTED_HOOK_URL、SLACK_CHANNEL にSlackの設定値を入れます。

from __future__ import print_function

import base64
import json
import zlib
import datetime
import boto3
import logging

from base64 import b64decode
from urllib2 import Request, urlopen, URLError, HTTPError

ENCRYPTED_HOOK_URL = ''  # Enter the base-64 encoded, encrypted key (CiphertextBlob)
SLACK_CHANNEL = '#'  # Enter the Slack channel to send a message to

HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext']

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
    data_json = json.loads(data)
    log_json = json.loads(json.dumps(data_json["logEvents"][0], ensure_ascii=False))
    if data_json["logGroup"]:
        date = datetime.datetime.fromtimestamp(int(str(log_json["timestamp"])[:10])) + datetime.timedelta(hours=9)
        message = str(date) + " [CloudWatch Log Alarm] " + data_json["logGroup"] + " / " + data_json["logStream"] + "\nLogMessages:"
        logger.info(data_json)
        for e in data_json["logEvents"]:
            message = message + '\n' + e['message']
            logger.info("LogMessage: " + str(message))

        slack_message = {
            'channel': SLACK_CHANNEL,
            'text': "%s" % (message)
        }

        req = Request(HOOK_URL, json.dumps(slack_message))
        try:
            response = urlopen(req)
            response.read()
            logger.info("Message posted to %s", slack_message['channel'])
        except HTTPError as e:
            logger.error("Request failed: %d %s", e.code, e.reason)
        except URLError as e:
            logger.error("Server connection failed: %s", e.reason)

IAM role 設定

Lambda Function 作成時に指定した Role に下記が実行できるよう権限を付与します。

  • lambda_basic_execution (Function作成時に付与するポリシー)
  • KMS復号化
  • filter-log-events実行

f:id:htnosm:20160504231807p:plain

Metric Filter 作成

CloudWatch Logs からメトリックフィルタを作成します。

  • CloudWatch -> Logs -> LogGroup -> Stream to AWS Lambda

f:id:htnosm:20160504231816p:plain

  • 作成した Lambda Function を指定

f:id:htnosm:20160504231809p:plain

  • Filter Pattern を設定

f:id:htnosm:20160504231810p:plain f:id:htnosm:20160504231811p:plain

  • 設定後のロググループ一覧

Subscription に Lambda が表示されます。

f:id:htnosm:20160504231812p:plain

Metric Filter の追加

CloudWatch 側からだけでなく、 Lambda 側からもメトリックフィルタの追加が可能です。

f:id:htnosm:20160504231813p:plain

  • [Lambda] -> [Function] -> [Event source]

f:id:htnosm:20160504231814p:plain

通知例

f:id:htnosm:20160504231815p:plain


CloudWatchLogsにログを転送していることが前提ですが、 EC2インスタンスに手を入れずにログ監視が行えます。
それ程クリティカルでなく、監視エージェントを導入する程では無いのであれば、 実装候補に入りうると思います。

AWS Lambda で CloudWatch Logs のログ本文をSlack通知(1)

CloudWatch Logsを使用したログ監視です。

f:id:htnosm:20160505000213p:plain

CloudWatch Logs のメトリックフィルタから Alarm を作成し、SNS メッセージをSlackへ投稿する Blueprint が提供されていますが、 通知されるメッセージだけでは Alarm が発生した事がわかるのみなので、ログ本文を通知したいと思いました。

f:id:htnosm:20160504231803p:plain

通知先(Slack)以外はAWSで完結可能な方法という所で、Lambda(Python)を使用した2パターンを試します。

f:id:htnosm:20160504231804p:plain

  • 図内黒矢印は Blueprint を使用した場合

メトリクスに過去発生頻度を残したいというのであれば(1)、 通知先固定でエラー発生がわかれば良いレベルなら(2)が使用できるのではないでしょうか。

利用サービスと設定箇所が少ないので、(2)の方がシンプルです。
更に、通知先を複数にしたいならLambdaからSNSへ飛ばしてSubscriptionさせれば実現できると思います。(SlackのEmail Subscriptionはスタンダードプラン以上が必要です。)

(1) SNSを経由してSlack通知

  • CloudWatch [Logs] -> Alarm -> SNS -> Lambda -> Slack
  • AWS提供の Slack Integration Blueprints を元にログ本文を追加

blueprint cloudwatch-alarm-to-slack-python 変更箇所

Blueprints冒頭に記載されている Slack Integration や KMS の設定については割愛します。

ライブラリをインポート

ログイベント取得時の日時指定のため、datetime、calendar ライブラリを追加します。

 import boto3
 import json
 import logging
+import datetime
+import calendar

取得条件

今回の取得条件は以下のようにしています。

  • FILTER_PATTERN
  • OUTPUT_LIMIT
    • 取得行(limit)は最大5行
  • TIME_FROM_MIN
    • Logs取得の開始時刻(timefrom)は CloudWatch Alarm の Period(今回は5分)に合わせる
+FILTER_PATTERN='{ $.log_level = "ALERT" }'
+OUTPUT_LIMIT=5
+TIME_FROM_MIN=5

ENCRYPTED_HOOK_URL = '<kmsEncyptedHookUrl>'  # Enter the base-64 encoded, encrypted key (CiphertextBlob)
SLACK_CHANNEL = '<slackChannel>'  # Enter the Slack channel to send a message to

filter_log_events

CloudWatch Logs からイベントを取得します。
ロググループ(logGroupName) はメトリクス名(MetricsName)と一致している前提にしています。

     reason = message['NewStateReason']
+    metric = message['Trigger']['MetricName']
+
+    timeto = datetime.datetime.strptime(message['StateChangeTime'][:19] ,'%Y-%m-%dT%H:%M:%S') + datetime.timedelta(minutes=1)
+    u_to = calendar.timegm(timeto.utctimetuple()) * 1000
+    timefrom = timeto - datetime.timedelta(minutes=TIME_FROM_MIN)
+    u_from = calendar.timegm(timefrom.utctimetuple()) * 1000
+    client = boto3.client('logs')
+    streams = client.describe_log_streams(logGroupName = metric, orderBy = 'LastEventTime')
+    stream = str(streams['logStreams'][0]['logStreamName'])
+    logger.info("logGroupName = " + str(metric) + ", logStreamNames = [" + str(stream) + "], filterPattern = " + str(FILTER_PATTERN) + ", startTime = " + str(u_from) + ", endTime = " + str(u_to) + ", limit = " + str(OUTPUT_LIMIT))
+    response = client.filter_log_events(logGroupName = metric, logStreamNames = [stream], filterPattern = FILTER_PATTERN, startTime = u_from, endTime = u_to, limit = OUTPUT_LIMIT)
+    log_events = response['events']
+    log_message = '[CloudWatch Log Alarm] ' + str(metric) + ' / ' + str(stream) + '\nLogMessages:'
+    for e in log_events:
+        date = datetime.datetime.fromtimestamp(int(str(e['timestamp'])[:10])) + datetime.timedelta(hours=9)
+        log_message = log_message + '\n{"Timestamp":"' + str(date) + '","Message":' + e['message'] + '}'

Slackメッセージにeventを追加

取得したイベントを通知メッセージに追加します。

     slack_message = {
         'channel': SLACK_CHANNEL,
-        'text': "%s state is now %s: %s" % (alarm_name, new_state, reason)
+        'text': "%s state is now %s: %s \n %s" % (alarm_name, new_state, reason, log_message)
     }

通知例

f:id:htnosm:20160504231805p:plain


次回 パターン(2)に続きます。 尚、Lambda の Code 全文は以下になります。

続きを読む

Mackerel事始め ホストの退役

退役=Mackerel上からの削除です。

f:id:htnosm:20160429183205p:plain

Web画面上での設定方法がパッと出なかったので流れを書いてみます。 何てことは無いのですが一覧から変更できるものと思っていたので少々彷徨いました。

Web画面からの退役設定

Hosts(ホスト一覧)画面で設定できるのは、Working, Standby, Maintenance, Power off の4種類のみです。 退役を表す retired は選択肢にありません。

f:id:htnosm:20160429183206p:plain

退役手順

Hosts → ホスト画面

f:id:htnosm:20160429183207p:plain

ホスト画面 → [設定]

f:id:htnosm:20160429183208p:plain

[このホストを退役させる] → [OK]

f:id:htnosm:20160429183209p:plain

退役後

f:id:htnosm:20160429183210p:plain

退役後の参照方法

Hosts → 退役ホストをみる

f:id:htnosm:20160429183211p:plain

退役ホスト一覧

f:id:htnosm:20160429183212p:plain

退役ホスト一覧上はサービスやロールでの絞り込みには対応していないようです。 絞込を行おうとすると所属しているはずのホストも表示されなくなりました。

f:id:htnosm:20160429183213p:plain

Mackerel事始め check-plugin プロセス監視

check-plugin を使ってプロセス監視を行います。

チェックプラグインの共通設定

  • notification_interval
    • 再送間隔(分)
    • 未指定=再送無し
    • 最短30分(30分未満を設定した場合も30分)
  • max_check_attempts
    • 指定回数連続でOK以外の結果の場合にアラート発報

再送30分、連続3回の設定の場合

設定例

[plugin.checks.check_ntpd]
command = "/usr/local/bin/check-procs --pattern ntpd -u ntp"
notification_interval = 30
max_check_attempts = 3

通知結果

f:id:htnosm:20160426215657p:plain

debuglogを確認

2016/04/26 20:23:19 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=OK message="Procs OK: Found 1 matching processes; cmd /ntpd/; user /ntp/\n"
# ntpdを停止、以降CRITICAL
2016/04/26 20:25:18 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"
2016/04/26 20:26:18 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"
2016/04/26 20:27:18 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"
# ↑で通知発生
2016/04/26 20:28:18 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"
# ・・・ 略 ・・・
2016/04/26 20:56:39 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"
2016/04/26 20:57:39 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"
# ↑で通知発生
2016/04/26 20:58:39 checker.go:81: DEBUG <checks> Checker "check_ntpd" status=CRITICAL message="Procs CRITICAL: Found 0 matching processes; cmd /ntpd/; user /ntp/\n"

check-procs.conf

予期しないプロセスを検索してしまう可能性があるため、 patternは極力正確に、userも指定した方が無難です。

[plugin.checks.check_http]
command = "/usr/local/bin/check-procs --pattern /usr/sbin/httpd"

[plugin.checks.check_ntpd]
command = "/usr/local/bin/check-procs --pattern ntpd -u ntp"
notification_interval = 30
max_check_attempts = 3

オプション

状態オプション

閾値設定を省略した場合、-W-Cのデフォルトが 1 なので、プロセスが存在していない(停止している)状態でCRITICALアラートが飛びます。

オプション ロングオプション 説明
-w | -W --warn-over | --warn-under WARNING閾値(プロセス数が指定値より 大きい|小さい)
-c | -C --critical-over | --critical-under CRITICAL閾値(プロセス数が指定値より 大きい|小さい)

検索オプション

m | M オプションは今一つ使い所がわかりませんでした。 また、s オプションは { R | S | D | T | Z } のいずれを指定してもマッチせずでした。
(原因について深くは調べていません)

オプション ロングオプション 説明
-p | -x --pattern | --exclude-pattern 検索パターン | 除外パターン
-f --file-pid 検索PID(.pidファイル指定ではない)
--ppid 検索親PID
-m | -M --match-self | --match-parent os.Getpid() | os.Getppid と比較(mackerel-agent のプロセスを含めるか否か?)
-z --virtual-memory-size VSZ[KB]閾値、指定値以下のプロセスが検索される
-r --resident-set-size RSS[KB]閾値、指定値以下のプロセスが検索される
-P --proportional-set-size PSS[KB]閾値、指定値以下のプロセスが検索される
-T --thread-count スレッド数閾値、指定値以下のプロセスが検索される
-s --state スレッド状態
-u | -U --user | --user-not プロセス所有者
-e | -E --esec-over | --esec-under プロセス実行時間[秒]が指定値より 大きい |小さい プロセスが検索される
-i | -I --cpu-over | --cpu-under CPU時間[秒]が指定値より 大きい|小さい プロセスが検索される

閾値超過時の例

Alert発生時のWebコンソール

f:id:htnosm:20160426215050p:plain

Slack通知

f:id:htnosm:20160426215051p:plain

Close時のWebコンソール

f:id:htnosm:20160426215052p:plain


Mackerel事始め check-plugin ログ監視(動作確認)

check-plugin を使ってログ監視の色々を確認してみました。

f:id:htnosm:20160424150544j:plain

初回登録時

対象ログファイルの先頭から読まれます。
大量通知を避けるため、一度コマンド実行でステートファイル作成後にconfファイル有効化を行った方が安全だと思います。

ログファイルのパーミッション

mackerel-agent は root で動作しているため、syslogなどroot権限でのみ参照可能なログファイルについても、パーミッション変更は不要です。

$ ps -ef | grep [m]ackerel
root     17493     1  0 07:30 pts/0    00:00:04 /usr/local/bin/mackerel-agent --pidfile=/var/run/mackerel-agent.pid --root=/var/lib/mackerel-agent

大括弧"[]"検索

エラーレベルが大括弧で囲われているパターンを指定したい場合は、 大括弧前にバックスラッシュ"\"を付与します。 付与しないと囲われていない文字列も一致してしまいます。

  • "[error]"を検索したい場合
正:
--pattern '\[error\]'
誤:
--pattern '[error]'

agentエラーメッセージ

  • 必須オプション不足
# pattern無し
the required flag `-p, --pattern' was not specified
# file無し
LOG UNKNOWN: No log file specified
  • ログファイルが存在しない
LOG UNKNOWN: open /var/log/test.log: no such file or directory
  • 権限不足(手動実行での確認時)
writeByteToSkip failed: mkdir /var/mackerel-cache: permission denied
  • confのKey重複
    • plugin.checks. 部分を変えなかった場合にagentが起動失敗となる
failed to load config: Failed to load the config file: while loading included config file /etc/mackerel-agent/conf.d/check-log.conf: Near line 13 (last key parsed 'plugin.checks'): Key 'plugin.checks.test-log' has already been defined.
  • 設定ファイル誤り(parseは通っている)
    • 通知メッセージのステータスが UNKNOWN となる

単一ファイル指定(fileオプション)時のログローテーション時の動作

ログローテーション等でログファイルが上書き・新規作成された場合の挙動を確認します。
エラーログ等で普段はログ出力されず、出力された時に同時にファイル作成・ローテーションが行われるような処理の場合等を想定しています。

出力前サイズ > 出力後サイズ

  • ログローテーション等でファイルが初期化された場合等を想定
    • 正常に動作(通知される)

出力前サイズ = 出力後サイズ

  • ログローテーション等でファイルが初期化された場合等を想定
  • 且つ前後でサイズが同じ場合に誤動作しないか
    • 通知されない

通知されませんでした。 単一ファイル指定の場合サイズのみで判断となります。 発生する状況はあまり無いと思いますが、出力数が少ないログなどは注意が必要です。

出力前サイズ < 出力後サイズ

  • stacktrace が出力されるログファイル等でローテーション後の出力がローテーション前のサイズより大きくなる場合を想定
    • 出力前サイズ以前の出力部分に関しては通知されない
    • 出力前サイズ以降の出力部分に関しては正常に動作

追記と見做されます。 想定通りの動作ですが、ログ出力とローテーションの方式によって発生する可能性はあります。

オプションで回避

常に追記ではなく上書きされるようなログには no-state 、 ログ出力時にファイル作成されるようなログには file-pattern で回避可能と思います。

Mackerel事始め check-plugin ログ監視(基本設定)

check-plugin を使ってログ監視を試してみます。公式に詳しい説明があります。

check-log.conf

必須オプションは対象ファイルと検索文字列です。

[plugin.checks.test-log]
command = "/usr/local/bin/check-log --file /var/log/messages --pattern error"

複数行で記載する場合は、コマンドをシングルクォート3つで囲みます。

[plugin.checks.test-log]
command = '''
  /usr/local/bin/check-log              \
    --file /var/log/messages            \
    --pattern 'error'                   \
    --warning-over 3 --critical-over 10 \
    --return
'''

オプション

組み合わせる事で様々な要件に対応できます。
filefile-pattern は併用可能なので、1コマンドで複数ファイルを対象とするような使い方も可能なようです。 尚、複数 file 指定は後勝ちなので、単一ファイル2つというような使い方は不可です。

オプション ロングオプション 必須 説明
-f --file 監視対象ログファイルパス
-F --file-pattern 複数ファイル指定
-p --pattern 検索文字列
-E --exclude 除外文字列
-w --warning-over WARNING閾値(件数/分)
-c --critical-over CRITICAL閾値(件数/分)
--warning-level
--critical-level
-r --return エラー行通知
-i --icase 大文字小文字を区別しない
-s --state-dir ステートファイル保存場所(デフォルト:/var/mackerel-cache/check-log)
--no-state ステートファイル未使用(ログファイル全体を読む)

仕様

公式記載の内容に沿って確認します。

間隔

ログのチェックは1分ごとにおこなわれ、前回チェックした行はスキップされます。

  • ステートファイルにログファイルのバイト数を保持し、以降の行をチェック

return オプション

--return オプションを付加することで、エラー行が出力され、その内容がMackerelに送信されます。
送信内容のサイズが大きい場合、表示が切り詰められることがあります。

  • マルチバイト(日本語)対応
  • サイズ制限は約 1,000 bytes?
    • 行数にも関係がある模様 (3行検知時:約2,000 bytes / 21行検知時:約1,000 bytes を確認)
  • 複数行検知された場合、検知順にサイズ制限まで複数行通知

return 無し

f:id:htnosm:20160424080017p:plain

return 有り

f:id:htnosm:20160424080106p:plain

サイズ制限による切り捨て

f:id:htnosm:20160424081310p:plain

warning-over | critical-over オプション

設定値より多くエラー行が検出された場合にwarning
設定値より多くエラー行が検出された場合にcritical

  • 設定値より多く
    • warning-over 5 の場合、6行検知すると通知

Warning例

f:id:htnosm:20160424080505p:plain

一つのログに対して複数のチェック監視を設定

--state-dir を指定して、別の場所にステートファイルを保存するようにして下さい。

  • conf例
    • plugin.checks. 部分を変える必要有り
[plugin.checks.test-log]
command = "/usr/local/bin/check-log --file /var/log/messages --pattern error"

[plugin.checks.test-log2]
command = "/usr/local/bin/check-log --file /var/log/messages --pattern fatal" --state-dir /var/mackerel-cache/check-log2
  • 上記例の場合、ステートファイルが以下のように格納される
    • /var/mackerel-cache/check-log/var/log/messages
    • /var/mackerel-cache/check-log2/var/log/messages

復旧通知について

必ず復旧通知がされてしまうようなのですが、復旧通知を無効にする設定箇所が見つけられていません。
通知グループを設定する際の監視ルールとしては表示されますが、監視ルールの設定ページには追加されていません。 そもそも復旧通知の有無が設定できないようなので仕様なのでしょうか。

Mackerel事始め agent-plugin ELB RDS

agent-pluginを使ってELB、RDSの監視設定を追加していきます。

IAM Policy

mackerel-plugin-aws-elb は GetMetricStatistics に加え、ListMetrics も必要とのことです。

  • cloudwatch:GetMetricStatistics
  • cloudwatch:ListMetrics

CLI で IAM User 作成

専用の IAM User を作成するなら以下のような感じでしょうか。

$ USER=mackerel_agent
# IAMユーザ作成
$ aws iam create-user --user-name $USER
{
    "User": {
        "UserName": "mackerel_agent",
        "Path": "/",
        "CreateDate": "2016-03-28T10:04:13.381Z",
        "UserId": "XXXXXXXXXXXXXXXXXXXXX",
        "Arn": "arn:aws:iam::999999999999:user/mackerel_agent"
    }
}
# IAMポリシー付与
$ aws iam attach-user-policy --user-name $USER --policy-arn arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess
# AccessKey生成
$ aws iam create-access-key --user-name $USER
{
    "AccessKey": {
        "UserName": "mackerel_agent",
        "Status": "Active",
        "CreateDate": "2016-03-28T10:06:33.644Z",
        "SecretAccessKey": "YYYYYYYYYYYYYYYYYYYYYYYYYYYY/zzzzzzzzzzz",
        "AccessKeyId": "WWWWWWWWWWWWWWWWWWWW"
    }
}

ELB

mackerel-plugin-aws-elb

aws-elb.conf

# mackerel-plugin-aws-elb [-lbname=<aws-load-blancer-name>] [-region=<aws-region>] [-access-key-id=<id>] [-secret-access-key=<key>] [-tempfile=<tempfile>]

[plugin.metrics.aws-elb]
command = "/usr/local/bin/mackerel-plugin-aws-elb -access-key-id=AWS_ACCESS_KEY_ID -secret-access-key=AWS_SECRET_ACCESS_KEY -lbname=ELB_NAME"

f:id:htnosm:20160423024441p:plain

lbnameを指定しない場合は参照可能なELBの平均値が取得されるようです。

RDS

aws-rds.conf

# mackerel-plugin-aws-rds -identifier=<db-instance-identifer> [-region=<aws-region>] [-access-key-id=<id>] [-secret-access-key=<key>] [-tempfile=<tempfile>] [-metric-key-prefix=<prefix>] [-metric-label-prefix=<label-prefix>]

[plugin.metrics.aws-rds]
command = "/usr/local/bin/mackerel-plugin-aws-rds -identifier=DBIDENTIFIER -access-key-id=AWS_ACCESS_KEY_ID -secret-access-key=AWS_SECRET_ACCESS_KEY"

f:id:htnosm:20160423024452p:plain

identifierは必須オプションになっており、指定しない場合は値取得不可(0)になります。 コマンド実行結果を確認すると以下の様なエラーが出力されます。

The parameter Dimensions.member.1.Value is required.


惜しむべきはカスタムメトリックとして登録されることでしょうか。 台数が少ない構成であれば問題無さそうですが、ELB/RDS等はホストとサービスメトリックとして登録したい所です。
公式よりfluentdを介することで実現するようです。