詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

ターミナルでSlackを読む

Slackはそのクライアントがそれなりに、かなりよくできていて、これでほとんど困ることはないんだけど、そうは言ってももうちょっとプログラマブルに取り扱いたいこともある。

そういう場合にもよいAPIが用意されていて、Real Time Messaging API ってのがある。こいつはWebSocketでSlackの発言をはじめ、あらゆるイベントのJSONを送りつけてくれるやつ。ひとまずこれを標準出力に流すことができれば、あとは好きに料理できるはずだ。

というわけで作ったのがこちら。書いたことなかったのでRustです。ちょうどいいネタだった。

GitHub - motemen/slack-stream-json

slack-stream-json というバイナリが、SLACK_TOKEN 環境変数を設定した上で起動してやると、RTM APIによって得られたイベントのJSONをそのまま標準出力に流してくれる、それだけのツール。あとは適当なツールやプログラムで欲しい情報を取り出せばよい。

コマンドラインオプション

というだけだとさすがに素朴すぎる(curl + jq + wscat と変わらない)ので、Slack用の便利オプションがいくつかある。

--inflate-fields

Slack RTM でやってくるイベントは、以下のような形状をしている。

{
  "type": "message",
  "channel": "C2147483705",
  "user": "U2147483697",
  "text": "Hello world",
  "ts": "1355517523.000005"
}

これをそのまま標準出力に出力されてもあまりうれしくない……。どんなユーザがどんなチャンネルに投稿したのか、直にはわからないからだ。そこでこの userchannel フィールドを、対応するオブジェクトに展開してやるオプション。

このオプションをつけると、上掲のJSONは以下のように出力される。

{
  "type": "message",
  "channel": {
    "id": "C2147483705",
    "name": "fun",
    "created": 1360782804,
    "creator": "U024BE7LH",
    ...
   },
   "user": {
     "id":"U2147483697",
     "name":"motemen",
     ...
   },
   "text": "Hello world",
   "ts": "1355517523.000005"
}

これは便利。ネストされたフィールドに関してはまだサポートしてないのと、途中から増えたチャンネルやユーザもまだ未対応。

--format-message

同様にSlackにおける発言も決まったフォーマットに則っていて、たとえばメンションは

{
  "text": "Hey <@U024BE7LH>, thanks for submitting your report.",
  ...
}

みたいな形でやってくる。こういうやつを解決して

{
  "text": "Hey @motemen, thanks for submitting your report.",
  ...
}

にしてくれる。テキストにするので、リンクのURLとかは失われます。

<https://www.example.com/|example> my site <https://www.example.com/>

example my site https://www.example.com/

になる感じ。:emoji: の展開はしません。emojifyを使うと絵文字も展開できるが、emojifyはGitHubスタイルの:emoji:にしか対応しておらず、GitHubとSlackは微妙に違うので展開できないものもある(:thinking_face:とか)。 ​

--print-start-response

チャンネルやユーザの情報は、rtm.start APIを叩いた時に返ってくるJSONに含まれている。JSONのストリームをプログラムから利用したい場合は、この情報もあったほうが便利ですよねー。ということで最初のJSONとしてこのレスポンスを表示します。

jqと組み合わせてSlackを読む

そういうわけで、jq と組み合わせればこういう使い方ができます:

% SLACK_TOKEN=xxx slack-stream-json -f -i | jq --raw-output --unbuffered 'select(.type == "message" and (.text | length) and .user) | "\u001b[30m\(.ts | tonumber | localtime | strftime("%X")) \u001b[32m#\(.channel.name) \u001b[33m@\(.user.name)\u001b[m \(.text)"'

全部のチャンネルの投稿が流れてくるので、往年のIRCクライアントみたいなビューが実現できて感激。色までつけちゃってます。

f:id:motemen:20191121201058p:plain
ひとりで喋っている

fzfでインタラクティブにフィルタする

とはいえすべてのチャンネルの発言がフラットに流れてくるだけだと普通に読みづらい……どうしたものか、と考えて編み出したのがfzfと組み合わせてログをフィルタする方法! 上述のストリームを fzf に食わせれば、気になった発言を見たときにチャンネル名 #xxx とか発言者 @yyy でコンテキストを絞り込むことができる。これはやべー!

% SLACK_TOKEN=xxx slack-stream-json -f -i | jq --raw-output --unbuffered 'select(.type == "message" and (.text | length) and .user) | "\u001b[30m\(.ts | tonumber | localtime | strftime("%X")) \u001b[32m#\(.channel.name) \u001b[33m@\(.user.name)\u001b[m \(.text)"' | fzf --ansi --no-sort --tac --nth=2..

f:id:motemen:20191121202718g:plain

まあ、ただ、出力をどんどん溜め込むのでメモリを食っちゃうことは予想できる。つい最近出たfzf 0.19.0のreloadってのを使ったらマシになるのかもしれない。

ネットワークが切れたときとか、復帰はうまくいかないと思うのだけど、それは宿題ということにしておく。どうぞご利用ください。


スクショ撮るのに適切なチームがなかったのでオンラインサロンMOTEMENを使いました^^;

curl でデータを逐次アップロードするには -T (--upload-file) を使う

最近知ってへーとなったのでメモ。標準入力から curl を通じてサーバにデータを送信したい場合、

curl --data-binary @- <url>

というのがイディオムだと思うけど、この場合は標準入力から全部読みきってからサーバに接続・送信するので、データをストリーミングすることができない。コマンドライン経由のリアルタイムなコミュニケーションに HTTP を気軽に使いたい身としては、これは不便。

不便だなーと思って調べていたら、curl には -T (--upload-file) というオプションがあって、これは PUT でファイルを送信するのだけど、これは Transfer-Encoding: chunked しつつ、データを逐次読み取りながらサーバに送信するものらしい。

たとえば

# Linux なら nc -l -p 9999
nc -l 9999

しつつ、別のシェルで

curl -s -T - localhost:9999

すれば、毎行を入力するごとにサーバ側で受信できていることが確認できる。

とくに標準入力から入力を受けとる場合、-T - の代わりに -T . とすれば標準入力がノンブロッキングになって、入力を待たずにサーバからの応答を得ることができる(のだと思ってる。細かい挙動がよくわかってない)。

Use the file name "-" (a single dash) to use stdin instead of a given file. Alternately, the file name "." (a single period) may be specified instead of "-" to use stdin in non-blocking mode to allow reading server output while stdin is being uploaded.

ISUCON9予選敗退(H::W::A::Abyss::Abyss::Abyss) #isucon

ISUCON9 2日目結果と本選出場者決定のお知らせ : ISUCON公式Blog

日曜日に行われたISUCON9予選に参加し、敗退してきました。前掲の記事の「失格となったチーム」がわたしたちです。チームメンバーは id:t_kytid:mechairoi

去年も予選敗退していて、最近LINE本社に行ってないな~と思っていたので今年は勝ちたかった。今年もはてな京都オフィスで予選に参加。

f:id:motemen:20190909133951p:plain

事前の作戦が大事なので、「コミュニケーションが大切」「マニュアルを読む」を確認した。

だいたいハマり出すとコミュニケーションが減ったり同じことをやりだしたりしてチームとしての効率が下がってしまうので、一時間ごとにタイマーを鳴らしてそこで話そう、という作戦を立てていた。あとは alp と pprof と pt-query-digest でボトルネックを見ていくというシンプルな戦略。

あと、1日目の結果を見ていて

美顔器 on Twitter: "近年の傾向を分析した結果、今回のISUCON戦略は「学生気分」とします"

という気づきを得たので方針は「学生気分」となった。

やったこと

  • 10:53 最初のベンチが取れるまでがだいたい1時間くらい。デプロイは手元で build してバイナリだけ rsync するような方法で、これはサイクル早く回せてよかった。
    • ベンチを回していると isucari の CPU 使用がボトルネックになっているのが分かったので、pprof。http.ListenAndServe(":6060", nil) して待ち受ける練習をしてたんだけど、よく考えたらセキュリティポリシーで塞いでいたので ssh でポートフォワードした。
  • 11:49 pprof すると postLogin 内の bcrypt が重いことが明らかだったので、「学生(10年前の自分)だったらどうするか?」と考えて即座にパスワードを平文化することを思いつき、ベンチを回してパスワードを収集する実装を投入。還元率 1 で 5,230 イスコイン。
    • あとで書くけど、この初手で地獄への道が敷かれていた。笑える。
    • まあ気づいていたとしても MD5 化くらいはやったと思うので、あとはそうだと思って読んでください。
  • 13:28 当然スコアは上がるんだけど、ベンチマークを回してパスワードを収集するたびにパフォーマンスが上がり、どこかのタイミングで臨界して POST /buy が失敗するようになったので、どうしようかねー、と話して外部 API へのアクセスをリトライするようにしたところ、うまく行って還元率 2 で 8,030 イスコイン。
    • これなんでリトライでうまく行ったんだろ。今考えるとよくわからない。500 になってたのかな?
  • かりそめの暫定一位を取ったところでお昼ごはん。
  • 14:03 どうせ3台構成にするから今のうちにやっとこ、ということで 1 台を nginx+アプリ、1 台を MySQL にしたところ還元率 0 で 2,710 イスコイン。還元率を 1 にすると以下のエラーが出て FAIL するようになってしまった。この失敗理由が見えないので一同相当にハマってしまった。Revert したり構成戻したり、でかなり時間を取ってしまった。なんでなの……。
POST /buy: リクエストに失敗しました (item_id: 50030),
POST /buy: リクエストに失敗しました (item_id: 50036),
POST /buy: リクエストに失敗しました (item_id: 50043)
  • 17:13 トランザクションを分離する変更を id:mechairoi がしてくれたのでそれを入れて、id:t_kyt が nginx の雑なチューニングをしたのを入れて、ヤケクソで還元率を 4 にして 9,950 イスコイン。
    • あと、カテゴリのオンメモリ化もどこかのタイミングで入っている。
    • いろいろやった結果ここで落ち着いた。「リクエストに失敗しました」の謎は結局解けなかった……。
  • その後 nginx の HTTP/2 を有効にしたり、還元率を 3 に変更したりして最高 10,590 イスコイン の最終 10,070 イスコイン でフィニッシュ。
  • 終了 10 分前くらいに再起動試験をしたら、アプリの起動時に DB アクセスしていたのが失敗するせいでアプリが起動しなくなっていて、あわてて修正した。最後までがんばった。

今思うと、

  • ShipmentService の状態はこの条件以降は変わらないよね~、ということを昼飯のときに id:mechairoi が言っていたのを拾えなかったのは少し惜しかった。こういう気づきを、いかに精度よくテンポよく実装に移せるか、が ISUCON ではいつも大事。
  • ベンチの終了時に外部 API が 502 を返していたのに気づいてなかった。途中で 502 が出てて、ログがバッファリングされているのかな……されてるわけないんだけど、と思ってしまっていたのはヘンだった。。
  • 還元率でユーザの行動傾向変わるのに気づいてもよかったな~。新着をいじってもよかったな~。
  • rsync でバイナリ(と *.sql のみ)デプロイしたのはよかった。自然と motemen がデプロイ担当になっていて、そこで整流できたのもよかったかな。

試合終了、そして

参加された皆さまはすでにお気付きのとおり、パスワードを平文で保存するのはレギュレーション違反である。

終了時にはそのことを知らず、それなりにやることはやった感もあったので、18 時ごろに再起動試験を終え、少し伸びた終了時刻までの間にビールを買ってきてやれやれ、と、同じくオフィスで予選に参加していた他のチームと感想戦をしながら Twitter や Discord を眺めているうちに、違和感が……。あれ? と思ってレギュレーションを再確認したらそれに気づいてしまった。実装した当の自分が最初に気づいていたわけで、なかなかショックがでかい。

チームメンバーに告げ、当然のお通夜ムードになった中、2日目の予選通過チームが発表されると全体の上位20チームの一番下に弊チームの名前が載っていた。どうやってチェックするつもりだったかはわからないけど、運営による追試をどうやらすり抜けてしまっていたらしい。メンバーの間では、報告しよう、ですぐに意見が一致したので 941 さんに DM を送って無事失格となりました。100万円は惜しかったけど、正直に告げることを選択できるチームだったのはよかった。

これでこの話は終わりです。

はてなで一緒に働きませんか?