feat: 登录日志独立存储 (#161)

请求日志自动分表
[skip ci]

Co-authored-by: tk <fiyne1a@dingtalk.com>
This commit is contained in:
2024-07-26 17:46:56 +08:00
committed by GitHub
parent e1bea2ec31
commit faaf5aa0fc
74 changed files with 1501 additions and 470 deletions

View File

@ -1,4 +1,3 @@
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Cache;
namespace NetAdmin.SysComponent.Application.Modules.Sys;
@ -16,5 +15,10 @@ public interface ICacheModule
/// <summary>
/// 获取所有缓存项
/// </summary>
Task<PagedQueryRsp<GetAllEntriesRsp>> GetAllEntriesAsync(PagedQueryReq<GetAllEntriesReq> req);
Task<IEnumerable<GetEntryRsp>> GetAllEntriesAsync(GetAllEntriesReq req);
/// <summary>
/// 获取缓存项
/// </summary>
Task<GetEntryRsp> GetEntryAsync(GetEntriesReq req);
}

View File

@ -0,0 +1,13 @@
using NetAdmin.Application.Modules;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.LoginLog;
namespace NetAdmin.SysComponent.Application.Modules.Sys;
/// <summary>
/// 登录日志模块
/// </summary>
public interface ILoginLogModule : ICrudModule<CreateLoginLogReq, QueryLoginLogRsp // 创建类型
, QueryLoginLogReq, QueryLoginLogRsp // 查询类型
, DelReq // 删除类型
>;

View File

@ -8,10 +8,11 @@ namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="IApiService" />
public sealed class ApiService(
BasicRepository<Sys_Api, string> rpo //
, XmlCommentReader xmlCommentReader //
, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) //
: RepositoryService<Sys_Api, string, IApiService>(rpo), IApiService
BasicRepository<Sys_Api, string> rpo //
, XmlCommentReader xmlCommentReader //
, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider
, RedLocker redLocker) //
: RedLockerService<Sys_Api, string, IApiService>(rpo, redLocker), IApiService
{
/// <inheritdoc />
public Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
@ -125,6 +126,7 @@ public sealed class ApiService(
/// <inheritdoc />
public async Task SyncAsync()
{
await using var locker = await GetLockerOnceAsync(nameof(SyncAsync)).ConfigureAwait(false);
_ = await Rpo.DeleteAsync(_ => true).ConfigureAwait(false);
var list = ReflectionList(false);

View File

@ -1,5 +1,5 @@
using System.Collections.Concurrent;
using NetAdmin.Application.Services;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.Cache;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
using StackExchange.Redis;
@ -31,23 +31,58 @@ public sealed class CacheService(IConnectionMultiplexer connectionMultiplexer) /
}
/// <inheritdoc />
public async Task<PagedQueryRsp<GetAllEntriesRsp>> GetAllEntriesAsync(PagedQueryReq<GetAllEntriesReq> req)
public async Task<IEnumerable<GetEntryRsp>> GetAllEntriesAsync(GetAllEntriesReq req)
{
req.ThrowIfInvalid();
#pragma warning disable VSTHRD103
var server = connectionMultiplexer.GetServers()[0];
var database = connectionMultiplexer.GetDatabase(_redisInstance.Database);
var redisResults = (RedisResult[])await database
.ExecuteAsync("scan", (req.Page - 1) * req.PageSize, "count"
, req.PageSize)
.ConfigureAwait(false);
var keys = server.Keys(_redisInstance.Database, $"*{req.Keywords}*", Numbers.MAX_LIMIT_BULK_REQ)
.Take(Numbers.MAX_LIMIT_BULK_REQ)
.ToList();
#pragma warning restore VSTHRD103
var list = ((string[])redisResults![1])!.Where(x => database.KeyType(x) == RedisType.Hash)
.Select(x => database.HashGetAll(x)
.Append(new HashEntry("key", x))
.ToArray()
.ToStringDictionary())
.ToList()
.ConvertAll(x => x.Adapt<GetAllEntriesRsp>());
var dic = new ConcurrentDictionary<string, (DateTime?, RedisType)>();
return new PagedQueryRsp<GetAllEntriesRsp>(req.Page, req.PageSize, 10000, list);
await Parallel
.ForEachAsync(
keys
, async (key, _) =>
dic.TryAdd(
key
, (DateTime.Now + await database.KeyTimeToLiveAsync(key).ConfigureAwait(false)
, await database.KeyTypeAsync(key).ConfigureAwait(false))))
.ConfigureAwait(false);
return dic.Select(x => new GetEntryRsp { Key = x.Key, ExpireTime = x.Value.Item1, Type = x.Value.Item2 });
}
/// <inheritdoc />
public async Task<GetEntryRsp> GetEntryAsync(GetEntriesReq req)
{
var database = connectionMultiplexer.GetDatabase(_redisInstance.Database);
var ret = new GetEntryRsp {
Type = await database.KeyTypeAsync(req.Key).ConfigureAwait(false)
, Key = req.Key
, ExpireTime = DateTime.Now +
await database.KeyTimeToLiveAsync(req.Key).ConfigureAwait(false)
};
#pragma warning disable IDE0072
ret.Data = ret.Type switch
#pragma warning restore IDE0072
{
RedisType.String => await database.StringGetAsync(req.Key).ConfigureAwait(false)
, RedisType.List => string.Join(", ", await database.ListRangeAsync(req.Key).ConfigureAwait(false))
, RedisType.Set => string.Join(", ", await database.SetMembersAsync(req.Key).ConfigureAwait(false))
, RedisType.SortedSet =>
string.Join(", ", await database.SortedSetRangeByRankAsync(req.Key).ConfigureAwait(false))
, RedisType.Hash => string.Join(
", ", await database.HashGetAllAsync(req.Key).ConfigureAwait(false))
, _ => "Unsupported key type"
};
return ret;
}
}

View File

@ -0,0 +1,9 @@
using NetAdmin.Application.Services;
using NetAdmin.SysComponent.Application.Modules.Sys;
namespace NetAdmin.SysComponent.Application.Services.Sys.Dependency;
/// <summary>
/// 登录日志服务
/// </summary>
public interface ILoginLogService : IService, ILoginLogModule;

View File

@ -0,0 +1,150 @@
using NetAdmin.Application.Repositories;
using NetAdmin.Application.Services;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys.LoginLog;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="ILoginLogService" />
public sealed class LoginLogService(BasicRepository<Sys_LoginLog, long> rpo) //
: RepositoryService<Sys_LoginLog, long, ILoginLogService>(rpo), ILoginLogService
{
/// <inheritdoc />
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
req.ThrowIfInvalid();
var ret = 0;
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in req.Items) {
ret += await DeleteAsync(item).ConfigureAwait(false);
}
return ret;
}
/// <inheritdoc />
public Task<long> CountAsync(QueryReq<QueryLoginLogReq> req)
{
req.ThrowIfInvalid();
return QueryInternal(req)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.CountAsync();
}
/// <inheritdoc />
public async Task<QueryLoginLogRsp> CreateAsync(CreateLoginLogReq req)
{
req.ThrowIfInvalid();
var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
return ret.Adapt<QueryLoginLogRsp>();
}
/// <inheritdoc />
public Task<int> DeleteAsync(DelReq req)
{
req.ThrowIfInvalid();
return Rpo.DeleteAsync(a => a.Id == req.Id);
}
/// <inheritdoc />
public Task<bool> ExistAsync(QueryReq<QueryLoginLogReq> req)
{
req.ThrowIfInvalid();
return QueryInternal(req)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.AnyAsync();
}
/// <inheritdoc />
public Task<IActionResult> ExportAsync(QueryReq<QueryLoginLogReq> req)
{
req.ThrowIfInvalid();
return ExportAsync<QueryLoginLogReq, ExportLoginLogRsp>(QueryInternal, req, Ln.);
}
/// <inheritdoc />
public async Task<QueryLoginLogRsp> GetAsync(QueryLoginLogReq req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(new QueryReq<QueryLoginLogReq> { Filter = req })
.ToOneAsync()
.ConfigureAwait(false);
return ret.Adapt<QueryLoginLogRsp>();
}
/// <inheritdoc />
public async Task<PagedQueryRsp<QueryLoginLogRsp>> PagedQueryAsync(PagedQueryReq<QueryLoginLogReq> req)
{
req.ThrowIfInvalid();
var list = await QueryInternal(req)
.Include(a => a.Owner)
.Page(req.Page, req.PageSize)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Count(out var total)
.ToListAsync(a => new {
a.CreatedClientIp
, a.CreatedTime
, a.CreatedUserAgent
, a.Duration
, a.ErrorCode
, a.HttpStatusCode
, a.Id
, a.LoginUserName
, Owner = new { a.Owner.Id, a.Owner.UserName }
, a.RequestUrl
, a.ServerIp
})
.ConfigureAwait(false);
return new PagedQueryRsp<QueryLoginLogRsp>(req.Page, req.PageSize, total, list.Adapt<List<QueryLoginLogRsp>>());
}
/// <inheritdoc />
public async Task<IEnumerable<QueryLoginLogRsp>> QueryAsync(QueryReq<QueryLoginLogReq> req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(req)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Take(req.Count)
.ToListAsync()
.ConfigureAwait(false);
return ret.Adapt<IEnumerable<QueryLoginLogRsp>>();
}
private ISelect<Sys_LoginLog> QueryInternal(QueryReq<QueryLoginLogReq> req)
{
var ret = Rpo.Select.WhereDynamicFilter(req.DynamicFilter).WhereDynamic(req.Filter);
if (req.Keywords?.Length > 0) {
ret = req.Keywords.IsIpV4()
? ret.Where(a => a.CreatedClientIp == req.Keywords.IpV4ToInt32())
: ret.Where(a => a.Id == req.Keywords.Int64Try(0) || a.OwnerId == req.Keywords.Int64Try(0) ||
a.LoginUserName == req.Keywords);
}
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (req.Order) {
case Orders.None:
return ret;
case Orders.Random:
return ret.OrderByRandom();
}
ret = ret.OrderByPropertyNameIf(req.Prop?.Length > 0, req.Prop, req.Order == Orders.Ascending);
if (!req.Prop?.Equals(nameof(req.Filter.Id), StringComparison.OrdinalIgnoreCase) ?? true) {
ret = ret.OrderByDescending(a => a.Id);
}
return ret;
}
}

View File

@ -2,19 +2,16 @@ using NetAdmin.Application.Repositories;
using NetAdmin.Application.Services;
using NetAdmin.Domain.Dto.Dependency;
using NetAdmin.Domain.Dto.Sys;
using NetAdmin.Domain.Dto.Sys.LoginLog;
using NetAdmin.Domain.Dto.Sys.RequestLog;
using NetAdmin.SysComponent.Application.Services.Sys.Dependency;
namespace NetAdmin.SysComponent.Application.Services.Sys;
/// <inheritdoc cref="IRequestLogService" />
public sealed class RequestLogService(
BasicRepository<Sys_RequestLog, long> rpo
, RequestLogDetailService requestLogDetailService) //
public sealed class RequestLogService(BasicRepository<Sys_RequestLog, long> rpo, LoginLogService loginLogService) //
: RepositoryService<Sys_RequestLog, long, IRequestLogService>(rpo), IRequestLogService
{
private static readonly Regex _regex = new(Chars.RGXL_IP_V4);
/// <inheritdoc />
public async Task<int> BulkDeleteAsync(BulkReq<DelReq> req)
{
@ -45,7 +42,12 @@ public sealed class RequestLogService(
{
req.ThrowIfInvalid();
var ret = await Rpo.InsertAsync(req).ConfigureAwait(false);
_ = await requestLogDetailService.CreateAsync(req.Detail).ConfigureAwait(false);
// 插入登录日志
if (req.ApiPathCrc32 == Chars.FLG_PATH_API_SYS_USER_LOGIN_BY_PWD.Crc32()) {
_ = await loginLogService.CreateAsync(req.Adapt<CreateLoginLogReq>()).ConfigureAwait(false);
}
return ret.Adapt<QueryRequestLogRsp>();
}
@ -88,7 +90,16 @@ public sealed class RequestLogService(
public async Task<QueryRequestLogRsp> GetAsync(QueryRequestLogReq req)
{
req.ThrowIfInvalid();
var ret = await QueryInternal(new QueryReq<QueryRequestLogReq> { Filter = req })
var df = new DynamicFilterInfo {
Field = nameof(QueryRequestLogReq.CreatedTime)
, Operator = DynamicFilterOperators.DateRange
, Value = new[] {
req.CreatedTime.AddHours(-1).yyyy_MM_dd_HH_mm_ss()
, req.CreatedTime.AddHours(1).yyyy_MM_dd_HH_mm_ss()
}.Json()
.Object<JsonElement>()
};
var ret = await QueryInternal(new QueryReq<QueryRequestLogReq> { Filter = req, DynamicFilter = df })
.Include(a => a.Detail)
.ToOneAsync()
.ConfigureAwait(false);
@ -155,43 +166,27 @@ public sealed class RequestLogService(
public async Task<PagedQueryRsp<QueryRequestLogRsp>> PagedQueryAsync(PagedQueryReq<QueryRequestLogReq> req)
{
req.ThrowIfInvalid();
var select = QueryInternal(req)
.Page(req.Page, req.PageSize)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Count(out var total);
var select = QueryInternal(req with { Order = Orders.None }, false);
var selectTemp = select.WithTempQuery(a => new { temp = a });
object ret
= req.DynamicFilter?.Filters?.Exists(
x => nameof(QueryRequestLogReq.ApiPathCrc32).Equals(x.Field, StringComparison.OrdinalIgnoreCase) &&
x.Value.ToString().Int32() == Chars.FLG_PATH_API_SYS_USER_LOGIN_BY_PWD.Crc32()) ?? false
? await select.Include(a => a.Detail)
.ToListAsync(a => new {
Api = new { a.Api.Summary, a.Api.Id }
, Owner = new { a.Owner.Id, a.Owner.UserName }
, a.CreatedClientIp
, a.CreatedTime
, a.Duration
, a.HttpMethod
, a.HttpStatusCode
, a.Id
, a.ApiPathCrc32
, Detail = new { a.Detail.RequestBody, a.Detail.CreatedUserAgent }
})
.ConfigureAwait(false)
: await select.ToListAsync(a => new {
Api = new { a.Api.Summary, a.Api.Id }
, Owner = new { a.Owner.Id, a.Owner.UserName }
, a.CreatedClientIp
, a.CreatedTime
, a.Duration
, a.HttpMethod
, a.HttpStatusCode
, a.Id
, a.ApiPathCrc32
})
.ConfigureAwait(false);
if (req.Order == Orders.Random) {
selectTemp = selectTemp.OrderByRandom();
}
else {
selectTemp = selectTemp.OrderBy( //
req.Prop?.Length > 0, $"{req.Prop} {(req.Order == Orders.Ascending ? "ASC" : "DESC")}");
if (!req.Prop?.Equals(nameof(req.Filter.CreatedTime), StringComparison.OrdinalIgnoreCase) ?? true) {
selectTemp = selectTemp.OrderByDescending(a => a.temp.CreatedTime);
}
}
var ret = await selectTemp.Page(req.Page, req.PageSize)
#if DBTYPE_SQLSERVER
.WithLock(SqlServerLock.NoLock | SqlServerLock.NoWait)
#endif
.Count(out var total)
.ToListAsync(a => a.temp)
.ConfigureAwait(false);
return new PagedQueryRsp<QueryRequestLogRsp>(req.Page, req.PageSize, total
, ret.Adapt<IEnumerable<QueryRequestLogRsp>>());
@ -229,10 +224,9 @@ public sealed class RequestLogService(
}
if (req.Keywords?.Length > 0) {
ret = _regex.IsMatch(req.Keywords)
ret = req.Keywords.IsIpV4()
? ret.Where(a => a.CreatedClientIp == req.Keywords.IpV4ToInt32())
: ret.Where(a => a.Id == req.Keywords.Int64Try(0) || a.OwnerId == req.Keywords.Int64Try(0) ||
a.Owner.UserName == req.Keywords);
: ret.Where(a => a.Id == req.Keywords.Int64Try(0) || a.OwnerId == req.Keywords.Int64Try(0));
}
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault

View File

@ -18,14 +18,15 @@ public sealed class ToolsService : ServiceBase<IToolsService>, IToolsService
/// <inheritdoc />
public Task<IEnumerable<GetModulesRsp>> GetModulesAsync()
{
return Task.FromResult<IEnumerable<GetModulesRsp>>( //
AppDomain.CurrentDomain.GetAssemblies()
.Where(x => !x.FullName?.Contains('#') ?? false)
.Select(x => {
var asm = x.GetName();
return new GetModulesRsp { Name = asm.Name, Version = asm.Version?.ToString() };
})
.OrderBy(x => x.Name));
return Task.FromResult<IEnumerable<GetModulesRsp>>(AppDomain.CurrentDomain.GetAssemblies()
.Select(x => {
var asm = x.GetName();
return new GetModulesRsp {
Name = asm.Name
, Version = asm.Version?.ToString()
};
})
.OrderBy(x => x.Name));
}
/// <inheritdoc />