この記事は、Groove Xアドベントカレンダー2022 12日目の記事です。
はじめに
こんばんは、GROOVE X の junya です。
更新がとても遅くなってしまってゴメンナサイ。
今回は、社内の slack で活用している grafana プレビュー機能についてご紹介します。 slack に grafana のリンクを共有すると、こんな感じで自動でプレビュー画像をつけてくれる仕組みです。
grafana とは?
オープンソースのデータ可視化ツールで、 BigQuery, Prometheus, InfluxDB, Spreadsheet など様々なデータソースに対応しています。 GROOVE X では主に、 Victoria Metrics で管理された機体のメトリクスの可視化に活用しています。
サービスの雰囲気は、公式のデモサイトがあるのでそちらで体験してみて下さい。
slack で grafana をプレビューする仕組み
以下のフローで、 slack に投稿された grafana の URL にプレビュー画像を自動補完しています。
- grafana の URL の投稿を slack の link_shared イベント として検知する
- シェアされた grafana の URL を元に、画像取得用の URL を生成し、画像を取得する
- slack のファイル管理に画像をアップロードする
- slack の unfurl 機能 を用いて、リンクにプレビュー画像を付与する
以下、詳しく説明していきます。
link_shared イベントの検知
slack アプリ作成時に Event Subscriptions の設定で link_shared
イベントを購読すると、アプリでリンク共有のイベントを取得できます。
以下のように、 slack-go を用いて HTTP リクエストを Slack のイベント型に変換して利用します。
package service import ( "io" "log" "net/http" "github.com/slack-go/slack/slackevents" ) func HandleLinkSharedEvent(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) defer r.Body.Close() evt, err := slackevents.ParseEvent(body, slackevents.OptionVerifyToken(&slackevents.TokenComparator{VerificationToken: SlackVerificationToken})) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } switch evt.Type { case slackevents.CallbackEvent: innerEvent := evt.InnerEvent switch ev := innerEvent.Data.(type) { case *slackevents.LinkSharedEvent: // handle link shared event here. return default: log.Printf("unhandled inner event: %v", ev) } default: log.Printf("unhandled event: %v", evt) } }
ダッシュボード画像の取得
grafana には Grafana Image Renderer というプラグインがあり、このプラグインが有効になっていれば、ダッシュボードのプレビュー画像を取得することができます。パスにある /d/
を /render/d-solo/
に変換し、 viewPanel
引数を panelId
引数に変えれば、画像を取得できるので、 Slack アプリの中では共有された URL を以下のように画像URLに変換して、画像を取得しています。
const BASEURL = "https://grafana.example.com" func BuildImageURL(url string) string { if strings.HasPrefix(url, BASEURL+"/d/") { if strings.Contains(url, "viewPanel=") { // grafana can render panel itself with /d-solo/ and "panelId" parameter url = strings.Replace(url, BASEURL+"/d/", BASEURL+"/render/d-solo/", 1) url = strings.Replace(url, "viewPanel=", "panelId=", 1) } else { url = strings.Replace(url, BASEURL+"/d/", BASEURL+"/render/d/", 1) } // add timezone parameter to show metrics with JST url += "&tz=Asia%2FTokyo" return url } return "" }
例えば、公式のデモサイトにある下記のダッシュボードは、
これを以下のような URL に変換すると画像を取得できます。
slack への画像アップロード
http client を用いて grafana から画像を取得したら、 slack の files.upload API を用いて、画像を slack へアップロードします。
slackClient := slack.New(SlackToken) ... // 画像URLを元に画像のバイナリを取得する img := GetImageFromGrafana(imageURL) reader := bytes.NewBuffer(img) remoteFile, err := slackClient.AddRemoteFile(slack.RemoteFileParameters{ ExternalID: "some id", ExternalURL: grafanaURL, Title: "some title", Filetype: "link", PreviewImageReader: reader, })
なお RemoteFile の機能は slack-go に無かったので、PR を出して機能を追加してもらいました。
grafana へのリンクにプレビュー画像を付与する
最後に slack の unfulring の仕組みを呼び出して完成です。
func (s *Service) handleLinkSharedEvent(ev *slackevents.LinkSharedEvent) { ... blocks := make([]slack.Block, 0, 2) if extraContent != "" { blocks = append(blocks, slack.NewSectionBlock(slack.NewTextBlockObject("mrkdwn", extraContent, false, false), nil, nil)) } blocks = append(blocks, slack.NewFileBlock("", remoteFile.ExternalID, "remote")) _, _, _, err = slackClient.UnfurlMessage( ev.Channel, ev.MessageTimeStamp.String(), map[string]slack.Attachment{ link.URL: { Blocks: slack.Blocks{ BlockSet: blocks, }, }, }, ) }
実際の運用では、プレビューに画像だけでなく、メトリクスに関連する付属情報も付与して、見る人にわかりやすくしています。
slack アプリのデプロイ
slack のアプリは cloud function にデプロイしています。bot の管理に serverless の仕組みは便利ですね。
Enterprise 版 Slack でのトラブル
この夏に社内の Slack を Enterprise へ移行したのですが、その際になぜかこの grafana プレビューの仕組みが動かなくなりました。 サポートに確認したところ、 Pro 版と Enterprise 版でファイル管理の仕様が異なり、
- Pro 版: アップロードした画像は、unfurl でそのまま使える
- Enterprise 版: チャネルに一度も投稿したことの無い画像は、スレッド内の unfurl 画像として利用できない
とのことでした。ちょっと腑に落ちない仕様ではあるのですが、ワークアラウンドとして、
- 画像投稿用の専用チャネルに画像を投稿してから、
- unfurl 画像として利用する
という方法で問題を回避しました。
さいごに
仕事って、ちょっとした工夫で楽しくなるし、業務効率も上がるなと思います。
GROOVE Xは、楽しくコードを書ける環境なので、ご興味のある方は是非ご連絡下さい!