時間がかかるコマンドの実行結果をSlackに通知する

  • 63
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

なに?

こういうやつ.
image.png (43.2 kB)

image.png (43.6 kB)

モチベーション

docker buildや機械学習,データ解析 ,画像処理等の「時間がかかるコマンド」について,いちいち終わったかなーみたいに確認しに行くのめんどくさいし頭のリソースのムダになる.実行完了/失敗したらSlackに通知したい.

通知先についてはどこでもいいんだけど,自分はbotとふたり暮らしのteam持ってるのでそこに流している.分報channel持ってるならそっちでもいいかもしれない.LINEやFBメッセンジャーに送るのもおもしろそうではある.

zsh hookを利用してSlack通知

アウトライン

function notify_preexec {
  # TODO: 開始時間と実行コマンドを取得
}

function notify_precmd {
  # TODO: ステータスや実行環境からペイロード生成
  # TODO: Slackその他に送りつける
}

add-zsh-hook preexec notify_preexec
add-zsh-hook precmd notify_precmd

「時間がかかるコマンド」?

$TTYIDLEを見ればいい(サンプルはbeepを鳴らす).

precmd() {
  [ $TTYIDLE -gt 10 ] && echo -n ^G
}

時間がかかるコマンドが終了したときに Beep! - f99aq8oveのブログ

実行したコマンドは?

preexecの引数見ればいいらしい.ついでに開始時間もとっておく?

function notify_preexec {
  notif_prev_executed_at=`date`
  notif_prev_command=$2
}

実行結果のsuccess/fail判定は?

$?を見る.precmdの先頭で隔離しておかないとifの条件式で上書きされてしまうので注意.

  notif_status=$?
  if [ -n "${SLACK_WEBHOOK_URL+x}" ] && [ -n "${SLACK_USER_NAME+x}" ] && [ $TTYIDLE -gt ${SLACK_NOTIF_THRESHOLD:-180} ]; then
    notif_title=$([ $notif_status -eq 0 ] && echo "Command succeeded :ok_woman:" || echo "Command failed :no_good:")
    notif_color=$([ $notif_status -eq 0 ] && echo "good" || echo "danger")
    notif_icon=$([ $notif_status -eq 0 ] && echo ":angel:" || echo ":smiling_imp:")

    # snip.
  fi
}

なお,このままだとCtrl-zCtrl-cでコマンドを強制終了させた場合にも通知が飛んでしまう.Jupyter NotebookやRails,その他WAFのサーバプロセスを殺した場合や,Gulp等タスクランナーのファイル監視プロセスを殺した場合にも通知が出ると厄介なので,そこはstatusみて回避する.

 if [ -n "${SLACK_WEBHOOK_URL+x}" ] && [ -n "${SLACK_USER_NAME+x}" ] && [ $TTYIDLE -gt ${SLACK_NOTIF_THRESHOLD:-180} ] && [ $notif_status -ne 130 ] && [ $notif_status -ne 146 ]; then
  # snip.
fi

送りつけるJSONは?

JSONをべちゃっと1行で書くのはちょっと遠慮したい.
bash/zsh等のヒアドキュメントは標準入力に流し込まれるので,catして変数に突っ込んでおく.

Slackのメッセージのフォーマットは公式のAPIドキュメント見てよしなに….

# ここではJSONの中身は省略してます
payload=`cat << EOS
{
  "attachments": [
    {
      "color": "$notif_color",
      "title": "$notif_title",
      "fields": [
        {
          "title": "command",
          "value": "$notif_prev_command",
          "short": false
        },
        {
          "title": "elapsed time",
          "value": "$TTYIDLE seconds",
          "short": true
        }
      ]
    }
  ]
}
EOS
`

Markdown(のようなもの)にする場合,mrkdwntrueをセットしてあげる.attachment内でMarkdown(のようなもの)を書きたい場合はmrkdwn_inに対象となる属性名を配列で渡してあげる.なお,コードブロックを含めたい場合,うまくエスケープしてあげないと死を招くので注意する.

payload=`cat << EOS
{
  "attachments": [
    {
      "mrkdwn_in": ["fields"],
      "fields": [
        {
          "title": "command",
          "value": "\\\`$notif_prev_command\\\`",
          "short": false
        }
      ]
    }
  ]
}
EOS

バッククォートを含める場合,「バックスラッシュをエスケープしたもの」と「バッククォートをエスケープしたもの」を並べて書くとpayloadに「バックスラッシュ+バッククォート」が入る.これでこの後のcurlでも適切にエスケープしてくれる.なに言ってるかわかんなくなってきた.

どんなリクエスト投げる?

こんな感じ.bodyに改行あったら厄介なのでtrで潰す.ついでにスペースも潰す.

curl --request POST \
  --header 'Content-type: application/json' \
  --data "$(echo "$payload" | tr '\n' ' ' | tr -s ' ')" \
  $SLACK_WEBHOOK_URL

完成形

実行コマンド,pwdhostnamewhoami,開始時間,実行時間 [sec]を投げてます.
SLACK_WEBHOOK_URLSLACK_USER_NAMEは各自いい感じにどうぞ.

if [ -z "${SLACK_WEBHOOK_URL+x}" ]; then
  echo "SLACK_WEBHOOK_URL is empty !!!"
fi

if [ -z "${SLACK_USER_NAME+x}" ]; then
  echo "SLACK_USER_NAME is empty !!!"
fi

function notify_preexec {
  notif_prev_executed_at=`date`
  notif_prev_command=$2
}

function notify_precmd {
  notif_status=$?
  if [ -n "${SLACK_WEBHOOK_URL+x}" ] && [ -n "${SLACK_USER_NAME+x}" ] && [ $TTYIDLE -gt ${SLACK_NOTIF_THRESHOLD:-180} ] && [ $notif_status -ne 130 ] && [ $notif_status -ne 146 ]; then
    notif_title=$([ $notif_status -eq 0 ] && echo "Command succeeded :ok_woman:" || echo "Command failed :no_good:")
    notif_color=$([ $notif_status -eq 0 ] && echo "good" || echo "danger")
    notif_icon=$([ $notif_status -eq 0 ] && echo ":angel:" || echo ":smiling_imp:")
    payload=`cat << EOS
{
  "username": "command result",
  "icon_emoji": "$notif_icon",
  "text": "<@$SLACK_USER_NAME>",
  "attachments": [
    {
      "color": "$notif_color",
      "title": "$notif_title",
      "mrkdwn_in": ["fields"],
      "fields": [
        {
          "title": "command",
          "value": "\\\`$notif_prev_command\\\`",
          "short": false
        },
        {
          "title": "directory",
          "value": "\\\`$(pwd)\\\`",
          "short": false
        },
        {
          "title": "hostname",
          "value": "$(hostname)",
          "short": true
        },
        {
          "title": "user",
          "value": "$(whoami)",
          "short": true
        },
        {
          "title": "executed at",
          "value": "$notif_prev_executed_at",
          "short": true
        },
        {
          "title": "elapsed time",
          "value": "$TTYIDLE seconds",
          "short": true
        }
      ]
    }
  ]
}
EOS
`
    curl --request POST \
      --header 'Content-type: application/json' \
      --data "$(echo "$payload" | tr '\n' ' ' | tr -s ' ')" \
      $SLACK_WEBHOOK_URL
  fi
}

add-zsh-hook preexec notify_preexec
add-zsh-hook precmd notify_precmd

References