#
# 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
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
# 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
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
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.
# Now make a copy
$arvados_config = $arvados_config_global.deep_dup
+$arvados_config["LoadTimestamp"] = load_time
def arrayToHash cfg, k, v
val = {}
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)
+++ /dev/null
-# 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
--- /dev/null
+# 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
# 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