なに?
モチベーション
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 }
実行したコマンドは?
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-z
やCtrl-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(のようなもの)にする場合,mrkdwn
にtrue
をセットしてあげる.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
完成形
実行コマンド,pwd
,hostname
,whoami
,開始時間,実行時間 [sec]を投げてます.
SLACK_WEBHOOK_URL
とSLACK_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