From 1c15e53669bb4f6838e7afef58407c2b458c9b11 Mon Sep 17 00:00:00 2001 From: Tom Clegg Date: Mon, 11 Apr 2022 14:35:03 -0400 Subject: [PATCH] 18794: Add /metrics endpoint to RailsAPI. Arvados-DCO-1.1-Signed-off-by: Tom Clegg --- ...controller.rb => management_controller.rb} | 24 +++++-- services/api/config/arvados_config.rb | 6 ++ services/api/config/routes.rb | 3 +- .../arvados/v1/healthcheck_controller_test.rb | 34 --------- .../arvados/v1/management_controller_test.rb | 71 +++++++++++++++++++ services/api/test/integration/errors_test.rb | 2 +- 6 files changed, 100 insertions(+), 40 deletions(-) rename services/api/app/controllers/arvados/v1/{healthcheck_controller.rb => management_controller.rb} (54%) delete mode 100644 services/api/test/functional/arvados/v1/healthcheck_controller_test.rb create mode 100644 services/api/test/functional/arvados/v1/management_controller_test.rb diff --git a/services/api/app/controllers/arvados/v1/healthcheck_controller.rb b/services/api/app/controllers/arvados/v1/management_controller.rb similarity index 54% rename from services/api/app/controllers/arvados/v1/healthcheck_controller.rb rename to services/api/app/controllers/arvados/v1/management_controller.rb index c562082077..55a00d3463 100644 --- a/services/api/app/controllers/arvados/v1/healthcheck_controller.rb +++ b/services/api/app/controllers/arvados/v1/management_controller.rb @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0 -class Arvados::V1::HealthcheckController < ApplicationController +class Arvados::V1::ManagementController < ApplicationController skip_before_action :catch_redirect_hint skip_before_action :find_objects_for_index skip_before_action :find_object_by_uuid @@ -29,8 +29,24 @@ class Arvados::V1::HealthcheckController < ApplicationController end end - def ping - resp = {"health" => "OK"} - send_json resp + def metrics + render content_type: 'text/plain', plain: <<~EOF +# HELP arvados_config_load_timestamp_seconds Time when config file was loaded. +# TYPE arvados_config_load_timestamp_seconds gauge +arvados_config_load_timestamp_seconds{sha256="#{Rails.configuration.SourceSHA256}"} #{Rails.configuration.LoadTimestamp.to_f} +# HELP arvados_config_source_timestamp_seconds Timestamp of config file when it was loaded. +# TYPE arvados_config_source_timestamp_seconds gauge +arvados_config_source_timestamp_seconds{sha256="#{Rails.configuration.SourceSHA256}"} #{Rails.configuration.SourceTimestamp.to_f} +EOF + end + + def health + case params[:check] + when 'ping' + resp = {"health" => "OK"} + send_json resp + else + send_json ({"errors" => "not found"}), status: 404 + end end end diff --git a/services/api/config/arvados_config.rb b/services/api/config/arvados_config.rb index 8a96c432a8..c0f7ee174f 100644 --- a/services/api/config/arvados_config.rb +++ b/services/api/config/arvados_config.rb @@ -30,6 +30,7 @@ end # Load the defaults, used by config:migrate and fallback loading # legacy application.yml +load_time = Time.now.utc defaultYAML, stderr, status = Open3.capture3("arvados-server", "config-dump", "-config=-", "-skip-legacy", stdin_data: "Clusters: {xxxxx: {}}") if !status.success? puts stderr @@ -39,6 +40,8 @@ confs = YAML.load(defaultYAML, deserialize_symbols: false) clusterID, clusterConfig = confs["Clusters"].first $arvados_config_defaults = clusterConfig $arvados_config_defaults["ClusterID"] = clusterID +$arvados_config_defaults["SourceTimestamp"] = Time.rfc3339(confs["SourceTimestamp"]) +$arvados_config_defaults["SourceSHA256"] = confs["SourceSHA256"] if ENV["ARVADOS_CONFIG"] == "none" # Don't load config. This magic value is set by packaging scripts so @@ -54,6 +57,8 @@ else clusterID, clusterConfig = confs["Clusters"].first $arvados_config_global = clusterConfig $arvados_config_global["ClusterID"] = clusterID + $arvados_config_global["SourceTimestamp"] = Time.rfc3339(confs["SourceTimestamp"]) + $arvados_config_global["SourceSHA256"] = confs["SourceSHA256"] else # config-dump failed, assume we will be loading from legacy # application.yml, initialize with defaults. @@ -64,6 +69,7 @@ end # Now make a copy $arvados_config = $arvados_config_global.deep_dup +$arvados_config["LoadTimestamp"] = load_time def arrayToHash cfg, k, v val = {} diff --git a/services/api/config/routes.rb b/services/api/config/routes.rb index 98f5788d65..9c7bfc3a7a 100644 --- a/services/api/config/routes.rb +++ b/services/api/config/routes.rb @@ -112,7 +112,8 @@ Rails.application.routes.draw do match '/static/login_failure', to: 'static#login_failure', as: :login_failure, via: [:get, :post] - match '/_health/ping', to: 'arvados/v1/healthcheck#ping', via: [:get] + match '/_health/:check', to: 'arvados/v1/management#health', via: [:get] + match '/metrics', to: 'arvados/v1/management#metrics', via: [:get] # Send unroutable requests to an arbitrary controller # (ends up at ApplicationController#render_not_found) diff --git a/services/api/test/functional/arvados/v1/healthcheck_controller_test.rb b/services/api/test/functional/arvados/v1/healthcheck_controller_test.rb deleted file mode 100644 index 76fdb0426d..0000000000 --- a/services/api/test/functional/arvados/v1/healthcheck_controller_test.rb +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) The Arvados Authors. All rights reserved. -# -# SPDX-License-Identifier: AGPL-3.0 - -require 'test_helper' - -class Arvados::V1::HealthcheckControllerTest < ActionController::TestCase - [ - [false, nil, 404, 'disabled'], - [true, nil, 401, 'authorization required'], - [true, 'badformatwithnoBearer', 403, 'authorization error'], - [true, 'Bearer wrongtoken', 403, 'authorization error'], - [true, 'Bearer configuredmanagementtoken', 200, '{"health":"OK"}'], - ].each do |enabled, header, error_code, error_msg| - test "ping when #{if enabled then 'enabled' else 'disabled' end} with header '#{header}'" do - if enabled - Rails.configuration.ManagementToken = 'configuredmanagementtoken' - else - Rails.configuration.ManagementToken = "" - end - - @request.headers['Authorization'] = header - get :ping - assert_response error_code - - resp = JSON.parse(@response.body) - if error_code == 200 - assert_equal(JSON.load('{"health":"OK"}'), resp) - else - assert_equal(error_msg, resp['errors']) - end - end - end -end diff --git a/services/api/test/functional/arvados/v1/management_controller_test.rb b/services/api/test/functional/arvados/v1/management_controller_test.rb new file mode 100644 index 0000000000..5b34f9fef9 --- /dev/null +++ b/services/api/test/functional/arvados/v1/management_controller_test.rb @@ -0,0 +1,71 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: AGPL-3.0 + +require 'test_helper' + +class Arvados::V1::ManagementControllerTest < ActionController::TestCase + [ + [false, nil, 404, 'disabled'], + [true, nil, 401, 'authorization required'], + [true, 'badformatwithnoBearer', 403, 'authorization error'], + [true, 'Bearer wrongtoken', 403, 'authorization error'], + [true, 'Bearer configuredmanagementtoken', 200, '{"health":"OK"}'], + ].each do |enabled, header, error_code, error_msg| + test "_health/ping when #{if enabled then 'enabled' else 'disabled' end} with header '#{header}'" do + if enabled + Rails.configuration.ManagementToken = 'configuredmanagementtoken' + else + Rails.configuration.ManagementToken = "" + end + + @request.headers['Authorization'] = header + get :health, params: {check: 'ping'} + assert_response error_code + + resp = JSON.parse(@response.body) + if error_code == 200 + assert_equal(JSON.load('{"health":"OK"}'), resp) + else + assert_equal(error_msg, resp['errors']) + end + end + end + + test "metrics" do + mtime = File.mtime(ENV["ARVADOS_CONFIG"]) + hash = Digest::SHA256.hexdigest(File.read(ENV["ARVADOS_CONFIG"])) + Rails.configuration.ManagementToken = "configuredmanagementtoken" + @request.headers['Authorization'] = "Bearer configuredmanagementtoken" + get :metrics + assert_response :success + assert_equal 'text/plain', @response.content_type + + assert_match /\narvados_config_source_timestamp_seconds{sha256="#{hash}"} #{Regexp.escape mtime.utc.to_f.to_s}\n/, @response.body + + # Expect mtime < loadtime < now + m = @response.body.match(/\narvados_config_load_timestamp_seconds{sha256="#{hash}"} (.*?)\n/) + assert_operator m[1].to_f, :>, mtime.utc.to_f + assert_operator m[1].to_f, :<, Time.now.utc.to_f + end + + test "metrics disabled" do + Rails.configuration.ManagementToken = "" + @request.headers['Authorization'] = "Bearer configuredmanagementtoken" + get :metrics + assert_response 404 + end + + test "metrics bad token" do + Rails.configuration.ManagementToken = "configuredmanagementtoken" + @request.headers['Authorization'] = "Bearer asdf" + get :metrics + assert_response 403 + end + + test "metrics unauthorized" do + Rails.configuration.ManagementToken = "configuredmanagementtoken" + get :metrics + assert_response 401 + end +end diff --git a/services/api/test/integration/errors_test.rb b/services/api/test/integration/errors_test.rb index a2a1545cee..a5359278e5 100644 --- a/services/api/test/integration/errors_test.rb +++ b/services/api/test/integration/errors_test.rb @@ -24,7 +24,7 @@ class ErrorsTest < ActionDispatch::IntegrationTest # Generally, new routes should appear under /arvados/v1/. If # they appear elsewhere, that might have been caused by default # rails generator behavior that we don't want. - assert_match(/^\/(|\*a|arvados\/v1\/.*|auth\/.*|login|logout|database\/reset|discovery\/.*|static\/.*|sys\/trash_sweep|themes\/.*|assets|_health\/.*)(\(\.:format\))?$/, + assert_match(/^\/(|\*a|arvados\/v1\/.*|auth\/.*|login|logout|database\/reset|discovery\/.*|static\/.*|sys\/trash_sweep|themes\/.*|assets|_health|metrics\/.*)(\(\.:format\))?$/, route.path.spec.to_s, "Unexpected new route: #{route.path.spec}") end -- 2.30.2