canvas-lms/lib/health_checks.rb

241 lines
7.7 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2022 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module HealthChecks
extend ApplicationHelper
class << self
def process_readiness_checks(is_deep_check)
readiness_checks
.transform_values { |v| component_check(v, is_deep_check) }
end
def process_deep_checks
critical = critical_checks
.transform_values { |v| execute_deep_check(v) }
.transform_values { |v| component_check(v, true) }
secondary = secondary_checks
.transform_values { |v| execute_deep_check(v) }
.transform_values { |v| component_check(v, true) }
{ critical:, secondary: }
end
def send_to_statsd(result = nil, additional_tags = {})
result ||= process_deep_checks.merge({ readiness: process_readiness_checks(true) })
result.each do |check_type, check_values|
check_values.each do |check_name, check_results|
tags = { type: check_type, key: check_name, **additional_tags }
InstStatsd::Statsd.timing("canvas.health_checks.response_time_ms", check_results[:time], tags:)
InstStatsd::Statsd.gauge("canvas.health_checks.status", check_results[:status] ? 1 : 0, tags:)
end
end
end
private
def readiness_checks
{
# ensures brandable_css_bundles_with_deps exists, returns a string (path), treated as truthy
common_css: -> { !!css_url_for("common") },
# ensures webpack worked; returns a string, treated as truthy
common_js: -> { Canvas::Cdn.registry.scripts_available? },
# returns a PrefixProxy instance, treated as truthy
consul: -> { DynamicSettings.find(tree: :private)[:readiness, failsafe: nil].nil? },
# returns the value of the block <integer>, treated as truthy
filesystem: lambda do
!!Tempfile.open("readiness", ENV["TMPDIR"] || Dir.tmpdir) { |f| f.write("readiness") }
end,
# returns a boolean
jobs: lambda do
Delayed::Job.connection.verify!
true
end,
# returns a boolean
postgresql: lambda do
Account.connection.verify!
GuardRail.activate(:secondary) { Account.connection.verify! }
true
end,
# nil response treated as truthy
ha_cache: -> { MultiCache.cache.fetch("readiness").nil? },
# ensures `gulp rev` has ran; returns a string, treated as truthy
rev_manifest: -> { Canvas::Cdn.registry.statics_available? },
# ensures we retrieved something back from Vault; returns a boolean
vault: -> { !Canvas::Vault.read("#{Canvas::Vault.kv_mount}/data/secrets").nil? }
}
end
def critical_checks
ret = {
default_shard: lambda do
Shard.connection.verify!
true
end
}
if InstFS.enabled?
ret[:inst_fs] = lambda do
CanvasHttp
.get(URI.join(InstFS.app_host, "/readiness").to_s)
.is_a?(Net::HTTPSuccess)
end
end
if Canvas.redis_enabled?
ret[:redis] = lambda do
nodes = Canvas.redis.try(:ring)&.nodes || Array.wrap(Canvas.redis)
nodes.all? { |node| node.get("deep_check").nil? }
end
end
if Services::RichContent.send(:service_settings)[:RICH_CONTENT_APP_HOST]
ret[:rich_content_service] = lambda do
CanvasHttp
.get(
URI::HTTPS.build(
host: Services::RichContent.send(:service_settings)[:RICH_CONTENT_APP_HOST],
path: "/readiness"
).to_s
).is_a?(Net::HTTPSuccess)
end
end
if MathMan.use_for_svg?
ret[:mathman] = lambda do
CanvasHttp
.get(MathMan.url_for(latex: "x", target: :svg))
.is_a?(Net::HTTPSuccess)
end
end
if LiveEvents::Client.config
ret[:live_events] = lambda do
!LiveEvents.send(:client).stream_client.put_records(
records: [
{
data: {
attributes: {
event_name: "noop",
event_time: Time.now.utc.iso8601(3)
},
body: {}
}.to_json,
partition_key: rand(1000).to_s
}
],
stream_name: LiveEvents.send(:client).stream_name
).nil?
end
end
if DynamicSettings.config.present?
ret[:consul] = lambda do
DynamicSettings.find(tree: :private)["health_check", ttl: 0.1]
true
end
end
ret
end
def secondary_checks
ret = {}
if PageView.pv4?
ret[:pv4] = lambda do
CanvasHttp
.get(URI.join(ConfigFile.load("pv4")["uri"], "/health_check").to_s)
.is_a?(Net::HTTPSuccess)
end
end
if Canvadocs.enabled?
ret[:canvadocs] = lambda do
CanvasHttp
.get(URI.join(Canvadocs.config["base_url"], "/readiness").to_s)
.is_a?(Net::HTTPSuccess)
end
end
if CutyCapt.enabled? && CutyCapt.screencap_service
ret[:screencap] = lambda do
Tempfile.create("example.png", encoding: "ascii-8bit") do |f|
CutyCapt.screencap_service.snapshot_url_to_file("about:blank", f)
end
end
end
if Account.site_admin.feature_enabled?(:notification_service)
ret[:notification_queue] = lambda do
!Services::NotificationService.process(Account.site_admin.global_id, nil, "noop", "nobody").nil?
end
end
if ReleaseNote.enabled?
ret[:release_notes] = lambda do
!ReleaseNote.ddb_client.update_item(
table_name: ReleaseNote.ddb_table_name,
key: { "PartitionKey" => "healthcheck",
"RangeKey" => "canvas" }
).nil?
end
end
if IncomingMailProcessor::IncomingMessageProcessor.run_periodically?
ret[:incoming_mail] = lambda do
IncomingMailProcessor::IncomingMessageProcessor.healthy?
end
end
ret
end
def execute_deep_check(proc)
Thread.new do
Thread.current.report_on_exception = false
proc.call
end
end
def component_check(component, is_deep_check)
status = false
message = "service is up"
exception_type = is_deep_check ? :deep_health_check : :readiness_health_check
timeout = Setting.get("healthcheck_timelimit", 5.seconds.to_s).to_f
response_time_ms =
Benchmark.ms do
Timeout.timeout(timeout, Timeout::Error) do
status = component.is_a?(Thread) ? component.value : component.call
end
rescue Timeout::Error => e
message = e.message
Canvas::Errors.capture_exception(exception_type, e.message, :warn)
rescue => e
message = e.message
Canvas::Errors.capture_exception(exception_type, e, :error)
end
{ status:, message:, time: response_time_ms }
end
end
end