health_check
This commit is contained in:
commit
0ee365b743
|
|
@ -0,0 +1,6 @@
|
|||
.bundle/
|
||||
log/*.log
|
||||
pkg/
|
||||
test/dummy/db/*.sqlite3
|
||||
test/dummy/log/*.log
|
||||
test/dummy/tmp/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Ruling Digital
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
= HealthCheck
|
||||
|
||||
Health check module for orbit-kernel.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
begin
|
||||
require "bundler/setup"
|
||||
rescue LoadError
|
||||
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
||||
end
|
||||
|
||||
Bundler::GemHelper.install_tasks
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
class Admin::HealthCheckMonitorsController < OrbitAdminController
|
||||
before_action ->(module_app = @app_title) { set_variables module_app }
|
||||
before_action :set_monitor, only: [:show, :edit, :update, :destroy, :manual_check, :logs, :clean_logs]
|
||||
|
||||
def index
|
||||
@monitors = HealthCheckMonitor.all.desc(:created_at).page(params[:page]).per(10)
|
||||
render :partial => "index" if request.xhr?
|
||||
end
|
||||
|
||||
def show
|
||||
redirect_to logs_admin_health_check_monitor_path(@monitor)
|
||||
end
|
||||
|
||||
def new
|
||||
session[:return_to] = request.referer
|
||||
@monitor = HealthCheckMonitor.new
|
||||
end
|
||||
|
||||
def create
|
||||
@monitor = HealthCheckMonitor.new(monitor_params)
|
||||
@monitor.create_user_id = current_user.id
|
||||
@monitor.update_user_id = current_user.id
|
||||
@monitor.save
|
||||
redirect_to session.delete(:return_to) || admin_health_check_monitors_path
|
||||
end
|
||||
|
||||
def edit
|
||||
session[:return_to] = request.referer
|
||||
end
|
||||
|
||||
def update
|
||||
@monitor.update_attributes(monitor_params)
|
||||
@monitor.update_user_id = current_user.id
|
||||
@monitor.save
|
||||
redirect_to session.delete(:return_to) || admin_health_check_monitors_path
|
||||
end
|
||||
|
||||
def destroy
|
||||
@monitor.destroy
|
||||
redirect_to admin_health_check_monitors_path
|
||||
end
|
||||
|
||||
def manual_check
|
||||
@results = @monitor.check_all
|
||||
alert_logs = []
|
||||
@results.each do |url, result|
|
||||
HealthCheckLog.create(
|
||||
:health_check_monitor_id => @monitor.id,
|
||||
:url => url,
|
||||
:status => result[:status],
|
||||
:response_time => result[:response_time],
|
||||
:error_message => result[:error]
|
||||
)
|
||||
target = @monitor.alert_target(url)
|
||||
alert_logs << target if target
|
||||
end
|
||||
@monitor.send_alert_email(alert_logs) if alert_logs.present?
|
||||
redirect_to logs_admin_health_check_monitor_path(@monitor)
|
||||
end
|
||||
|
||||
def logs
|
||||
@logs = HealthCheckLog.where(:health_check_monitor_id => @monitor.id).desc(:created_at).page(params[:page]).per(20)
|
||||
end
|
||||
|
||||
def clean_logs
|
||||
HealthCheckLog.where(:health_check_monitor_id => @monitor.id).destroy_all
|
||||
redirect_to logs_admin_health_check_monitor_path(@monitor)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_monitor
|
||||
@monitor = HealthCheckMonitor.find(params[:id])
|
||||
end
|
||||
|
||||
def monitor_params
|
||||
params.require(:health_check_monitor).permit!
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
module Admin::HealthCheckMonitorsHelper
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# encoding: utf-8
|
||||
class HealthCheckAlert
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
|
||||
field :health_check_monitor_id, type: BSON::ObjectId
|
||||
field :url, type: String
|
||||
|
||||
index({ health_check_monitor_id: 1, url: 1, created_at: -1 })
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# encoding: utf-8
|
||||
class HealthCheckLog
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
|
||||
field :url, type: String
|
||||
field :status, type: Integer
|
||||
field :response_time, type: Float
|
||||
field :error_message, type: String
|
||||
|
||||
belongs_to :health_check_monitor, class_name: "HealthCheckMonitor", optional: true
|
||||
|
||||
index({ health_check_monitor_id: 1, created_at: -1 })
|
||||
end
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
# encoding: utf-8
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
class HealthCheckMonitor
|
||||
include Mongoid::Document
|
||||
include Mongoid::Timestamps
|
||||
|
||||
# 視為失敗的狀態碼。0 代表連不上(逾時、DNS 失敗、憑證錯誤等)
|
||||
# 若只要 502 才算失敗,改成 [502]
|
||||
FAILURE_STATUSES = [0, 500, 502, 503, 504]
|
||||
|
||||
# 告警視窗。只看這段時間內的檢查紀錄,同一網址在這段時間內也只寄一封
|
||||
ALERT_WINDOW = 4.hours
|
||||
|
||||
field :name, type: String
|
||||
field :target_urls, type: Array, default: []
|
||||
field :notification_emails, type: Array, default: []
|
||||
field :check_interval, type: Integer, default: 300
|
||||
field :fail_threshold, type: Integer, default: 3
|
||||
field :timeout, type: Integer, default: 10
|
||||
field :enabled, type: Boolean, default: true
|
||||
field :last_checked_at, type: DateTime
|
||||
field :create_user_id
|
||||
field :update_user_id
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
scope :is_enabled, -> { where(enabled: true) }
|
||||
|
||||
def target_urls_text
|
||||
(target_urls || []).join("\n")
|
||||
end
|
||||
|
||||
def target_urls_text=(value)
|
||||
self.target_urls = value.to_s.split(/\r?\n/).map(&:strip).reject(&:blank?).map do |u|
|
||||
u.match(/\Ahttps?:\/\//) ? u : "https://#{u}"
|
||||
end
|
||||
end
|
||||
|
||||
def notification_emails_text
|
||||
(notification_emails || []).join(", ")
|
||||
end
|
||||
|
||||
def notification_emails_text=(value)
|
||||
self.notification_emails = value.to_s.split(",").map(&:strip).reject(&:blank?)
|
||||
end
|
||||
|
||||
def threshold
|
||||
fail_threshold.to_i > 0 ? fail_threshold.to_i : 3
|
||||
end
|
||||
|
||||
def check_all
|
||||
results = {}
|
||||
(target_urls || []).each do |url|
|
||||
results[url] = check_url(url)
|
||||
end
|
||||
self.last_checked_at = Time.now
|
||||
self.save
|
||||
results
|
||||
end
|
||||
|
||||
def check_url(url)
|
||||
uri = URI.parse(url)
|
||||
started = Time.now
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = (uri.scheme == 'https')
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
http.open_timeout = self.timeout
|
||||
http.read_timeout = self.timeout
|
||||
response = http.request(Net::HTTP::Get.new(uri.request_uri))
|
||||
elapsed = ((Time.now - started) * 1000).round(2)
|
||||
{ :status => response.code.to_i, :response_time => elapsed, :error => nil }
|
||||
rescue => e
|
||||
{ :status => 0, :response_time => 0, :error => e.message }
|
||||
end
|
||||
|
||||
def failure?(status)
|
||||
FAILURE_STATUSES.include?(status.to_i)
|
||||
end
|
||||
|
||||
# 這次檢查寫完 log 之後呼叫。
|
||||
# 條件:4 小時內已檢查滿 threshold 次、最近 threshold 次全部失敗、且 4 小時內還沒寄過這個網址
|
||||
def alert_target(url)
|
||||
n = threshold
|
||||
window_start = Time.now - ALERT_WINDOW
|
||||
logs = HealthCheckLog.where(:health_check_monitor_id => self.id,
|
||||
:url => url,
|
||||
:created_at.gte => window_start)
|
||||
.desc(:created_at).limit(n).to_a
|
||||
return nil if logs.size < n
|
||||
return nil unless logs.all? { |log| failure?(log.status) }
|
||||
return nil if alerted_recently?(url)
|
||||
logs.first
|
||||
end
|
||||
|
||||
def alerted_recently?(url)
|
||||
HealthCheckAlert.where(:health_check_monitor_id => self.id,
|
||||
:url => url,
|
||||
:created_at.gte => Time.now - ALERT_WINDOW).exists?
|
||||
end
|
||||
|
||||
def mark_alerted(url)
|
||||
HealthCheckAlert.create(:health_check_monitor_id => self.id, :url => url)
|
||||
end
|
||||
|
||||
# logs 是這次剛好達標的 HealthCheckLog 陣列,全部合併成一封
|
||||
def send_alert_email(logs)
|
||||
return false if notification_emails.blank? || logs.blank?
|
||||
rows = logs.map do |log|
|
||||
{
|
||||
'url' => log.url.to_s,
|
||||
'status' => log.status.to_s,
|
||||
'error' => log.error_message.to_s
|
||||
}
|
||||
end
|
||||
email = Email.new(
|
||||
:module_app_key => 'health_check',
|
||||
:mail_to => notification_emails,
|
||||
:mail_subject => "[網站監控] #{self.name} - #{logs.size} 個網址失敗",
|
||||
:template => 'email/health_check_alert.html.erb',
|
||||
:template_data => {
|
||||
'monitor_name' => self.name.to_s,
|
||||
'threshold' => threshold,
|
||||
'rows' => rows,
|
||||
'checked_at' => logs.first.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
},
|
||||
:mail_sentdate => Time.now - 1.minute
|
||||
)
|
||||
email.save
|
||||
begin
|
||||
email.deliver
|
||||
logs.each { |log| mark_alerted(log.url) }
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.error("[health_check] 寄信失敗: #{e}")
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<div class="input-area">
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.name") %></label>
|
||||
<div class="controls">
|
||||
<%= f.text_field :name, :class => "input-block-level" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.target_urls") %></label>
|
||||
<div class="controls">
|
||||
<%= f.text_area :target_urls_text, :rows => 8, :class => "input-block-level", :placeholder => t("health_check_monitor.one_per_line") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.notification_emails") %></label>
|
||||
<div class="controls">
|
||||
<%= f.text_field :notification_emails_text, :class => "input-block-level", :placeholder => t("health_check_monitor.comma_separated") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.check_interval") %></label>
|
||||
<div class="controls">
|
||||
<%= f.number_field :check_interval %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.fail_threshold") %></label>
|
||||
<div class="controls">
|
||||
<%= f.number_field :fail_threshold %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.timeout") %></label>
|
||||
<div class="controls">
|
||||
<%= f.number_field :timeout %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label muted"><%= t("health_check_monitor.enabled") %></label>
|
||||
<div class="controls">
|
||||
<%= f.check_box :enabled %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<%= f.submit t(:submit), :class => "btn btn-primary" %>
|
||||
<%= link_to t(:cancel), admin_health_check_monitors_path, :class => "btn" %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<table class="table main-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t("health_check_monitor.name") %></th>
|
||||
<th><%= t("health_check_monitor.target_urls") %></th>
|
||||
<th><%= t("health_check_monitor.check_interval") %></th>
|
||||
<th><%= t("health_check_monitor.enabled") %></th>
|
||||
<th><%= t("health_check_monitor.last_checked") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @monitors.each do |monitor| %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= monitor.name %>
|
||||
<div class="quick-edit">
|
||||
<ul class="nav nav-pills">
|
||||
<li><%= link_to t(:edit), edit_admin_health_check_monitor_path(monitor) %></li>
|
||||
<li><%= link_to t("health_check_monitor.logs"), logs_admin_health_check_monitor_path(monitor) %></li>
|
||||
<li><%= link_to t("health_check_monitor.manual_check"), manual_check_admin_health_check_monitor_path(monitor), :method => :post %></li>
|
||||
<li><%= link_to t(:delete_), admin_health_check_monitor_path(monitor), :method => :delete, :data => { :confirm => t(:sure?) } %></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td><%= monitor.target_urls.join("<br/>").html_safe %></td>
|
||||
<td><%= monitor.check_interval %></td>
|
||||
<td><%= monitor.enabled ? t(:yes_) : t(:no_) %></td>
|
||||
<td><%= monitor.last_checked_at.blank? ? "-" : monitor.last_checked_at.strftime("%Y-%m-%d %H:%M") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%=
|
||||
content_tag :div, class: "bottomnav clearfix" do
|
||||
content_tag(:div, paginate(@monitors), class: "pagination pagination-centered") +
|
||||
content_tag(:div, link_to(t(:new_), new_admin_health_check_monitor_path, :class=>"btn btn-primary"), class: "pull-right")
|
||||
end
|
||||
%>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= form_for @monitor, :url => admin_health_check_monitor_path(@monitor), :html => { :class => "form-horizontal main-forms" } do |f| %>
|
||||
<%= render :partial => "form", :locals => { :f => f } %>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<%= render :partial => "index" %>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<h3><%= @monitor.name %> - <%= t("health_check_monitor.logs") %></h3>
|
||||
|
||||
<p>
|
||||
<%= link_to t("health_check_monitor.manual_check"), manual_check_admin_health_check_monitor_path(@monitor), :method => :post, :class => "btn btn-primary" %>
|
||||
<%= link_to t("health_check_monitor.clean_logs"), clean_logs_admin_health_check_monitor_path(@monitor), :method => :post, :class => "btn", :data => { :confirm => t(:sure?) } %>
|
||||
<%= link_to t(:back), admin_health_check_monitors_path, :class => "btn" %>
|
||||
</p>
|
||||
|
||||
<table class="table main-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>URL</th>
|
||||
<th><%= t("health_check_monitor.status") %></th>
|
||||
<th><%= t("health_check_monitor.response_time") %></th>
|
||||
<th>Error</th>
|
||||
<th><%= t("health_check_monitor.last_checked") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @logs.each do |log| %>
|
||||
<tr>
|
||||
<td><%= log.url %></td>
|
||||
<td><%= log.status %></td>
|
||||
<td><%= log.response_time %></td>
|
||||
<td><%= log.error_message %></td>
|
||||
<td><%= log.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="bottomnav clearfix">
|
||||
<div class="pagination pagination-centered">
|
||||
<%= paginate @logs %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<%= form_for @monitor, :url => admin_health_check_monitors_path, :html => { :class => "form-horizontal main-forms" } do |f| %>
|
||||
<%= render :partial => "form", :locals => { :f => f } %>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
|
||||
</head>
|
||||
<body>
|
||||
<p>監控項目:<%= @data['monitor_name'] %></p>
|
||||
<p>以下 <%= @data['rows'].size %> 個網址連續 <%= @data['threshold'] %> 次檢查失敗:</p>
|
||||
<table border="1" cellpadding="6" style="border-collapse:collapse">
|
||||
<tr>
|
||||
<th>網址</th>
|
||||
<th>狀態碼</th>
|
||||
<th>錯誤訊息</th>
|
||||
</tr>
|
||||
<% @data['rows'].each do |row| %>
|
||||
<tr>
|
||||
<td><%= row['url'] %></td>
|
||||
<td><%= row['status'] %></td>
|
||||
<td><%= row['error'] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<p>檢查時間:<%= @data['checked_at'] %></p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
en:
|
||||
health_check: Health Check
|
||||
health_check_monitor:
|
||||
name: Name
|
||||
target_urls: Target URLs
|
||||
check_interval: Check Interval (sec)
|
||||
fail_threshold: Alert After N Consecutive Failures
|
||||
timeout: Timeout (sec)
|
||||
notification_emails: Notification Emails
|
||||
enabled: Enabled
|
||||
status: Status
|
||||
response_time: Response Time (ms)
|
||||
last_checked: Last Checked
|
||||
manual_check: Check Now
|
||||
logs: Logs
|
||||
clean_logs: Clear All Logs
|
||||
one_per_line: One URL per line, must include http:// or https:// (defaults to https://)
|
||||
comma_separated: Comma separated
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
zh_tw:
|
||||
health_check: 健康檢查
|
||||
health_check_monitor:
|
||||
name: 監控名稱
|
||||
target_urls: 監控網址
|
||||
check_interval: 檢查間隔(秒)
|
||||
fail_threshold: 連續失敗幾次寄信
|
||||
timeout: 逾時(秒)
|
||||
notification_emails: 通知信箱
|
||||
enabled: 啟用
|
||||
status: 狀態
|
||||
response_time: 回應時間(ms)
|
||||
last_checked: 最後檢查
|
||||
manual_check: 立即檢查
|
||||
logs: 檢查紀錄
|
||||
clean_logs: 清除全部紀錄
|
||||
one_per_line: 一行一個網址,需含 http:// 或 https://(未填會自動補 https://)
|
||||
comma_separated: 以逗號分隔
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
Rails.application.routes.draw do
|
||||
|
||||
locales = Site.find_by(site_active: true).in_use_locales rescue I18n.available_locales
|
||||
|
||||
scope "(:locale)", locale: Regexp.new(locales.join("|")) do
|
||||
namespace :admin do
|
||||
resources :health_check_monitors do
|
||||
member do
|
||||
post :manual_check
|
||||
get :logs
|
||||
post :clean_logs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
$:.push File.expand_path("../lib", __FILE__)
|
||||
|
||||
# Maintain your gem's version:
|
||||
require "health_check/version"
|
||||
|
||||
# Describe your gem and declare its dependencies:
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "health_check"
|
||||
s.version = HealthCheck::VERSION
|
||||
s.authors = ["Orbitek"]
|
||||
s.email = ["service@orbitek.co"]
|
||||
s.homepage = "https://orbitek.co"
|
||||
s.summary = "Health Check for orbitek"
|
||||
s.description = "Health Check for orbitek"
|
||||
s.license = "MIT"
|
||||
|
||||
s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
|
||||
end
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
require "health_check/engine"
|
||||
|
||||
module HealthCheck
|
||||
end
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
module HealthCheck
|
||||
class Engine < ::Rails::Engine
|
||||
initializer "health_check" do
|
||||
Rails.application.config.to_prepare do
|
||||
OrbitApp.registration "HealthCheck", :type => "ModuleApp" do
|
||||
module_label "health_check.health_check"
|
||||
base_url File.expand_path File.dirname(__FILE__)
|
||||
|
||||
authorizable
|
||||
|
||||
side_bar do
|
||||
head_label_i18n 'health_check', :icon_class=>"icons-cycle"
|
||||
available_for "users"
|
||||
active_for_controllers (['admin/health_check_monitors'])
|
||||
head_link_path "admin_health_check_monitors_path"
|
||||
|
||||
context_link 'list_',
|
||||
:link_path=>"admin_health_check_monitors_path" ,
|
||||
:priority=>1,
|
||||
:active_for_action=>{'admin/health_check_monitors'=>'index'},
|
||||
:available_for => 'users'
|
||||
|
||||
context_link 'new_',
|
||||
:link_path=>"new_admin_health_check_monitor_path" ,
|
||||
:priority=>2,
|
||||
:active_for_action=>{'admin/health_check_monitors'=>'new'},
|
||||
:available_for => 'sub_managers'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module HealthCheck
|
||||
VERSION = "0.0.1"
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# desc "Explaining what the task does"
|
||||
# task :health_check do
|
||||
# end
|
||||
Loading…
Reference in New Issue