人は忘れる生き物です 〜gasを使ったリマインドソリューション〜


まえがき

当記事は OPENLOGI Advent Calendar 2019 18日目の記事です。

普段はもっぱらROM専でQiitaに記事を投稿することなんてないのですが、せっかくお声を掛けていただいたので、技術を使って社内業務を改善とまではいかなくとも、改善の「己」くらいまではやったったぜアピールしたいと思います。大層なタイトルですが、内容は至って初心者向けです。

皆さんGAS書いてますか?
いやーGASって本当に良いものですね

ちょちょっとまってください!新しい技術の話とかは出てきませんが、もう少しだけ書かせてください…!!

GASの良いところ!
それは環境だなんだって神経をすり減らすこともなく、
jsライクな馴染みのある記法で書くことができ、
トリガー機能でスケジュール実行できるので、
カジュアルに何かしら自動化出来てしまうところ!!だと思ってます。
私のような普段からコーディングをメインでやっているわけではないにわかエンジニアにはとてもありがたいツールです。

スプレッドシートやGoogleカレンダーとの連携が王道ですが、redashやslackと組み合わせればテクニカルな通知を実現することが可能です。もちろんそういった類の仕組みはプロジェクト本体に仕込まれているのがベストかもしれませんが、限定的な運用だったり、カジュアルな要件であればGASを使った小回りが活きてきます。しかもプロジェクトのリポジトリを汚さずとも実現出来てしまうのですから、我々がこれを活用しない手はないわけです。

さて、タイトルから話が逸れた風になりましたが、ここからが本題。

人は忘れます。

あの日のあの時間までにやらなければいけなかったこと、忘れてはいけないとわかっていたはず。
約束や誕生日。入籍記念日、結婚記念日(まとめてくれ)、そして付き合って3ヶ月記念日みたいなやつ…
これを読んでいる貴方も幾度となくそれらを忘れ、ごめんなさいしてきたことでしょう。

それを責めたりはしません、にんげんだですもの

人類はそんな人の性とも言える忘却に立ち向かうため、リマインダーという道具を生み出しました。
画期的なこの新技術に当時は民衆の心は沸き立ったそうです。「リマインダーばんざーい!!!」と。

しかし本当にそれだけで良いのでしょうか?
リマインダーがあるから大丈夫!なんて単純な話であれば、なぜ私達は未だに謝り続けているのでしょうか。

A「あの件、対応していただけました?」
B「あ、いや…対応してないです。忘れていましたすみません。」
A「え、なんで?リマインダーでメンション飛んでるはずですよね?」
B「その、通知は確認したんですが、その瞬間別の対応をしていて、その対応が終わった頃には通知が来たことを忘れてしまいました。。」
A「」

↑日次でとあるデータ更新の運用をしていた時、こんなケースが度々発生していました。
(それこそ自動でやれよって話ですが、これにはのっぴきならない事情があったのです)

属人的な運用にしてなるものかとチームにリマインダー発射 →漏れる
ならばチェック体制を用意して、二人がかりで対応 →漏れる
何をやっても漏れました。自分で書いていてこのチーム大丈夫かと心配になってくるレベルですが、みんな忙しいのです。
責任感なんて言葉で片付けないでください。人は忘れる生き物なのですから。様々なタスクの山に忙殺される日々から脱出するため、技術の力を使う時が来たようです。

そこで我は「データが更新されていることを確認できるまでしつこくメンション飛ばしてくるリマインダー」を開発しようと決意しました。(えっ、ちゃっちwwって思いましたね)

利用するサービスはこの3つ。
・Slack
・GAS
・redash
Slackは弊社がメインで利用しているコミュニケーションツールです。redashも各部署で活用されているツールなので、導入は非常にスムーズでした。以下のスクリプトは全てgasで書いています。

redashからデータの取得

まずお目当てのデータが作られているかどうか、redashから取得します。最短これだけでOK。

main.gs
function yourFunction() {
  var response = UrlFetchApp.fetch('http://redash.{your_domain}.com/api/queries/{query_no}/results.json?api_key={api_key}');
  if (response.query_result.data.rows.length === 0) {
    // 0件だからslackで問い詰める
  }
}

これ、fetchする前ににredashのクエリを実行してやらないとキャッシュされたデータが返ってくるので(request時に実行されるわけではない)、redashのAPIを叩いてやりましょう。たまにrefreshされないことがあったので何かしら策を講じたほうが良いかも知れません。クエリの実行時間を見て古かったらrefresh再実行とかね。

main.gs
function refreshRedash() {
    var options =
        {
          "endpoint" : "query_refresh",
          "headers": {
            "Authorization": {api_key}
          },
          "payload" : {"body":""}
        };
    UrlFetchApp.fetch("https://redash.{your_domain}.com/api/queries/{query_no}/refresh", options);
}

Slackへポスト

後はもう何かしらslackにポストしてやればOKなのでこんな関数を用意。

main.gs
function notificationSlack(webhook, username, icon, message, attachments) {
  var jsonData =
  {
    "username" : username,
    "icon_emoji": icon,
    "text" : message,
    "attachments": attachments
  };
  var payload = JSON.stringify(jsonData);
  var options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };
  UrlFetchApp.fetch(webhook, options);
}

※余談ですが、slack上でリッチな表現をしたい場合は今後 attachments ではなく blocks ってプロパティを使ってほしいみたいですね。

上記を組み合わせると…

main.gs
function yourFunction() {
  var response = UrlFetchApp.fetch('http://redash.{your_domain}.com/api/queries/{query_no}/results.json?api_key={api_key}');
  if (response.query_result.data.rows.length === 0) {
    // 0件だからslackで問い詰める
    notificationSlack(
      'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxx',
      'Bot',
      ':cat:',
      '<@U3XBDH17T> データ更新しましたか?',
      null
    );
  } else {
    // 0件じゃなかったら労う
  }
}

さてこれだけでredashの実行結果を見てslackに通知する仕組みができました。
※エラーハンドリングはお好みでトッピングしてください。

gasのスケジュール実行について(分刻み編)

最後にこれらのスクリプトのスケジュール実行についてご説明します。
Google Apps Scriptの日毎のトリガーで時間をもっと細かく設定する
詳しい話は こちらの記事 を参照していただきたいのですが、要するにgasの日次実行のトリガーは分刻みの設定ができません。x時〜x+1時というアバウトな設定のみ可能です。しかし単発実行のトリガーなら分刻みの設定ができるので、これを利用して「日次のトリガーで実行される トリガーを作るスクリプト 」を用意してやるのです。

main.gs
//トリガー作るやつ
function setTrigger(function_name, execute_time_h, execute_time_m) {
    var triggerDay = new Date(); 
    triggerDay.setHours(execute_time_h);
    triggerDay.setMinutes(execute_time_m);
    ScriptApp.newTrigger(function_name).timeBased().at(triggerDay).create();
}

//トリガー消すやつ
function deleteTrigger(function_name) {
  var triggers = ScriptApp.getProjectTriggers();
  triggers.map(function(trigger){
    if (trigger.getHandlerFunction() == function_name) {
      ScriptApp.deleteTrigger(trigger);
    }
  });
}

//dailyのトリガーで動かす時間指定のトリガーを作るやつ
function Scheduler() {
  if (!isHoliday() && !isWeekend()) { // 平日しか実行しない
    setTrigger('yourFunction','17','30');
    setTrigger('yourFunction','18','00');
    setTrigger('yourFunction','18','30');
    setTrigger('yourFunction','19','00');
    setTrigger('yourFunction','19','30');
    setTrigger('yourFunction','20','00');
    setTrigger('yourFunction','23','00');
  }
}

そして最終的に、データが確認できたらそれ以降のアラートのトリガーを削除するという処理を加えます

main.gs
function yourFunction() {
  var response = UrlFetchApp.fetch('http://redash.{your_domain}.com/api/queries/{query_no}/results.json?api_key={api_key}');
  if (response.query_result.data.rows.length === 0) {
    // 0件だからslackでメンション攻撃する
    notificationSlack(
      'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/xxxxxxxxxxxxxxxxxxxxxxxx',
      'Bot',
      ':cat:',
      '<@U3XBDH17T> データ更新しましたか?',
      null
    );
  } else {
    // 0件じゃなかったら労う
    deleteTrigger('yourFunction');
  }
}

これでお目当てのデータが作られるまでしつこくメンションしてくるアラートの完成です。
トリガーを作る関数(Scheduler)を一日一回朝とかに動かしてやってください。

ここから設定できます。

あとがき

このgasによるアラートが作られてから、その運用が忘れられることはありませんでした。
担当者によってよほどメンションがウザかったのでしょう。
まあこれが用意されてから数日で運用そのものが終わったってオチもありますが。

オープンロジでは、このSlackとGASとredashを使ったソリューションを、
頭文字を取って、SGR(スグル)と呼んでいます。

それは嘘ですが、この記事がプログラム初心者の皆さんの何かしらの手助けになれば幸いです。
以上、お目汚し失礼致しました。

※redashのアラート機能で事足りね?っていうご意見は真摯に受け止めます。