feat: 增加健康检查
This commit is contained in:
parent
86522a81a4
commit
441cdfb336
|
@ -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.
|
||||||
|
|
|
@ -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; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
@page "/Admin/Healths"
|
|
@ -0,0 +1,5 @@
|
||||||
|
namespace BootstrapAdmin.Web.Pages.Admin;
|
||||||
|
|
||||||
|
public partial class Healths
|
||||||
|
{
|
||||||
|
}
|
Loading…
Reference in New Issue