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

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

GuiceとSpringを共存させたい

もともとGuiceを使っているプロジェクトがあってそれをSpringに移植したい,だとか,Guiceをバリバリ使っているコンポーネントをSpringのアプリに組み込むことで資源の再利用をしたい,だとか,そういう事になることがある.あるのです.この前実際になった.

で,"spring guice together" とかそういう適当な検索ワードで調べると大体「そんな事はするな」「そうしたい意味がわからん」「そう考える時点でおかしいのでは」などといった,「とにかくやるな」という結果が引っかかる.確かに.しかしやらなければならない時があるのだ……というわけでそのメモを記す.

TL;DR

やらないで済むならやらないほうが良い.

本編

ご存知の通りGuiceGoogle製のDI Frameworkで,一方のSpring frameworkはWeb Application Frameworkである.この二者間になぜ共存の問題が発生するかというと,Spring Frameworkもその内部にDI Frameworkを持っているから,というかそもそもDI機構がSpringの中核を担っているからである.つまりDI Frameworkが2個ある状態となるなのでそれらを矛盾なく管理する必要が出てくる.どうすれば良いのか.

アプローチ

SpringのDIをメインで利用して,Guiceはサブとして扱うという方法を今回は採用した.簡単に言うと,SpringのDIコンテナに対してGuiceのInjectorをInjectするという手法.
こんな感じ.

@Configuration
public class GuiceModuleConfig {
    @Bean
    Injector injector() {
        return Guice.createInjector(new AppModule());
    }
}

AppModuleGuiceのModuleで,このModuleを元にinjectorを作成し,そのinjectorをSpringのDIにinjectしてSpring DIから取得可能なようにしておく.そんでもって以下のようにAppModuleで提供されているinjecteeをSpringのDIを通じて取得できるようにしてやる.

@Bean
DataSource dataSource(final Injector injector) {
    return injector.getInstance(DataSource.class);
}

@Bean
FluentLogger fluentLogger(final Injector injector) {
    return injector.getInstance(FluentLogger.class);
}

つまりはSpringのDIにinjectしたGuiceのinjectorをSpring DI経由で引っ張ってきて,更にそのinjectorからGuice経由で任意のinstanceを取ってくるという感じ.とりあえずこれで基本的なGuiceSpring FrameworkのDI機構を共存させることが可能となる.なおSpringのDIについてはGuiceがあろうとなかろうといつものように利用できる.

Guiceで言うところのServletScopes.REQUEST使いたいときにどうするのか

@RequestScopeを使う.以下のように.

@Bean
@RequestScope
Connection connection(final Injector injector) {
    return injector.getInstance(Connection.class);
}

しかしGuice経由でinstanceを取ってきている場合はこれだけでは不十分で,GuiceServletContextListenerを継承したClassをServletContextListenerとして登録してやる必要がある.

@WebListener
public class AppGuiceConfig extends GuiceServletContextListener {
    private ServletContext servletContext;

    @Override
    public void contextInitialized(final ServletContextEvent servletContextEvent) {
        servletContext = servletContextEvent.getServletContext();
        super.contextInitialized(servletContextEvent);
    }

    @Override
    protected Injector getInjector() {
        return Guice.createInjector(new AppModule());
    }
}

なおSpring-Boot等でweb.xmlを利用しない場合は上記のように@WebListenerを用いてListenerとして認識させてやる必要がある.

雑感

2つのDI Frameworkを共存させ,管理する事となるので複雑さとコストが増すように感じる.また,適切に双方のinjecteeのライフサイクルのハンドリングも正しく行わなければならないのでその点においても複雑さが増大する.やらなくて済むのであればやらないに越したことは無いと思う.

以上です.我々はやっていっています.

[追記]

https://github.com/spring-projects/spring-guice

これ使えば楽に出来そうな雰囲気があるんだけど,maven centralに上がっていなくて面倒だったので採用を見送ったという経緯があります.未検証.

Springの起動時に好きな画像をAAにして表示する

Spring Boot 1.4.1 1.4.0 (訂正しました.詳細は id:dayflower さんのコメントを参照下さい) の機能として,Springの起動時に任意の画像をAAとして表示できる機能が追加されたようです.
なんかここに書いてた.

www.infoq.com

やり方は非常に簡単で,

  • banner.jpgbanner.pngあるいはbanner.gifという名前の画像ファイルを/src/main/resourcesの直下に置く (あるいはbanner.image.locationで設定した場所に置く)

以上.早速やってみましょう.

f:id:moznion:20161024232806p:plain

普段のSpringの起動後のターミナルといえばこんなのでしたが……

f:id:moznion:20161024232835p:plain

この機能を使うとこの通り! ちなみに元画像は僕のアイコンのこれです.

f:id:moznion:20161024232911p:plain

若干ホラー感がある……と思いきや遠目からAAが中々の再現度であることがわかります.すごい!


当初はこの機能について「なぜウェブアプリケーションフレームワークの機能として入ったんだ……」と疑いの眼差しを向けていたのですが,実際に使ってみると想像以上に気分が上がります.自分のプロジェクトのロゴなんかを入れると良い感じですね!

以上です.


[追記]
画像が大きいとAAも大きくなります.「そんなAA大きくなくて良い!」という時は,application.propertiesのbanner.image.widthもしくはbanner.image.heightを設定することでサイズの調整ができます.便利便利.

ソフトウェアライセンスのURLが軒並みhttpsが使えた

READMEだとかLICENSEファイルだとかに書くソフトウェアライセンスの文言には,そのライセンスのURLが含まれていることが多いと思う.
最近ソフトウェアライセンス文言をめちゃめちゃ書く事があって,その時にふと「ライセンスのURLは軒並みhttpで表記されてるけど実はhttpsでいけるんじゃないのか?」と思ってざっくり調べたら色々なライセンスがhttpsのコンテンツを提供していることがわかった.全部書くと大変なので一部だけれどこんな感じ.


ライセンスの文言って大体コピペとかでパパっと済ませてしまうのでURLが元のままだったりするのだけど,実はhttpsで提供されている事があるので今後はそっちを使っていこうと思った.


[追記]
id:r7kamuraさんに教えてもらったんだけれど,opensource.orgSSLに対応していてHTTPでアクセスするとHTTPSにリダイレクトされる様子.時代はSSL

雑に特定のホストの特定のポートと疎通できてるかどうか確かめる

新しくサーバ立てた時やサーバ追加した時に,そのサーバが他のホストの特定のポートと疎通できるかどうかチェックする必要が出てくる時がある.ACLとかの兼ね合い.
そういうのは本番の環境だと監視のシステム等に組み込まれていたり,あるいはserverspecとかで確認されていることが多いと思うのだけれど,その場で雑に確認したくなることがあると思う.そういう時はtelnetで繋いで,quitして,というのを繰り返していく感じになりがちなのだけれど,対象となるホストの数が多くなってくるとそういうことを手でやるのも大変になってくる……というわけでこれです.

$ (sleep 0.1; echo quit) | telnet $HOST $PORT)

こういう風にしておくとtelnetで繋いだ後にquitを発行するということを自動でやってくれる.
とはいうものの,こうすると正しく繋げているかどうかを目視で確認する必要が出てくる (つまり以下の様な文字列が正しく出力されているかどうかを目視で確かめる必要がある).

Trying ::1...
Connected to localhost.
Escape character is '^]'.

更にこの方法だと Connection closed by foreign host となるために,exit codeが1となってしまう.その為,スクリプト中でexit codeを使って良い感じにするのが難しい.


というような話をしていたら,id:karupaneruraさんから「それncで出来るよ」と教えてもらって以下の様な感じになった.

if [ -z $((sleep 0.1; echo "\nquit") | nc $HOST $PORT) ]; then
    echo "Fail $HOST:$PORT"
fi

見た目としてはtelnetの代わりにncを使って,送り込む文字列を quit から \ncode にしたという様子.
前者はまあその通りという感じ.後者について少し説明すると,ncで正しく繋げている場合に先に改行文字を送り込むと ERROR というレスポンスがncから返ってきた後にquitする.一方で正しく繋げていない場合はncから何もレスポンスが返ってこないという挙動になるので,それを利用して文字列が空か空ではないかで正しく繋げているか否かを判断している (つまり文字列が空でなければ正しく繋げていると判断している).

なお補足すると,ncだとこういう風に動くんだけど,CentOS6のncatコマンドだとquit時に+OKという文字列が出てくるので最初に改行文字を送り込む必要は無かったりする.みたいな感じで微妙にnc (ncat) コマンドの挙動に差があったりするので,そこら辺はまあ臨機応変に対応しましょうという感じ.


これを適当にforの中に組み込んでおくと雑に疎通のチェックが出来て便利,という話でした.ncを使うという発想がなかったので勉強になった.


[追記]

コメントでid:bearminiさんが /dev/tcp を使う方法を教えてくれたんですが,これめちゃめちゃ便利ですね.知らなかった……

tinyorm 1.11.0 is out

tinyormのversion 1.11.0が出ました.

Maven Repository: me.geso » tinyorm » 1.11.0

新しい機能としては

に書いたように

  • Connection を2つ持てるようになった
  • Lazily な connection borrowing に対応した

という2つが挙げられます.
rc1の時点では中々にバギーだったんですが,改善を重ねていった結果,プロダクションでも安定して動作するようになったのでこの度rcを外して正式なバージョンとしてリリースしました *1

今回入った機能についてはConnectionというセクションのドキュメントでも説明しているので併せて参照ください.

*1:その結果rc7までバージョンが育ったという逸話があります

ltsv_exporter書いた

ltsv_exporterというPrometheusのexporterを書いた.

その名の通り,LTSV形式のテキストを読み込んでPrometheusが解釈できる形式に吐き出すexporter.

利用方法としては2つのパターンを想定している.

1つはリモートで配信されているLTSVを読むというスタイル.

$ ltsv_exporter --url https://example.com/sample.ltsv

このようにすると https://example.com/sample.ltsv で配信されているLTSVを読み込んでそれをPrometheus形式で出力するようになる.

もう1つはローカルに設置されているLTSVを読むというスタイル.

$ ltsv_exporter --file /path/to/sample.ltsv

このようにすると /path/to/sample.ltsv に設置されているLTSVを読み込んでPrometheusの形式で出力するようになる.

Exporterの出力としてはこのような感じ.

# HELP ltsv_value LTSV value
# TYPE ltsv_value gauge
ltsv_value{key="duration"} 4.097
ltsv_value{key="size"} 123

key というラベルにLTSVのkeyが入り,値の部分にはそのままLTSVのvalueが入る.なお,LTSVのvalueが数値ではなかった場合は,exporterはその値を出力に含めずに読み飛ばす.
あとはこのexporterのhttp越しの出力をPrometheusで収集すれば良い.


というわけでメトリクスを取りたいものがあったり監視したいものがあった時は,雑にサーバでLTSVとして配信したり,もっと雑にLTSVとしてローカルファイルに吐き出したりするだけでltsv_exporterで読み込んでPrometheus形式で出力できるようになった.
LTSVはシリアライズ(?)する時に特別なシリアライザが不要で,シンプルに文字列組み立てだけで作れるので楽で良いですね.


今後としては,最後の1行だけ読み込んでパーズするモードなんかがあると便利なのかな〜と思っている.

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でやってます.