その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

CasperJSを使ってGrafanaのグラフのスクリーンキャプチャを撮る

最近はサーバのメトリクス収集にPrometheus,その可視化の為のフロントエンドとしてGrafanaをハードに使っている.Grafanaは予想よりもはるかに格好良いグラフが生成されるのでやる気が出て良い.デモを触るとなんとなく温度感がわかると思う.
そうしているうちに,これは便利なのでサーバのメトリクス以外の情報も入れてみましょうという発想があり,サービスのKPIに関するような情報も入れ始めている.
とまあここまでは良いのだけれど,そうやって収集したデータも,格好良く可視化したグラフも,人に見られなければ一切の意味が無いのでそうした無意味を避けるためにデイリーでグラフを社内チャットに放流することにした.


という事になるとGrafanaで描画したグラフを画像としてエクスポートする必要が出てくる.
Grafanaにはグラフを画像としてシェアする機能があるのだけれど,これはブラウザをポチポチする必要があるみたいで,APIもぱっと見つからなかった (もしあったら教えて下さい) ので利用を見送った.あとどうやらこの画像データはSQLiteに入るみたいでホイホイ使って良いのかどうかあまり判断が付かなかった (間違ってたら教えて下さい).
というわけで今回はCasperJSを使ってGrafanaのグラフをキャプチャすることにした (結局CasperJS使うんだったらそれでGrafanaの画像シェア機能をポチポチすれば良かったんではないか,という意見もあることだろう.確かに!).
CasperJSはご存知の通りPhantomJS (かSlimerJS) を使ってスクレイピングやテストの実行をするのを助ける便利ライブラリ.CSSセレクタで指定すると,その範囲だけスクリーンキャプチャを撮ってくれるという機能があって大変便利なので使うことにした.

以下の様な感じでキャプチャが撮れる.

var casper = require('casper').create();

// login処理
casper.start('https://your-grafana.example.com/dashboard/db/your-dashboard?from=now-24h', function () {
    this.fill('div.login-inner-box > form', {
        username: 'Grafana user name',
        password: 'Grafana password',
    }, true);
});
casper.then(function () {
    this.click('div.gf-form-button-row > button');
});

// キャプチャ撮る
var waitMillis = 2000;
casper.viewport(1536, 1536).wait(waitMillis, function () {
    this.captureSelector('capture.png', 'div.grafana-row');
});

casper.run();

ログイン処理の部分は最初にfillしてからログインボタンをclickするように書いてある.シンプルなフォームだとfillの部分だけで認証が通るのだけれど,Grafanaはリッチな感じ (buttonのng-clickで処理が走るっぽい) なのでマニュアルでclickする処理を入れている.
認証が通ったらキャプチャを撮る処理を走らせる.ここで即座にキャプチャを撮ってしまうとグラフが未描画の場合があるので,気持ち待ってからキャプチャを撮る処理を開始している (例だと2秒).


で,これらをやる上で考えるべき点としては

  • グラフの時間スケール
  • viewportのサイズ
  • 対象となるグラフのCSSセレクタ

というのが挙げられる.


グラフの時間スケールと言うのは,どの時間スケール (直近5分とか直近24時間とか) のグラフを取得するかという点.Grafanaは時間スケールの情報はダッシュボード自身と紐付いていて,自分あるいは自分以外のユーザが時間スケールを変更すると全てのユーザに影響する.
そうした状態で時間スケールを特に指定せずにグラフを取得してキャプチャを撮ってしまうとめちゃくちゃになってしまうのでちゃんと指定してやる必要がある,という感じ.
例で言う所の https://your-grafana.example.com/dashboard/db/your-dashboard?from=now-24h?from=now-24h の部分がそれに当たる.


viewpointのサイズというのはCasperJSの話.要は仮想的なブラウザの大きさをどうするかみたいなもので,デフォルトの状態のままだとかなり小さめに設定されている.従ってほぼ確実に調整が必要になる部分だと思う.
Grafanaのグラフの大きさはブラウザの画面サイズに応じて可変なので,良い感じの数値を探っていくという泥臭い作業が要求されるのだけれど,まあ適当に妥協しましょう.


対象となるグラフのCSSセレクタというのは若干厄介.
今回は div.grafana-row というのを指定しているのだけれど,これはGrafanaのダッシュボードのrowそのものを表しているので,そのrowに属するグラフ等のパネルを全てキャプチャできる.
div.panel-container というのを指定すると個別のパネルをキャプチャできるので,用途に応じて使い分けると良い……という感じなのだがそう簡単には済まない.
なぜかというと,例えばrowが2つ有ると div.grafana-row というCSSセレクタも2つhtmlに表れてしまう為,先に出た方が優先されてしまう.つまりどちらか片方しかキャプチャができない.id要素などが付いているわけではない,本当に同一のCSSセレクタが出てきてしまうので面倒くさい……という感じ.もちろんパネルが複数個あればその個数分だけ div.panel-container が出てくるので問題は変わらない.
まあここら辺をなんとかする方法としては

  • 気合
  • キャプチャ用のダッシュボードを新たに作る

などといった解決方法があるので頑張れば問題がない.頑張りましょう.



こんな感じでGrafanaのグラフをキャプチャできるようになった.それを社内Gyazoに投げつけた上で社内チャットに毎日放流するという事が自動行えるようになって便利!
なお自動化はcronでやってます.

そして物語は何度目かのアプリ内通知再実装を迎える

というタイトルでKyoto.なんか #2で発表してきました.


そして物語は何度目かのアプリ内通知再実装を迎える / Reimplement in app notification // Speaker Deck

スライドの内容としては,アプリ内通知 (Twitter appで言うところの「通知」タブにあたる部分) のサーバサイドを実装する際にどういう問題があって,それをどういう風に実装したかという葛藤の記録となっています.

Webアプリケーションやスマートフォンアプリケーションを書いていると,そこそこの確率でアプリ内通知を書くことになると思うんですが,ところがどっこい「実際にどういう風に実装しているか」みたいな知見が共有されている感じがあまりありません.みんな実装しているはずなのに,ググってもあまり情報が出てこなくて寂しい.地味な機能だから?
という思いがあり,そこら辺アプリ内通知周辺の技術交流・意見交換などしたいなあというモチベーションでこの度発表してみた次第です.賽は投げられた.


ところで,この発表の最後で言おうと思っていたのに言っていない事があったのでこの場に書きますが,実際は「アプリ内通知」というか「通知タブ」のようなものはアプリケーションが個別に自作すべきものではないのでは? と思っています.
iOSAndroidには受け取ったpush通知を貯めて表示する通知センター的な領域が用意されているので,そうしたOSが提供してくれている機能に乗るようにアプリケーションを作成すればこんなに悩まなくても済みます (例えば今回の場合だと,push通知を送るだけ送るようにしておいて,クライアントが通知を貯めておくようにしておけばサーバサイドで通知情報を保持する必要がなくなる).


通知に限らず,他にもそうしたコンポーネントと言うのは様々あると思っていて,OSが提供してくれている機能で要件を満足できるのであればそれを活用するのが良いんじゃないかな,と思っています.
とはいえアプリケーション・サービスの特性によってはそうも言っていられないので,今回のように自作することになります.ケースバイケースと言えるでしょう.OSの通知センター使いたかった……




Kyoto.なんかは色々な技術的な話を聞けてとても有益でした.
特にid:r7kamuraさんのamakanのシリーズ判定の話がすこぶる面白かった.

r7kamura.hatenablog.com

主催のid:hakobe932さん,id:hitode909さんありがとうございました.また参加します.

Guice で独自の scope 切って inject したいって時

これを見ると良い.要は CDI のようなものをやるという話である.
CustomScopes · google/guice Wiki · GitHub


ここに書かれているように,

1. 任意の Scope を表す annotation を作る
2. Scope を実装する
3. Scope の実装と annotation を紐付ける (これは Module 内で行なう)
4. Scope#enter()Scope#exit() で Scope のライフサイクルを管理する

という流れに従う.


ここで少し難しく感じるのは2の「Scope を実装する」だと思う.
Wiki に書いてあるように基本的に Wiki に書いてある実装をコピペして使えという感じなのだが,まあ中身は読めばそこまで難しいことはしていない.
enter() する時に Scope が保持する objectを管理する Map を作成し,exit() する時にその Map を破棄するという感じ.あとは seed() で scope が持つ Map に直で object を登録したり,scope() で scope が保持している object Map から対応する object を取ってくるという処理を書いたら良い.
基本的に自分で Scope の実装を書く時はデフォルトの実装をそのまま使い,どこかで独自の処理をしたくなったらその部分を書き換えるという方法で良いと思う.元は thread safe な実装になっているし「大抵は」満足できる.


んでもって,あとは4のライフサイクル管理が若干面倒,一方で直感的ではあるかなあとは思う.
しかし,Module 内で injectee が bind されている時の挙動が個人的にわかりにくかったので書いておく.

例えば Module が以下のようになっていて,

final SimpleScope batchScope = new SimpleScope();
bindScope(BatchScoped.class, batchScope);
bind(SimpleScope.class)
    .annotatedWith(Names.named("batchScope"))
    .toInstance(batchScope);

bind(SomeObject.class)
    .toProvider(SomeObjectProvider.class)
    .in(BatchScoped.class);

Scope を管理する部分が以下のようになっている時,

final SimpleScope scope = injector.getInstance(Key.get(SimpleScope.class, Names.named("batchScope")));
try {
    scope.enter();

    injector.injectMembers(instance);

    // do something
} finally {
    scope.exit();
}

scope 内で injectMembers() しないと SomeObject は inject されない.
scope 内で injectMembers() された時は Module で指定されたように,SomeObject のライフサイクルは scope 内に固定される.ここは直感的.
なお Wiki の注意書きにも書かれているが,finally でちゃんと Scope#exit() しないと scope が閉じられず,ずっと生きながらえてしまうという問題があるので注意が必要.try-catch-resources が使えれば良いような気はしているが……


と,ここまで散々書いたのだけれど,Wiki の冒頭にあるように Custom Scope は自前で積極的に書くべきものではない.
基本的に,Singleton ServletModule が提供する RequestScopedSessionScoped で事足りるはずで,それで足りないケースというのはあまり存在しない.足りないケースの時だけ書くべきであるという事を留意されたし (今回はやんごとなき事情で自前で書きました).

tinyorm 1.11.0.rc1 is out

tinyorm 1.11.0.rc1 が出ました.以下の様な変更が入っています.

Connection を2つ持てるようになった

「Read/Write できる connection」と「Read だけができる connection」の2つの connection を持てるようになりました.tinyorm は内部でクエリのタイプ (read or write) に応じてこれらの connection を使い分けるようになっています.
これにより,write 系のクエリは master へ向いた「Read/Write できる connection」を使いつつ,read 系のクエリは slave へ向いた「Read だけができる connection」を利用するというような事が可能になりました.

今までは「Write 用の tinyorm」と「Read 用の tinyorm」をそれぞれ用意して利用するという感じで運用していたのですが,「Read 用の tinyorm」でひいてきた Row object に対して update をうっかり発行すると例外が上がってしまうという使い勝手の悪さがあり,今回このような改善を入れたという次第になります.

もちろん従来通り,単一の connection を利用することも出来ます.この場合はその1つの connection を全てのクエリで利用します.

Lazily な connection borrowing に対応した

Lazily な connection borrowing,つまり実際に connection が必要になったタイミングで connection を取ってくる (都度 establish するなり,connection pool から取ってくるなり) よう指定することが可能となりました.

方法としては,tinyorm を instantiate する際に javax.inject.Provider<Connection> を渡してやることで,connection が必要になったときに Provider<>#get() を利用して connection を borrow してくるようになります.なお,一度 borrow してからはその tinyorm instance 内では borrow した connection を使いまわします.

今後

内部的な変更が大きかったので,ひとまず rc 版ということで maven central にリリースしてあります.既存のテストケースは全て通っていますが,機能のすべてを保証できるとは言い切れないというステータスです.
安定して利用出来ることが検証でき次第,正式版としてリリースする予定です (rc が外れるタイミングでドキュメントが書かれる予定です).

以上です.よろしくお願いします.

2016-08-15 19:44 追記

rc1 は全く話にならず,現在は rc4 がお目見えしています.これはちゃんと動いてるのでもし試されるのであればこちらが良いです.

thin-cacheというJava向けのキャッシュライブラリ書いた

thin-cacheというキャッシュライブラリ書いた.いつものようにmaven centralにも置いてある.そしていつものようにJava 8以降が対象となっている.

maven central

Microservicesみたいな感じでサービスを作っていると,他のサーバにrequestを飛ばして得られた値を加工して,あるいはそのまま利用するみたいなコードを書く事が多くなってくる.毎回毎回他サーバにrequestを飛ばして良いのであればまあ飛ばせば良いのだけれど,そうもいかない場合も多い (トラフィックなり応答速度なりの理由で).そういう場合に対策として考えうる手段のひとつとしてキャッシュが挙げられると思う.
今まではそういったキャッシュを良い感じでコントロールするコードを都度都度書いていたのだけれど,それにもそろそろ飽きてきたのでいい感じに抽出してライブラリにした.

Javaでキャッシュ回りを司るライブラリは色々あると認識していて,例えばguava のcacheなどが挙げられる.最初からguavaが入っているのであれば良いんだけどそうではない場合にキャッシュの用途のみでguavaを入れるのもオーバーキルな感じを抱いており,シンプルなキャッシュ機能だけを提供するライブラリが欲しくなったのでthin-cacheを書いた次第.
シン・ゴジラが流行っているからこういう名前にしたわけではなく,薄くてシンプルなライブラリということでこういう名前にした.

機能

シンプルな使い方としては以下の通り.

final AutoRefreshCache<Long> autoRefreshCache = new AutoRefreshCache<>(10, false, new Supplier<Long>() {
    private long i = 0;

    @Override
    public Long get() {
        return ++i;
    }
});

autoRefreshCache.get(); // => 1L (initialize cache)
autoRefreshCache.get(); // => 1L (hit cache)

// 10 seconds spent...

autoRefreshCache.get(); // => 2L (expired, refresh cache)
autoRefreshCache.get(); // => 2L (hit cache)

第三引数として渡したSupplier<T>の実装により得られた値を,第一引数で指定した秒数分だけ保持するという,見ての通りの感じ (第二引数については後述).
キャッシュが効いている間にAutoRefreshCache#get()が呼ばれるとキャッシュ内容がそのまま返却され,キャッシュが期限切れになってから (つまり保持期限が過ぎてから) AutoRefreshCache#get()が呼ばれると supplier が実行されてその値を結果として返すと同時に,キャッシュに supplier の結果を格納しつつ保持期限の更新をしている.シンプル.

キャッシュを操作するメソッドとしては以下の4種類がある.

  • get()
  • forceGet()
  • getWithRefreshAheadAsync()
  • forceGetWithRefreshAheadAsync()
get()

Read throughにキャッシュを取得してくるメソッド.
キャッシュが存在しない (初期化されていないとか,expireしたとか) 時に,キャッシュコンテンツを生成しそれをキャッシュすると同時に返却する.

forceGet()

Write throughにキャッシュを取得してくるメソッド.
キャッシュが存在しているか否かに関わらず,常にキャッシュコンテンツを生成しつつそれをキャッシュすると同時に返却する.

getWithRefreshAheadAsync()

Refresh aheadにキャッシュを取得してくるメソッド.
キャッシュが存在しない場合,即座に古い (一世代前の) キャッシュコンテンツを取得・返却しつつ,そのバックグラウンドでキャッシュコンテンツの生成及びストアを非同期に行なう.戻り値としてはキャッシュオブジェクトとFutureのタプルを返しており,キャッシュコンテンツの生成とストアが完了するとそのFutureは完了状態になる.

なおキャッシュが初期化されていない場合はget()と同じ動き,つまり同期的に動作する.

forceGetWithRefreshAheadAsync()

Refresh aheadにキャッシュを取得してくるメソッド.
キャッシュが存在していようがいまいが,即座に古い (一世代前の) キャッシュコンテンツを取得・返却しつつ,そのバックグラウンドでキャッシュコンテンツの生成及びストアを非同期に行なう.以下略.

なおキャッシュが初期化されていない場合はforceGet()と同じ動き,つまり同期的に動作する.

工夫した点

工夫した点 (というか欲しかった機能) として,キャッシュ生成時に例外が発生した場合の動作を指定出来るようにしている.「例外が発生した時にそのまま例外を上流に放る」か,「例外内容をエラーログに出しながら (slf4jを使っている) キャッシュ済みの古いオブジェクトを返す」かを制御できるようになっている.例えば,他のサーバから値を取ってこようとする際に相手が500を返してくるケースとかは事実あると思っていて,そういう状況でこちらまで巻き込まれていては良くない.そういう場合でもサービスを継続できるようにすべく,このような状況下での動作をコントロールしたかったのでこういう機能を付けた.

所感

こういう小粒なライブラリでも,非同期性とかスレッドセーフとかを意識しつつコードを書くと少し複雑になることが分かった.毎度毎度こういうコードを書いていては身体や精神が持たないので,ライブラリにすることで再利用できるようにした.最近こういう小さなライブラリばかり書いている気がするのだけれど,どんどん小さなライブラリを作りつつ組み合わせて作っていくぞという精神世界になっている.

ところで,read through とか write through とか refresh ahead とか,そういう概念は学生時代に勉強したりパタヘネ本で勉強したりしていたはずなのにすっかり忘れていて厳しい気持ちになった.一方でこういうライブラリを実装しつつドキュメントを書くことで知識を定着させることが出来て良かったと思う.実感を伴わせることで知識が身につくという事を久々に実感できて体験があった.

Docuss というテストとドキュメント出力を一緒にやるライブラリ書いた

Docuss というものを書いた.Java のライブラリ.なお Java 8 以上じゃないと動かない.

どんなライブラリかというと,

  • HTTP request を対象 URI に送り,それによって得られた response の内容をテストする為のラッパ的な機能を提供する
  • response のテストが通ったら,その request と response の内容 (便宜上これを document としている) を所望の形式・方法で出力する

というライブラリ.要は HTTP request/response をテストしつつその内容をゲロっと吐くような機能を提供している.
つまりは貧者の autodoc と言い換えることが出来る.


詳しい使い方は README とか javadoc とかを見てもらうとして,実装上工夫している点としては以下の通り.

  • 出力フォーマットをコントロールできる
  • 出力方法をコントロールできる
  • 特定の http client 実装に依存していない
出力フォーマットをコントロールできる

出力フォーマットをコントロールできるというのは,response/request をどういうフォーマットで出力するかをコントロール出来るということ.
方法としては request と response の内容を任意のフォーマットに変換する formatter を出力する formatter generator を書いて (DocussFormatterGenerator という Interface を実装する必要がある) それをコンストラクタに渡すことで出力フォーマットを制御できる.

先の autodoc は綺麗な markdown が出て格好良いのだけれど,個人的にそこまでのモチベーションが無かったのでデフォルトで提供している formatter generator は単に内容を YAML で吐き出すようになっている (ファイル追記とかを考えた時に YAML は単純に append できて楽,そして綺麗なドキュメントにしたければ YAML を解釈して任意の形式のドキュメントに落とすツールを書けば十分だと思ったという理由から).
もしも綺麗な markdown で出力したくなったらその時にその為の formatter generator を書けば良い.

出力方式をコントロールできる

出力方法をコントロールできるというのはつまり出力先を制御できるということで,出力フォーマットと同様に DocussPresenter という Interface を実装することでコントロールが出来る.
デフォルトでは標準出力に吐く presenter と,ファイルに追記していくタイプの presenter を提供している.

特定の http client 実装に依存していない

特定の http client 実装に依存していないので,もし特定の http client を利用したい場合は DocussHttpClient という Interface を実装して利用すれば良い.基本的に DocussHttpClient の実装クラスがコアな責務を負っているという感じ.
デフォルトでは Apache httpclient のシンプルな実装を提供している (テスト用とだとそこまで凝った実装は必要ないだろう,という推測と現状から).


できるだけ特定のものに依存したくなくて,以上に挙げたようなものについては制御可能にすることで柔軟性を持たせた (と思っている).

まとめ

テストコードからドキュメント (的なもの) を生成するというのは流れとして自然というか正しいと思っていて,そういったもの,そしてシンプルなものが欲しかったので今回 Java 向けに書いた次第.
実コードを書いているとどうしても息切れをしてしまって,ドキュメントを書くところまで体力を保てないことがある.そうした中でもしっかりドキュメントを書けるように,こうしたサポートのツールは重要なのではないかと思っている.
気が向いたらどうぞお使いください.もうどんどん書いていくという精神になっている.

ここでオリジナルの autodoc author のありがたいお言葉を見てみましょう.

以上です.

YAPC Asia Hachioji 2016 に行ってきた & 喋ってきた

YAPC Asia Hachioji 2016 に行ってきて,ついでに喋ってきました.
諸般の事情で2日目しか行けなかったんですが,濃いトークをたくさん聞けて良かった.

特に,@ さんの M-V-Whatever の話 は非常に参考になって良かった.ああした知識の裏付けがあって,なおかつ体系づけて物事を説明できるというのは最早アートの一種だと思う.僕はそういった知識や能力が欠如していると思っていて,なにかと直感でやりがちなのでものすごく羨ましい.
@ さんの Unlisp の話 もかなり面白かった.言語処理系を作る時のメンタリティとか,実装する上での哲学や程よい妥協感,また実装上の葛藤なんかを生々しく聞けてとても良かった.言語処理系はかねがね書きたいと思っていて,多分それを実行に移す時にこの発表のことを思い出すのだろうな,という予感がある.端的に言ってとにかく格好良い発表だった.
@ さんの開発フローケーススタディもめちゃめちゃに良かった,良かったというか羨ましさを感じた.チームで1つのプロジェクトを遂行するにあたっての最初から最後までを第三者でも分かるように1本のトークになっていて,お話的な面白さを含みつつ,チームビルディングの妙技を感じられて非常に良かった.印象に残っているのは進捗度とか見積もりをしっかり可視化していたということで,ああいった裏付けが取れている状態で「こうしましょう」「ああしましょう」みたいな提案をしつつプロジェクトを進行していけるのは格好いいなあと思った.メンバーの納得感とか,プロジェクトにそぐわなくなってきたら手法を変えるとか,そういう話がされていたのも良かった.
あと,@ さんのCPANの依存モジュールをもう少し正しく検出したいというトークは,perl の静的解析ツールを書いていると出てくるあるあるみたいな感じで「あー,見覚えあるな〜〜」となり,過去の体験が程よくフラッシュバックしてきて面白かった.後で perl の構文は難しいですねえ,という話をできて楽しかったです.


ところで僕のトークは以下の様な感じ.

speakerdeck.com

普段のトークとは打って変わってというか,あまり技術の詳細の話はせずに,普段新しい言語を趣味とかでやるときに自分がやっている事を紹介したというストーリーです.なんか当日のトークでは結構色々言ったのでスライドだけでは伝わらない部分も多いかもしれない.後々動画が公開されるらしいので,なんかわからない所あったら見てみてください.
とにかく言いたかったことの主は「恐怖 = 無知」という部分で,これは僕がプログラミングを始めた時に抱いていた感情そのもので,今でもたまに味わうことがある.これを克服しないことにはどうにもならんのでとにかくやるしかない!!! みたいな話が主題です.
ちなみに,自己紹介の画像は gif アニメだったんだけど,pdf にしたら静止画になってしまった.以下に gif のオリジナルを示します.

f:id:moznion:20160704120517g:plain

おかげさまでこのトーク,ベストスピーカー賞2位を頂きましてとても嬉しかったです.皆様ありがとうございます.


あと LT もしてきたので,その資料も貼ります.

speakerdeck.com

総務省が配ってる,市外局番の doc (word のファイル) を解析して,S 式として扱って tokenize しましょうという内容.結局時間が足りなくてその実装は書けなかったのだけれど,いずれ実装したいと思っている.


あと,久々に会った人と廊下やら懇親会やらで色々喋れて楽しかったです.


YAPC Asia Hachioji,良い意味での手作り感で趣があり,完全に楽しいイベントだった.
主催の @ さん,運営の皆様ありがとうございました.また参加します.