feat: 增加健康检查
This commit is contained in:
parent
86522a81a4
commit
441cdfb336
|
@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Bootstrap.Admin.Controllers
|
||||
namespace BootstrapAdmin.Web.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <param name="builder"></param>
|
||||
/// <returns></returns>
|
||||
public static IApplicationBuilder UseBootstrapBlazorAdmin(this IApplicationBuilder builder)
|
||||
public static WebApplication UseBootstrapBlazorAdmin(this WebApplication builder)
|
||||
{
|
||||
// 开启健康检查
|
||||
builder.MapBootstrapHealthChecks();
|
||||
|
||||
builder.UseAuthentication();
|
||||
builder.UseAuthorization();
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ using BootstrapAdmin.Web.Services.SMS.Tencent;
|
|||
using BootstrapAdmin.Web.Utils;
|
||||
//using Microsoft.EntityFrameworkCore;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
|
@ -19,16 +21,21 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
public static IServiceCollection AddBootstrapBlazorAdmin(this IServiceCollection services)
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
services.AddLogging(logging => logging.AddFileLogger().AddCloudLogger().AddDBLogger(ExceptionsHelper.Log));
|
||||
services.AddCors();
|
||||
services.AddResponseCompression();
|
||||
|
||||
// 增加编码服务
|
||||
services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All));
|
||||
|
||||
// 增加 健康检查服务
|
||||
services.AddAdminHealthChecks();
|
||||
|
||||
// 增加 BootstrapBlazor 组件
|
||||
services.AddBootstrapBlazor();
|
||||
|
||||
// 增加手机短信服务
|
||||
services.AddScoped<ISMSProvider, TencentSMSProvider>();
|
||||
services.AddSingleton<ISMSProvider, TencentSMSProvider>();
|
||||
|
||||
// 增加认证授权服务
|
||||
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