最近、fireworq管理用のTUIアプリを趣味で作っています。
「縁日」です。fireworkが英語で花火なので、花火に関連する名前をとりました。
(縁日って別に花火の打ち上げはないのかもしかして?)
fireworqははてな社がOSSで公開しているgolang製のジョブキューです。
詳しく知りたいかたはググってアドベントカレンダーで書いた以下の記事を読んで頂ければと思います。
作ってる動機
curlでfireworqのAPIを叩いて管理するのがめんどくさかったためです。
fireworqonsolという標準のWeb UIもあるのですが、
そんなに頻繁にキューの設定を更新するわけではないので、fireworqonsole用のインスタンス(or コンテナ)を常時起動しておくのは無駄で、TUIクライアントを作るのがちょうど良いかなと考えました。
(メトリクス周りはエージェントを使って、別途モニタリングツールに送信して管理したほうが良いでしょうし)
あとfireworqonsoleにはユーザ管理の機能とか無いので、前段にnginx配置してベーシック認証かけるとか必要になってきますしね。。。
サポートしている機能
使い方とかはリポジトリのREADMEを読んでください。
現在サポートしてる機能は以下です。
- キュー一覧の参照
- キュー設定の参照
- キュー毎のルーティング設定の参照
- キューの新規作成
- キューのルーティング設定の新規作成
今後の実装予定
- キュー設定の更新機能
- キュー削除機能
- キューのルーティング設定の削除機能
使った言語、ライブラリなど
言語はGo、TUIライブラリにtviewを使いました。
Goを使ったのはワンバイナリ、かつクロスコンパイル可能であるため。
tviewはGo用のTUIライブラリで一番メジャーっぽかったためです。
また、fireworqのAPIを叩くクライアントライブラリとして、拙作のtsutsuを使っています。
困ったところ
コンポーネント管理のセオリーが分からない
TUIを構成するパーツ(ここでは便宜的にコンポーネントとよぶことにします)の管理方法をどうしたらいいのかわからず、少し困りました。
「コンポーネントAでイベントA'が発生すると、コンポーネントBにxxxをさせる」みたいなインタラクションをどうやって書くべきなのかということですね。
とりあえず、以下のようにしました
type app struct {
root *tview.Application
pages *tview.Pages
queueList *QueueList
queueInfoTable *QueueInfoTable
jobCategoryTable *JobCategoryTable
logWindow *LogWindow
host string
client *tsutsu.Tsutsu
routings []model.Routing
queues []model.Queue
routingMap map[string][]string
logger zerolog.Logger
}
ルートとなる構造体を定義して、そこにぶっこんで管理するスタイルですね。
QueueListとかQueueInfoTableはtviewのコンポーネントをラップした構造体になってます。
type QueueList struct {
root *app
list *tview.List
}
app構造体で管理してるコンポーネント自体にもapp構造体のポインタをもたせてます。
自身のイベントに応じて、他のコンポーネントにインタラクションを発生させたい場合、他のコンポーネントに生えてる関数を直接叩くのではなく、app構造体に生えてる関数を叩く方式をとりました。
で、main関数はこんなん
package main
import (
"gopkg.in/alecthomas/kingpin.v2"
"log"
"sync"
)
var host = kingpin.Flag("host", "fireworq host url").Short('h').Required().String()
func main() {
kingpin.Parse()
application := newApp(*host)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
if err := application.run(); err != nil {
log.Fatal(err)
}
}()
application.root.QueueUpdate(func() {
application.logger.Info().Msg("application start")
})
wg.Wait()
}
application.run()の中で各種コンポーネントの初期化を行って、そのままtview.ApplicationのRun関数を叩いてTUIが起動します。
application.run()をgoroutineの中で叩いているのは、メインのgoroutineで実行してしまうとTUI起動後の起動ログの出力ができないためです。
今のところこれで困ることはないのでこの方式でいくことにしましたが、他のtviewを使用しているアプリケーションのリポジトリを見てみたところ、パッケージ変数としてvarでtviewのコンポーネントを宣言して、どこからでもアクセスできるようなスタイルをとっているものもありました。
それと比較すると、構造体の中にまとめて管理する方式はカプセル可を意識しているというか、オブジェクト指向的だと思うんですが、Goでオブジェクト指向とか意識しないほうがいいっていう言説が結構みられますよね。
どうしたらいいでしょうね。はぁGoなんもわからん。。。
ログ出力ってどうしたらいいんだ?
このアプリケーションはユーザ操作に応じてfireworqのAPIを叩くことになるのでAPIクライアントがerrorを返した場合はなんらかのログを出力したいのですが、普通に標準出力に出しても画面には表示されません。
当然一度errorが返ってきた時点で即panicにするわけにはいかないので、なんらかのログ出力の仕組みを作る必要があります。
ここは手っ取り早くログ出力用のウィンドウをtviewで作ることにしました。(他に標準的な仕組みがあるのかは知らない)
ログ出力用のウィンドウにはtview.TextViewを使いました。
TextViewはio.Writerインターフェイスを実装しているので、以下のようにzerologのNew関数の引数に渡してLoggerのインスタンスを作成することができます。
textView := tview.NewTextView()
logger := zerolog.New(textView)
logger.Info().Msg("hogehoge") //textViewに出力される
このloggerインスタンスを上述のapp構造体にもたせておけば、どのコンポーネントからでtextViewに対してログが出力できるという寸法です。
TUIのユニットテストってどうやって書くの?
書いてません!!
誰か書き方教えてください!!
で、本番で使ってるの?
まだ使ってないです。
aws cloudshellから使いたいのですが、cloudshellは現在VPCのプライベートサブネット内へのアクセスをサポートしていないためです。(近日対応予定らしいですが。。)
最後に
fireworqに興味がでてきた方はアルファアーキテクトっていう会社を受けてみましょう
アルファアーキテクト株式会社