!20 feat(#I10L13): 提交健康检查功能

Merge pull request !20 from Argo/dev-Health
This commit is contained in:
Argo 2019-08-14 11:39:06 +08:00 committed by Gitee
commit 04b146369f
14 changed files with 445 additions and 4 deletions

View File

@ -71,6 +71,12 @@ namespace Bootstrap.Admin.Controllers
/// <returns></returns> /// <returns></returns>
public ActionResult FAIcon() => View(new NavigatorBarModel(this)); public ActionResult FAIcon() => View(new NavigatorBarModel(this));
/// <summary>
///
/// </summary>
/// <returns></returns>
public ActionResult Healths() => View(new NavigatorBarModel(this));
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>

View File

@ -0,0 +1,39 @@
using Bootstrap.DataAccess;
using Bootstrap.Security;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Task = System.Threading.Tasks.Task;
namespace Bootstrap.Admin.HealthChecks
{
/// <summary>
/// 数据库检查类
/// </summary>
public class DBHealthCheck : IHealthCheck
{
/// <summary>
/// 异步检查方法
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
using (var db = DbManager.Create())
{
var connStr = db.ConnectionString;
var dicts = db.Fetch<BootstrapDict>("Select * from Dicts");
var data = new Dictionary<string, object>()
{
{ "ConnectionString", connStr },
{ "DbType", db.Provider.GetType().Name },
{ "Dicts", dicts.Count }
};
return dicts.Any() ? Task.FromResult(HealthCheckResult.Healthy("Ok", data)) : Task.FromResult(HealthCheckResult.Degraded("No init data in DB"));
}
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Bootstrap.Admin.HealthChecks
{
/// <summary>
/// 文件健康检查类
/// </summary>
public class FileHealCheck : IHealthCheck
{
private readonly IHostingEnvironment _env;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="env"></param>
public FileHealCheck(IHostingEnvironment env)
{
_env = env;
}
/// <summary>
/// 异步检查方法
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var file = _env.IsDevelopment() ? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Longbow.lic") : Path.Combine(_env.ContentRootPath, "Longbow.lic");
var data = new Dictionary<string, object>
{
{ "ApplicationName", _env.ApplicationName },
{ "EnvironmentName", _env.EnvironmentName },
{ "ContentRootPath", _env.ContentRootPath },
{ "WebRootPath", _env.WebRootPath },
{ "CheckFile", file }
};
return Task.FromResult(File.Exists(file) ? HealthCheckResult.Healthy("Ok", data) : HealthCheckResult.Unhealthy($"Missing file {file}", null, data));
}
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Bootstrap.Admin.HealthChecks
{
/// <summary>
/// 内存状态检查其
/// </summary>
public class GCHealthCheck : IHealthCheck
{
/// <summary>
/// 异步检查方法
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
// Include GC information in the reported diagnostics.
var allocated = GC.GetTotalMemory(forceFullCollection: false);
var data = new Dictionary<string, object>()
{
{ "AllocatedMBytes", allocated / 1024 / 1024 },
{ "Gen0Collections", GC.CollectionCount(0) },
{ "Gen1Collections", GC.CollectionCount(1) },
{ "Gen2Collections", GC.CollectionCount(2) },
};
return Task.FromResult(HealthCheckResult.Healthy("OK", data));
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Bootstrap.Admin.HealthChecks
{
/// <summary>
/// Gitee 接口检查器
/// </summary>
public class GiteeHttpHealthCheck : IHealthCheck
{
private readonly HttpClient _client;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="factory"></param>
/// <param name="accessor"></param>
public GiteeHttpHealthCheck(IHttpClientFactory factory, IHttpContextAccessor accessor)
{
_client = factory.CreateClient();
_client.BaseAddress = new Uri($"{accessor.HttpContext.Request.Scheme}://{accessor.HttpContext.Request.Host}/{accessor.HttpContext.Request.PathBase}");
}
/// <summary>
/// 异步检查方法
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var urls = new string[] { "Issues", "Pulls", "Releases", "Builds" };
var data = new Dictionary<string, object>();
var sw = Stopwatch.StartNew();
foreach (var url in urls)
{
sw.Restart();
await _client.GetStringAsync($"api/Gitee/{url}").ConfigureAwait(false);
sw.Stop();
data.Add(url, sw.Elapsed);
}
return HealthCheckResult.Healthy("Ok", data);
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Newtonsoft.Json;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// BootstrapAdmin 健康检查扩展类
/// </summary>
public static class HealthChecksAppBuilderExtensions
{
/// <summary>
/// 启用健康检查
/// </summary>
/// <param name="app"></param>
/// <param name="path"></param>
/// <returns></returns>
public static IApplicationBuilder UseBootstrapHealthChecks(this IApplicationBuilder app, PathString path = default)
{
if (path == default) path = "/Healths";
app.UseHealthChecks(path, new HealthCheckOptions()
{
ResponseWriter = (context, report) =>
{
context.Response.ContentType = "application/json";
return context.Response.WriteAsync(JsonConvert.SerializeObject(new { report.Entries.Keys, Report = report }));
},
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status200OK
}
});
app.UseWhen(context => context.Request.Path == "/healths-ui", builder => builder.Run(request =>
{
request.Response.Redirect("/html/Healths-UI.html");
return Task.CompletedTask;
}));
return app;
}
}
}

View File

@ -0,0 +1,25 @@
using Bootstrap.Admin.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// 健康检查扩展类
/// </summary>
public static class HealthChecksBuilderExtensions
{
/// <summary>
/// 添加 BootstrapAdmin 健康检查
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IHealthChecksBuilder AddBootstrapAdminHealthChecks(this IHealthChecksBuilder builder)
{
builder.AddCheck<DBHealthCheck>("db");
builder.AddCheck<FileHealCheck>("file");
builder.AddCheck<GCHealthCheck>("gc");
builder.AddCheck<MemoryHealthCheck>("mem");
builder.AddCheck<GiteeHttpHealthCheck>("Gitee");
return builder;
}
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Bootstrap.Admin.HealthChecks
{
/// <summary>
/// 内存检查器
/// </summary>
public class MemoryHealthCheck : IHealthCheck
{
/// <summary>
/// 异步检查方法
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var process = Process.GetCurrentProcess();
var data = new Dictionary<string, object>()
{
{ "Id", process.Id },
{ "WorkingSet", process.WorkingSet64 },
{ "PrivateMemory", process.PrivateMemorySize64 },
{ "VirtualMemory", process.VirtualMemorySize64 },
};
return Task.FromResult(HealthCheckResult.Healthy("Ok", data));
}
}
}

View File

@ -64,6 +64,7 @@ namespace Bootstrap.Admin
services.AddSwagger(); services.AddSwagger();
services.AddButtonAuthorization(); services.AddButtonAuthorization();
services.AddDemoTask(); services.AddDemoTask();
services.AddHealthChecks().AddBootstrapAdminHealthChecks();
services.AddMvc(options => services.AddMvc(options =>
{ {
options.Filters.Add<BootstrapAdminAuthorizeFilter>(); options.Filters.Add<BootstrapAdminAuthorizeFilter>();
@ -107,6 +108,7 @@ namespace Bootstrap.Admin
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseResponseCompression(); app.UseResponseCompression();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseBootstrapHealthChecks();
app.UseBootstrapAdminAuthentication(RoleHelper.RetrievesByUserName, RoleHelper.RetrievesByUrl, AppHelper.RetrievesByUserName); app.UseBootstrapAdminAuthentication(RoleHelper.RetrievesByUserName, RoleHelper.RetrievesByUrl, AppHelper.RetrievesByUserName);
app.UseOnlineUsers(callback: TraceHelper.Save); app.UseOnlineUsers(callback: TraceHelper.Save);
app.UseCacheManager(); app.UseCacheManager();

View File

@ -0,0 +1,37 @@
@model NavigatorBarModel
@{
ViewBag.Title = "健康检查";
}
@section css {
<environment include="Development">
<link href="~/lib/bootstrap-table/bootstrap-table.css" rel="stylesheet" />
</environment>
<environment exclude="Development">
<link href="~/lib/bootstrap-table/bootstrap-table.min.css" rel="stylesheet" />
</environment>
<style>
.popover-body .bootstrap-table tbody td:first-child {
white-space: nowrap;
}
</style>
}
@section javascript {
<environment include="Development">
<script src="~/lib/bootstrap-table/bootstrap-table.js"></script>
<script src="~/lib/bootstrap-table/locale/bootstrap-table-zh-CN.js"></script>
</environment>
<environment exclude="Development">
<script src="~/lib/bootstrap-table/bootstrap-table.min.js"></script>
<script src="~/lib/bootstrap-table/locale/bootstrap-table-zh-CN.min.js"></script>
</environment>
<script src="~/js/healths.js" asp-append-version="true"></script>
}
<div class="card">
<div class="card-header">
健康检查结果
</div>
<div class="card-body">
<div>耗时:<span id="checkTotalEplsed">--</span></div>
<table></table>
</div>
</div>

View File

@ -28,8 +28,12 @@ img {
text-align: left; text-align: left;
} }
a, a:hover, a:focus { a {
cursor: pointer;
}
a, a:hover, a:focus {
text-decoration: none; text-decoration: none;
outline: none; outline: none;
color: #371ed4; color: #371ed4;
} }

View File

@ -709,11 +709,19 @@ input.pending {
min-width: unset; min-width: unset;
} }
.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before {
border-bottom: 0;
}
.popover { .popover {
max-width: 320px; max-width: 320px;
padding: 0; padding: 0;
} }
.popover-body .bootstrap-table {
margin: 0.25rem 0;
}
.popover-content { .popover-content {
max-height: 240px; max-height: 240px;
overflow-y: auto; overflow-y: auto;

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="../favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="../favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="../favicon.png" />
<title>健康检查</title>
<link href="../lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<link href="../lib/bootstrap-table/bootstrap-table.min.css" rel="stylesheet" />
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<link href="../css/theme.css" rel="stylesheet" asp-append-version="true" />
<link href="../css/theme-responsive.css" rel="stylesheet" asp-append-version="true" />
<link href="../css/site.css" rel="stylesheet" asp-append-version="true" />
<link href="../css/site-responsive.css" rel="stylesheet" asp-append-version="true" />
<!--[if lt IE 10 ]>
<link href="../css/IE8.css" rel="stylesheet" />
<![endif]-->
</head>
<body>
<!--[if lt IE 10 ]>
<div id="ieAlert" class="alert alert-danger alert-dismissible">
<div>当前浏览器版本太低不支持本系统请升级到至少IE10 <a href="../browser/IE10.exe" target="_blank">本地下载</a> <a href="https://support.microsoft.com/zh-cn/help/17621/internet-explorer-downloads" target="_blank">微软下载</a>或者使用Chrome浏览器 <a href="../browser/ChromeSetup.exe" target="_blank">本地下载</a></div>
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">关闭</span></button>
</div>
<![endif]-->
<div class="container-fluid">
<div class="card">
<div class="card-header">
健康检查结果
</div>
<div class="card-body">
<div>耗时:<span id="checkTotalEplsed">--</span></div>
<table></table>
</div>
</div>
</div>
<a id="pathBase" href="../" hidden></a>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="../lib/jquery/jquery.min.js"></script>
<script src="../lib/twitter-bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="../lib/bootstrap-table/bootstrap-table.min.js"></script>
<script src="../lib/bootstrap-table/locale/bootstrap-table-zh-CN.min.js"></script>
<script src="../lib/longbow/longbow.common.js"></script>
<script src="../js/healths.js"></script>
</body>
</html>

View File

@ -0,0 +1,60 @@
$(function () {
var healthStatus = ["不健康", "亚健康", "健康"];
var StatusFormatter = function (value) {
return healthStatus[value];
};
var cate = { "db": "数据库", "file": "组件文件", "mem": "内存", "Gitee": "Gitee 接口", "gc": "垃圾回收器" };
var CategoryFormatter = function (value) {
return cate[value];
};
var $table = $('table').smartTable({
sidePagination: "client",
showToggle: false,
showRefresh: false,
showColumns: false,
columns: [
{ title: "分类", field: "Name", formatter: CategoryFormatter },
{ title: "描述", field: "Description" },
{ title: "异常信息", field: "Exception" },
{ title: "耗时", field: "Duration" },
{ title: "检查结果", field: "Status", formatter: StatusFormatter },
{
title: "明细数据", field: "Data", formatter: function (value, row, index) {
return '<button class="detail btn btn-info" data-trigger="focus" data-container="body" data-toggle="popover" data-placement="left"><i class="fa fa-info"></i><span>明细</span></button>';
},
events: {
'click .detail': function (e, value, row, index) {
if ($.isEmptyObject(row.Data)) return;
var $button = $(e.currentTarget);
if (!$button.data($.fn.popover.Constructor.DATA_KEY)) {
var content = $.map(row.Data, function (v, name) {
return $.format("<tr><td>{0}</td><td>{1}</td></tr>", name, v);
}).join("");
content = $.format('<div class="bootstrap-table"><div class="fixed-table-container"><div class="fixed-table-body"><table class="table table-bordered table-hover table-sm"><thead><tr><th><div class="th-inner"><b>检查项</b><div></th><th><div class="th-inner"><b>检查值</b></div></th></tr></thead><tbody>{0}</tbody></table></div></div></div>', content);
$button.popover({ title: cate[row.Name], html: true, content: content, placement: $(window).width() < 768 ? "bottom" : "left" });
}
$button.popover('show');
}
}
}
]
});
$table.bootstrapTable('showLoading');
$('#checkTotalEplsed').text('--');
$.bc({
url: 'healths',
callback: function (result) {
var data = $.map(result.Keys, function (name) {
return { Name: name, Duration: result.Report.Entries[name].Duration, Status: result.Report.Entries[name].Status, Exception: result.Report.Entries[name].Exception, Description: result.Report.Entries[name].Description, Data: result.Report.Entries[name].Data };
});
$table.bootstrapTable('hideLoading');
$table.bootstrapTable('load', data);
$('#checkTotalEplsed').text(result.Report.TotalDuration);
$.footer();
}
});
});