Browse Source

日志记录支持timescaledb

懒得勤快 3 years ago
parent
commit
5027ec059a

+ 106 - 4
src/Masuit.MyBlogs.Core/Common/PerfCounter.cs

@@ -1,13 +1,18 @@
-using Masuit.Tools;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Diagnostics;
+using Masuit.MyBlogs.Core.Infrastructure;
+using Masuit.Tools;
 using Masuit.Tools.DateTimeExt;
 using Masuit.Tools.Hardware;
 using Masuit.Tools.Systems;
+using Microsoft.Extensions.DependencyInjection.Extensions;
 
 namespace Masuit.MyBlogs.Core.Common
 {
-    public static class PerfCounter
+    public interface IPerfCounter
     {
-        public static ConcurrentLimitedQueue<PerformanceCounter> List { get; set; } = new(50000);
+        public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
+
         public static readonly DateTime StartTime = DateTime.Now;
 
         public static void Init()
@@ -59,16 +64,113 @@ namespace Masuit.MyBlogs.Core.Common
                 Upload = up
             };
         }
+
+        IQueryable<PerformanceCounter> CreateDataSource();
+
+        void Process();
+    }
+
+    public class DefaultPerfCounter : IPerfCounter
+    {
+        static DefaultPerfCounter()
+        {
+        }
+
+        public IQueryable<PerformanceCounter> CreateDataSource()
+        {
+            return IPerfCounter.List.AsQueryable();
+        }
+
+        public void Process()
+        {
+        }
+    }
+
+    public class PerfCounterInDatabase : IPerfCounter
+    {
+        public static ConcurrentLimitedQueue<PerformanceCounter> List { get; } = new(50000);
+
+        private readonly LoggerDbContext _dbContext;
+
+        public PerfCounterInDatabase(LoggerDbContext dbContext)
+        {
+            _dbContext = dbContext;
+        }
+
+        public IQueryable<PerformanceCounter> CreateDataSource()
+        {
+            return _dbContext.Set<PerformanceCounter>();
+        }
+
+        public void Process()
+        {
+            if (Debugger.IsAttached)
+            {
+                return;
+            }
+
+            while (IPerfCounter.List.TryDequeue(out var result))
+            {
+                _dbContext.Add(result);
+            }
+
+            _dbContext.SaveChanges();
+            var start = DateTime.Now.AddMonths(-6).GetTotalMilliseconds();
+            _dbContext.Set<PerformanceCounter>().Where(e => e.Time < start).DeleteFromQuery();
+        }
+    }
+
+    public class PerfCounterBackService : BackgroundService
+    {
+        private readonly IPerfCounter _manager;
+
+        public PerfCounterBackService(IPerfCounter manager)
+        {
+            _manager = manager;
+        }
+
+        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            while (true)
+            {
+                _manager.Process();
+                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
+            }
+        }
+    }
+
+    public static class PerfCounterServiceExtension
+    {
+        public static IServiceCollection AddPerfCounterManager(this IServiceCollection services, IConfiguration configuration)
+        {
+            IPerfCounter.Init();
+            switch (configuration["PerfCounterStorage"])
+            {
+                case "database":
+                    services.AddScoped<IPerfCounter, PerfCounterInDatabase>();
+                    services.TryAddScoped<PerfCounterInDatabase>();
+                    break;
+
+                default:
+                    services.AddSingleton<IPerfCounter, DefaultPerfCounter>();
+                    break;
+            }
+
+            services.AddHostedService<PerfCounterBackService>();
+            return services;
+        }
     }
 
     /// <summary>
     /// 性能计数器
     /// </summary>
+    [Table(nameof(PerformanceCounter))]
     public class PerformanceCounter
     {
         /// <summary>
         /// 当前时间戳
         /// </summary>
+        [HypertableColumn]
         public long Time { get; set; }
 
         /// <summary>
@@ -101,4 +203,4 @@ namespace Masuit.MyBlogs.Core.Common
         /// </summary>
         public float Download { get; set; }
     }
-}
+}

+ 13 - 10
src/Masuit.MyBlogs.Core/Common/TrackData.cs

@@ -18,19 +18,22 @@ namespace Masuit.MyBlogs.Core.Common
         /// </summary>
         public static void DumpLog()
         {
-            var logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "req.txt").CreateFileIfNotExist();
-            File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.RequestUrls).GroupBy(s => s).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
-            File.AppendAllLines(logPath, new[] { "", $"累计处理请求数:{RequestLogs.Sum(kv => kv.Value.Count)}" });
+            if (RequestLogs.Count > 0)
+            {
+                var logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "req.txt").CreateFileIfNotExist();
+                File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.RequestUrls).GroupBy(s => s).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
+                File.AppendAllLines(logPath, new[] { "", $"累计处理请求数:{RequestLogs.Sum(kv => kv.Value.Count)}" });
 
-            logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ua.txt").CreateFileIfNotExist();
-            File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.UserAgents).Where(s => !string.IsNullOrEmpty(s)).Select(UserAgent.Parse).Where(ua => !(ua.IsBrowser || ua.IsMobile)).GroupBy(s => s.ToString()).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
+                logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ua.txt").CreateFileIfNotExist();
+                File.WriteAllLines(logPath, RequestLogs.Values.SelectMany(g => g.UserAgents).Where(s => !string.IsNullOrEmpty(s)).Select(UserAgent.Parse).Where(ua => !(ua.IsBrowser || ua.IsMobile)).GroupBy(s => s.ToString()).ToDictionary(x => x.Key, x => x.Count()).OrderBy(x => x.Key).ThenByDescending(x => x.Value).Select(g => g.Value + "\t" + g.Key), Encoding.UTF8);
 
-            logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "raw.json").CreateFileIfNotExist();
-            File.WriteAllText(logPath, RequestLogs.ToJsonString(new JsonSerializerSettings() { Formatting = Formatting.Indented }), Encoding.UTF8);
+                logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "raw.json").CreateFileIfNotExist();
+                File.WriteAllText(logPath, RequestLogs.ToJsonString(new JsonSerializerSettings() { Formatting = Formatting.Indented }), Encoding.UTF8);
 
-            logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ip.txt").CreateFileIfNotExist();
-            File.WriteAllLines(logPath, RequestLogs.Keys.Select(s => new { s, loc = s.GetIPLocation().ToString() }).OrderBy(x => x.loc).Select(g => g.s + "\t" + g.loc), Encoding.UTF8);
-            RequestLogs.Clear();
+                logPath = Path.Combine(AppContext.BaseDirectory + "logs", DateTime.Now.ToString("yyyyMMdd"), "ip.txt").CreateFileIfNotExist();
+                File.WriteAllLines(logPath, RequestLogs.Keys.Select(s => new { s, loc = s.GetIPLocation().ToString() }).OrderBy(x => x.loc).Select(g => g.s + "\t" + g.loc), Encoding.UTF8);
+                RequestLogs.Clear();
+            }
         }
 
         private static string CreateFileIfNotExist(this string filepath)

+ 4 - 2
src/Masuit.MyBlogs.Core/Controllers/PostController.cs

@@ -335,8 +335,8 @@ namespace Masuit.MyBlogs.Core.Controllers
         public async Task<ActionResult> All()
         {
             ViewBag.tags = new Dictionary<string, int>(PostService.GetTags().Where(x => x.Value > 1).OrderBy(x => x.Key));
-            ViewBag.cats = await CategoryService.GetQuery(c => c.Post.Count > 0, c => c.Post.Count, false).Include(c => c.Parent).ToDictionaryAsync(c => c.Id, c => c.Path()); //category
-            ViewBag.seminars = await SeminarService.GetAll(c => c.Post.Count, false).ToDictionaryAsync(c => c.Id, c => c.Title); //seminars
+            ViewBag.cats = await CategoryService.GetQuery(c => c.Post.Count > 0, c => c.Post.Count, false).Include(c => c.Parent).ThenInclude(c => c.Parent).AsNoTracking().ToDictionaryAsync(c => c.Id, c => c.Path()); //category
+            ViewBag.seminars = await SeminarService.GetAll(c => c.Post.Count, false).AsNoTracking().ToDictionaryAsync(c => c.Id, c => c.Title); //seminars
             return View();
         }
 
@@ -922,6 +922,7 @@ namespace Masuit.MyBlogs.Core.Controllers
         /// <param name="id">文章id</param>
         /// <returns></returns>
         [MyAuthorize]
+        [HttpPost("post/{id}/DisableComment")]
         public async Task<ActionResult> DisableComment(int id)
         {
             var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");
@@ -935,6 +936,7 @@ namespace Masuit.MyBlogs.Core.Controllers
         /// <param name="id">文章id</param>
         /// <returns></returns>
         [MyAuthorize]
+        [HttpPost("post/{id}/DisableCopy")]
         public async Task<ActionResult> DisableCopy(int id)
         {
             var post = await PostService.GetByIdAsync(id) ?? throw new NotFoundException("文章未找到");

+ 23 - 10
src/Masuit.MyBlogs.Core/Controllers/SystemController.cs

@@ -31,23 +31,36 @@ namespace Masuit.MyBlogs.Core.Controllers
         /// </summary>
         public ISystemSettingService SystemSettingService { get; set; }
 
+        public IPerfCounter PerfCounter { get; set; }
+
         /// <summary>
         /// 获取历史性能计数器
         /// </summary>
         /// <returns></returns>
         public IActionResult GetCounterHistory()
         {
-            var counters = PerfCounter.List.OrderBy(c => c.Time);
-            var list = counters.Count() < 5000 ? counters : counters.GroupBy(c => c.Time / 60000).Select(g => new PerformanceCounter
+            var counters = PerfCounter.CreateDataSource();
+            var count = counters.Count();
+            var ticks = count switch
             {
-                Time = g.Key * 60000,
-                CpuLoad = g.OrderBy(c => c.CpuLoad).Skip(1).Take(g.Count() - 2).Select(c => c.CpuLoad).DefaultIfEmpty().Average(),
-                DiskRead = g.OrderBy(c => c.DiskRead).Skip(1).Take(g.Count() - 2).Select(c => c.DiskRead).DefaultIfEmpty().Average(),
-                DiskWrite = g.OrderBy(c => c.DiskWrite).Skip(1).Take(g.Count() - 2).Select(c => c.DiskWrite).DefaultIfEmpty().Average(),
-                Download = g.OrderBy(c => c.Download).Skip(1).Take(g.Count() - 2).Select(c => c.Download).DefaultIfEmpty().Average(),
-                Upload = g.OrderBy(c => c.Upload).Skip(1).Take(g.Count() - 2).Select(c => c.Upload).DefaultIfEmpty().Average(),
-                MemoryUsage = g.OrderBy(c => c.MemoryUsage).Skip(1).Take(g.Count() - 2).Select(c => c.MemoryUsage).DefaultIfEmpty().Average()
-            });
+                > 5000 and <= 10000 => 3,
+                > 10000 and <= 20000 => 6,
+                > 20000 and <= 50000 => 12,
+                > 50000 and <= 100000 => 24,
+                > 100000 and <= 200000 => 48,
+                > 200000 and <= 300000 => 72,
+                _ => count
+            } * 10000;
+            var list = count < 5000 ? counters.ToList() : counters.GroupBy(c => c.Time / ticks).Select(g => new PerformanceCounter
+            {
+                Time = g.Key * ticks,
+                CpuLoad = g.Average(c => c.CpuLoad),
+                DiskRead = g.Average(c => c.DiskRead),
+                DiskWrite = g.Average(c => c.DiskWrite),
+                Download = g.Average(c => c.Download),
+                Upload = g.Average(c => c.Upload),
+                MemoryUsage = g.Average(c => c.MemoryUsage)
+            }).ToList();
             return Ok(new
             {
                 cpu = list.Select(c => new[]

+ 139 - 0
src/Masuit.MyBlogs.Core/Extensions/Firewall/IRequestLogger.cs

@@ -0,0 +1,139 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using Hangfire;
+using Masuit.MyBlogs.Core.Common;
+using Masuit.MyBlogs.Core.Extensions.Hangfire;
+using Masuit.MyBlogs.Core.Infrastructure;
+using Masuit.MyBlogs.Core.Models.Entity;
+using Masuit.Tools.Systems;
+using MaxMind.Db;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Graph.CallRecords;
+
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+public interface IRequestLogger
+{
+    void Log(string ip, string url, string userAgent);
+
+    void Process();
+}
+
+public class RequestNoneLogger : IRequestLogger
+{
+    public void Log(string ip, string url, string userAgent)
+    {
+    }
+
+    public void Process()
+    {
+    }
+}
+
+public class RequestFileLogger : IRequestLogger
+{
+    public void Log(string ip, string url, string userAgent)
+    {
+        TrackData.RequestLogs.AddOrUpdate(ip, new RequestLog()
+        {
+            Count = 1,
+            RequestUrls = { url },
+            UserAgents = { userAgent }
+        }, (_, i) =>
+        {
+            i.UserAgents.Add(userAgent);
+            i.RequestUrls.Add(url);
+            i.Count++;
+            return i;
+        });
+    }
+
+    public void Process()
+    {
+    }
+}
+
+public class RequestDatabaseLogger : IRequestLogger
+{
+    private readonly LoggerDbContext _dataContext;
+    private static readonly ConcurrentQueue<RequestLogDetail> Queue = new();
+
+    public RequestDatabaseLogger(LoggerDbContext dataContext)
+    {
+        _dataContext = dataContext;
+    }
+
+    public void Log(string ip, string url, string userAgent)
+    {
+        Queue.Enqueue(new RequestLogDetail
+        {
+            Time = DateTime.Now,
+            UserAgent = userAgent,
+            RequestUrl = url,
+            IP = ip
+        });
+    }
+
+    public void Process()
+    {
+        if (Debugger.IsAttached)
+        {
+            return;
+        }
+
+        while (Queue.TryDequeue(out var result))
+        {
+            var (location, network, info) = result.IP.GetIPLocation();
+            result.Location = location;
+            result.Network = network;
+            _dataContext.Add(result);
+        }
+        _dataContext.SaveChanges();
+        var start = DateTime.Now.AddMonths(-6);
+        _dataContext.Set<RequestLogDetail>().Where(e => e.Time < start).DeleteFromQuery();
+    }
+}
+
+public class RequestLoggerBackService : BackgroundService
+{
+    private readonly IRequestLogger _logger;
+
+    public RequestLoggerBackService(IRequestLogger logger)
+    {
+        _logger = logger;
+    }
+
+    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+    {
+        while (true)
+        {
+            _logger.Process();
+            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
+        }
+    }
+}
+
+public static class RequestLoggerServiceExtension
+{
+    public static IServiceCollection AddRequestLogger(this IServiceCollection services, IConfiguration configuration)
+    {
+        switch (configuration["RequestLogStorage"])
+        {
+            case "database":
+                services.AddScoped<IRequestLogger, RequestDatabaseLogger>();
+                services.TryAddScoped<RequestDatabaseLogger>();
+                break;
+
+            case "file":
+                services.AddSingleton<IRequestLogger, RequestFileLogger>();
+                break;
+
+            default:
+                services.AddSingleton<IRequestLogger, RequestNoneLogger>();
+                break;
+        }
+
+        services.AddHostedService<RequestLoggerBackService>();
+        return services;
+    }
+}

+ 81 - 91
src/Masuit.MyBlogs.Core/Extensions/Firewall/RequestInterceptMiddleware.cs

@@ -11,120 +11,110 @@ using System.Text.RegularExpressions;
 using System.Web;
 using HeaderNames = Microsoft.Net.Http.Headers.HeaderNames;
 
-namespace Masuit.MyBlogs.Core.Extensions.Firewall
+namespace Masuit.MyBlogs.Core.Extensions.Firewall;
+
+/// <summary>
+/// 请求拦截器
+/// </summary>
+public class RequestInterceptMiddleware
 {
+    private readonly RequestDelegate _next;
+    private readonly IRequestLogger _requestLogger;
+
     /// <summary>
-    /// 请求拦截器
+    /// 构造函数
     /// </summary>
-    public class RequestInterceptMiddleware
+    /// <param name="next"></param>
+    public RequestInterceptMiddleware(RequestDelegate next, IRequestLogger requestLogger)
     {
-        private readonly RequestDelegate _next;
+        _next = next;
+        _requestLogger = requestLogger;
+    }
 
-        /// <summary>
-        /// 构造函数
-        /// </summary>
-        /// <param name="next"></param>
-        public RequestInterceptMiddleware(RequestDelegate next)
-        {
-            _next = next;
-        }
+    public Task Invoke(HttpContext context)
+    {
+        var request = context.Request;
 
-        public Task Invoke(HttpContext context)
+        //启用读取request
+        request.EnableBuffering();
+        if (!AppConfig.EnableIPDirect && request.Host.Host.MatchInetAddress() && !request.Host.Host.IsPrivateIP())
         {
-            var request = context.Request;
-
-            //启用读取request
-            request.EnableBuffering();
-            if (!AppConfig.EnableIPDirect && request.Host.Host.MatchInetAddress() && !request.Host.Host.IsPrivateIP())
-            {
-                context.Response.Redirect("https://www.baidu.com", true);
+            context.Response.Redirect("https://www.baidu.com", true);
 
-                //context.Response.StatusCode = 404;
-                return Task.CompletedTask;
-            }
-            var ip = context.Connection.RemoteIpAddress!.ToString();
-            var path = HttpUtility.UrlDecode(request.Path + request.QueryString, Encoding.UTF8);
-            var requestUrl = HttpUtility.UrlDecode(request.Scheme + "://" + request.Host + path);
-            var match = Regex.Match(path ?? "", CommonHelper.BanRegex);
-            if (match.Length > 0)
+            //context.Response.StatusCode = 404;
+            return Task.CompletedTask;
+        }
+        var ip = context.Connection.RemoteIpAddress!.ToString();
+        var path = HttpUtility.UrlDecode(request.Path + request.QueryString, Encoding.UTF8);
+        var requestUrl = HttpUtility.UrlDecode(request.Scheme + "://" + request.Host + path);
+        var match = Regex.Match(path ?? "", CommonHelper.BanRegex);
+        if (match.Length > 0)
+        {
+            RedisHelper.IncrBy("interceptCount");
+            RedisHelper.LPush("intercept", new IpIntercepter()
             {
-                RedisHelper.IncrBy("interceptCount");
-                RedisHelper.LPush("intercept", new IpIntercepter()
-                {
-                    IP = ip,
-                    RequestUrl = requestUrl,
-                    Time = DateTime.Now,
-                    Referer = request.Headers[HeaderNames.Referer],
-                    UserAgent = request.Headers[HeaderNames.UserAgent],
-                    Remark = $"检测到敏感词拦截:{match.Value}",
-                    Address = request.Location(),
-                    HttpVersion = request.Protocol,
-                    Headers = request.Headers.ToJsonString()
-                });
-                context.Response.StatusCode = 404;
-                context.Response.ContentType = "text/html; charset=utf-8";
-                return context.Response.WriteAsync("参数不合法!", Encoding.UTF8);
-            }
+                IP = ip,
+                RequestUrl = requestUrl,
+                Time = DateTime.Now,
+                Referer = request.Headers[HeaderNames.Referer],
+                UserAgent = request.Headers[HeaderNames.UserAgent],
+                Remark = $"检测到敏感词拦截:{match.Value}",
+                Address = request.Location(),
+                HttpVersion = request.Protocol,
+                Headers = request.Headers.ToJsonString()
+            });
+            context.Response.StatusCode = 404;
+            context.Response.ContentType = "text/html; charset=utf-8";
+            return context.Response.WriteAsync("参数不合法!", Encoding.UTF8);
+        }
 
-            if (!context.Session.TryGetValue("session", out _) && !context.Request.IsRobot())
+        if (!context.Session.TryGetValue("session", out _) && !context.Request.IsRobot())
+        {
+            context.Session.Set("session", 0);
+            var referer = context.Request.Headers[HeaderNames.Referer].ToString();
+            if (!string.IsNullOrEmpty(referer))
             {
-                context.Session.Set("session", 0);
-                var referer = context.Request.Headers[HeaderNames.Referer].ToString();
-                if (!string.IsNullOrEmpty(referer))
+                try
                 {
-                    try
-                    {
-                        new Uri(referer);//判断是不是一个合法的referer
-                        if (!referer.Contains(context.Request.Host.Value) && !referer.Contains(new[] { "baidu.com", "google", "sogou", "so.com", "bing.com", "sm.cn" }))
-                        {
-                            BackgroundJob.Enqueue<IHangfireBackJob>(job => job.UpdateLinkWeight(referer, ip));
-                        }
-                    }
-                    catch
+                    new Uri(referer);//判断是不是一个合法的referer
+                    if (!referer.Contains(context.Request.Host.Value) && !referer.Contains(new[] { "baidu.com", "google", "sogou", "so.com", "bing.com", "sm.cn" }))
                     {
-                        context.Response.StatusCode = 405;
-                        context.Response.ContentType = "text/html; charset=utf-8";
-                        return context.Response.WriteAsync("您的浏览器不支持访问本站!", Encoding.UTF8);
+                        BackgroundJob.Enqueue<IHangfireBackJob>(job => job.UpdateLinkWeight(referer, ip));
                     }
                 }
-            }
-
-            if (!context.Request.IsRobot())
-            {
-                if (request.QueryString.HasValue && request.QueryString.Value.Contains("="))
+                catch
                 {
-                    var q = request.QueryString.Value.Trim('?');
-                    requestUrl = requestUrl.Replace(q, q.Split('&').Where(s => !s.StartsWith("cid") && !s.StartsWith("uid")).Join("&"));
+                    context.Response.StatusCode = 405;
+                    context.Response.ContentType = "text/html; charset=utf-8";
+                    return context.Response.WriteAsync("您的浏览器不支持访问本站!", Encoding.UTF8);
                 }
-                TrackData.RequestLogs.AddOrUpdate(ip, new RequestLog()
-                {
-                    Count = 1,
-                    RequestUrls = { requestUrl },
-                    UserAgents = { request.Headers[HeaderNames.UserAgent] }
-                }, (_, i) =>
-                {
-                    i.UserAgents.Add(request.Headers[HeaderNames.UserAgent]);
-                    i.RequestUrls.Add(requestUrl);
-                    i.Count++;
-                    return i;
-                });
             }
+        }
 
-            if (string.IsNullOrEmpty(context.Session.Get<string>(SessionKey.TimeZone)))
+        if (!context.Request.IsRobot())
+        {
+            if (request.QueryString.HasValue && request.QueryString.Value.Contains("="))
             {
-                context.Session.Set(SessionKey.TimeZone, context.Connection.RemoteIpAddress.GetClientTimeZone());
+                var q = request.QueryString.Value.Trim('?');
+                requestUrl = requestUrl.Replace(q, q.Split('&').Where(s => !s.StartsWith("cid") && !s.StartsWith("uid")).Join("&"));
             }
+            _requestLogger.Log(ip, requestUrl, context.Request.Headers[HeaderNames.UserAgent]);
+        }
 
-            if (!context.Request.Cookies.ContainsKey(SessionKey.RawIP))
-            {
-                context.Response.Cookies.Append(SessionKey.RawIP, ip.Base64Encrypt(), new CookieOptions()
-                {
-                    Expires = DateTimeOffset.Now.AddDays(1),
-                    SameSite = SameSiteMode.Lax
-                });
-            }
+        if (string.IsNullOrEmpty(context.Session.Get<string>(SessionKey.TimeZone)))
+        {
+            context.Session.Set(SessionKey.TimeZone, context.Connection.RemoteIpAddress.GetClientTimeZone());
+        }
 
-            return _next(context);
+        if (!context.Request.Cookies.ContainsKey(SessionKey.RawIP))
+        {
+            context.Response.Cookies.Append(SessionKey.RawIP, ip.Base64Encrypt(), new CookieOptions()
+            {
+                Expires = DateTimeOffset.Now.AddDays(1),
+                SameSite = SameSiteMode.Lax
+            });
         }
+
+        return _next(context);
     }
 }

+ 62 - 0
src/Masuit.MyBlogs.Core/Infrastructure/LoggerDbContext.cs

@@ -0,0 +1,62 @@
+using Masuit.MyBlogs.Core.Models.Entity;
+using Microsoft.EntityFrameworkCore;
+using System.Reflection;
+using Masuit.MyBlogs.Core.Common;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+namespace Masuit.MyBlogs.Core.Infrastructure;
+
+public class LoggerDbContext : DbContext
+{
+    public LoggerDbContext(DbContextOptions<LoggerDbContext> options) : base(options)
+    {
+    }
+
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        optionsBuilder.EnableDetailedErrors().UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
+    }
+
+    protected override void OnModelCreating(ModelBuilder modelBuilder)
+    {
+        modelBuilder.Entity<RequestLogDetail>().HasKey(e => new { e.Id, e.Time });
+        modelBuilder.Entity<PerformanceCounter>().HasKey(e => e.Time);
+    }
+}
+
+[AttributeUsage(AttributeTargets.Property)]
+public class HypertableColumnAttribute : Attribute
+{ }
+
+public static class TimeScaleExtensions
+{
+    public static void ApplyHypertables(this DbContext context)
+    {
+        if (context.Database.IsNpgsql())
+        {
+            context.Database.ExecuteSqlRaw("CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;");
+            var entityTypes = context.Model.GetEntityTypes();
+            foreach (var entityType in entityTypes)
+            {
+                foreach (var property in entityType.GetProperties())
+                {
+                    if (property.PropertyInfo.GetCustomAttribute(typeof(HypertableColumnAttribute)) != null)
+                    {
+                        var tableName = entityType.GetTableName();
+                        var schema = entityType.GetSchema();
+                        var identifier = StoreObjectIdentifier.Table(tableName, schema);
+                        var columnName = property.GetColumnName(identifier);
+                        if (property.ClrType == typeof(DateTime))
+                        {
+                            context.Database.ExecuteSqlRaw($"SELECT create_hypertable('\"{tableName}\"', '{columnName}');");
+                        }
+                        else
+                        {
+                            context.Database.ExecuteSqlRaw($"SELECT create_hypertable('\"{tableName}\"', '{columnName}', chunk_time_interval => 100000);");
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 4 - 4
src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj

@@ -38,7 +38,7 @@
     </ItemGroup>
 
     <ItemGroup>
-        <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0" />
+        <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
         <PackageReference Include="AutoMapper.Extensions.ExpressionMapping" Version="5.0.2" />
         <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
         <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
@@ -47,8 +47,8 @@
         <PackageReference Include="CHTCHSConv" Version="1.0.0" />
         <PackageReference Include="CLRStats" Version="1.0.0" />
         <PackageReference Include="Collections.Pooled" Version="1.0.82" />
-        <PackageReference Include="CSRedisCore" Version="3.6.9" />
-        <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.4.0" />
+        <PackageReference Include="CSRedisCore" Version="3.8.2" />
+        <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.5.0" />
         <PackageReference Include="Hangfire" Version="1.7.29" />
         <PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
         <PackageReference Include="htmldiff.net-core" Version="1.3.6" />
@@ -72,7 +72,7 @@
         <PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.18" />
         <PackageReference Include="TimeZoneConverter" Version="5.0.0" />
         <PackageReference Include="WilderMinds.RssSyndication" Version="1.7.0" />
-        <PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="6.13.20" />
+        <PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="6.13.21" />
     </ItemGroup>
     <ItemGroup>
         <Content Update="appsettings.json">

+ 37 - 0
src/Masuit.MyBlogs.Core/Models/Entity/RequestLogDetail.cs

@@ -0,0 +1,37 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Masuit.MyBlogs.Core.Infrastructure;
+using Masuit.Tools.Systems;
+using Microsoft.EntityFrameworkCore;
+
+namespace Masuit.MyBlogs.Core.Models.Entity;
+
+[Table(nameof(RequestLogDetail))]
+public class RequestLogDetail
+{
+    public RequestLogDetail()
+    {
+        Id = SnowFlake.NewId;
+    }
+
+    [StringLength(32)]
+    public string Id { get; set; }
+
+    [Column(TypeName = "timestamp"), HypertableColumn]
+    public DateTime Time { get; set; }
+
+    [StringLength(1024), Unicode]
+    public string UserAgent { get; set; }
+
+    [StringLength(4096), Unicode]
+    public string RequestUrl { get; set; }
+
+    [StringLength(128), Unicode]
+    public string IP { get; set; }
+
+    [StringLength(256), Unicode]
+    public string Location { get; set; }
+
+    [StringLength(256)]
+    public string Network { get; set; }
+}

+ 0 - 1
src/Masuit.MyBlogs.Core/Program.cs

@@ -24,7 +24,6 @@ if (!"223.5.5.5".GetIPLocation().Contains("阿里"))
 }
 
 InitOneDrive(); // 初始化Onedrive程序
-PerfCounter.Init(); // 初始化性能计数器
 Host.CreateDefaultBuilder(args).UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureWebHostDefaults(hostBuilder => hostBuilder.UseKestrel(opt =>
 {
     var config = opt.ApplicationServices.GetService<IConfiguration>();

+ 31 - 4
src/Masuit.MyBlogs.Core/Startup.cs

@@ -100,6 +100,27 @@ namespace Masuit.MyBlogs.Core
 
                 opt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>()).EnableSensitiveDataLogging();
             }); //配置数据库
+            services.AddDbContext<LoggerDbContext>(opt =>
+            {
+                switch (Configuration["Database:Provider"])
+                {
+                    case "pgsql":
+                        opt.UseNpgsql(AppConfig.ConnString);
+                        break;
+
+                    case "mysql":
+                        opt.UseMySql(AppConfig.ConnString, ServerVersion.AutoDetect(AppConfig.ConnString), builder => builder.EnableRetryOnFailure(10));
+                        break;
+
+                    case "mssql":
+                        opt.UseSqlServer(AppConfig.ConnString, builder => builder.EnableRetryOnFailure(10));
+                        break;
+
+                    case "sqlite":
+                        opt.UseSqlite(AppConfig.ConnString);
+                        break;
+                }
+            }); //配置数据库
             services.ConfigureOptions();
             services.AddHttpsRedirection(options =>
             {
@@ -132,7 +153,7 @@ namespace Masuit.MyBlogs.Core
                 return new HttpClientHandler();
             }); //注入HttpClient
             services.AddHttpClient<ImagebedClient>().AddTransientHttpErrorPolicy(builder => builder.Or<TaskCanceledException>().Or<OperationCanceledException>().Or<TimeoutException>().OrResult(res => !res.IsSuccessStatusCode).RetryAsync(3)); //注入HttpClient
-            services.AddMailSender(Configuration).AddFirewallReporter(Configuration);
+            services.AddMailSender(Configuration).AddFirewallReporter(Configuration).AddRequestLogger(Configuration).AddPerfCounterManager(Configuration);
             services.AddBundling().UseDefaults(_env).UseNUglify().EnableMinification().EnableChangeDetection().EnableCacheHeader(TimeSpan.FromHours(1));
             services.SetupMiniProfile();
             services.AddSingleton<IMimeMapper, MimeMapper>(p => new MimeMapper());
@@ -152,13 +173,19 @@ namespace Masuit.MyBlogs.Core
         /// </summary>
         /// <param name="app"></param>
         /// <param name="env"></param>
-        /// <param name="db"></param>
         /// <param name="hangfire"></param>
         /// <param name="luceneIndexerOptions"></param>
-        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext db, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions)
+        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHangfireBackJob hangfire, LuceneIndexerOptions luceneIndexerOptions)
         {
-            db.Database.EnsureCreated();
             ServiceProvider = app.ApplicationServices;
+            var maindb = ServiceProvider.GetRequiredService<DataContext>();
+            var loggerdb = ServiceProvider.GetRequiredService<LoggerDbContext>();
+            maindb.Database.EnsureCreated();
+            if (loggerdb.Database.EnsureCreated())
+            {
+                loggerdb.ApplyHypertables();
+            }
+
             app.InitSettings();
             app.UseLuceneSearch(env, hangfire, luceneIndexerOptions);
             app.UseForwardedHeaders().UseCertificateForwarding(); // X-Forwarded-For

+ 5 - 4
src/Masuit.MyBlogs.Core/Views/Dashboard/Counter.razor

@@ -7,6 +7,7 @@
 @using Collections.Pooled
 @using Masuit.Tools.Logging
 @using PerformanceCounter = Masuit.MyBlogs.Core.Common.PerformanceCounter
+@inject IPerfCounter PerfCounter;
 @inject IJSRuntime JS;
 
 <h3 class="text-center">
@@ -130,7 +131,7 @@
     protected override void OnInitialized()
     {
         var boot = DateTime.Now - SystemInfo.BootTime();
-        var span = DateTime.Now - PerfCounter.StartTime;
+        var span = DateTime.Now - IPerfCounter.StartTime;
         runningTime = $"{span.Days}天{span.Hours}小时{span.Minutes}分钟";
         bootTime = $"{boot.Days}天{boot.Hours}小时{boot.Minutes}分钟";
     }
@@ -144,7 +145,7 @@
     public static PerformanceCounter GetCurrentPerformanceCounter()
     {
         try {
-            return PerfCounter.GetCurrentPerformanceCounter();
+            return IPerfCounter.GetCurrentPerformanceCounter();
         }
         catch (Exception e) {
             LogManager.Error(e.Demystify());
@@ -160,8 +161,8 @@
 
     public Dictionary<string, dynamic> GetCounterPercent()
     {
-        var counters = PerfCounter.List;
-        var count = counters.Count;
+        var counters = PerfCounter.CreateDataSource();
+        var count = counters.Count();
         return new()
         {
             ["CPU使用率(%)"] = new

+ 2 - 0
src/Masuit.MyBlogs.Core/appsettings.json

@@ -38,6 +38,8 @@
             "AuthKey": "AuthKey" // apikey
         }
     },
+    "RequestLogStorage": "database", // 请求日志存储介质,file 或 database 或 none,默认none
+    "PerfCounterStorage": "database", // 性能监控存储介质,memory 或 database,默认memory
     "Imgbed": { // 图床相关配置
         "EnableLocalStorage": false, // 允许本地硬盘存储?
         "EnableExternalImgbed": false, // 允许上传至第三方图床(网易图床/Sohu图床/头条图床)?

+ 2 - 6
src/Masuit.MyBlogs.Core/wwwroot/ng-views/controllers/post.js

@@ -234,9 +234,7 @@
     }
     
     $scope.toggleDisableComment= function(row) {
-        $scope.request("/post/DisableComment", {
-            id: row.Id
-        }, function(data) {
+        $scope.request(`/post/${row.Id}/DisableComment`, null, function(data) {
             window.notie.alert({
                 type: 1,
                 text: data.Message,
@@ -246,9 +244,7 @@
     }
 
     $scope.toggleDisableCopy= function(row) {
-        $scope.request("/post/DisableCopy", {
-            id: row.Id
-        }, function(data) {
+        $scope.request(`/post/${row.Id}/DisableCopy`, null, function(data) {
             window.notie.alert({
                 type: 1,
                 text: data.Message,