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: +

+
+ + + + + + + +
日期级别房间号消息
+
+ + + +