読者です 読者をやめる 読者になる 読者になる

Studio3104::BLOG.new

uninitialized constant Studio3104 (NameError)

HRForecast に値を投げた時に一緒に複合グラフも作っちゃいたい!

HRForecast の API で値を投げた時に返ってくる JSON に、これまではエラーの有無とエラーメッセージだけでしたが、POST 成功時にはグラフの情報が含まれるようになりました。

{
    :metricses => [
        [0] {
            :section_name => "test_section",
                      :id => "1",
                  :colors => "[\"#99cc33\"]",
              :updated_at => nil,
            :service_name => "test_service",
              :graph_name => "test_graph",
                   :color => "#99cc33",
                    :meta => "{\"color\":\"#99cc33\"}",
              :created_at => nil,
                    :sort => "0"
        }
    ],
        :error => 0
}

他にもいくつかの JSON API が追加され、ますます便利になってしまっています。 https://github.com/kazeburo/HRForecast/commit/a5b28908a0fe53c45f9b576687df6a58af385b17

値を投げた時にグラフの情報が返ってくれると例えば何が嬉しいのか

複合グラフを作成する API 自体はないのですが、これまででも POST してあげれば WEB UI からでなくても複合グラフを作成することは出来ました。 しかし複合グラフを作成するには、複合する対象のグラフの ID を送ってあげる必要があります。 ちょっと変な実装をすればグラフ ID を HTTP リクエストで取得することは出来ますが、トリッキーなことをせずとも複合グラフの作成が出来るようになります。

こんな感じでどうでしょうか

ふたつのグラフ(test_1, test_2)に値を投げて、複合グラフ(test_complex)がなければ作成します。
ちょっと工夫すれば test_complex がすでに作られていて、そこに test_3 をつっこみたい、なんてこともカンタンに出来るでしょう。

require "net/http"
require "json"

class HRForecastRequestError < StandardError; end
class HRForecast
  def initialize(fqdn, bind_port, enable_https = false)
    @base_url = enable_https ? "https://" + fqdn : "http://" + fqdn
    @bind_port = bind_port
  end

  # datetime のサポートするフォーマットはドキュメント参照: @base_url/docs
  def post_value(service_name, section_name, graph_name, post_bodies)
    value = post_bodies[:value]
    datetime = post_bodies[:datetime] ? post_bodies[:datetime] : Time.now

    graph_path = [service_name, section_name, graph_name].join("/")
    result = post_request("/api/" + graph_path, number: value, datetime: datetime)

    # 失敗すると、200 で body にエラーメッセージの入った JSON が返るので成否はそこを見て判断
    if result[:error] == 1
      raise HRForecastRequestError, "could not post value to #{graph_path} (#{result[:messages].to_s.chomp})"
    end

    result[:metricses].first[:id].to_i
  end

  def create_complex_graph(service_name, section_name, graph_name, path_data, graph_options = {})
    description = graph_options[:description] ? graph_options[:description] : ""
    stack = graph_options[:stack] ? graph_options[:stack] : 0
    sort = graph_options[:sort] ? graph_options[:sort] : 19

    result = post_request("/add_complex", {
      service_name: service_name,
      section_name: section_name,
      graph_name: graph_name,
      description: description,
      stack: stack, # 0:積み上げないグラフ, 1:積み上げるグラフ
      sort: sort, # 値の大きいモノ順で list に表示される (0..19)
      :"path-data" => path_data # 複合グラフに突っ込むグラフの ID を配列で渡す
    })

    # 失敗すると、200 で body にエラーメッセージの入った JSON が返るので成否はそこを見て判断
    if result[:error] == 1
      graph_path = [service_name, section_name, graph_name].join("/")
      raise HRForecastRequestError, "could not create complex graph #{graph_path} (#{result[:messages].to_s.chomp})"
    end
  end

  def complex_graph_exist?(service_name, section_name, graph_name)
    url = URI.parse(@base_url + ":" + @bind_port.to_s + ["/json_complex", service_name, section_name, graph_name].join("/"))
    http = Net::HTTP.new(url.host, url.port)
    http.get(url.path).is_a?(Net::HTTPSuccess)
  end

  private
  def post_request(request_path, post_data)
    url = @base_url + ":" + @bind_port.to_s + request_path
    response = Net::HTTP.post_form(URI.parse(url), post_data)

    raise HRForecastRequestError, "post request was not success" unless  response.is_a?(Net::HTTPSuccess)
    JSON.parse(response.body, symbolize_names: true)
  end
end
hf = HRForecast.new("test.hrforecast.com", 5127)
service_name = "test_service"
section_name = "test_section"
graph_name_prefix = "test"

test1_graph_id = hf.post_value(service_name, section_name, graph_name_prefix + "_1", value: rand(100) + 1)
test2_graph_id = hf.post_value(service_name, section_name, graph_name_prefix + "_2", value: rand(100) + 1)

complex_graph_name = graph_name_prefix + "_complex"
if !hf.complex_graph_exist?(service_name, section_name, complex_graph_name) && test1_graph_id && test2_graph_id
  hf.create_complex_graph(
    service_name, section_name, complex_graph_name,
    [ test1_graph_id, test2_graph_id ]
  )
end

余談

前述のとおり、このような変な実装によってグラフ ID を取得することは出来ます。

def get_graph_id(service_name, section_name, graph_name)
  # HTTP リクエストでグラフ ID を取得出来るクチが csv ダウンロードリンクしかなかったぽい
  url = URI.parse(@base_url + ":" + @bind_port.to_s + ["/csv", service_name, section_name, graph_name].join("/"))
  http = Net::HTTP.new(url.host, url.port)

  # "d=1" をつけてリクエストすると content-disposition の csv のファイル名からグラフ ID が取得できる
  response = http.head(url.path + "?d=1")
  return $1.to_i if response["content-disposition"] =~ %r{attachment; filename=\"metrics_(\d+).csv\"}
end

自分で書いてて違和感があったので、「複合グラフを作成するために、値が POSTされたときにグラフ ID を返すようにって出来ますか?」って kazeburo さんに相談したら、数分で実装してくれました。
はじめからお願いすれば良かった感が強いですけど、自分はこの変な実装をするために結構な時間を費やしてしまったのですが HRForecast のコードを読んでみたりだとか色々勉強になったこともあったのでまぁそれはそれで良かったと考えます。前向きに。