stefafafan の fa は3つです

"すてにゃん" こと id:stefafafan のブログです

MackerelとGoogle Apps Scriptでオフィスの環境を監視

こんにちは!現在はてなのMackerelチームにてアプリケーションエンジニアをしています、 id:stefafafan です。今回はMackerel Advent Calendar 2016 10日目の記事として、オフィスの環境改善でMackerelを利用したことについて書きたいと思います。

qiita.com

前日は id:ariarijp さんの「mackerel-client-goを使ったBotを作る」でした。
ariarijp.hatenablog.com


もくじ

オフィスの環境を改善したい

皆さん仕事中「ちょっとだるいな」「ねむい」「頭が痛くなってきた」などと思ったことありませんか。こういうときどうすれば良いのでしょう。閉め切ってるなら窓を開けて換気するなり、昼休みに散歩に行くと改善されるかもしれませんね。ただ、実は自分が体調悪いだけで、オフィスが閉め切ってるせいではないのかもしれないという不安があると思います。そういうときにオフィスの環境を数値化できると便利です。

netatmoウェザーステーション

今回使用したのはこちらです。
www.netatmo.com

これをオフィスに設置すると、気温、湿度、気圧、騒音、CO2濃度まで取得することができて、その上APIまでもが用意されているので簡単に値が測定できて、この記事のようにMackerelに投稿することも可能です。設置は簡単で、公式サイトや公式のモバイルアプリにて遠隔でオフィスの状態が確認できて便利です。

実装

今回やりたいことは

  • オフィスの環境が悪化したらSlackで気づけるようにしたい
  • 換気してオフィスの環境がマシになったときもSlackで気づけると嬉しい

なので、netatmoのAPIを叩いて値を取得し、それをMackerelに投稿した上で、通知の設定自体はMackerel側で設定すればいけそうです。Mackerelのアラート通知の通知先はSlack以外にもいろいろあるので、例えばCO2濃度が高すぎたらLINEで連絡をもらうとかTwilioなどを利用すれば電話をもらうことも可能です。

mackerel.io

データの取得

こういう記事を参考にしました。
qiita.com

どうやらnpmのパッケージがあるのでそれを利用すればわりと簡単に値は取れました。以下のサイトにあるExampleを実行すると値がうまい具合に取れました。
www.npmjs.com

getMeasure関数で欲しい値がいろいろ出力されて便利。

Mackerel, Slackとの連携

さて値は取れました(気温25℃、CO2濃度700ppmなどなど)。これをMackerelでグラフとしてみせるにはどうすれば良いでしょうか。

Mackerelにはサービスメトリックというものがあります。サービスメトリックとはホストに紐付かないメトリックのことで、数値であれば基本的にグラフにできます。
mackerel.io

今回やりたいことはnetatmoから取得した値をサービスメトリックとして投稿なのでMackerelでそのAPIを叩けば良いです。
mackerel.io

なお、Mackerelを利用したことがなければ、ログインした際にServiceの画面で新しいサービスを作り、「サービスメトリック」のタブにいくと以下のようにcurlでサービスメトリックを送る例が表示されるのでそちらを参考にするのも良いです。
f:id:stefafafan:20170621105254p:plain

メトリック名を決めて、値を取得した時刻(エポック秒)と値と一緒にPOSTするとグラフに描画されます。なお、403などが返された場合はAPIキーに書き込み権限がない可能性があるのでそちらを確認ください。

また、複数のメトリックを同じprefixでまとめれば一緒のグラフに描画されます(例: office.temperature と office.humidity というメトリックを投稿すると、 office.* というグラフに一緒に描画されます)。

POSTするメトリックは配列であるので、一度に複数のメトリックを投稿することができます。なので今回の場合も1回のPOSTで気温や湿度やCO2濃度を送ることができます。

Google Apps Script

メトリックの取得とMackerelのPOSTができれば大体完成です。あとはこれが一生動き続ければ良いです(ちなみにnetatmoの値は10分置きに更新されるので取得も最短でも10分置きとするのが良さそうです)。

nodeだとforeverなどを使うと一生動くようにできるみたいです。

github.com

ただ今回自分はGoogle Apps Scriptでも「N分置きに実行」というのができるじゃないか!ということでGASでやることにしました。

GASでやる際の注意点

上記ではnpmのnetatmoパッケージを利用していましたが、GASでいきなり require('netatmo'); なんてできるわけではないので、アクセストークンの取得などを自前でやることになりました。
詳しくは以下のサイトあたりに書いてありますが、初回はアクセストークンとリフレッシュトークンを取得し、それ以降アクセストークンの期限が切れたらリフレッシュトークンで更新するというようなことをすればOKでした。
https://dev.netatmo.com/dev/resources/technical/guides/authentication

GASの便利概念
  • Logger.log - console.logみたいなノリで使っています
  • UrlFetchApp - GETとかPOSTをするときに使っています
  • CacheService - 値を一時的に保存するのに利用してみました、今回の場合10分置きにmain関数が呼ばれてその度にアクセストークンを取得されては困るのでリフレッシュトークンを保存するようにしました

以下のような感じでアクセストークンを更新したりしました。

// NetatmoのAPIを呼ぶためにつかうアクセストークンの取得
// キャッシュを利用して、expireしてなければ使う、そうでなければtokenをrefreshする
// firstTimeAccessTokenRetrievalは基本的に最初しかよばれない
// ref. https://dev.netatmo.com/dev/resources/technical/guides/developerguidelines
// 'Your app will be banned if we detect you are using more password grant type authorization calls than refresh token grant type methods.'
function retrieveAccessToken() {
  var access_token = cache.get('access_token');
  var refresh_token = cache.get('refresh_token');
  if (!refresh_token) {
    firstTimeAccessTokenRetrieval();
    access_token = cache.get('access_token');
  } else if (!access_token) {
    refreshAccessToken(refresh_token);
    access_token = cache.get('access_token');
  }
  return access_token;
}

// refresh_tokenもないときに呼ばれる、1度しか呼ばれないはず
function firstTimeAccessTokenRetrieval() {
  var form_data = {
    'grant_type': 'password',
    'client_id': NETATMO_CLIENT_ID,
    'client_secret': NETATMO_CLIENT_SECRET,
    'username': NETATMO_USERNAME,
    'password': NETATMO_PASSWORD,
    'scope': NETATMO_SCOPE
  };
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded;charset=UTF-8',
    'payload': form_data
  };
  storeResponseToCache(options)
}

// refresh_tokenを利用してaccess_tokenを更新
function refreshAccessToken(refresh_token) {
  var form_data = {
    'grant_type': 'refresh_token',
    'refresh_token': refresh_token,
    'client_id': NETATMO_CLIENT_ID,
    'client_secret': NETATMO_CLIENT_SECRET
  };
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded;charset=UTF-8',
    'payload': form_data
  };
  storeResponseToCache(options);
}

// トークン情報をキャッシュ
function storeResponseToCache(options) {
  var response = UrlFetchApp.fetch('https://api.netatmo.com/oauth2/token', options).getContentText('UTF-8');
  var data = JSON.parse(response);
  var access_token = data['access_token'];
  var refresh_token = data['refresh_token'];
  var expires_in = data['expires_in'];
  cache.put('access_token', access_token, expires_in);
  cache.put('refresh_token', refresh_token, 21600); // refresh_token は使いまわせるのでとりあえず長い時間キャッシュいれとく
}

コード

トークンさえあればNetatmoからの値の取得とMackerelでグラフ化するコードはシンプルに書けました。

測定値の取得

// ref. https://dev.netatmo.com/dev/resources/technical/reference/common/getmeasure
function retrieveMeasureData(access_token) {
  var form_data = {
    'access_token': access_token,
    'device_id': NETATMO_DEVICE_ID,
    'scale': 'max',
    'date_end': 'last',
    'type': 'Temperature,Humidity,CO2,Pressure,Noise'
  };
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded;charset=UTF-8',
    'payload': form_data
  };
  var response = UrlFetchApp.fetch('https://api.netatmo.com/api/getmeasure', options).getContentText('UTF-8');
  var data = JSON.parse(response)['body'][0]['value'][0];
  
  return data;
}

Mackerelへ投稿

function postMeasureDataToMackerel(data) {
  var epoch = Date.now() / 1000; // epoch秒
  
  var payload = [
    { 'name': 'temperature.temperature', 'time': epoch, 'value': data[0]},
    { 'name': 'temperature.humidity', 'time': epoch, 'value': data[1]},
    { 'name': 'air.co2', 'time': epoch, 'value': data[2]},
    { 'name': 'pressure.pressure', 'time': epoch, 'value': data[3]},
    { 'name': 'sound.noise', 'time': epoch, 'value': data[4]},
  ];
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'headers': { 'X-Api-Key': MACKEREL_API_KEY },
    'payload': JSON.stringify(payload)
  }
  var url = 'https://mackerel.io/api/v0/services/' + MACKEREL_SERVICE_NAME + '/tsdb';
  var response = UrlFetchApp.fetch(url, options);
}

Mackerelの設定

上記のスクリプトが問題なく実行できると、指定したorgのサービスにグラフが投稿されているはずです。ここから先どうすれば良いでしょうか。
f:id:stefafafan:20161205164709p:plain

監視設定

グラフに描画されている値をみてるだけでも楽しいですが、ずっと画面みてるわけにはいきません。Mackerelで監視設定を指定してあげると、異常事態が起きた際知らせてくれます。

まずはMonitorsのページに行くとこういう画面になります。
f:id:stefafafan:20161205165250p:plain

ここから「監視ルールの追加」を選び、「サービスメトリック監視」のタブにて以下のように値を指定します。
f:id:stefafafan:20161205165340p:plain

サービスメトリックの場合は以下の値を指定します:

  • 監視対象のメトリック - CO2だったり気温だったりはここから選択できます
  • アラートの発生条件 - Warning/Criticalの閾値を設定できます。「以上」「以下」など設定できますし、閾値を超えた瞬間アラートではなくてN回分の平均を指定できたりします
  • 監視ルール名 - こちらは識別しやすいように好きなものに設定してください
  • 通知の再送間隔 - こちらはアラートがあがってる状態のまま何分か経過した際再度通知をするかどうかの設定です。

画像ではCO2濃度が1000ppm超えた場合Warning、2000ppm超えた場合Critical。そしてその状態が30分続くたびに通知がくるようにしました。

チャンネル設定

監視設定ができたところで、次に通知先のチャンネルを設定していきましょう。まず通知チャンネルの設定ページに飛びます。

現在これだけのサービスに対応していますが、今回はSlack用の通知チャンネルを作成します。
f:id:stefafafan:20161205172138p:plain

Slackのチャンネルの設定に関してはドキュメントに詳しく書いてあります。
mackerel.io

なお、「通知チャンネル名」とは、Mackerel内で呼ぶ名前なので好きなものに指定してください。Slackの「チャンネル」とは別物です。

Slackの通知チャンネルを作成し終えたところで、チャンネル一覧画面に戻ると、テスト通知ができます。これを押すと、指定したSlackにテスト通知がいきますので、いつでも確認ができます。
f:id:stefafafan:20161205172616p:plain
f:id:stefafafan:20161205182400p:plain

通知グループ

Mackerelには通知チャンネルの他に通知グループというものがあります。今のままでも別に問題はないですが、例えば「このメンバーにはこのアラートを飛ばしたい」みたいな用途のとき便利です。様々な通知チャンネルをまとめたようなものです。詳しくはドキュメントを参照ください。

mackerel.io

アラート

上記の設定がうまくいけば、例えばCO2濃度が高くなりすぎた際アラートがSlackに飛んできます。
f:id:stefafafan:20161205173603p:plain

Slackから直接アラートの画面にいき、どういう具合にCO2濃度があがったのかを確認できますし、それをみてあわてて窓をあけたり、なんらかの間違いである場合は手動で閉じることができます。
f:id:stefafafan:20161205173856p:plain

「しばらくアラートは飛んでほしくないな」という場合は「監視設定」の画面に戻り、監視設定ごと無効にするか監視設定ごとにミュートをすることができます。
f:id:stefafafan:20161205173424p:plain
f:id:stefafafan:20161205173432p:plain

グラフの状態をメンバーと共有

アラートが飛んでいないときでもSlackなどにグラフの現在の状況を共有したい場合があると思います。そういうときはグラフの右上のカメラアイコンを押すと通知チャンネルを選択するモーダルが表示されるので、そこでチャンネルを選択して共有できます。
f:id:stefafafan:20161205182315p:plain

詳しくはドキュメントをご覧ください。
mackerel.io

式による監視 (実験的機能)

ここからはおまけみたいなものですが、実験的機能である「式による監視」についてです。

実験的機能はユーザの設定によりオン・オフできます。
mackerel.io

さて、式による監視とはなんなのかというと、メトリックにたいして、様々な関数を使って計算をし、その結果を監視するというものです。今回は「気温」「湿度」がわかっているので、それらを利用して「不快指数」のグラフを式を使って作ってみました。

式による監視や利用できる関数については以下を参照ください。
mackerel.io
mackerel.io

不快指数

不快指数の求め方をググります。Wikipediaにのっていました。
不快指数 - Wikipedia

サービスメトリックに関してはservice()関数を使うと取得できます。
定数倍したい場合はscale()関数。
名前をつけたい場合はalias()関数。
…などなど カスタマイズしたグラフを表示する - Mackerel ヘルプ を参考に書いていくと以下のように不快指数の式が完成しました。

alias(
  offset(
    sum(
      group(
        scale(
          service('aoyama-office', 'temperature.temperature'), 
          0.81
        ),
        product(
          group(
            scale(
              service('aoyama-office', 'temperature.humidity'), 
              0.01
              ),
            offset(
              scale(
                service('aoyama-office', 'temperature.temperature'), 
                0.99
              ), 
              -14.3
            )
          )
        )
      )
    ),
    46.3
  ), 
  '不快指数'
)

こちらを「式による監視」の監視設定のエディタに入力すると、プレビューにて描画されるので確認できてべんりです。
f:id:stefafafan:20161205175908p:plain

今回の用途だと不快指数自体をGASで計算してメトリックとして送ればよかったですが、もう少し現実的な用途だと例えばtimeLeftForecast()関数を利用すれば線形回帰によって異常を前もって予測できたりします。

最後に

今回は主にMackerelのサービスメトリックを中心に紹介しました。

Mackerelはサーバ監視がメインですので今回の用途は少し違いますが、Mackerelがどういった機能を持っているのか実感いただけたら幸いです。

またMackerel(もしくはGASやNetatmoでも)の使い方で困ったことなどあれば気軽に相談ください〜!

明日はid:arata3da4さんの「Github管理しているmackerelの監視ルールが変更されたらJenkinsからPRを投げる」です。便利そうですね!
arata.hatenadiary.com