diff --git a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
index fb45729..d0972f7 100644
--- a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
+++ b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
@@ -31,6 +31,7 @@
+
diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs
index 395e16f..9345f9a 100644
--- a/BililiveRecorder.Cli/Program.cs
+++ b/BililiveRecorder.Cli/Program.cs
@@ -19,6 +19,7 @@ using BililiveRecorder.DependencyInjection;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.ToolBox;
using BililiveRecorder.Web;
+using BililiveRecorder.Web.Models.Rest.Logs;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
@@ -112,7 +113,7 @@ namespace BililiveRecorder.Cli
{
var path = Path.GetFullPath(args.Path);
- using var logger = BuildLogger(args.LogLevel, args.LogFileLevel);
+ using var logger = BuildLogger(args.LogLevel, args.LogFileLevel, enableWebLog: args.HttpBind is not null);
Log.Logger = logger;
path = Path.GetFullPath(path);
@@ -145,7 +146,7 @@ namespace BililiveRecorder.Cli
private static async Task RunPortableModeAsync(PortableModeArguments args)
{
- using var logger = BuildLogger(args.LogLevel, args.LogFileLevel);
+ using var logger = BuildLogger(args.LogLevel, args.LogFileLevel, enableWebLog: args.HttpBind is not null);
Log.Logger = logger;
var config = new ConfigV3()
@@ -426,7 +427,7 @@ namespace BililiveRecorder.Cli
.AddRecorder()
.BuildServiceProvider();
- private static Logger BuildLogger(LogEventLevel logLevel, LogEventLevel logFileLevel)
+ private static Logger BuildLogger(LogEventLevel logLevel, LogEventLevel logFileLevel, bool enableWebLog = false)
{
var logFilePath = Environment.GetEnvironmentVariable("BILILIVERECORDER_LOG_FILE_PATH");
if (string.IsNullOrWhiteSpace(logFilePath))
@@ -470,6 +471,18 @@ namespace BililiveRecorder.Cli
;
});
+ if (enableWebLog)
+ {
+ var webSink = new WebApiLogEventSink(new CompactJsonFormatter());
+ WebApiLogEventSink.Instance = webSink;
+ builder.WriteTo.Logger(sl =>
+ {
+ sl
+ .Filter.ByExcluding(matchMicrosoft)
+ .WriteTo.Async(l => l.Sink(webSink, restrictedToMinimumLevel: LogEventLevel.Debug));
+ });
+ }
+
if (ansiColorSupport)
{
builder.WriteTo.Console(new ExpressionTemplate("[{@t:HH:mm:ss} {@l:u3}{#if SourceContext is not null} ({SourceContext}){#end}]{#if RoomId is not null} [{RoomId}]{#end} {@m}{#if ExceptionDetail is not null}\n [{ExceptionDetail['Type']}]: {ExceptionDetail['Message']}{#end}\n", theme: Serilog.Templates.Themes.TemplateTheme.Code), logLevel);
diff --git a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj
index 880a7f8..1d2c5ad 100644
--- a/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj
+++ b/BililiveRecorder.WPF/BililiveRecorder.WPF.csproj
@@ -377,6 +377,9 @@
1.1.0
+
+ 1.5.0
+
4.0.1
diff --git a/BililiveRecorder.WPF/Program.cs b/BililiveRecorder.WPF/Program.cs
index 62b33b4..c44a057 100644
--- a/BililiveRecorder.WPF/Program.cs
+++ b/BililiveRecorder.WPF/Program.cs
@@ -301,11 +301,11 @@ namespace BililiveRecorder.WPF
.WriteTo.Console(levelSwitch: levelSwitchConsole)
#if DEBUG
.WriteTo.Debug()
- .WriteTo.Sink(Serilog.Events.LogEventLevel.Debug)
+ .WriteTo.Async(l => l.Sink(Serilog.Events.LogEventLevel.Debug))
#else
- .WriteTo.Sink(Serilog.Events.LogEventLevel.Information)
+ .WriteTo.Async(l => l.Sink(Serilog.Events.LogEventLevel.Information))
#endif
- .WriteTo.File(new CompactJsonFormatter(), logFilePath, shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
+ .WriteTo.Async(l => l.File(new CompactJsonFormatter(), logFilePath, shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true))
.WriteTo.Sentry(o =>
{
o.Dsn = "https://6f92720d5ce84b2dba5db75ab5a5014d@o210546.ingest.sentry.io/5556540";
diff --git a/BililiveRecorder.Web/Api/LogController.cs b/BililiveRecorder.Web/Api/LogController.cs
new file mode 100644
index 0000000..5deec5f
--- /dev/null
+++ b/BililiveRecorder.Web/Api/LogController.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.Linq;
+using BililiveRecorder.Web.Models.Rest.Logs;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using StructLinq;
+
+namespace BililiveRecorder.Web.Api
+{
+ [ApiController, Route("api/[controller]", Name = "[controller] [action]")]
+ public sealed class LogController : ControllerBase
+ {
+ public LogController()
+ {
+ }
+
+ ///
+ /// 获取 JSON 日志
+ ///
+ /// 只获取此 id 之后的日志
+ ///
+ [HttpGet("fetch")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetJsonLog([FromQuery] long? after)
+ {
+ var sink = WebApiLogEventSink.Instance;
+ if (sink is null)
+ {
+ return new JsonLogDto();
+ }
+
+ List logs = null!;
+
+ sink.ReadLogs(queue =>
+ {
+ logs = queue.ToList();
+ });
+
+ if (!after.HasValue)
+ {
+ return new JsonLogDto
+ {
+ Continuous = false,
+ Cursor = logs[^1].Id,
+ Logs = logs.Select(x => x.Log)
+ };
+ }
+ else
+ {
+ var index = logs.BinarySearch(new JsonLog { Id = after.Value });
+ return new JsonLogDto
+ {
+ Continuous = index >= 0,
+ Cursor = logs[^1].Id,
+ Logs = logs.TakeLast((index >= 0) ? (logs.Count - index - 1) : logs.Count).Select(x => x.Log)
+ };
+ }
+ }
+ }
+}
diff --git a/BililiveRecorder.Web/Models/Rest/Logs/JsonLog.cs b/BililiveRecorder.Web/Models/Rest/Logs/JsonLog.cs
new file mode 100644
index 0000000..a4735ca
--- /dev/null
+++ b/BililiveRecorder.Web/Models/Rest/Logs/JsonLog.cs
@@ -0,0 +1,15 @@
+using System;
+using Newtonsoft.Json;
+
+namespace BililiveRecorder.Web.Models.Rest.Logs
+{
+ public class JsonLog : IComparable
+ {
+ public long Id { get; set; }
+
+ [JsonConverter(typeof(RawJsonStringConverter))]
+ public string Log { get; set; } = "{}";
+
+ int IComparable.CompareTo(JsonLog? other) => this.Id.CompareTo(other?.Id);
+ }
+}
diff --git a/BililiveRecorder.Web/Models/Rest/Logs/JsonLogDto.cs b/BililiveRecorder.Web/Models/Rest/Logs/JsonLogDto.cs
new file mode 100644
index 0000000..dbb6305
--- /dev/null
+++ b/BililiveRecorder.Web/Models/Rest/Logs/JsonLogDto.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace BililiveRecorder.Web.Models.Rest.Logs
+{
+ public class JsonLogDto
+ {
+ public bool Continuous { get; set; }
+
+ public long Cursor { get; set; }
+
+ [JsonProperty(ItemConverterType = typeof(RawJsonStringConverter))]
+ public IEnumerable Logs { get; set; } = Array.Empty();
+ }
+}
diff --git a/BililiveRecorder.Web/Models/Rest/Logs/RawJsonStringConverter.cs b/BililiveRecorder.Web/Models/Rest/Logs/RawJsonStringConverter.cs
new file mode 100644
index 0000000..b3e01ef
--- /dev/null
+++ b/BililiveRecorder.Web/Models/Rest/Logs/RawJsonStringConverter.cs
@@ -0,0 +1,12 @@
+using System;
+using Newtonsoft.Json;
+
+namespace BililiveRecorder.Web.Models.Rest.Logs
+{
+ internal sealed class RawJsonStringConverter : JsonConverter
+ {
+ public override string? ReadJson(JsonReader reader, Type objectType, string? existingValue, bool hasExistingValue, JsonSerializer serializer) => (string?)reader.Value;
+
+ public override void WriteJson(JsonWriter writer, string? value, JsonSerializer serializer) => writer.WriteRawValue(value);
+ }
+}
diff --git a/BililiveRecorder.Web/Models/Rest/Logs/WebApiLogEventSink.cs b/BililiveRecorder.Web/Models/Rest/Logs/WebApiLogEventSink.cs
new file mode 100644
index 0000000..37c487e
--- /dev/null
+++ b/BililiveRecorder.Web/Models/Rest/Logs/WebApiLogEventSink.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using Serilog.Core;
+using Serilog.Events;
+using Serilog.Formatting;
+
+namespace BililiveRecorder.Web.Models.Rest.Logs
+{
+ public class WebApiLogEventSink : ILogEventSink
+ {
+ public static WebApiLogEventSink? Instance;
+
+ private const int MAX_LOG = 100;
+
+ private readonly ReaderWriterLockSlim readerWriterLock = new();
+ private readonly ITextFormatter textFormatter;
+
+ private readonly Queue logs = new Queue();
+
+ private int logId = 0;
+
+ public WebApiLogEventSink(ITextFormatter textFormatter)
+ {
+ this.textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
+ }
+
+ public void Emit(LogEvent logEvent)
+ {
+ using var writer = new StringWriter();
+ this.textFormatter.Format(logEvent, writer);
+ var json = writer.ToString();
+
+ if (this.readerWriterLock.TryEnterWriteLock(5000))
+ {
+ try
+ {
+ this.logs.Enqueue(new JsonLog { Id = Interlocked.Increment(ref this.logId), Log = json });
+
+ while (this.logs.Count > MAX_LOG)
+ this.logs.Dequeue();
+ }
+ finally
+ {
+ this.readerWriterLock.ExitWriteLock();
+ }
+ }
+ }
+
+ public void ReadLogs(Action> callback)
+ {
+ if (this.readerWriterLock.TryEnterReadLock(1000))
+ {
+ try
+ {
+ callback(this.logs);
+ }
+ finally
+ {
+ this.readerWriterLock.ExitReadLock();
+ }
+ }
+ }
+ }
+}
diff --git a/BililiveRecorder.Web/embeded/logs.html b/BililiveRecorder.Web/embeded/logs.html
new file mode 100644
index 0000000..62c5f57
--- /dev/null
+++ b/BililiveRecorder.Web/embeded/logs.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+ 录播姬日志演示页
+
+
+
+
+ 当前 cursor:
+
+
+
+
+ 日期 |
+ 级别 |
+ 房间号 |
+ 消息 |
+
+
+
+
+
+
+