Mackerel

Mackerelのカスタムダッシュボードでリソース確認がしやすくなった

リリースされてから少し経ちますが、Mackerelのダッシュボードが新しくなりました。

これまでのダッシュボードはレガシーとして扱われ、新しいものはカスタムダッシュボードとされています。

まだレガシーからカスタムダッシュボードに置き換えて間もないですが、意外と良いかもと思ったので、そのことに関して書きます。


どんなダッシュボードか

見たほうが手っ取り早いので、新旧並べてみます。

古いほう

old_dashboard.png

新しいほう

new_dashboard.png


私のユースケース

一般的に便利!というと語弊があるので、私が求めていたものを書いておきます。

ダッシュボードなので、各組織ごと見たいものは違うでしょう。

私は何か問題が発生したとき、原因特定のために利用しようとしています。


  • ユーザ体験が悪化していた期間の特定

  • その期間において各ホストのグラフにおかしなパターンがないか確認

  • おかしなパターンがあれば原因の仮設を立てる

あとはログを見たりなどして仮設が正しいかどうかの確認を繰り返しです。

間違っていれば再度仮設を立てます。

難しく書きましたが、要はグラフで問題のありそうなところにあたりをつける、ということです。


ダッシュボードで何をしようとしたか

問題の原因確認中、グラフの期間指定のパターンとしては以下のパターンがあることに気づきました。


  • 短/中/長期的な観点で、問題の発生している期間を特定する

  • 特定した期間だけを切り取ったグラフを確認する

  • 特定した期間のグラフの中で短/中/長期的に見ておかしな値を示しているものを特定する

  • 問題の原因または原因によって引き起こされた異常値として仮説に組み込む

このようなパターンが決まってるなら、わざわざタイムレンジを頻繁に更新せずとも一覧できるようにしたいと思いました。

絵にするとこんな感じです。

image.png

これはレガシーのダッシュボードでは無理でした(たぶん)。


カスタムダッシュボードで嬉しくなったこと

なんと言っても期間指定がダッシュボード上でできるようになったことです。

1つのグラフ上からタイムレンジを指定すると、他のグラフのタイムレンジも変わってくれます。

しかし、期間固定をしているとタイムレンジが変わりません。

結果、ダッシュボードでしようとしていたことが実現できるようになりました。


ままならないこと

ダッシュボードが大量にできました。

このような表のようにグラフを並べると、グラフの数が多くて表示させるのに時間がかかります。

1ダッシュボードあたりのグラフの量を少なくするためにダッシュボードを分けます。

どれがなんのダッシュボードだよ・・・という流れです。

そもそもダッシュボードの在り方を見直したほうが良いのかもしれないなとは思いましたが、

とりあえず運用してみようと思います。


まとめ

正直なところレガシーからカスタムに移す工数かける理由がないなと思っていました。

ただ、おすすめされて使ってみたら昔出来なかったことができるようになっていて、やはりとりあえず試すべきだなと思いました。

このようなやり方ではなくdrill downダッシュボードのほうが良いかもしれないと思ったりもしたので、まだまだ改善の余地はありそうです。


[おまけ]GUIから作ると

大変なので、Rubyでスクリプト書きました。

Web上からはマークダウンより遥かに楽に作れるようになったのですが、それでも表示させるグラフが多いとGUIつらい・・・となります。

作る際はAPIを利用することをおすすめします。

なぐり書きしたものを載せておきます。

※コードは読み飛ばして問題ありません


dashboard.rb

require 'dotenv'

require 'yaml'
require 'json'
require 'faraday'

Dotenv.load

module Mackerel
class ObjectCache
@@hash_objects = {}
def self.write(key, object)
@@hash_objects[key] = object
end

def self.read(key)
@@hash_objects[key]
end
end

class Client
ENDPOINT = "https://api.mackerelio.com/"

def create_dashboard(payload)
res = connection.post do |req|
req.url '/api/v0/dashboards'
req.headers['X-Api-Key'] = ENV['MACKEREL_APIKEY']
req.headers['Content-Type'] = 'application/json'
req.body = payload
end
res.body
end

def delete_dashboard(dashboard_id)
res = connection.delete do |req|
req.url "/api/v0/dashboards/#{dashboard_id}"
req.headers['X-Api-Key'] = ENV['MACKEREL_APIKEY']
req.headers['Content-Type'] = 'application/json'
end
res.body
end

def search_dashboard_id_by_urlpath(url_path)
dashboards = ObjectCache::read('dashboards')
if dashboards.nil?
res = connection.get do |req|
req.url '/api/v0/dashboards'
req.headers['X-Api-Key'] = ENV['MACKEREL_APIKEY']
req.headers['Content-Type'] = 'application/json'
end
dashboards = JSON.parse(res.body)
ObjectCache::write('dashboards', dashboards)
end

ids = dashboards["dashboards"].select{|d| d["urlPath"] == url_path }.map{|d| d["id"]}
ids.first
end

def list_hosts_with_role(role_fullname)
res = connection.get do |req|
req.url '/api/v0/hosts'
req.headers['X-Api-Key'] = ENV['MACKEREL_APIKEY']
req.headers['Content-Type'] = 'application/json'
req.params['status'] = ["working", "standby", "maintenance", "poweroff"]
end
hosts = JSON.parse(res.body)
hosts["hosts"].select! do |host|
host["roles"].map do |s, roles|
roles.map{|r| "#{s}:#{r}"}
end.flatten.include?(role_fullname)
end
end

def connection
@conn ||= Faraday::Connection.new(:url => ENDPOINT) do |builder|
builder.use Faraday::Request::UrlEncoded
builder.use Faraday::Adapter::NetHttp
builder.options.params_encoder = Faraday::FlatParamsEncoder
end
end
end

class BulkDashboardMaker
def load_config
@yaml = YAML.load_file("config.yml")
end

def bulk_create
load_config
client = Client.new
old_id = client.search_dashboard_id_by_urlpath(@yaml['urlPath'])
client.delete_dashboard(old_id) unless old_id.nil?
payload = Dashboard.new.build(@yaml)
client.create_dashboard(payload)
end
end

class Dashboard
attr_reader :title
attr_reader :urlPath
attr_reader :memo
attr_reader :widgets

def build(yaml)
@title = yaml["title"] || ""
@urlPath = yaml["urlPath"] || "error"
@memo = yaml["memo"] || ""
@widgets = []

y = 0

wgf = WidgetGroupFactory.new
yaml["widget_params"].each do |param, i|
group = wgf.create(param["type"])
group.build(y, param).each do |widget|
@widgets << widget
end
y += group.height
end

self.to_json
end

def to_json
{
"title" => self.title,
"urlPath" => self.urlPath,
"memo" => self.memo,
"widgets" => self.widgets
}.to_json
end
end

class WidgetGroupFactory
def create(name)
case name
when "role" then RoleGroup.new
when "host" then HostGroup.new
when "header" then HeaderGroup.new
end
end
end

class WidgetGroup
MAX_COLUMN = 24
attr_reader :row

def build(y, param)
end

def max_width
(MAX_COLUMN / ranges.size).to_i
end

def height
6 #default
end

def row_height
6 #All type adopted
end

def make_layout(y, i)
{
"x" => max_width * i,
"y" => y,
"height" => row_height,
"width" => 6
}
end

def ranges
@ranges ||= [
{},
{"type" => "relative", "period" => 21600, "offset" => 0},
{"type" => "relative", "period" => 259200, "offset" => 0},
{"type" => "relative", "period" => 2592000, "offset" => 0}
]
end
end

class HeaderGroup < WidgetGroup
def build(y, param)
param['markdowns'].map.with_index do |mkd, i|
{
"type" => "markdown",
"title" => "",
"layout" => make_layout(y, i),
"markdown" => mkd
}
end
end

def height
2
end

def row_height
2
end
end

class RoleGroup < WidgetGroup
def build(y, param)
ranges.map.with_index do |range, i|
_tmp_obj = {
"type" => "graph",
"title" => "",
"layout" => make_layout(y, i),
"graph" => param,
}
_tmp_obj['range'] = range unless range.empty?
_tmp_obj
end
end

def height
6
end
def row_height
6
end
end

class HostGroup < WidgetGroup
def build(y, param)
get_host_ids(param['roleFullname']).map.with_index do |id, i|
ranges.map.with_index do |range, j|
_tmp_obj = {
"type" => "graph",
"title" => "",
"layout" => make_layout(y + HEIGHT * i, j),
"graph" => {
"type" => "host",
"hostId" => id,
"name" => param["name"]
}
}
_tmp_obj['range'] = range unless range.empty?
_tmp_obj
end
end.flatten
end

HEIGHT = 6
def height
@ids.nil? ? raise("Please call after #get_host_ids") : @ids.size * HEIGHT
end

def row_height
HEIGHT
end

def get_host_ids(role_fullname)
@ids = ObjectCache::read(role_fullname)
if @ids.nil?
hosts = Client.new.list_hosts_with_role(role_fullname)
@ids = hosts.sort{|a,b| a["name"] <=> b["name"]}.map{|host| host['id']}
ObjectCache::write(role_fullname, @ids)
end
@ids
end
end
end

Mackerel::BulkDashboardMaker.new.bulk_create


ダッシュボードの定義


config.yml

title: えびばでぃれっつごー

urlPath: aft_sch_clm_grl
memo: yeah yeah
widget_params:
- type: header
markdowns:
- 自由
- 6時間
- 3日間
- 1ヶ月
- type: role
roleFullname: dev:web
name: custom.multicore.loadavg_per_core.loadavg5
isStacked: false
- type: host
roleFullname: dev:web
name: memory

使う


Gemfile

source "https://rubygems.org"

gem "dotenv"
gem "faraday"
gem "json"


export MACKEREL_APIKEY="XXXXXXX"

bundle install
bundle exec ruby dashboard.rb