🍊 feat(Http): 新增远程请求配置以及请求日志

This commit is contained in:
喵你个汪呀 2025-08-20 09:10:10 +08:00
parent d4a7dd7856
commit 91bf37c03d
17 changed files with 1264 additions and 0 deletions

View File

@ -0,0 +1,27 @@
{
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
"HttpRemotes": {
"Proxy": { //
"Encrypt": false,
"MaxRetries": 3,
"Address": "http://route.xiongmaodaili.com/xxxxxxxx",
"Account": "",
"Password": ""
},
"HttpBin": { //
"EnabledLog": true,
"UseCookies": false,
"EnabledProxy": false,
"HttpName": "HTTP_BIN",
"BaseAddress": "http://httpbin.org/ip",
"Headers": {
"Connection": "Keep-Alive",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
},
"Timeout": 5
}
}
}

View File

@ -141,6 +141,11 @@ public class ConfigConst
/// </summary>
public const string SysOnlineNotice = "sys_online_notice";
/// <summary>
/// 请求日志
/// </summary>
public const string SysLogHttp = "sys_http_log";
/// <summary>
/// 支付宝授权页面地址
/// </summary>

View File

@ -0,0 +1,97 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using System.Net;
namespace Admin.NET.Core;
/// <summary>
/// Http请求日志表
/// </summary>
[SugarTable(null, "Http请求日志表")]
[SysTable]
[LogTable]
public partial class SysLogHttp : EntityBaseId
{
/// <summary>
/// 请求方式
/// </summary>
[SugarColumn(ColumnDescription = "请求方式", Length = 8)]
[MaxLength(8)]
public string? HttpMethod { get; set; }
/// <summary>
/// 是否成功
/// </summary>
[SugarColumn(ColumnDescription = "是否成功")]
public YesNoEnum? IsSuccessStatusCode { get; set; }
/// <summary>
/// 请求地址
/// </summary>
[SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? RequestUrl { get; set; }
/// <summary>
/// 请求头
/// </summary>
[SugarColumn(ColumnDescription = "请求头", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? RequestHeaders { get; set; }
/// <summary>
/// 请求体
/// </summary>
[SugarColumn(ColumnDescription = "请求体", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? RequestBody { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
[SugarColumn(ColumnDescription = "响应状态码")]
public HttpStatusCode? StatusCode { get; set; }
/// <summary>
/// 响应头
/// </summary>
[SugarColumn(ColumnDescription = "响应头", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? ResponseHeaders { get; set; }
/// <summary>
/// 响应体
/// </summary>
[SugarColumn(ColumnDescription = "响应体", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? ResponseBody { get; set; }
/// <summary>
/// 异常信息
/// </summary>
[SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string? Exception { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[SugarColumn(ColumnDescription = "开始时间")]
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
[SugarColumn(ColumnDescription = "结束时间")]
public DateTime? EndTime { get; set; }
/// <summary>
/// 耗时(毫秒)
/// </summary>
[SugarColumn(ColumnDescription = "耗时(毫秒)")]
public long? Elapsed { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间")]
public DateTime CreateTime { get; set; }
}

View File

@ -18,6 +18,20 @@ public class AppEventSubscriber : IEventSubscriber, ISingleton, IDisposable
_serviceScope = scopeFactory.CreateScope();
}
/// <summary>
/// 增加Http日志
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[EventSubscribe(nameof(CreateHttpLog))]
public async Task CreateHttpLog(EventHandlerExecutingContext context)
{
var db = SqlSugarSetup.ITenant.IsAnyConnection(SqlSugarConst.LogConfigId)
? SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.LogConfigId)
: SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.MainConfigId);
await db.CopyNew().Insertable(context.GetPayload<SysLogHttp>()).ExecuteCommandAsync();
}
/// <summary>
/// 增加异常日志
/// </summary>

View File

@ -0,0 +1,37 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// Http远程服务扩展
/// </summary>
public static class HttpRemotesExtension {
/// <summary>
/// 添加Http远程服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddHttpRemoteClientService(this IServiceCollection services)
{
var options = App.GetOptions<HttpRemotesOptions>();
foreach (var prop in options.GetType().GetProperties())
{
if (prop.GetValue(options) is not HttpRemoteItem opt) continue;
services.AddHttpClient(opt.HttpName, client =>
{
client.BaseAddress = new Uri(opt.BaseAddress);
client.Timeout = TimeSpan.FromSeconds(opt.Timeout);
foreach (var kv in opt.Headers) client.DefaultRequestHeaders.Add(kv.Key, kv.Value);
})
.AddHttpMessageHandler<HttpLoggingHandler>()
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler {
UseCookies = opt.UseCookies
});
}
return services;
}
}

View File

@ -0,0 +1,77 @@
////////////////////////////////////////////////////////////////////
// 作者Ir0nMax
// 时间2025/03/05
// 邮箱ir0nmax@wogof.com
////////////////////////////////////////////////////////////////////
namespace Admin.NET.Core;
/// <summary>
/// http日志处理
/// </summary>
public class HttpLoggingHandler : DelegatingHandler, ITransient
{
private const string HttpName = "__HTTP_CLIENT_NAME__";
private readonly Dictionary<string, bool> _enabledLogMap;
private readonly SysConfigService _sysConfigService;
private readonly IEventPublisher _eventPublisher;
public HttpLoggingHandler(IEventPublisher eventPublisher, SysConfigService sysConfigService, IOptions<HttpRemotesOptions> options)
{
_eventPublisher = eventPublisher;
HttpRemotesOptions httpRemotesOptions = options.Value;
_sysConfigService = sysConfigService;
_enabledLogMap = typeof(HttpRemotesOptions).GetProperties()
.Where(u => u.PropertyType == typeof(HttpRemoteItem))
.ToDictionary(u => u.GetValue(httpRemotesOptions) is HttpRemoteItem opt ? opt.HttpName : "",
u => u.GetValue(httpRemotesOptions) is HttpRemoteItem { EnabledLog: true });
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// 判断全局Http日志开关
var enabledLog = await _sysConfigService.GetConfigValueByCode<bool>(ConfigConst.SysLogHttp);
if (!enabledLog) return await base.SendAsync(request, cancellationToken);
// 判断当前配置日志开关
request.Options.TryGetValue<string>(HttpName, out var clientName);
if (!string.IsNullOrWhiteSpace(clientName)) enabledLog = _enabledLogMap.GetOrDefault(clientName);
if (!enabledLog) return await base.SendAsync(request, cancellationToken);
var sysLogHttp = new SysLogHttp();
sysLogHttp.HttpMethod = request.Method.Method;
sysLogHttp.RequestUrl = request.RequestUri?.ToString();
sysLogHttp.RequestHeaders = request.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson();
if (request.Content != null) sysLogHttp.RequestBody = await request.Content.ReadAsStringAsync(cancellationToken);
sysLogHttp.StartTime = DateTime.Now;
var stopWatch = Stopwatch.StartNew();
try
{
var response = await base.SendAsync(request, cancellationToken);
stopWatch.Stop();
sysLogHttp.EndTime = DateTime.Now;
sysLogHttp.ResponseHeaders = response.Headers.ToDictionary(u => u.Key, u => u.Value.Join(";")).ToJson();
sysLogHttp.IsSuccessStatusCode = response.IsSuccessStatusCode ? YesNoEnum.Y : YesNoEnum.N;
sysLogHttp.StatusCode = response.StatusCode;
sysLogHttp.ResponseBody = await response.Content.ReadAsStringAsync(cancellationToken);
return response;
}
catch (Exception ex)
{
stopWatch.Stop();
sysLogHttp.EndTime = DateTime.Now;
sysLogHttp.IsSuccessStatusCode = YesNoEnum.N;
sysLogHttp.Exception = JSON.Serialize(SerializableException.FromException(ex));
throw;
}
finally
{
sysLogHttp.Elapsed = stopWatch.ElapsedMilliseconds;
await _eventPublisher.PublishAsync(nameof(AppEventSubscriber.CreateHttpLog), sysLogHttp, cancellationToken);
}
}
}

View File

@ -0,0 +1,95 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 远程请求配置
/// </summary>
public sealed class HttpRemotesOptions : IConfigurableOptions
{
/// <summary>
/// 代理配置
/// </summary>
public HttpProxyOption Proxy { get; set; }
/// <summary>
/// 获取Ip地址接口
/// </summary>
public HttpRemoteItem HttpBin { get; set; }
}
/// <summary>
/// 代理配置
/// </summary>
public class HttpProxyOption
{
/// <summary>
/// 是否加密
/// </summary>
public bool Encrypt { get; set; }
/// <summary>
/// 最大重试次数
/// </summary>
public string MaxRetries { get; set; }
/// <summary>
/// 代理服务器地址
/// </summary>
public string Address { get; set; }
/// <summary>
/// 代理服务器认证账号
/// </summary>
public string Account { get; set; }
/// <summary>
/// 代理服务器认证账号密码
/// </summary>
public string Password { get; set; }
}
/// <summary>
/// 远程请求配置项
/// </summary>
public sealed class HttpRemoteItem
{
/// <summary>
/// 是否启用日志
/// </summary>
public bool EnabledLog { get; set; }
/// <summary>
/// 是否启用代理
/// </summary>
public bool EnabledProxy { get; set; }
/// <summary>
/// 服务名称
/// </summary>
public string HttpName { get; set; }
/// <summary>
/// 服务地址
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// 请求超时时间
/// </summary>
public int Timeout { get; set; }
/// <summary>
/// 是否自动处理Cookie
/// </summary>
public bool UseCookies { get; set; }
/// <summary>
/// 请求头
/// </summary>
public Dictionary<string, string> Headers { get; set; }
}

View File

@ -42,6 +42,7 @@ public class SysConfigSeedData : ISqlSugarEntitySeedData<SysConfig>
new SysConfig{ Id=1300000000281, Name="多语言切换", Code=ConfigConst.SysI18NSwitch, Value="True", SysFlag=YesNoEnum.Y, Remark="是否显示多语言切换按钮", OrderNo=230, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
new SysConfig{ Id=1300000000291, Name="闲置超时时间", Code=ConfigConst.SysIdleTimeout, Value="0", SysFlag=YesNoEnum.Y, Remark="闲置超时时间超时强制退出0 表示不限制", OrderNo=240, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2024-12-20 00:00:00") },
new SysConfig{ Id=1300000000301, Name="开启上线通知", Code=ConfigConst.SysOnlineNotice, Value="True", SysFlag=YesNoEnum.Y, Remark="开启用户上线、下线通知", OrderNo=250, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-06-06 00:00:00") },
new SysConfig{ Id=1300000000302, Name="开启Http日志", Code=ConfigConst.SysLogHttp, Value="True", SysFlag=YesNoEnum.Y, Remark="开启远程请求日志记录功能", OrderNo=250, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-06-06 00:00:00") },
new SysConfig{ Id=1300000000999, Name="系统版本号", Code=ConfigConst.SysVersion, Value="v1.0", SysFlag=YesNoEnum.Y, Remark= "系统版本号", OrderNo=1000, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2025-04-10 00:00:00") },
];

View File

@ -212,6 +212,9 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
new SysMenu{ Id=1310000000551, Pid=1310000000501, Title="消息日志", Path="/log/logmsg", Name="sysLogMsg", Component="/system/log/logmsg/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 },
new SysMenu{ Id=1310000000552, Pid=1310000000551, Title="查询", Permission="sysLogMsg/page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
new SysMenu{ Id=1310000000553, Pid=1310000000551, Title="清空", Permission="sysLogMsg/clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
new SysMenu{ Id=1310000000561, Pid=1310000000501, Title="请求日志", Path="/log/loghttp", Name="sysLogHttp", Component="/system/log/loghttp/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 },
new SysMenu{ Id=1310000000562, Pid=1310000000561, Title="查询", Permission="sysLogHttp/page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
new SysMenu{ Id=1310000000563, Pid=1310000000561, Title="清空", Permission="sysLogHttp/clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
new SysMenu{ Id=1310000000601, Pid=0, Title="开发工具", Path="/develop", Name="develop", Component="Layout", Icon="ele-Cpu", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=13000 },
new SysMenu{ Id=1310000000611, Pid=1310000000601, Title="库表管理", Path="/develop/database", Name="sysDatabase", Component="/system/database/index",Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },

View File

@ -0,0 +1,241 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 请求日志基础输入参数
/// </summary>
public class LogHttpBaseInput
{
/// <summary>
/// 请求方式
/// </summary>
public virtual string HttpMethod { get; set; }
/// <summary>
/// 是否成功
/// </summary>
public virtual YesNoEnum? IsSuccessStatusCode { get; set; }
/// <summary>
/// 请求地址
/// </summary>
public virtual string RequestUrl { get; set; }
/// <summary>
/// 请求头
/// </summary>
public virtual string RequestHeaders { get; set; }
/// <summary>
/// 请求体
/// </summary>
public virtual string RequestBody { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
public virtual int? StatusCode { get; set; }
/// <summary>
/// 响应头
/// </summary>
public virtual string ResponseHeaders { get; set; }
/// <summary>
/// 响应体
/// </summary>
public virtual string ResponseBody { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public virtual string Exception { get; set; }
/// <summary>
/// 开始时间
/// </summary>
public virtual DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public virtual DateTime? EndTime { get; set; }
/// <summary>
/// 耗时(毫秒)
/// </summary>
public virtual long? Elapsed { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public virtual DateTime? CreateTime { get; set; }
}
/// <summary>
/// 请求日志分页查询输入参数
/// </summary>
public class PageLogHttpInput : BasePageInput
{
/// <summary>
/// 关键字查询
/// </summary>
public string SearchKey { get; set; }
/// <summary>
/// 请求方式
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// 是否成功
/// </summary>
[Dict(nameof(YesNoEnum))]
public YesNoEnum? IsSuccessStatusCode { get; set; }
/// <summary>
/// 请求地址
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// 请求体
/// </summary>
public string RequestBody { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
public int? StatusCode { get; set; }
/// <summary>
/// 响应体
/// </summary>
public string ResponseBody { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public string Exception { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreateTime { get; set; }
/// <summary>
/// 创建时间范围
/// </summary>
public DateTime?[] CreateTimeRange { get; set; }
}
/// <summary>
/// 请求日志主键查询输入参数
/// </summary>
public class ExportLogHttpInput : PageLogHttpInput
{
/// <summary>
/// 需要导入的主键集
/// </summary>
[ImporterHeader(IsIgnore = true)]
[ExporterHeader(IsIgnore = true)]
public List<long> SelectKeyList { get; set; }
}
/// <summary>
/// 请求日志数据导入实体
/// </summary>
[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)]
public class ImportLogHttpInput : BaseImportInput
{
/// <summary>
/// 请求方式
/// </summary>
[ImporterHeader(Name = "请求方式")]
[ExporterHeader("请求方式", Format = "", Width = 25, IsBold = true)]
public string HttpMethod { get; set; }
/// <summary>
/// 是否成功
/// </summary>
[Dict(nameof(YesNoEnum))]
[ImporterHeader(Name = "是否成功")]
[ExporterHeader("是否成功", Format = "", Width = 25, IsBold = true)]
public YesNoEnum? IsSuccessStatusCode { get; set; }
/// <summary>
/// 请求地址
/// </summary>
[ImporterHeader(Name = "请求地址")]
[ExporterHeader("请求地址", Format = "", Width = 25, IsBold = true)]
public string RequestUrl { get; set; }
/// <summary>
/// 请求头
/// </summary>
[ImporterHeader(Name = "请求头")]
[ExporterHeader("请求头", Format = "", Width = 25, IsBold = true)]
public string RequestHeaders { get; set; }
/// <summary>
/// 请求体
/// </summary>
[ImporterHeader(Name = "请求体")]
[ExporterHeader("请求体", Format = "", Width = 25, IsBold = true)]
public string RequestBody { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
[ImporterHeader(Name = "响应状态码")]
[ExporterHeader("响应状态码", Format = "", Width = 25, IsBold = true)]
public int? StatusCode { get; set; }
/// <summary>
/// 响应头
/// </summary>
[ImporterHeader(Name = "响应头")]
[ExporterHeader("响应头", Format = "", Width = 25, IsBold = true)]
public string ResponseHeaders { get; set; }
/// <summary>
/// 响应体
/// </summary>
[ImporterHeader(Name = "响应体")]
[ExporterHeader("响应体", Format = "", Width = 25, IsBold = true)]
public string ResponseBody { get; set; }
/// <summary>
/// 异常信息
/// </summary>
[ImporterHeader(Name = "异常信息")]
[ExporterHeader("异常信息", Format = "", Width = 25, IsBold = true)]
public string Exception { get; set; }
/// <summary>
/// 开始时间
/// </summary>
[ImporterHeader(Name = "开始时间")]
[ExporterHeader("开始时间", Format = "", Width = 25, IsBold = true)]
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
[ImporterHeader(Name = "结束时间")]
[ExporterHeader("结束时间", Format = "", Width = 25, IsBold = true)]
public DateTime? EndTime { get; set; }
/// <summary>
/// 耗时(毫秒)
/// </summary>
[ImporterHeader(Name = "耗时(毫秒)")]
[ExporterHeader("耗时(毫秒)", Format = "", Width = 25, IsBold = true)]
public long? Elapsed { get; set; }
}

View File

@ -0,0 +1,94 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 请求日志输出参数
/// </summary>
public class PageLogHttpOutput
{
/// <summary>
/// 主键Id
/// </summary>
public long? Id { get; set; }
/// <summary>
/// 请求方式
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// 是否成功
/// </summary>
public YesNoEnum? IsSuccessStatusCode { get; set; }
/// <summary>
/// 请求地址
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// 请求头
/// </summary>
public string RequestHeaders { get; set; }
/// <summary>
/// 请求体
/// </summary>
public string RequestBody { get; set; }
/// <summary>
/// 响应状态码
/// </summary>
public int? StatusCode { get; set; }
/// <summary>
/// 响应头
/// </summary>
public string ResponseHeaders { get; set; }
/// <summary>
/// 响应体
/// </summary>
public string ResponseBody { get; set; }
/// <summary>
/// 异常信息
/// </summary>
public string Exception { get; set; }
/// <summary>
/// 开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 耗时(毫秒)
/// </summary>
public long? Elapsed { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime? CreateTime { get; set; }
}
/// <summary>
/// 请求日志数据导出模板实体
/// </summary>
public class ExportLogHttpOutput : ImportLogHttpInput
{
[ImporterHeader(IsIgnore = true)]
[ExporterHeader(IsIgnore = true)]
public override string Error { get; set; }
}

View File

@ -0,0 +1,124 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using System.Net;
namespace Admin.NET.Core.Service;
/// <summary>
/// 请求日志服务 🧩
/// </summary>
[ApiDescriptionSettings(Order = 330, Description = "请求日志")]
public class SysLogHttpService : IDynamicApiController, ITransient
{
private readonly SqlSugarRepository<SysLogHttp> _sysLogHttpRep;
public SysLogHttpService(SqlSugarRepository<SysLogHttp> sysLogHttpRep, SysDictTypeService sysDictTypeService, ISqlSugarClient sqlSugarClient)
{
_sysLogHttpRep = sysLogHttpRep;
}
/// <summary>
/// 分页查询请求日志 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("分页查询请求日志")]
[ApiDescriptionSettings(Name = "Page"), HttpPost]
public async Task<SqlSugarPagedList<PageLogHttpOutput>> Page(PageLogHttpInput input)
{
input.Keyword = input.Keyword?.Trim();
var query = _sysLogHttpRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.RequestUrl.Contains(input.Keyword) || u.RequestBody.Contains(input.Keyword) || u.ResponseBody.Contains(input.Keyword) || u.Exception.Contains(input.Keyword))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestUrl), u => u.RequestUrl.Contains(input.RequestUrl.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestBody), u => u.RequestBody.Contains(input.RequestBody.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.ResponseBody), u => u.ResponseBody.Contains(input.ResponseBody.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.Exception), u => u.Exception.Contains(input.Exception.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.HttpMethod), u => u.HttpMethod == input.HttpMethod)
.WhereIF(input.IsSuccessStatusCode != null, u => u.IsSuccessStatusCode == input.IsSuccessStatusCode)
.WhereIF(input.StatusCode != null, u => u.StatusCode == (HttpStatusCode)input.StatusCode)
.WhereIF(input.CreateTimeRange?.Length == 2, u => u.CreateTime >= input.CreateTimeRange[0] && u.CreateTime <= input.CreateTimeRange[1])
.Select<PageLogHttpOutput>();
query = query.MergeTable();
return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
}
/// <summary>
/// 获取请求日志列表 🔖
/// </summary>
/// <returns></returns>
[DisplayName("获取请求日志列表")]
[ApiDescriptionSettings(Name = "List"), HttpGet]
public async Task<List<SysLogHttp>> GetList([FromQuery] PageLogHttpInput input)
{
return await _sysLogHttpRep.AsQueryable()
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestUrl?.Trim()), u => u.RequestUrl.Contains(input.RequestUrl.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.RequestBody?.Trim()), u => u.RequestBody.Contains(input.RequestBody.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.ResponseBody?.Trim()), u => u.ResponseBody.Contains(input.ResponseBody.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.Exception?.Trim()), u => u.Exception.Contains(input.Exception.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.HttpMethod?.Trim()), u => u.HttpMethod == input.HttpMethod)
.WhereIF(input.IsSuccessStatusCode != null, u => u.IsSuccessStatusCode == input.IsSuccessStatusCode)
.WhereIF(input.StatusCode != null, u => u.StatusCode == (HttpStatusCode)input.StatusCode)
.WhereIF(input.CreateTimeRange?.Length == 2, u => u.CreateTime >= input.CreateTimeRange[0] && u.CreateTime <= input.CreateTimeRange[1])
.ToListAsync();
}
/// <summary>
/// 获取请求日志详情
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("获取请求日志详情")]
[ApiDescriptionSettings(Name = "Detail"), HttpGet]
public async Task<SysLogHttp> Detail([FromQuery] BaseIdInput input)
{
return await _sysLogHttpRep.GetFirstAsync(u => u.Id == input.Id);
}
/// <summary>
/// 按年按天数统计消息日志 🔖
/// </summary>
/// <returns></returns>
[DisplayName("按年按天数统计消息日志")]
public async Task<List<StatLogOutput>> GetYearDayStats()
{
var _db = _sysLogHttpRep.AsSugarClient();
var now = DateTime.Now;
var days = (now - now.AddYears(-1)).Days + 1;
var day365 = Enumerable.Range(0, days).Select(u => now.AddDays(-u)).ToList();
var queryableLeft = _db.Reportable(day365).ToQueryable<DateTime>();
var queryableRight = _db.Queryable<SysLogHttp>(); //.SplitTable(tab => tab);
var list = await _db.Queryable(queryableLeft, queryableRight, JoinType.Left,
(x1, x2) => x1.ColumnName.Date == x2.CreateTime.Date)
.GroupBy((x1, x2) => x1.ColumnName)
.Select((x1, x2) => new StatLogOutput
{
Count = SqlFunc.AggregateSum(SqlFunc.IIF(x2.Id > 0, 1, 0)),
Date = x1.ColumnName.ToString("yyyy-MM-dd")
})
.MergeTable()
.OrderBy(x => x.Date)
.ToListAsync();
return list;
}
/// <summary>
/// 导出请求日志记录 🔖
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[DisplayName("导出请求日志记录")]
[ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify]
public async Task<IActionResult> Export(ExportLogHttpInput input)
{
var list = (await Page(input)).Items?.Adapt<List<ExportLogHttpOutput>>() ?? new();
if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.Id)).ToList();
return ExcelHelper.ExportTemplate(list, "请求日志导出记录");
}
}

View File

@ -0,0 +1,29 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 可序列化的异常
/// </summary>
public class SerializableException
{
public string Message { get; set; }
public string StackTrace { get; set; }
public string Source { get; set; }
public string TypeName { get; set; }
public static SerializableException FromException(Exception ex)
{
return new SerializableException
{
Message = ex.Message,
StackTrace = ex.StackTrace,
Source = ex.Source,
TypeName = ex.GetType().FullName
};
}
}

View File

@ -38,6 +38,7 @@ public static class ProjectOptions
services.AddConfigurableOptions<EventBusOptions>();
services.AddConfigurableOptions<AlipayOptions>();
services.AddConfigurableOptions<MqttOptions>();
services.AddConfigurableOptions<HttpRemotesOptions>();
services.Configure<IpRateLimitOptions>(App.Configuration.GetSection("IpRateLimiting"));
services.Configure<IpRateLimitPolicies>(App.Configuration.GetSection("IpRateLimitPolicies"));
services.Configure<ClientRateLimitOptions>(App.Configuration.GetSection("ClientRateLimiting"));

View File

@ -32,6 +32,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -317,6 +319,9 @@ public class Startup : AppStartup
// 注册启动执行任务
services.AddHostedService<StartHostedService>();
services.AddHostedService<MqttHostedService>();
// 添加自定义的Http远程服务
services.AddHttpRemoteClientService();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View File

@ -0,0 +1,86 @@
<template>
<el-dialog v-model="state.visible" draggable overflow destroy-on-close>
<template #header>
<div style="color: #fff">
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Document /> </el-icon>
<span> 日志详情 </span>
</div>
</template>
<el-scrollbar height="calc(100vh - 250px)">
<el-form :model="data" label-width="auto">
<el-tabs v-model="state.selectedTabName">
<el-tab-pane label="请求信息">
<el-form-item label="请求地址">
{{ data.requestUrl?.indexOf('?') == -1 ? data.requestUrl : data.requestUrl?.substring(0, data.requestUrl.indexOf('?')) }}
</el-form-item>
<el-form-item label="Query参数" v-if="data.requestUrl?.indexOf('?') != -1">
<el-row v-for="(value, key, index) in queryObject">
<el-col :span="4">
<el-input :value="key" readonly>
</el-input>
</el-col>
<el-col :span="20">
<vue-json-pretty :data="tryJsonParse(value)" showLength showIcon showSelectController />
</el-col>
</el-row>
</el-form-item>
<el-form-item label="请求头">
<vue-json-pretty :data="data.requestHeaders" showLength showIcon showLineNumber showSelectController />
</el-form-item>
<el-form-item label="请求体">
<vue-json-pretty :data="data.requestBody" showLength showIcon showLineNumber showSelectController />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="响应信息">
<el-form-item label="响应头">
<vue-json-pretty :data="data.responseHeaders" showLength showIcon showLineNumber showSelectController />
</el-form-item>
<el-form-item label="响应体">
<vue-json-pretty :data="data.responseBody" showLength showIcon showLineNumber showSelectController />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="异常信息" v-if="data.exception">
<vue-json-pretty :data="data.exception" showLength showIcon showLineNumber showSelectController />
</el-tab-pane>
</el-tabs>
</el-form>
</el-scrollbar>
<template #footer>
<el-button @click="state.visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import {ref, reactive, computed} from 'vue';
import { StringToObj } from '/@/utils/json-utils';
import VueJsonPretty from 'vue-json-pretty';
import { LogHttp } from '/@/api-services/system';
const state = reactive({
visible: false,
selectedTabName: '0',
});
const data = ref<LogHttp>({});
const openDialog = (row: any) => {
state.visible = true;
data.value ??= {};
state.selectedTabName = '0';
data.value.requestUrl = StringToObj(row.requestUrl);
data.value.requestHeaders = StringToObj(row.requestHeaders);
data.value.requestBody = StringToObj(row.requestBody);
data.value.responseHeaders = StringToObj(row.responseHeaders);
data.value.responseBody = StringToObj(row.responseBody);
data.value.exception = StringToObj(row.exception);
};
const queryObject = computed(() => Object.fromEntries(new URLSearchParams(new URL(data.value?.requestUrl ?? '').search)) ?? {});
const tryJsonParse = (str: string) => {
try {
return JSON.parse(str);
} catch (e) {
return str;
}
};
defineExpose({
openDialog,
});
</script>

View File

@ -0,0 +1,328 @@
<template>
<div class="sys-sysLogHttp-container">
<el-card shadow="hover" :body-style="{ padding: '5px 5px 0 5px' }">
<el-collapse @change="collapseChange">
<el-collapse-item title="请求日志">
<scEcharts ref="echartRef" height="200px" :option="echartsOption" :autoDraw="false" @clickData="clickData"></scEcharts>
</el-collapse-item>
</el-collapse>
</el-card>
<el-card shadow="hover" :body-style="{ padding: '5px', display: 'flex', width: '100%', height: '100%', alignItems: 'start' }">
<el-form :model="state.queryParams" ref="queryForm" :show-message="false" :inlineMessage="true" label-width="auto" style="flex: 1 1 0%">
<el-row :gutter="10">
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="请求方式" prop="httpMethod">
<el-input v-model="state.queryParams.httpMethod" placeholder="请输入请求方式" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="是否成功" prop="isSuccessStatusCode">
<g-sys-dict v-model="state.queryParams.isSuccessStatusCode" :code="'YesNoEnum'" render-as="select" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="请求地址" prop="requestUrl">
<el-input v-model="state.queryParams.requestUrl" placeholder="请输入请求地址" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="请求体" prop="requestBody">
<el-input v-model="state.queryParams.requestBody" placeholder="请输入请求体" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="响应状态码" prop="statusCode">
<el-input v-model="state.queryParams.statusCode" placeholder="请输入响应状态码" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="响应体" prop="responseBody">
<el-input v-model="state.queryParams.responseBody" placeholder="请输入响应体" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="异常信息" prop="exception">
<el-input v-model="state.queryParams.exception" placeholder="请输入异常信息" clearable @keyup.enter.native="handleQuery(true)" />
</el-form-item>
</el-col>
<el-col class="mb5" :sm="12" :md="8" :lg="6" :xl="6">
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
type="daterange"
v-model="state.queryParams.createTimeRange"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-divider style="height: calc(100% - 5px); margin: 0 10px" direction="vertical" />
<el-row>
<el-col>
<el-button-group>
<el-button type="primary" icon="ele-Search" @click="handleQuery(true)" v-auth="'sysLogHttp/page'"> 查询 </el-button>
<el-button icon="ele-Refresh" @click="resetQuery"> 重置 </el-button>
</el-button-group>
</el-col>
</el-row>
</el-card>
<el-card class="full-table" shadow="hover" style="margin-top: 5px">
<vxe-grid ref="xGrid" class="xGrid-style" v-bind="options" v-on="gridEvents">
<template #toolbar_buttons> </template>
<template #toolbar_tools></template>
<template #empty><el-empty :image-size="200" /></template>
<template #row_record="{ row }"><ModifyRecord :data="row" /></template>
<template #row_isSuccessStatusCode="{ row, $index }">
<g-sys-dict v-model="row.isSuccessStatusCode" :code="'YesNoEnum'" />
</template>
<template #row_requestUrl="{ row, $index }">
<el-button v-if="row.requestUrl" class="ml5" icon="ele-CopyDocument" text type="primary" @click="(event: any) => handleCopyUrl(event, row.requestUrl)" />
{{ row.requestUrl.substring(0, row.requestUrl.length >= 50 ? 50 : row.requestUrl.length) }}
{{ row.requestUrl.length >= 50 ? '...' : ''}}
</template>
<template #row_requestBody="{ row, $index }">
{{ row.requestBody }}
<el-button v-if="row.requestBody" class="ml5" icon="ele-CopyDocument" text type="primary" @click="commonFun.copyText(row.requestBody?.toString())" />
</template>
<template #row_responseBody="{ row, $index }">
{{ row.responseBody }}
<el-button v-if="row.responseBody" class="ml5" icon="ele-CopyDocument" text type="primary" @click="commonFun.copyText(row.responseBody?.toString())" />
</template>
<template #row_exception="{ row, $index }">
{{ row.exception }}
<el-button v-if="row.exception" class="ml5" icon="ele-CopyDocument" text type="primary" @click="commonFun.copyText(row.exception?.toString())" />
</template>
<template #row_startTime="{ row, $index }">
{{ commonFun.dateFormatYMDHMS(row, $index, row.startTime) }}
</template>
<template #row_endTime="{ row, $index }">
{{ commonFun.dateFormatYMDHMS(row, $index, row.endTime) }}
</template>
<template #row_buttons="{ row }"> </template>
</vxe-grid>
</el-card>
<logDetail ref="logDetailRef"></logDetail>
</div>
</template>
<script lang="ts" setup name="sysLogHttp">
import { ElMessage } from 'element-plus';
import { Local } from '/@/utils/storage';
import { getAPI } from '/@/utils/axios-utils';
import { useVxeTable } from '/@/hooks/useVxeTableOptionsHook';
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { VxeGridInstance, VxeGridListeners, VxeGridPropTypes } from 'vxe-table';
import { PageLogHttpInput, PageLogHttpOutput } from '/@/api-services/system/models';
import { SysLogHttpApi } from '/@/api-services/system/api';
import ModifyRecord from '/@/components/table/modifyRecord.vue';
import commonFunction from '/@/utils/commonFunction';
import logDetail from './component/logDetail.vue';
import 'vue-json-pretty/lib/styles.css';
const scEcharts = defineAsyncComponent(() => import('/@/components/scEcharts/index.vue'));
const commonFun = commonFunction();
const xGrid = ref<VxeGridInstance>();
const logDetailRef = ref<InstanceType<typeof logDetail>>();
const state = reactive({
queryParams: {} as PageLogHttpInput,
localPageParam: {
pageSize: 20 as number,
defaultSort: { field: 'id', order: 'asc', descStr: 'asc' },
},
visible: false,
logMaxValue: 1,
});
//
const localPageParamKey = 'localPageParam:sysLogHttp';
//
const options = useVxeTable<PageLogHttpOutput>(
{
id: 'sysLogHttp',
name: '请求日志',
columns: [
// { type: 'checkbox', width: 40, fixed: 'left' },
{ field: 'seq', type: 'seq', title: '序号', width: 60, fixed: 'left' },
{ field: 'createTime', title: '创建时间', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'httpMethod', title: '请求方式', minWidth: 60, showOverflow: 'tooltip' },
{ field: 'isSuccessStatusCode', title: '是否成功', minWidth: 60, showOverflow: 'tooltip', slots: { default: 'row_isSuccessStatusCode' } },
{ field: 'requestUrl', title: '请求地址', minWidth: 150, align: 'left', showOverflow: 'tooltip', slots: { default: 'row_requestUrl' } },
{ field: 'requestHeaders', title: '请求头', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'requestBody', title: '请求体', minWidth: 150, showOverflow: 'tooltip', slots: { default: 'row_requestBody' } },
{ field: 'statusCode', title: '响应状态码', minWidth: 70, showOverflow: 'tooltip' },
{ field: 'responseHeaders', title: '响应头', minWidth: 150, showOverflow: 'tooltip' },
{ field: 'responseBody', title: '响应体', minWidth: 150, showOverflow: 'tooltip', slots: { default: 'row_responseBody' } },
{ field: 'exception', title: '异常信息', minWidth: 150, showOverflow: 'tooltip', slots: { default: 'row_exception' } },
{ field: 'startTime', title: '开始时间', minWidth: 150, showOverflow: 'tooltip', slots: { default: 'row_startTime' } },
{ field: 'endTime', title: '结束时间', minWidth: 150, showOverflow: 'tooltip', slots: { default: 'row_endTime' } },
{ field: 'elapsed', title: '耗时(毫秒)', minWidth: 90, showOverflow: 'tooltip' },
],
},
// vxeGrid()vxe-table
{
//
proxyConfig: { autoLoad: true, ajax: { query: ({ page }) => handleQueryApi(page) } },
//
sortConfig: { defaultSort: Local.get(localPageParamKey)?.defaultSort || state.localPageParam.defaultSort },
//
pagerConfig: { pageSize: Local.get(localPageParamKey)?.pageSize || state.localPageParam.pageSize },
//
// importConfig: { remote: true, importMethod: (options: any) => handleImport(options), slots: { top: 'import_sysLogHttp' } },
//
toolbarConfig: { import: false, export: true },
}
);
//
onMounted(async () => {
state.localPageParam = Local.get(localPageParamKey) || state.localPageParam;
});
// api
const handleQueryApi = async (page: VxeGridPropTypes.ProxyAjaxQueryPageParams) => {
const params = Object.assign(state.queryParams, {
page: page.currentPage,
pageSize: page.pageSize,
field: state.localPageParam.defaultSort.field,
order: state.localPageParam.defaultSort.order,
descStr: 'asc',
}) as PageLogHttpInput;
return getAPI(SysLogHttpApi).apiSysLogHttpPagePost(params);
};
//
const handleQuery = async (reset = false) => {
options.loading = true;
reset ? await xGrid.value?.commitProxy('reload') : await xGrid.value?.commitProxy('query');
options.loading = false;
};
//
const handleCopyUrl = (event: PointerEvent, url: string) => {
event.stopPropagation(); //
commonFun.copyText(url);
}
//
const resetQuery = async () => {
state.queryParams.httpMethod = undefined;
state.queryParams.isSuccessStatusCode = undefined;
state.queryParams.requestUrl = undefined;
state.queryParams.requestBody = undefined;
state.queryParams.statusCode = undefined;
state.queryParams.responseBody = undefined;
state.queryParams.exception = undefined;
state.queryParams.createTime = undefined;
await xGrid.value?.commitProxy('reload');
};
const echartsOption = ref({
title: {
top: 30,
left: 'center',
text: '日志统计',
show: false,
},
tooltip: {
formatter: function (p: any) {
return p.data[1] + ' 数据量:' + p.data[0];
},
},
visualMap: {
show: true,
// inRange: {
// color: ['#fbeee2', '#f2cac9', '#efafad', '#f19790', '#f1908c', '#f17666', '#f05a46', '#ed3b2f', '#ec2b24', '#de2a18'],
// },
min: 0,
max: 1000,
maxOpen: {
type: 'piecewise',
},
type: 'piecewise',
orient: 'horizontal',
left: 'right',
},
calendar: {
top: 30,
left: 30,
right: 30,
bottom: 30,
cellSize: ['auto', 20],
range: ['', ''],
splitLine: true,
dayLabel: {
firstDay: 1,
nameMap: 'ZH',
},
itemStyle: {
color: '#ccc',
borderWidth: 3,
borderColor: '#fff',
},
monthLabel: {
nameMap: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
},
yearLabel: {
show: false,
},
},
series: {
type: 'heatmap',
coordinateSystem: 'calendar',
data: [],
},
});
const clickData = (e: any) => {
if (e[1] < 1) return ElMessage.warning('没有日志数据');
state.queryParams.createTimeRange = [];
state.queryParams.createTimeRange.push(e[0]);
var today = new Date(e[0]);
let endTime = today.setDate(today.getDate() + 1);
state.queryParams.createTimeRange.push(new Date(endTime));
xGrid.value?.commitProxy('query');
};
//
const getYearDayStatsData = async () => {
let data = [] as any;
var res = await getAPI(SysLogHttpApi).apiSysLogHttpYearDayStatsGet();
res.data.result?.forEach((item: any) => {
data.push([item.date, item.count]);
if (item.count > state.logMaxValue) state.logMaxValue = item.count; //
});
echartsOption.value.visualMap.max = state.logMaxValue;
echartsOption.value.series.data = data;
echartsOption.value.calendar.range = [data[0][0], data[data.length - 1][0]];
};
//
const gridEvents: VxeGridListeners<PageLogHttpOutput> = {
// pager-config
async pageChange({ pageSize }) {
state.localPageParam.pageSize = pageSize;
Local.set(localPageParamKey, state.localPageParam);
},
//
async sortChange({ field, order }) {
state.localPageParam.defaultSort = { field: field, order: order!, descStr: 'desc' };
Local.set(localPageParamKey, state.localPageParam);
},
cellClick({ row, column }) {
if (['请求地址', '请求头', '请求体', '响应头', '响应体', '异常信息'].includes(column.title)) {
logDetailRef.value?.openDialog(row);
}
},
};
const echartRef = ref();
const collapseChange = async () => {
await getYearDayStatsData();
if (echartRef.value.myChart == null) {
echartRef.value.draw();
}
};
</script>
<style scoped></style>