!20 feat(#I10L13): 提交健康检查功能
Merge pull request !20 from Argo/dev-Health
This commit is contained in:
commit
04b146369f
|
@ -71,6 +71,12 @@ namespace Bootstrap.Admin.Controllers
|
|||
/// <returns></returns>
|
||||
public ActionResult FAIcon() => View(new NavigatorBarModel(this));
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ActionResult Healths() => View(new NavigatorBarModel(this));
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@ namespace Bootstrap.Admin
|
|||
services.AddSwagger();
|
||||
services.AddButtonAuthorization();
|
||||
services.AddDemoTask();
|
||||
services.AddHealthChecks().AddBootstrapAdminHealthChecks();
|
||||
services.AddMvc(options =>
|
||||
{
|
||||
options.Filters.Add<BootstrapAdminAuthorizeFilter>();
|
||||
|
@ -107,6 +108,7 @@ namespace Bootstrap.Admin
|
|||
app.UseHttpsRedirection();
|
||||
app.UseResponseCompression();
|
||||
app.UseStaticFiles();
|
||||
app.UseBootstrapHealthChecks();
|
||||
app.UseBootstrapAdminAuthentication(RoleHelper.RetrievesByUserName, RoleHelper.RetrievesByUrl, AppHelper.RetrievesByUserName);
|
||||
app.UseOnlineUsers(callback: TraceHelper.Save);
|
||||
app.UseCacheManager();
|
||||
|
|
|
@ -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>
|
|
@ -28,8 +28,12 @@ img {
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
a, a:hover, a:focus {
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a, a:hover, a:focus {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
color: #371ed4;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -709,11 +709,19 @@ input.pending {
|
|||
min-width: unset;
|
||||
}
|
||||
|
||||
.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^="bottom"] .popover-header::before {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: 320px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popover-body .bootstrap-table {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -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">×</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>
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue