From 4e518097d454134ab934802ec383e53f9a601130 Mon Sep 17 00:00:00 2001 From: colin Date: Thu, 25 Dec 2025 16:48:24 +0800 Subject: [PATCH] feat(metrics): Add system info metrics module --- .../FodyWeavers.xml | 3 + .../FodyWeavers.xsd | 30 ++ .../LINGYUN.Abp.SystemInfo.HttpApi.csproj | 22 ++ .../SystemInfo/AbpSystemInfoHttpApiModule.cs | 17 + .../SystemInfo/Models/ComponentInfoModel.cs | 27 ++ .../SystemInfo/Models/ComponentKeyModel.cs | 20 + .../Abp/SystemInfo/Models/SystemInfoModel.cs | 9 + .../SystemInfoPermissionDefinitionProvider.cs | 20 + .../Permissions/SystemInfoPermissions.cs | 8 + .../Abp/SystemInfo/SystemInfoController.cs | 361 ++++++++++++++++++ .../LINGYUN/Abp/SystemInfo/Utils/GCMonitor.cs | 63 +++ .../Abp/SystemInfo/Utils/MemoryMonitor.cs | 54 +++ .../Abp/SystemInfo/Utils/ThreadMonitor.cs | 59 +++ ...LY.MicroService.Applications.Single.csproj | 3 + ...rviceApplicationsSingleModule.Configure.cs | 27 +- .../MicroServiceApplicationsSingleModule.cs | 8 +- .../appsettings.Development.json | 2 +- 17 files changed, 730 insertions(+), 3 deletions(-) create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xml create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xsd create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN.Abp.SystemInfo.HttpApi.csproj create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/AbpSystemInfoHttpApiModule.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentInfoModel.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentKeyModel.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/SystemInfoModel.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissionDefinitionProvider.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissions.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/SystemInfoController.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/GCMonitor.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/MemoryMonitor.cs create mode 100644 aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/ThreadMonitor.cs diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xml b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xml new file mode 100644 index 000000000..5d6962159 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xsd b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN.Abp.SystemInfo.HttpApi.csproj b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN.Abp.SystemInfo.HttpApi.csproj new file mode 100644 index 000000000..e43e91925 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN.Abp.SystemInfo.HttpApi.csproj @@ -0,0 +1,22 @@ + + + + + + + net9.0 + LINGYUN.Abp.SystemInfo.HttpApi + LINGYUN.Abp.SystemInfo.HttpApi + false + false + false + + + + + + + + + + diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/AbpSystemInfoHttpApiModule.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/AbpSystemInfoHttpApiModule.cs new file mode 100644 index 000000000..a36834de0 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/AbpSystemInfoHttpApiModule.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.SystemInfo; + +[DependsOn(typeof(AbpAspNetCoreMvcModule))] +public class AbpSystemInfoHttpApiModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpSystemInfoHttpApiModule).Assembly); + }); + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentInfoModel.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentInfoModel.cs new file mode 100644 index 000000000..216f36741 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentInfoModel.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace LINGYUN.Abp.SystemInfo.Models; +/// +/// 组件 +/// +public class ComponentInfoModel +{ + /// + /// 组件名称 + /// + public string Name { get; set; } + /// + /// 组件名称集合 + /// + public ComponentKeyModel[] Keys { get; set; } + /// + /// 组件状态集合 + /// + public Dictionary Details { get; set; } + public ComponentInfoModel(string name, ComponentKeyModel[] keys, Dictionary details) + { + Name = name; + Keys = keys; + Details = details; + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentKeyModel.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentKeyModel.cs new file mode 100644 index 000000000..6299bb203 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/ComponentKeyModel.cs @@ -0,0 +1,20 @@ +namespace LINGYUN.Abp.SystemInfo.Models; +/// +/// 组件名称 +/// +public class ComponentKeyModel +{ + /// + /// 组件名称 + /// + public string Name { get; set; } + /// + /// 组件显示名称 + /// + public string DisplayName { get; set; } + public ComponentKeyModel(string name, string displayName) + { + Name = name; + DisplayName = displayName; + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/SystemInfoModel.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/SystemInfoModel.cs new file mode 100644 index 000000000..e12a794af --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Models/SystemInfoModel.cs @@ -0,0 +1,9 @@ +namespace LINGYUN.Abp.SystemInfo.Models; + +public class SystemInfoModel +{ + /// + /// 组件状态集合 + /// + public ComponentInfoModel[] Components { get; set; } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissionDefinitionProvider.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissionDefinitionProvider.cs new file mode 100644 index 000000000..479c2b8a2 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissionDefinitionProvider.cs @@ -0,0 +1,20 @@ +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.SystemInfo.Permissions; + +public class SystemInfoPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var systemInfoGroup = context.AddGroup( + SystemInfoPermissions.GroupName, + new FixedLocalizableString("系统详情")); + + systemInfoGroup.AddPermission( + SystemInfoPermissions.Default, + new FixedLocalizableString("获取系统详情"), + Volo.Abp.MultiTenancy.MultiTenancySides.Host) + .WithProviders(ClientPermissionValueProvider.ProviderName); // 使用客户端授权 + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissions.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissions.cs new file mode 100644 index 000000000..702cf70d6 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Permissions/SystemInfoPermissions.cs @@ -0,0 +1,8 @@ +namespace LINGYUN.Abp.SystemInfo.Permissions; + +public static class SystemInfoPermissions +{ + public const string GroupName = "SystemInfo"; + + public const string Default = GroupName + ".Get"; +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/SystemInfoController.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/SystemInfoController.cs new file mode 100644 index 000000000..76e091c4c --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/SystemInfoController.cs @@ -0,0 +1,361 @@ +using DotNetCore.CAP; +using LINGYUN.Abp.SystemInfo.Models; +using LINGYUN.Abp.SystemInfo.Permissions; +using LINGYUN.Abp.SystemInfo.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc; + +namespace LINGYUN.Abp.SystemInfo; + +[Controller] +[Route("api/system-info")] +[Authorize(SystemInfoPermissions.Default)] +public class SystemInfoController : AbpControllerBase +{ + [HttpGet] + public async virtual Task GetSystemInfoAsync() + { + return new SystemInfoModel + { + Components = new ComponentInfoModel[] + { + GetSystemInfo(), + GetPerformanceInfo(), + GetRedisInfo(), + GetCapInfo(), + }, + }; + } + + private ComponentInfoModel GetSystemInfo() + { + var env = HttpContext.RequestServices.GetService(); + using var process = Process.GetCurrentProcess(); + + var systemKeys = new List + { + new ComponentKeyModel("sys_machine_name", "机器名称"), + new ComponentKeyModel("sys_environment", "运行环境"), + new ComponentKeyModel("sys_framework_version", ".NET版本"), + new ComponentKeyModel("sys_os_version", "操作系统版本"), + new ComponentKeyModel("sys_app_db_provider", "数据库"), + new ComponentKeyModel("sys_app_version", "应用版本"), + new ComponentKeyModel("sys_app_build_time", "编译时间"), + new ComponentKeyModel("sys_app_start_time", "启动时间"), + }; + + var systemDetails = new Dictionary + { + { "sys_machine_name", Environment.MachineName }, + { "sys_environment", env?.EnvironmentName ?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") }, + { "sys_framework_version", RuntimeInformation.FrameworkDescription }, + { "sys_os_version", RuntimeInformation.OSDescription }, + { "sys_app_db_provider", Environment.GetEnvironmentVariable("APPLICATION_DATABASE_PROVIDER") }, + { "sys_app_start_time", process.StartTime.ToString("yyyy-MM-dd HH:mm:ss") }, + }; + + try + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); + var assemblyName = assembly.GetName(); + var buildTime = GetBuildTime(assembly); + + systemDetails.Add("sys_app_version", fileVersionInfo.FileVersion ?? assemblyName.Version?.ToString() ?? "N/A"); + systemDetails.Add("sys_app_build_time", buildTime.HasValue ? buildTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : "N/A"); + } + catch + { + systemDetails.Add("sys_app_version", "N/A"); + systemDetails.Add("sys_app_build_time", "N/A"); + } + + return new ComponentInfoModel("系统", systemKeys.ToArray(), systemDetails); + } + + private ComponentInfoModel GetPerformanceInfo() + { + using var process = Process.GetCurrentProcess(); + + var memoryMetrics = new MemoryMonitor().GetMemoryMetrics(); + var threadMetrics = new ThreadMonitor().GetThreadMetrics(); + var gcMetrics = new GCMonitor().GetGCMetrics(); + + return new ComponentInfoModel( + "性能", + new ComponentKeyModel[] + { + new ComponentKeyModel("perf_total_memory", "总内存(MB)"), + new ComponentKeyModel("perf_working_set", "工作集内存(MB)"), + new ComponentKeyModel("perf_private_memory", "私有内存(MB)"), + new ComponentKeyModel("perf_gc_heap_size", "GC堆大小(MB)"), + new ComponentKeyModel("perf_gc0", "Gen0回收次数"), + new ComponentKeyModel("perf_gc1", "Gen1回收次数"), + new ComponentKeyModel("perf_gc2", "Gen2回收次数"), + new ComponentKeyModel("perf_active_total_memory", "总可用内存"), + new ComponentKeyModel("perf_memory_load", "内存负载"), + new ComponentKeyModel("perf_fragmented_bytes", "内存碎片大小"), + new ComponentKeyModel("perf_available_worker_threads", "可用工作线程数"), + new ComponentKeyModel("perf_available_completion_port_threads", "可用I/O线程数"), + new ComponentKeyModel("perf_max_worker_threads", "最大工作线程数"), + new ComponentKeyModel("perf_max_completion_port_threads", "最大I/O线程数"), + new ComponentKeyModel("perf_active_threads", "进程总线程数"), + new ComponentKeyModel("perf_thread_pool_thread_count", "线程池活动线程数"), + }, + new Dictionary + { + { "perf_total_memory", memoryMetrics.TotalMemory }, + { "perf_working_set", memoryMetrics.WorkingSet }, + { "perf_private_memory", memoryMetrics.PrivateMemory }, + { "perf_gc_heap_size", memoryMetrics.GCHeapSize }, + { "perf_gc0", gcMetrics.Gen0Collections }, + { "perf_gc1", gcMetrics.Gen1Collections }, + { "perf_gc2", gcMetrics.Gen2Collections }, + { "perf_active_total_memory", gcMetrics.TotalMemory }, + { "perf_memory_load", gcMetrics.MemoryLoad }, + { "perf_fragmented_bytes", gcMetrics.FragmentedBytes }, + { "perf_available_worker_threads", threadMetrics.AvailableWorkerThreads }, + { "perf_available_completion_port_threads", threadMetrics.AvailableCompletionPortThreads }, + { "perf_max_worker_threads", threadMetrics.MaxWorkerThreads }, + { "perf_max_completion_port_threads", threadMetrics.MaxCompletionPortThreads }, + { "perf_active_threads", threadMetrics.ActiveThreads }, + { "perf_thread_pool_thread_count", threadMetrics.ThreadPoolThreadCount }, + }); + } + + private ComponentInfoModel GetCapInfo() + { + var cap = HttpContext.RequestServices.GetService(); + var storage = HttpContext.RequestServices.GetService(); + var broker = HttpContext.RequestServices.GetService(); + + var capKeys = new List + { + new ComponentKeyModel("cap_status", "状态"), + new ComponentKeyModel("cap_version", "版本"), + new ComponentKeyModel("cap_storage", "持久化"), + new ComponentKeyModel("cap_transport", "传输"), + }; + var capDetails = new Dictionary + { + { "cap_status", cap != null ? "正常" : "未启用" }, + { "cap_version", cap != null ? cap.Version.Substring(0, 5) : "N/A" }, + { "cap_storage", storage != null ? storage.Name : "N/A" }, + { "cap_transport", broker != null ? broker.Name : "N/A" } + }; + + return new ComponentInfoModel("CAP组件", capKeys.ToArray(), capDetails); + } + + private ComponentInfoModel GetRedisInfo() + { + var redis = HttpContext.RequestServices.GetService(); + if (redis == null) + { + return new ComponentInfoModel( + "Redis组件", + new ComponentKeyModel[] + { + new ComponentKeyModel("redis_status", "状态") + }, + new Dictionary + { + { "redis_status", "未注册" }, + }); + } + var redisInfo = new Dictionary(); + + try + { + var server = redis.GetServer(redis.GetEndPoints().First()); + var serverInfo = server.Info(); + foreach (var section in serverInfo) + { + foreach (var item in section) + { + redisInfo[item.Key] = item.Value; + } + } + } + catch(Exception ex) + { + Logger.LogWarning("There is an exception in connecting to the redis server: {message}", ex.Message); + + return new ComponentInfoModel( + "Redis组件", + new ComponentKeyModel[] + { + new ComponentKeyModel("redis_status", "状态") + }, + new Dictionary + { + { "redis_status", "连接异常" }, + }); + } + + var uptimeSeconds = redisInfo.ContainsKey("uptime_in_seconds") ? long.Parse(redisInfo["uptime_in_seconds"]) : 0; + var days = Math.Floor((double)uptimeSeconds / 86400); + var hours = Math.Floor((double)uptimeSeconds % 86400 / 3600); + var minutes = Math.Floor((double)uptimeSeconds % 3600 / 60); + + var usedMemory = redisInfo.ContainsKey("used_memory") ? long.Parse(redisInfo["used_memory"]) : 0; + var maxMemory = redisInfo.ContainsKey("maxmemory") ? long.Parse(redisInfo["maxmemory"]) : 0; + var totalKeys = CalculateTotalKeys(redisInfo); + var expiresKeys = CalculateExpires(redisInfo); + var avgTtl = CalculateAvgTtl(redisInfo); + + var redisKeys = new List() + { + new ComponentKeyModel("redis_version", "版本"), + new ComponentKeyModel("redis_mode", "运行模式"), + new ComponentKeyModel("redis_os", "操作系统"), + new ComponentKeyModel("redis_uptime", "运行时间"), + new ComponentKeyModel("redis_used_cpu_sys", "系统CPU使用时间"), + new ComponentKeyModel("redis_used_cpu_user", "用户CPU使用时间"), + new ComponentKeyModel("redis_used_memory_mb", "使用内存(MB)"), + new ComponentKeyModel("redis_used_memory_rss_mb", "物理内存(MB)"), + new ComponentKeyModel("redis_used_memory_peak_mb", "内存使用峰值(MB)"), + new ComponentKeyModel("redis_maxmemory_mb", "最大内存限制(MB)"), + new ComponentKeyModel("redis_memory_usage_percent", "内存使用率(%)"), + new ComponentKeyModel("redis_mem_fragmentation_ratio", "内存碎片率"), + new ComponentKeyModel("redis_connected_clients", "已连接客户端数"), + new ComponentKeyModel("redis_blocked_clients", "阻塞客户端数"), + new ComponentKeyModel("redis_client_longest_output_list", "客户端最长输出列表"), + new ComponentKeyModel("redis_client_biggest_input_buf", "客户端最大输入缓冲区"), + new ComponentKeyModel("redis_maxclients", "最大客户端数"), + new ComponentKeyModel("redis_total_keys", "总键数量"), + new ComponentKeyModel("redis_expires_keys", "设置过期键数量"), + new ComponentKeyModel("redis_expired_keys", "已过期键数量"), + new ComponentKeyModel("redis_evicted_keys", "被驱逐键数量"), + new ComponentKeyModel("redis_avg_ttl_seconds", "平均TTL(秒)"), + }; + var redisDetails = new Dictionary() + { + { "redis_version", redisInfo.GetValueOrDefault("redis_version", "unknown") }, + { "redis_mode", redisInfo.GetValueOrDefault("redis_mode", "unknown") }, + { "redis_os", redisInfo.GetValueOrDefault("os", "unknown") }, + { "redis_uptime", $"{days}天{hours}时{minutes}分" }, + { "redis_used_cpu_sys", redisInfo.GetValueOrDefault("used_cpu_sys", "0") }, + { "redis_used_cpu_user", redisInfo.GetValueOrDefault("used_cpu_user", "0") }, + { "redis_used_memory_mb", Math.Round((double)usedMemory / 1024 / 1024, 2) }, + { "redis_used_memory_rss_mb", redisInfo.ContainsKey("used_memory_rss") ? + Math.Round(double.Parse(redisInfo["used_memory_rss"]) / 1024 / 1024, 2) : 0 }, + { "redis_used_memory_peak_mb", redisInfo.ContainsKey("used_memory_peak") ? + Math.Round(double.Parse(redisInfo["used_memory_peak"]) / 1024 / 1024, 2) : 0 }, + { "redis_maxmemory_mb", maxMemory > 0 ? Math.Round((double)maxMemory / 1024 / 1024, 2) : 0 }, + { "redis_memory_usage_percent", maxMemory > 0 ? Math.Round((double)usedMemory / maxMemory * 100, 2) : 0 }, + { "redis_mem_fragmentation_ratio", redisInfo.GetValueOrDefault("mem_fragmentation_ratio", "0") }, + { "redis_connected_clients", redisInfo.GetValueOrDefault("connected_clients", "0") }, + { "redis_blocked_clients", redisInfo.GetValueOrDefault("blocked_clients", "0") }, + { "redis_client_longest_output_list", redisInfo.GetValueOrDefault("client_longest_output_list", "0") }, + { "redis_client_biggest_input_buf", redisInfo.GetValueOrDefault("client_biggest_input_buf", "0") }, + { "redis_maxclients", redisInfo.GetValueOrDefault("maxclients", "unlimited") }, + { "redis_total_keys", totalKeys }, + { "redis_expires_keys", expiresKeys }, + { "redis_expired_keys", redisInfo.GetValueOrDefault("expired_keys", "0") }, + { "redis_evicted_keys", redisInfo.GetValueOrDefault("evicted_keys", "0") }, + { "redis_avg_ttl_seconds", avgTtl }, + }; + + return new ComponentInfoModel("Redis组件", redisKeys.ToArray(), redisDetails); + } + + private long CalculateTotalKeys(Dictionary redisInfo) + { + long total = 0; + + foreach (var item in redisInfo) + { + if (item.Key.StartsWith("db")) + { + var keysPart = item.Value.Split(',').FirstOrDefault(x => x.StartsWith("keys=")); + if (keysPart != null && long.TryParse(keysPart.Split('=')[1], out var count)) + { + total += count; + } + } + } + + return total; + } + + private long CalculateExpires(Dictionary redisInfo) + { + long expires = 0; + + foreach (var item in redisInfo) + { + if (item.Key.StartsWith("db")) + { + var expiresPart = item.Value.Split(',').FirstOrDefault(x => x.StartsWith("expires=")); + if (expiresPart != null && long.TryParse(expiresPart.Split('=')[1], out var count)) + { + expires += count; + } + } + } + + return expires; + } + + private long CalculateAvgTtl(Dictionary redisInfo) + { + long totalTtl = 0; + long dbCount = 0; + + foreach (var item in redisInfo) + { + if (item.Key.StartsWith("db")) + { + var ttlPart = item.Value.Split(',').FirstOrDefault(x => x.StartsWith("avg_ttl=")); + if (ttlPart != null && long.TryParse(ttlPart.Split('=')[1], out var ttl) && ttl > 0) + { + totalTtl += ttl; + dbCount++; + } + } + } + + return dbCount > 0 ? totalTtl / dbCount : -1; + } + + private DateTime? GetBuildTime(Assembly assembly) + { + try + { + var attribute = assembly.GetCustomAttribute(); + if (attribute != null && attribute.Key == "BuildTimestamp") + { + if (DateTime.TryParse(attribute.Value, out var buildTime)) + { + return buildTime; + } + } + + var fileInfo = new FileInfo(assembly.Location); + if (fileInfo.Exists) + { + return fileInfo.LastWriteTime; + } + + return null; + } + catch + { + return null; + } + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/GCMonitor.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/GCMonitor.cs new file mode 100644 index 000000000..b3f8cfa02 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/GCMonitor.cs @@ -0,0 +1,63 @@ +using System; + +namespace LINGYUN.Abp.SystemInfo.Utils; + +public class GCMetrics +{ + /// + /// Gen0回收次数 + /// + public int Gen0Collections { get; set; } + /// + /// Gen1回收次数 + /// + public int Gen1Collections { get; set; } + /// + /// Gen2回收次数 + /// + public int Gen2Collections { get; set; } + /// + /// 总可用内存 + /// + public long TotalMemory { get; set; } + /// + /// 内存负载 + /// + public long MemoryLoad { get; set; } + /// + /// 内存碎片大小 + /// + public long FragmentedBytes { get; set; } +} + +public class GCMonitor +{ + private int _lastGen0; + private int _lastGen1; + private int _lastGen2; + + public GCMetrics GetGCMetrics() + { + var currentGen0 = GC.CollectionCount(0); + var currentGen1 = GC.CollectionCount(1); + var currentGen2 = GC.CollectionCount(2); + + var metrics = new GCMetrics + { + Gen0Collections = currentGen0 - _lastGen0, + Gen1Collections = currentGen1 - _lastGen1, + Gen2Collections = currentGen2 - _lastGen2 + }; + + var gcInfo = GC.GetGCMemoryInfo(); + metrics.TotalMemory = gcInfo.TotalAvailableMemoryBytes; + metrics.MemoryLoad = gcInfo.MemoryLoadBytes; + metrics.FragmentedBytes = gcInfo.FragmentedBytes; + + _lastGen0 = currentGen0; + _lastGen1 = currentGen1; + _lastGen2 = currentGen2; + + return metrics; + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/MemoryMonitor.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/MemoryMonitor.cs new file mode 100644 index 000000000..892842cb7 --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/MemoryMonitor.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; + +namespace LINGYUN.Abp.SystemInfo.Utils; + +public class MemoryMetrics +{ + /// + /// 总物理内存 + /// + public long TotalMemory { get; set; } + /// + /// 工作集内存 + /// + public long WorkingSet { get; set; } + /// + /// 私有内存 + /// + public long PrivateMemory { get; set; } + /// + /// 虚拟内存 + /// + public long VirtualMemory { get; set; } + /// + /// GC堆大小 + /// + public long GCHeapSize { get; set; } +} + +public class MemoryMonitor +{ + // 转换为MB + const long MB = 1024 * 1024; + + public MemoryMetrics GetMemoryMetrics() + { + var process = Process.GetCurrentProcess(); + + return new MemoryMetrics + { + TotalMemory = GC.GetTotalMemory(false) / MB, + WorkingSet = process.WorkingSet64 / MB, + PrivateMemory = process.PrivateMemorySize64 / MB, + VirtualMemory = process.VirtualMemorySize64 / MB, + GCHeapSize = GetGCHeapSize() / MB + }; + } + + private long GetGCHeapSize() + { + var gcInfo = GC.GetGCMemoryInfo(); + return (long)gcInfo.HeapSizeBytes; + } +} diff --git a/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/ThreadMonitor.cs b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/ThreadMonitor.cs new file mode 100644 index 000000000..b1367d0ff --- /dev/null +++ b/aspnet-core/modules/system-info/LINGYUN.Abp.SystemInfo.HttpApi/LINGYUN/Abp/SystemInfo/Utils/ThreadMonitor.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using System.Threading; + +namespace LINGYUN.Abp.SystemInfo.Utils; + +public class ThreadMetrics +{ + /// + /// 可用工作线程数 + /// + public int AvailableWorkerThreads { get; set; } + /// + /// 可用I/O线程数 + /// + public int AvailableCompletionPortThreads { get; set; } + /// + /// 最大工作线程数 + /// + public int MaxWorkerThreads { get; set; } + /// + /// 最大I/O线程数 + /// + public int MaxCompletionPortThreads { get; set; } + /// + /// 进程总线程数 + /// + public int ActiveThreads { get; set; } + /// + /// 线程池活动线程数 + /// + public int ThreadPoolThreadCount { get; set; } +} + +public class ThreadMonitor +{ + public ThreadMetrics GetThreadMetrics() + { + ThreadPool.GetAvailableThreads( + out var availableWorkerThreads, + out var availableCompletionPortThreads); + ThreadPool.GetMaxThreads( + out var maxWorkerThreads, + out var maxCompletionPortThreads); + + // 获取进程线程数(近似活动线程数) + var process = Process.GetCurrentProcess(); + var threads = process.Threads; + + return new ThreadMetrics + { + AvailableWorkerThreads = availableWorkerThreads, + AvailableCompletionPortThreads = availableCompletionPortThreads, + MaxWorkerThreads = maxWorkerThreads, + MaxCompletionPortThreads = maxCompletionPortThreads, + ActiveThreads = threads.Count, + ThreadPoolThreadCount = ThreadPool.ThreadCount + }; + } +} diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj index 54f1418d6..21e51f851 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj +++ b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj @@ -7,6 +7,7 @@ enable LY.MicroService.Applications.Single enable + 9.3.6.2 @@ -22,6 +23,7 @@ + @@ -247,6 +249,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs index b4c33806c..b77959e79 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.SignalR; using VoloAbpExceptionHandlingOptions = Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingOptions; namespace LY.MicroService.Applications.Single; @@ -135,6 +136,28 @@ public partial class MicroServiceApplicationsSingleModule } } + private void PreConfigureSignalR(IConfiguration configuration) + { + PreConfigure(builder => + { + var redisEnabled = configuration["SignalR:Redis:IsEnabled"]; + if (redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled)) + { + builder.AddStackExchangeRedis(redis => + { + var redisConfiguration = configuration["SignalR:Redis:Configuration"]; + if (!redisConfiguration.IsNullOrEmpty()) + { + redis.ConnectionFactory = async (writer) => + { + return await ConnectionMultiplexer.ConnectAsync(redisConfiguration); + }; + } + }); + } + }); + } + private void PreConfigureQuartz(IConfiguration configuration) { PreConfigure(options => @@ -522,7 +545,7 @@ public partial class MicroServiceApplicationsSingleModule }); } - private void ConfigureCaching(IConfiguration configuration) + private void ConfigureCaching(IServiceCollection services, IConfiguration configuration) { Configure(options => { @@ -535,6 +558,8 @@ public partial class MicroServiceApplicationsSingleModule options.ConfigurationOptions = redisConfig; options.InstanceName = configuration["Redis:InstanceName"]; }); + + services.AddSingleton(sp => ConnectionMultiplexer.Connect(configuration["Redis:Configuration"])); } private void ConfigureMultiTenancy(IConfiguration configuration) diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs index ef5ac78d9..b41c08a84 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs @@ -1,3 +1,5 @@ +using LINGYUN.Abp.SystemInfo; + namespace LY.MicroService.Applications.Single; [DependsOn( @@ -382,6 +384,9 @@ namespace LY.MicroService.Applications.Single; typeof(AbpMailKitModule), typeof(AbpAutofacModule), + // 单体应用系统状态接口 + typeof(AbpSystemInfoHttpApiModule), + // MySql typeof(SingleMigrationsEntityFrameworkCoreMySqlModule), // SqlServer @@ -402,6 +407,7 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule PreConfigureApp(configuration); PreConfigureCAP(configuration); PreConfigureQuartz(configuration); + PreConfigureSignalR(configuration); PreConfigureAuthServer(configuration); PreConfigureElsa(context.Services, configuration); PreConfigureCertificate(configuration, hostingEnvironment); @@ -424,7 +430,6 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule ConfigureVirtualFileSystem(); ConfigureEntityDataProtected(); ConfigureUrls(configuration); - ConfigureCaching(configuration); ConfigureAuditing(configuration); ConfigureIdentity(configuration); ConfigureDbContext(configuration); @@ -440,6 +445,7 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule ConfigureNotificationManagement(configuration); ConfigureCors(context.Services, configuration); ConfigureSwagger(context.Services, configuration); + ConfigureCaching(context.Services, configuration); ConfigureDistributedLock(context.Services, configuration); ConfigureKestrelServer(configuration, hostingEnvironment); ConfigureSecurity(context.Services, configuration, hostingEnvironment.IsDevelopment()); diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/appsettings.Development.json b/aspnet-core/services/LY.MicroService.Applications.Single/appsettings.Development.json index 583184e92..d8e058b4a 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/appsettings.Development.json +++ b/aspnet-core/services/LY.MicroService.Applications.Single/appsettings.Development.json @@ -93,7 +93,7 @@ }, "Redis": { "IsEnabled": true, - "Configuration": "127.0.0.1,defaultDatabase=15", + "Configuration": "127.0.0.1,defaultDatabase=15,allowAdmin=true", "InstanceName": "LINGYUN.Abp.Application" }, "Features": {