feat: 增加健康检查

This commit is contained in:
Argo-Tianyi 2022-01-02 21:46:31 +08:00
parent 86522a81a4
commit 441cdfb336
10 changed files with 354 additions and 4 deletions

View File

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using System.Security.Claims; using System.Security.Claims;
namespace Bootstrap.Admin.Controllers namespace BootstrapAdmin.Web.Controllers
{ {
/// <summary> /// <summary>
/// Account controller. /// Account controller.

View File

@ -0,0 +1,109 @@
using BootstrapAdmin.Web.HealthChecks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text.RegularExpressions;
using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute;
namespace BootstrapAdmin.Web.Controllers.Api
{
/// <summary>
/// Gitee 网站信息接口类
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
[AllowAnonymous]
public class GiteeController : ControllerBase
{
/// <summary>
/// 获取 Gitee 网站 Issues 信息
/// </summary>
/// <param name="client"></param>
/// <param name="userName"></param>
/// <param name="repoName"></param>
/// <param name="label"></param>
/// <param name="color"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult> Issues([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
{
var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/issues");
var regex = Regex.Matches(content, "<div class='ui mini circular label'>([\\d]+)</div>", RegexOptions.IgnoreCase);
var labels = new string[] { "open", "progressing", "closed", "rejected" };
var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}");
var msg = string.Join(" ", result);
color = msg.StartsWith("open 0 progressing 0", StringComparison.OrdinalIgnoreCase) ? "success" : color;
return new JsonResult(new { schemaVersion = 1, label, message = msg, color });
}
/// <summary>
/// 获取 Gitee 网站 Pulls 信息
/// </summary>
/// <param name="client"></param>
/// <param name="userName"></param>
/// <param name="repoName"></param>
/// <param name="label"></param>
/// <param name="color"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult> Pulls([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
{
var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/pulls") ?? "";
var regex = Regex.Matches(content, "<div class='ui mini circular label'>([\\d]+)</div>", RegexOptions.IgnoreCase);
var labels = new string[] { "open", "merged", "closed" };
var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}");
var msg = string.Join(" ", result);
return new JsonResult(new { schemaVersion = 1, label, message = msg, color });
}
/// <summary>
/// 获取 Gitee 网站 Releases 信息
/// </summary>
/// <param name="client"></param>
/// <param name="userName"></param>
/// <param name="repoName"></param>
/// <param name="label"></param>
/// <param name="color"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult> Releases([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
{
var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/releases") ?? "";
var regex = Regex.Match(content, $"<a href=\"/{userName}/{repoName}/releases/([^\\s]+)\" target=\"_blank\">", RegexOptions.IgnoreCase);
var msg = regex?.Groups[1].Value ?? "unknown";
return new JsonResult(new { schemaVersion = 1, label, message = msg, color });
}
/// <summary>
/// 获取 Gitee 网站 Builds 信息
/// </summary>
/// <param name="client"></param>
/// <param name="userName"></param>
/// <param name="projName"></param>
/// <param name="branchName"></param>
/// <param name="label"></param>
/// <param name="color"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult> Builds([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "ArgoZhang", [FromQuery] string? projName = "bootstrapadmin", [FromQuery] string? branchName = "master", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange")
{
var content = await client.HttpClient.GetFromJsonAsync<AppveyorBuildResult>($"https://ci.appveyor.com/api/projects/{userName}/{projName}/branch/{branchName}", new CancellationTokenSource(10000).Token);
return new JsonResult(new { schemaVersion = 1, label, message = content?.Build.Version ?? "unknown", color });
}
private class AppveyorBuildResult
{
/// <summary>
/// Appveyor 编译版本实例
/// </summary>
public Build Build { get; set; } = new Build();
}
private class Build
{
/// <summary>
/// Build 版本信息
/// </summary>
public string Version { get; set; } = "";
}
}
}

View File

@ -10,8 +10,11 @@
/// </summary> /// </summary>
/// <param name="builder"></param> /// <param name="builder"></param>
/// <returns></returns> /// <returns></returns>
public static IApplicationBuilder UseBootstrapBlazorAdmin(this IApplicationBuilder builder) public static WebApplication UseBootstrapBlazorAdmin(this WebApplication builder)
{ {
// 开启健康检查
builder.MapBootstrapHealthChecks();
builder.UseAuthentication(); builder.UseAuthentication();
builder.UseAuthorization(); builder.UseAuthorization();

View File

@ -4,6 +4,8 @@ using BootstrapAdmin.Web.Services.SMS.Tencent;
using BootstrapAdmin.Web.Utils; using BootstrapAdmin.Web.Utils;
//using Microsoft.EntityFrameworkCore; //using Microsoft.EntityFrameworkCore;
using System.Text; using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
@ -19,16 +21,21 @@ namespace Microsoft.Extensions.DependencyInjection
public static IServiceCollection AddBootstrapBlazorAdmin(this IServiceCollection services) public static IServiceCollection AddBootstrapBlazorAdmin(this IServiceCollection services)
{ {
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
services.AddLogging(logging => logging.AddFileLogger().AddCloudLogger().AddDBLogger(ExceptionsHelper.Log)); services.AddLogging(logging => logging.AddFileLogger().AddCloudLogger().AddDBLogger(ExceptionsHelper.Log));
services.AddCors(); services.AddCors();
services.AddResponseCompression(); services.AddResponseCompression();
// 增加编码服务
services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
// 增加 健康检查服务
services.AddAdminHealthChecks();
// 增加 BootstrapBlazor 组件 // 增加 BootstrapBlazor 组件
services.AddBootstrapBlazor(); services.AddBootstrapBlazor();
// 增加手机短信服务 // 增加手机短信服务
services.AddScoped<ISMSProvider, TencentSMSProvider>(); services.AddSingleton<ISMSProvider, TencentSMSProvider>();
// 增加认证授权服务 // 增加认证授权服务
services.AddBootstrapAdminSecurity<AdminService>(); services.AddBootstrapAdminSecurity<AdminService>();

View File

@ -0,0 +1,125 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace BootstrapAdmin.Web.HealthChecks;
/// <summary>
/// 数据库检查类
/// </summary>
class DBHealthCheck : IHealthCheck
{
private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor _httpContextAccessor;
private static readonly Func<IConfiguration, string, string?> ConnectionStringResolve = (c, name) => string.IsNullOrEmpty(name)
? c.GetSection("ConnectionStrings").GetChildren().FirstOrDefault()?.Value
: c.GetConnectionString(name);
/// <summary>
/// 构造函数
/// </summary>
/// <param name="configuration"></param>
/// <param name="httpContextAccessor"></param>
public DBHealthCheck(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 异步检查方法
/// </summary>
/// <param name="context"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
//var db = _configuration.GetSection("DB").GetChildren()
// .Select(config => new DbOption()
// {
// Enabled = bool.TryParse(config["Enabled"], out var en) ? en : false,
// ProviderName = config["ProviderName"],
// Widget = config["Widget"],
// ConnectionString = ConnectionStringResolve(config.GetSection("ConnectionStrings").Exists() ? config : _configuration, string.Empty)
// }).FirstOrDefault(i => i.Enabled) ?? new DbOption()
// {
// Enabled = true,
// ProviderName = Longbow.Data.DatabaseProviderType.SqlServer.ToString(),
// Widget = typeof(User).Assembly.FullName,
// ConnectionString = Longbow.Data.DbManager.GetConnectionString()
// };
//// 检查 当前用户 账户权限
//var loginUser = _httpContextAccessor.HttpContext?.User.Identity?.Name;
//var userName = loginUser ?? "Admin";
//var dictsCount = 0;
//var menusCount = 0;
//var roles = string.Empty;
//var displayName = string.Empty;
//var healths = false;
//Exception? error = null;
//try
//{
// var user = UserHelper.RetrieveUserByUserName(userName);
// displayName = user?.DisplayName ?? string.Empty;
// roles = string.Join(",", RoleHelper.RetrievesByUserName(userName) ?? new string[0]);
// menusCount = MenuHelper.RetrieveMenusByUserName(userName)?.Count() ?? 0;
// dictsCount = DictHelper.RetrieveDicts()?.Count() ?? 0;
// healths = user != null && !string.IsNullOrEmpty(roles) && menusCount > 0 && dictsCount > 0;
// // 检查数据库是否可写
// var dict = new BootstrapDict()
// {
// Category = "DB-Check",
// Name = "WriteTest",
// Code = "1"
// };
// if (DictHelper.Save(dict) && !string.IsNullOrEmpty(dict.Id)) DictHelper.Delete(new string[] { dict.Id });
//}
//catch (Exception ex)
//{
// error = ex;
//}
//var data = new Dictionary<string, object>()
//{
// { "ConnectionString", db.ConnectionString ?? string.Empty },
// { "Reference", DbContextManager.Create<Dict>()?.GetType().Assembly.FullName ?? db.Widget ?? string.Empty },
// { "DbType", db?.ProviderName ?? string.Empty },
// { "Dicts", dictsCount },
// { "LoginName", userName },
// { "DisplayName", displayName },
// { "Roles", roles },
// { "Navigations", menusCount }
//};
//if (string.IsNullOrEmpty(db?.ConnectionString))
//{
// // 未启用连接字符串
// data["ConnectionString"] = "未配置数据库连接字符串";
// return Task.FromResult(HealthCheckResult.Unhealthy("Error", null, data));
//}
//if (DbContextManager.Exception != null) error = DbContextManager.Exception;
//if (error != null)
//{
// data.Add("Exception", error.Message);
// if (error.Message.Contains("SQLite Error 8: 'attempt to write a readonly database'.")) data.Add("解决办法", "更改数据库文件为可读,并授予进程可写权限");
// if (error.Message.Contains("Could not load", StringComparison.OrdinalIgnoreCase)) data.Add("解决办法", "Nuget 引用相对应的数据库驱动 dll");
// // UNDONE: Json 序列化循环引用导致异常 NET 5.0 修复此问题
// // 目前使用 new Exception() 临时解决
// return Task.FromResult(HealthCheckResult.Unhealthy("Error", new Exception(error.Message), data));
//}
//return healths ? Task.FromResult(HealthCheckResult.Healthy("Ok", data)) : Task.FromResult(HealthCheckResult.Degraded("Failed", null, data));
return Task.FromResult(HealthCheckResult.Healthy("Ok"));
}
private class DbOption
{
public bool Enabled { get; set; }
public string? ProviderName { get; set; }
public string? Widget { get; set; }
public string? ConnectionString { get; set; }
}
}

View File

@ -0,0 +1,23 @@
namespace BootstrapAdmin.Web.HealthChecks;
/// <summary>
/// Gitee HttpClient 操作类
/// </summary>
public class GiteeHttpClient
{
/// <summary>
/// HttpClient 实例
/// </summary>
public HttpClient HttpClient { get; private set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="client"></param>
public GiteeHttpClient(HttpClient client)
{
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Connection.Add("keep-alive");
HttpClient = client;
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Diagnostics;
namespace BootstrapAdmin.Web.HealthChecks;
/// <summary>
/// Gitee 接口检查器
/// </summary>
class GiteeHttpHealthCheck : IHealthCheck
{
private readonly GiteeHttpClient _client;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="client"></param>
/// <param name="accessor"></param>
public GiteeHttpHealthCheck(GiteeHttpClient client, IHttpContextAccessor accessor)
{
_client = client;
_client.HttpClient.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 Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var urls = new string[] { "Issues", "Pulls", "Releases", "Builds" };
var data = new Dictionary<string, object>();
Task.WaitAll(urls.Select(url => Task.Run(async () =>
{
var sw = Stopwatch.StartNew();
Exception? error = null;
object? result = null;
try
{
result = await _client.HttpClient.GetFromJsonAsync<object>($"/api/Gitee/{url}", cancellationToken);
}
catch (Exception ex) { error = ex; }
sw.Stop();
data.Add(url, error == null
? $"{result} Elapsed: {sw.Elapsed}"
: $"Elapsed: {sw.Elapsed} Exception: {error}");
})).ToArray(), cancellationToken);
return Task.FromResult(HealthCheckResult.Healthy("Ok", data));
}
}

View File

@ -0,0 +1,26 @@
using BootstrapAdmin.Web.HealthChecks;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// 健康检查扩展类
/// </summary>
static class HealthChecksBuilderExtensions
{
/// <summary>
/// 添加 BootstrapAdmin 健康检查
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddAdminHealthChecks(this IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddHttpClient<GiteeHttpClient>();
var builder = services.AddHealthChecks();
builder.AddCheck<DBHealthCheck>("db");
builder.AddBootstrapAdminHealthChecks();
builder.AddCheck<GiteeHttpHealthCheck>("Gitee");
return services;
}
}

View File

@ -0,0 +1 @@
@page "/Admin/Healths"

View File

@ -0,0 +1,5 @@
namespace BootstrapAdmin.Web.Pages.Admin;
public partial class Healths
{
}