添加请求日志

This commit is contained in:
token
2024-01-19 02:49:24 +08:00
parent 93e2b1e493
commit 413004fb57
17 changed files with 526 additions and 506 deletions
@@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Identity;
namespace Gateway.Domain.Users.Aggregations;
public class User : IdentityUser<long>
{
protected User()
{
}
public User(string userName,string email) : base(userName)
{
Email = email;
}
public void SetPassword(string password)
{
PasswordHash = new PasswordHasher<User>().HashPassword(this, password);
}
public void SetPhoneNumber(string phoneNumber)
{
PhoneNumber = phoneNumber;
}
public void Lockout()
{
LockoutEnabled = true;
LockoutEnd = DateTimeOffset.MaxValue;
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace Gateway.Dto;
public class RequestLogPanel
{
public int RequestCount { get; set; }
public DateTime StartTime { get; set; }
}
+53
View File
@@ -0,0 +1,53 @@
using FreeSql.DataAnnotations;
namespace Gateway.Entities;
[Index("uk_path", nameof(Path), false)]
[Index("uk_ip", nameof(Ip), false)]
public class RequestLog
{
[Column(IsIdentity = true, IsPrimary = true)]
public int Id { get; set; }
public string Path { get; set; }
public string Method { get; set; }
public string QueryString { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 请求IP
/// </summary>
public string? Ip { get; set; }
/// <summary>
/// 目标地址
/// </summary>
public string? Target { get; set; }
/// <summary>
/// 状态码
/// </summary>
public int StatusCode { get; set; }
/// <summary>
/// 执行时长(ms)
/// </summary>
public double ExecutionDuration { get; set; }
/// <summary>
/// 浏览器信息
/// </summary>
public string BrowserInfo { get; set; }
/// <summary>
/// 扩展属性
/// </summary>
[Column(MapType = typeof(string), StringLength = -1)]
public Dictionary<string,string> ExtraProperties { get; set; } = new();
}
+3 -2
View File
@@ -8,8 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CoreFlex.Jwt" Version="0.1.4" />
<PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="FreeSql" Version="3.2.810" />
<PackageReference Include="FreeSql.DbContext" Version="3.2.810" />
<PackageReference Include="FreeSql.Provider.Sqlite" Version="3.2.810" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
</ItemGroup>
-13
View File
@@ -1,13 +0,0 @@
using CoreFlex.Jwt;
using CoreFlex.Module;
namespace Gateway;
[DependOns(typeof(CoreFlexJwtModule))]
public class GatewayModule : CoreFlexModule
{
public override void ConfigureServices(CoreFlexServiceContext context)
{
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace Gateway.Options;
public class RequestOptions
{
/// <summary>
/// 过滤后缀
/// </summary>
public static string[] FilterSuffixes { get; set; } = Array.Empty<string>();
/// <summary>
/// 需要过滤的ContentType
/// </summary>
public static string[] FilterContentTypes { get; set; } = Array.Empty<string>();
}
+52 -16
View File
@@ -1,31 +1,67 @@
using CoreFlex.Jwt;
using CoreFlex.Module.Extensions;
using Gateway;
using System.Diagnostics;
using FreeSql;
using FreeSql.Internal;
using Gateway.Entities;
using Gateway.Options;
using Gateway.Services;
using LiteDB;
using Gateway.TypeHandlers;
Utils.TypeHandlers.TryAdd(typeof(Dictionary<string, string>), new StringJsonHandler());
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.GetSection(nameof(JwtOptions)).Get<JwtOptions>();
builder.Configuration.GetSection(nameof(RequestOptions)).Get<RequestOptions>();
await builder.AddCoreFlexAutoInjectAsync<GatewayModule>();
builder.Services.AddSingleton<ILiteDatabase>(_ =>
builder.Services.AddSingleton<RequestLogService>();
builder.Services.AddSingleton<IFreeSql>((_ =>
{
var dbPath = Path.Combine(builder.Environment.ContentRootPath, "gateway.db");
return new LiteDatabase(dbPath);
});
var freeSql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, builder.Configuration.GetConnectionString("DefaultConnection"))
.UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}")) //监听SQL语句
.UseAutoSyncStructure(true) //自动同步实体结构到数据库,FreeSql不会扫描程序集,只有CRUD时才会生成表。
.Build();
return freeSql;
}));
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.Use((async (context, next) =>
{
if (RequestOptions.FilterSuffixes.Any(x => context.Request.Path.Value?.EndsWith(x) == true))
return;
var requestLogService = context.RequestServices.GetRequiredService<RequestLogService>();
var ip = context.Connection.RemoteIpAddress?.ToString();
var requestLog = new RequestLog
{
Path = context.Request.Path,
Method = context.Request.Method,
QueryString = context.Request.QueryString.ToString(),
CreatedTime = DateTime.Now,
Ip = ip,
BrowserInfo = context.Request.Headers.UserAgent.ToString(),
};
context.Items["RequestLog"] = requestLog;
var stopwatch = Stopwatch.StartNew();
await next();
stopwatch.Stop();
// 过滤Content-Type
if (RequestOptions.FilterContentTypes.Any(x => context.Response.ContentType?.Contains(x) == true))
return;
requestLog.ExecutionDuration = stopwatch.Elapsed.TotalMilliseconds;
requestLog.StatusCode = context.Response.StatusCode;
await requestLogService.LogAsync(requestLog);
}));
app.MapGet("/api/panel", async (RequestLogService requestLogService, int hours) =>
await requestLogService.PanelAsync(hours));
app.MapReverseProxy();
app.MapPost("/api/authority/token", async (AuthorityService service, string userName, string password) =>
await service.GetTokenAsync(userName, password));
await app.Services.UseCoreFlexAsync();
await app.RunAsync();
-49
View File
@@ -1,49 +0,0 @@
using System.Security.Claims;
using CoreFlex.Jwt;
using CoreFlex.Module;
using Gateway.Domain.Users.Aggregations;
using LiteDB;
using Microsoft.AspNetCore.Identity;
namespace Gateway.Services;
public class AuthorityService(ILiteDatabase database) : IScopedDependency
{
public async Task<string> GetTokenAsync(string userName, string password)
{
var userCollection = database.GetCollection<User>();
var user = userCollection.FindOne(x => x.UserName == userName);
if (user == null)
{
throw new Exception("用户不存在");
}
var passwordHasher = new PasswordHasher<User>();
var verifyResult = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
if (verifyResult == PasswordVerificationResult.Failed)
{
throw new Exception("密码错误");
}
if (verifyResult == PasswordVerificationResult.SuccessRehashNeeded)
{
user.SetPassword(password);
userCollection.Update(user);
}
var doc = new Dictionary<string, object>();
// 使用微软提供的角色常量
doc.Add(ClaimTypes.Role, "admin");
doc.Add(ClaimTypes.NameIdentifier, user!.Id.ToString());
doc.Add(ClaimTypes.Name, user.UserName);
doc.Add(ClaimTypes.Email, user.Email);
var token = JwtHelper.GeneratorAccessToken(doc);
return token;
}
}
+76
View File
@@ -0,0 +1,76 @@
using System.Text.Json;
using System.Threading.Channels;
using Gateway.Dto;
using Gateway.Entities;
namespace Gateway.Services;
/// <summary>
/// 请求日志服务处理
/// </summary>
public class RequestLogService
{
private readonly Channel<RequestLog> _channel;
private readonly IFreeSql _freeSql;
public RequestLogService(IFreeSql freeSql)
{
_freeSql = freeSql;
_channel = Channel.CreateUnbounded<RequestLog>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
});
Task.Run(HandleAsync);
}
private async Task HandleAsync()
{
while (await _channel.Reader.WaitToReadAsync())
{
while (_channel.Reader.TryRead(out var requestLog))
{
await _freeSql.Insert(requestLog).ExecuteAffrowsAsync();
}
}
}
public async ValueTask LogAsync(RequestLog requestLog)
{
await _channel.Writer.WriteAsync(requestLog);
}
public async Task<List<RequestLogPanel>> PanelAsync(int hours)
{
// hours是传递的多少小时,查询每小时的请求量,返回一个数组
var result = await _freeSql.Ado.QueryAsync<RequestLogPanel>(
$"""
WITH RECURSIVE HoursBefore AS (
SELECT strftime('%Y-%m-%d %H:00:00', 'now', 'localtime') AS StartTime,
strftime('%Y-%m-%d %H:59:59', 'now', 'localtime') AS EndTime
UNION ALL
SELECT datetime(StartTime, '-1 hour') AS StartTime,
datetime(EndTime, '-1 hour') AS EndTime
FROM HoursBefore
WHERE StartTime > datetime('now', 'localtime', '-' || {hours} || ' hours')
)
SELECT
HB.StartTime,
COUNT(Log.Id) AS RequestCount
FROM
HoursBefore HB
LEFT JOIN
(SELECT * FROM RequestLog WHERE CreatedTime BETWEEN datetime('now', 'localtime', '-' || {hours} || ' hours') AND datetime('now', 'localtime')) AS Log
ON
Log.CreatedTime >= HB.StartTime
AND Log.CreatedTime <= HB.EndTime
GROUP BY
HB.StartTime
ORDER BY
HB.StartTime DESC
""");
return result;
}
}
@@ -0,0 +1,17 @@
using System.Text.Json;
using FreeSql.Internal.Model;
namespace Gateway.TypeHandlers;
public class StringJsonHandler : TypeHandler<Dictionary<string,string>>
{
public override Dictionary<string, string> Deserialize(object value)
{
return JsonSerializer.Deserialize<Dictionary<string,string>>((string)value);
}
public override object Serialize(Dictionary<string, string> value)
{
return JsonSerializer.Serialize(value);
}
}
+33 -5
View File
@@ -7,11 +7,39 @@
}
},
"AllowedHosts": "*",
"JwtOptions": {
"Secret": "123456789asdfghjkl1&345678*asdfghjkl",
"EffectiveHours": 12,
"Issuer": "Gateway",
"Audience": "gateway"
"ConnectionStrings": {
"DefaultConnection": "Data Source=./Gateway.db"
},
"RequestOptions": {
"FilterSuffixes": [
".js",
".css",
".jpg",
".png",
".gif",
".ico",
".svg",
".woff",
".woff2",
".ttf",
".js.map",
".css.map"
],
"FilterContentTypes": [
"text/html",
"text/css",
"text/javascript",
"application/javascript",
"application/json",
"image/jpeg",
"image/png",
"image/gif",
"image/x-icon",
"image/svg+xml",
"font/woff",
"font/woff2",
"font/ttf"
]
},
"ReverseProxy": {
"Routes": {
+15
View File
@@ -4,6 +4,9 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import MainLayout from './layout/main-layout';
import Home from './pages/home';
import 'reset-css';
import Gateway from './pages/gateway';
import Logger from './pages/logger';
import Setting from './pages/settings';
const router = createBrowserRouter([{
path: "/",
@@ -12,6 +15,18 @@ const router = createBrowserRouter([{
{
path: "",
element: <Home />
},
{
path: "Gateway",
element: <Gateway />
},
{
path: "Logger",
element: <Logger />
},
{
path: "Setting",
element: <Setting />
}
]
}]);
+17 -20
View File
@@ -1,12 +1,14 @@
import React from 'react';
import { Layout, Nav, Button, Avatar, Icon } from '@douyinfe/semi-ui';
import { IconBell, IconHelpCircle, IconGithubLogo, IconHome, IconHistogram, IconLive, IconSetting, IconSemiLogo } from '@douyinfe/semi-icons';
import { Gateway } from '../components/icon';
import { IconBell, IconHelpCircle, IconGithubLogo, IconHome, IconServerStroked, IconLive, IconSetting, } from '@douyinfe/semi-icons';
import { Outlet,useNavigate } from 'react-router-dom';
export default function MainLayout() {
const [selectedKeys, setSelectedKeys] = React.useState(['Home']);
const { Header, Footer, Sider, Content } = Layout;
const navigate = useNavigate();
return (
<Layout style={{ border: '1px solid var(--semi-color-border)', height: '100vh' }}>
<Sider style={{ backgroundColor: 'var(--semi-color-bg-1)' }}>
@@ -14,11 +16,20 @@ export default function MainLayout() {
defaultSelectedKeys={selectedKeys}
onSelect={key => {
setSelectedKeys([key.itemKey.toString()]);
if (key.itemKey === 'Home') {
navigate('/');
} else if (key.itemKey === 'Gateway') {
navigate('/Gateway');
} else if (key.itemKey === 'Logger') {
navigate('/Logger');
} else if (key.itemKey === 'Setting') {
navigate('/Setting');
}
}}
style={{ maxWidth: 220, height: '100%' }}
items={[
{ itemKey: 'Home', text: '首页', icon: <IconHome size="large" /> },
{ itemKey: 'Gateway', text: '代理设置', icon: <Icon svg={<Gateway color={(selectedKeys[0] === "Gateway" ? "#3661F8" : "#333333")} />}></Icon> },
{ itemKey: 'Gateway', text: '代理设置', icon: <IconServerStroked /> },
{ itemKey: 'Logger', text: '日志', icon: <IconLive size="large" /> },
{ itemKey: 'Setting', text: '设置', icon: <IconSetting size="large" /> },
]}
@@ -37,24 +48,9 @@ export default function MainLayout() {
mode="horizontal"
footer={
<>
<Button
theme="borderless"
icon={<IconBell size="large" />}
style={{
color: 'var(--semi-color-text-2)',
marginRight: '12px',
}}
/>
<Button
theme="borderless"
icon={<IconHelpCircle size="large" />}
style={{
color: 'var(--semi-color-text-2)',
marginRight: '12px',
}}
/>
<Avatar color="orange" size="small">
YJ
Gateway
</Avatar>
</>
}
@@ -66,6 +62,7 @@ export default function MainLayout() {
backgroundColor: 'var(--semi-color-bg-0)',
}}
>
<Outlet />
</Content>
<Footer
style={{
+51
View File
@@ -0,0 +1,51 @@
import { TabPane, Tabs } from "@douyinfe/semi-ui";
export default function Gateway() {
return (
<>
<Tabs tabPosition="left" type='line'>
<TabPane
tab={
<span>
集群
</span>
}
itemKey="1"
>
<div style={{ padding: '0 24px' }}>
</div>
</TabPane>
<TabPane
tab={
<span>
路由
</span>
}
itemKey="2"
>
<div style={{ padding: '0 24px' }}>
<h3>快速起步</h3>
<pre
style={{
margin: '24px 0',
padding: '20px',
border: 'none',
whiteSpace: 'normal',
borderRadius: '6px',
color: 'var(--semi-color-text-1)',
backgroundColor: 'var(--semi-color-fill-0)',
}}
>
<code>
yarn add @douyinfe/semi-ui
</code>
</pre>
</div>
</TabPane>
</Tabs>
</>
)
}
+5
View File
@@ -0,0 +1,5 @@
export default function Logger() {
return (
<div>Logger</div>
)
}
+5
View File
@@ -0,0 +1,5 @@
export default function Setting() {
return (
<div>Setting</div>
)
}
+177 -369
View File
File diff suppressed because it is too large Load Diff