diff options
| -rw-r--r-- | Emby.Server.Implementations/ApplicationHost.cs | 3 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/ClientLogController.cs | 72 | ||||
| -rw-r--r-- | Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs | 54 | ||||
| -rw-r--r-- | Jellyfin.Server/Jellyfin.Server.csproj | 1 | ||||
| -rw-r--r-- | Jellyfin.Server/Program.cs | 48 | ||||
| -rw-r--r-- | MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs | 38 | ||||
| -rw-r--r-- | MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs | 16 | ||||
| -rw-r--r-- | MediaBrowser.Model/ClientLog/ClientLogEvent.cs | 75 |
8 files changed, 296 insertions, 11 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6fd152a42..4cfd5cb37 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -56,6 +56,7 @@ using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -672,6 +673,8 @@ namespace Emby.Server.Implementations ServiceCollection.AddScoped<AudioHelper>(); ServiceCollection.AddScoped<DynamicHlsHelper>(); + ServiceCollection.AddScoped<IClientEventLogger, ClientEventLogger>(); + ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>(); } diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs new file mode 100644 index 000000000..b894deb84 --- /dev/null +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -0,0 +1,72 @@ +using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.ClientLogDtos; +using MediaBrowser.Controller.ClientEvent; +using MediaBrowser.Model.ClientLog; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Client log controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class ClientLogController : BaseJellyfinApiController + { + private readonly IClientEventLogger _clientEventLogger; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogController"/> class. + /// </summary> + /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> + public ClientLogController(IClientEventLogger clientEventLogger) + { + _clientEventLogger = clientEventLogger; + } + + /// <summary> + /// Post event from client. + /// </summary> + /// <param name="clientLogEventDto">The client log dto.</param> + /// <response code="204">Event logged.</response> + /// <returns>Submission status.</returns> + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult LogEvent([FromBody] ClientLogEventDto clientLogEventDto) + { + Log(clientLogEventDto); + return NoContent(); + } + + /// <summary> + /// Bulk post events from client. + /// </summary> + /// <param name="clientLogEventDtos">The list of client log dtos.</param> + /// <response code="204">All events logged.</response> + /// <returns>Submission status.</returns> + [HttpPost("Bulk")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult LogEvents([FromBody] ClientLogEventDto[] clientLogEventDtos) + { + foreach (var dto in clientLogEventDtos) + { + Log(dto); + } + + return NoContent(); + } + + private void Log(ClientLogEventDto dto) + { + _clientEventLogger.Log(new ClientLogEvent( + dto.Timestamp, + dto.Level, + dto.UserId, + dto.ClientName, + dto.ClientVersion, + dto.DeviceId, + dto.Message)); + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs new file mode 100644 index 000000000..04d97047a --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs @@ -0,0 +1,54 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// The client log dto. + /// </summary> + public class ClientLogEventDto + { + /// <summary> + /// Gets or sets the event timestamp. + /// </summary> + [Required] + public DateTime Timestamp { get; set; } + + /// <summary> + /// Gets or sets the log level. + /// </summary> + [Required] + public LogLevel Level { get; set; } + + /// <summary> + /// Gets or sets the user id. + /// </summary> + public Guid? UserId { get; set; } + + /// <summary> + /// Gets or sets the client name. + /// </summary> + [Required] + public string ClientName { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the client version. + /// </summary> + [Required] + public string ClientVersion { get; set; } = string.Empty; + + /// + /// <summary> + /// Gets or sets the device id. + /// </summary> + [Required] + public string DeviceId { get; set; } = string.Empty; + + /// <summary> + /// Gets or sets the log message. + /// </summary> + [Required] + public string Message { get; set; } = string.Empty; + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 8983eb50f..cb3ff5508 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -44,6 +44,7 @@ <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" /> + <PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.6" /> </ItemGroup> diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 45699f3af..778e53cf6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -14,6 +14,7 @@ using Emby.Server.Implementations.IO; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Hosting; @@ -26,6 +27,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; +using Serilog.Filters; using SQLitePCL; using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -595,22 +597,46 @@ namespace Jellyfin.Server { // Serilog.Log is used by SerilogLoggerFactory when no logger is specified Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Enrich.FromLogContext() - .Enrich.WithThreadId() + .WriteTo.Logger(lc => + lc.ReadFrom.Configuration(configuration) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .Filter.ByExcluding(Matching.FromSource<ClientEventLogger>())) + .WriteTo.Logger(lc => + lc.WriteTo.Map( + "ClientName", + (clientName, wt) + => wt.File( + Path.Combine(appPaths.LogDirectoryPath, clientName + "_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Message:l}{NewLine}{Exception}", + encoding: Encoding.UTF8)) + .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>())) .CreateLogger(); } catch (Exception ex) { Log.Logger = new LoggerConfiguration() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") - .WriteTo.Async(x => x.File( - Path.Combine(appPaths.LogDirectoryPath, "log_.log"), - rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", - encoding: Encoding.UTF8)) - .Enrich.FromLogContext() - .Enrich.WithThreadId() + .WriteTo.Logger(lc => + lc.WriteTo.Async(x => x.File( + Path.Combine(appPaths.LogDirectoryPath, "log_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Message:l}{NewLine}{Exception}", + encoding: Encoding.UTF8)) + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") + .Enrich.FromLogContext() + .Enrich.WithThreadId()) + .WriteTo.Logger(lc => + lc + .WriteTo.Map( + "ClientName", + (clientName, wt) + => wt.File( + Path.Combine(appPaths.LogDirectoryPath, clientName + "_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Message:l}{NewLine}{Exception}", + encoding: Encoding.UTF8)) + .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>())) .CreateLogger(); Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs new file mode 100644 index 000000000..c00a38d1b --- /dev/null +++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs @@ -0,0 +1,38 @@ +using System; +using MediaBrowser.Model.ClientLog; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.ClientEvent +{ + /// <inheritdoc /> + public class ClientEventLogger : IClientEventLogger + { + private const string LogString = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level}] [{ClientName}:{ClientVersion}]: UserId: {UserId} DeviceId: {DeviceId}{NewLine}{Message}"; + private readonly ILogger<ClientEventLogger> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientEventLogger"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{ClientEventLogger}"/> interface.</param> + public ClientEventLogger(ILogger<ClientEventLogger> logger) + { + _logger = logger; + } + + /// <inheritdoc /> + public void Log(ClientLogEvent clientLogEvent) + { + _logger.Log( + LogLevel.Critical, + LogString, + clientLogEvent.Timestamp, + clientLogEvent.Level.ToString(), + clientLogEvent.ClientName, + clientLogEvent.ClientVersion, + clientLogEvent.UserId ?? Guid.Empty, + clientLogEvent.DeviceId, + Environment.NewLine, + clientLogEvent.Message); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs new file mode 100644 index 000000000..bf799c7bf --- /dev/null +++ b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs @@ -0,0 +1,16 @@ +using MediaBrowser.Model.ClientLog; + +namespace MediaBrowser.Controller.ClientEvent +{ + /// <summary> + /// The client event logger. + /// </summary> + public interface IClientEventLogger + { + /// <summary> + /// Logs the event from the client. + /// </summary> + /// <param name="clientLogEvent">The client log event.</param> + void Log(ClientLogEvent clientLogEvent); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Model/ClientLog/ClientLogEvent.cs b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs new file mode 100644 index 000000000..e4ee88145 --- /dev/null +++ b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs @@ -0,0 +1,75 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Model.ClientLog +{ + /// <summary> + /// The client log event. + /// </summary> + public class ClientLogEvent + { + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogEvent"/> class. + /// </summary> + /// <param name="timestamp">The log timestamp.</param> + /// <param name="level">The log level.</param> + /// <param name="userId">The user id.</param> + /// <param name="clientName">The client name.</param> + /// <param name="clientVersion">The client version.</param> + /// <param name="deviceId">The device id.</param> + /// <param name="message">The message.</param> + public ClientLogEvent( + DateTime timestamp, + LogLevel level, + Guid? userId, + string clientName, + string clientVersion, + string deviceId, + string message) + { + Timestamp = timestamp; + UserId = userId; + ClientName = clientName; + ClientVersion = clientVersion; + DeviceId = deviceId; + Message = message; + Level = level; + } + + /// <summary> + /// Gets the event timestamp. + /// </summary> + public DateTime Timestamp { get; } + + /// <summary> + /// Gets the log level. + /// </summary> + public LogLevel Level { get; } + + /// <summary> + /// Gets the user id. + /// </summary> + public Guid? UserId { get; } + + /// <summary> + /// Gets the client name. + /// </summary> + public string ClientName { get; } + + /// <summary> + /// Gets the client version. + /// </summary> + public string ClientVersion { get; } + + /// + /// <summary> + /// Gets the device id. + /// </summary> + public string DeviceId { get; } + + /// <summary> + /// Gets the log message. + /// </summary> + public string Message { get; } + } +}
\ No newline at end of file |
