diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index c86078033..0ff5e8bb5 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -58,6 +58,8 @@ }; + __MINI_PROFILER__ + <% for (key in htmlWebpackPlugin.files.js) { %><% } %> <% for (key in htmlWebpackPlugin.files.css) { %><% } %> diff --git a/src/NzbDrone.Common/Options/AppOptions.cs b/src/NzbDrone.Common/Options/AppOptions.cs index 74cdf1d29..e38f5664c 100644 --- a/src/NzbDrone.Common/Options/AppOptions.cs +++ b/src/NzbDrone.Common/Options/AppOptions.cs @@ -5,4 +5,6 @@ public class AppOptions public string InstanceName { get; set; } public string Theme { get; set; } public bool? LaunchBrowser { get; set; } + public bool? ProfilerEnabled { get; set; } + public string ProfilerPosition { get; set; } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 415e20576..8cd48849b 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -69,6 +69,8 @@ namespace NzbDrone.Core.Configuration string PostgresMainDbConnectionString { get; } string PostgresLogDbConnectionString { get; } bool TrustCgnatIpAddresses { get; } + bool ProfilerEnabled { get; } + string ProfilerPosition { get; } } public class ConfigFileProvider : IConfigFileProvider @@ -234,6 +236,8 @@ namespace NzbDrone.Core.Configuration ? enumValue : GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled); + public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false); + public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false); public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant(); @@ -312,6 +316,9 @@ namespace NzbDrone.Core.Configuration public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant(); + public bool ProfilerEnabled => _appOptions.ProfilerEnabled ?? GetValueBoolean("ProfilerEnabled", false, persist: false); + public string ProfilerPosition => _appOptions.ProfilerPosition ?? GetValue("ProfilerPosition", "bottom-right", persist: false); + public int GetValueInt(string key, int defaultValue, bool persist = true) { return Convert.ToInt32(GetValue(key, defaultValue, persist)); @@ -499,7 +506,5 @@ namespace NzbDrone.Core.Configuration { SetValue("ApiKey", GenerateApiKey()); } - - public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false); } } diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs index 741a22f0b..02e687552 100644 --- a/src/NzbDrone.Core/Datastore/Database.cs +++ b/src/NzbDrone.Core/Datastore/Database.cs @@ -1,5 +1,4 @@ using System; -using System.Data; using System.Data.Common; using System.Data.SQLite; using Dapper; @@ -10,7 +9,7 @@ namespace NzbDrone.Core.Datastore { public interface IDatabase { - IDbConnection OpenConnection(); + DbConnection OpenConnection(); Version Version { get; } int Migration { get; } DatabaseType DatabaseType { get; } @@ -20,17 +19,17 @@ namespace NzbDrone.Core.Datastore public class Database : IDatabase { private readonly string _databaseName; - private readonly Func _datamapperFactory; + private readonly Func _datamapperFactory; private readonly Logger _logger = NzbDroneLogger.GetLogger(typeof(Database)); - public Database(string databaseName, Func datamapperFactory) + public Database(string databaseName, Func datamapperFactory) { _databaseName = databaseName; _datamapperFactory = datamapperFactory; } - public IDbConnection OpenConnection() + public DbConnection OpenConnection() { return _datamapperFactory(); } @@ -50,7 +49,7 @@ namespace NzbDrone.Core.Datastore get { using var db = _datamapperFactory(); - var dbConnection = db as DbConnection; + var dbConnection = db; return DatabaseVersionParser.ParseServerVersion(dbConnection.ServerVersion); } diff --git a/src/NzbDrone.Core/Datastore/LogDatabase.cs b/src/NzbDrone.Core/Datastore/LogDatabase.cs index a770c2661..f996986d8 100644 --- a/src/NzbDrone.Core/Datastore/LogDatabase.cs +++ b/src/NzbDrone.Core/Datastore/LogDatabase.cs @@ -1,5 +1,5 @@ using System; -using System.Data; +using System.Data.Common; namespace NzbDrone.Core.Datastore { @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore _databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType; } - public IDbConnection OpenConnection() + public DbConnection OpenConnection() { return _database.OpenConnection(); } diff --git a/src/NzbDrone.Core/Datastore/MainDatabase.cs b/src/NzbDrone.Core/Datastore/MainDatabase.cs index 521293299..7e39b1356 100644 --- a/src/NzbDrone.Core/Datastore/MainDatabase.cs +++ b/src/NzbDrone.Core/Datastore/MainDatabase.cs @@ -1,5 +1,7 @@ using System; -using System.Data; +using System.Data.Common; +using StackExchange.Profiling; +using StackExchange.Profiling.Data; namespace NzbDrone.Core.Datastore { @@ -18,9 +20,16 @@ namespace NzbDrone.Core.Datastore _databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType; } - public IDbConnection OpenConnection() + public DbConnection OpenConnection() { - return _database.OpenConnection(); + var connection = _database.OpenConnection(); + + if (_databaseType == DatabaseType.PostgreSQL) + { + return new ProfiledImplementations.NpgSqlConnection(connection, MiniProfiler.Current); + } + + return new ProfiledDbConnection(connection, MiniProfiler.Current); } public Version Version => _database.Version; diff --git a/src/NzbDrone.Core/Datastore/ProfiledImplementations.cs b/src/NzbDrone.Core/Datastore/ProfiledImplementations.cs new file mode 100644 index 000000000..040866066 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/ProfiledImplementations.cs @@ -0,0 +1,15 @@ +using System.Data.Common; +using StackExchange.Profiling.Data; + +namespace NzbDrone.Core.Datastore; + +public static class ProfiledImplementations +{ + public class NpgSqlConnection : ProfiledDbConnection + { + public NpgSqlConnection(DbConnection connection, IDbProfiler profiler) + : base(connection, profiler) + { + } + } +} diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 92f2334dd..cb37130f2 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -9,6 +9,7 @@ + diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index f3fc5e09d..1a672d797 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -4,6 +4,7 @@ Library + diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index fc1c7e957..85b1bb55e 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Threading.Tasks; using DryIoc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -35,6 +36,7 @@ using Sonarr.Http.ClientSchema; using Sonarr.Http.ErrorManagement; using Sonarr.Http.Frontend; using Sonarr.Http.Middleware; +using StackExchange.Profiling; using IPNetwork = System.Net.IPNetwork; using LogLevel = Microsoft.Extensions.Logging.LogLevel; @@ -236,6 +238,45 @@ namespace NzbDrone.Host }); services.AddAppAuthentication(); + + services.AddOptions() + .Configure((options, configFileProvider) => + { + options.RouteBasePath = "/profiler"; + + switch (configFileProvider.Theme) + { + case "light": + options.ColorScheme = ColorScheme.Light; + break; + case "dark": + options.ColorScheme = ColorScheme.Dark; + break; + default: + options.ColorScheme = ColorScheme.Auto; + break; + } + + switch (configFileProvider.ProfilerPosition) + { + case "top-left": + options.PopupRenderPosition = RenderPosition.Left; + break; + case "top-right": + options.PopupRenderPosition = RenderPosition.Right; + break; + case "bottom-left": + options.PopupRenderPosition = RenderPosition.BottomLeft; + break; + default: + options.PopupRenderPosition = RenderPosition.BottomRight; + break; + } + + options.IgnoredPaths.Add("/MediaCover"); + }); + + services.AddMiniProfiler(); } public void Configure(IApplicationBuilder app, @@ -312,6 +353,7 @@ namespace NzbDrone.Host app.UseMiddleware(new List { "/api/v3/command", "/api/v5/command" }); app.UseWebSockets(); + app.UseMiniProfiler(); // Enable middleware to serve generated Swagger as a JSON endpoint. if (BuildInfo.IsDebug) @@ -325,6 +367,7 @@ namespace NzbDrone.Host app.UseEndpoints(x => { x.MapHub("/signalr/messages").RequireAuthorization("SignalR"); + x.MapPost("/profiler/results", context => Task.CompletedTask).RequireAuthorization("UI"); x.MapControllers(); }); } diff --git a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs index efa8a7882..f0605b24b 100644 --- a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -27,9 +28,9 @@ namespace Sonarr.Http.Frontend.Mappers protected string HtmlPath; protected string UrlBase; - protected override Stream GetContentStream(string filePath) + protected override Stream GetContentStream(HttpContext context, string filePath) { - var text = GetHtmlText(); + var text = GetHtmlText(context); var stream = new MemoryStream(); var writer = new StreamWriter(stream); @@ -39,7 +40,7 @@ namespace Sonarr.Http.Frontend.Mappers return stream; } - protected virtual string GetHtmlText() + protected virtual string GetHtmlText(HttpContext context) { if (RuntimeInfo.IsProduction && _generatedContent != null) { diff --git a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 0f8ebe74d..82572e307 100644 --- a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Sonarr.Http.Frontend.Mappers @@ -7,6 +8,6 @@ namespace Sonarr.Http.Frontend.Mappers { string Map(string resourceUrl); bool CanHandle(string resourceUrl); - Task GetResponse(string resourceUrl); + Task GetResponse(HttpContext context, string resourceUrl); } } diff --git a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index d8cbaab63..0c8763d11 100644 --- a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,9 +1,12 @@ using System; using System.IO; +using Microsoft.AspNetCore.Http; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using StackExchange.Profiling; namespace Sonarr.Http.Frontend.Mappers { @@ -38,5 +41,30 @@ namespace Sonarr.Http.Frontend.Mappers !resourceUrl.Contains('.') && !resourceUrl.StartsWith("/login"); } + + protected override string GetHtmlText(HttpContext context) + { + var html = base.GetHtmlText(context); + + if (_configFileProvider.ProfilerEnabled) + { + var includes = MiniProfiler.Current?.RenderIncludes(context); + + if (includes == null || includes.Value.IsNullOrWhiteSpace()) + { + html = html.Replace("__MINI_PROFILER__", ""); + } + else + { + html = html.Replace("__MINI_PROFILER__", includes.Value); + } + } + else + { + html = html.Replace("__MINI_PROFILER__", ""); + } + + return html; + } } } diff --git a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index ca325add6..2f2c8faee 100644 --- a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using Microsoft.AspNetCore.Http; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -33,9 +34,9 @@ namespace Sonarr.Http.Frontend.Mappers return resourceUrl.StartsWith("/login"); } - protected override string GetHtmlText() + protected override string GetHtmlText(HttpContext context) { - var html = base.GetHtmlText(); + var html = base.GetHtmlText(context); var theme = _configFileProvider.Theme; html = html.Replace("_THEME_", theme); diff --git a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs index 295e31645..6351a55d2 100644 --- a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs @@ -2,6 +2,7 @@ using System; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using NzbDrone.Core.MediaCover; @@ -31,7 +32,7 @@ namespace Sonarr.Http.Frontend.Mappers return resourceUrl.StartsWith("/MediaCoverProxy/", StringComparison.InvariantCultureIgnoreCase); } - public async Task GetResponse(string resourceUrl) + public async Task GetResponse(HttpContext context, string resourceUrl) { var match = _regex.Match(resourceUrl); diff --git a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 3f3982ac1..ace688b2f 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Net.Http.Headers; @@ -31,7 +32,7 @@ namespace Sonarr.Http.Frontend.Mappers public abstract bool CanHandle(string resourceUrl); - public Task GetResponse(string resourceUrl) + public Task GetResponse(HttpContext context, string resourceUrl) { var filePath = Map(resourceUrl); @@ -42,7 +43,7 @@ namespace Sonarr.Http.Frontend.Mappers contentType = "application/octet-stream"; } - return Task.FromResult(new FileStreamResult(GetContentStream(filePath), new MediaTypeHeaderValue(contentType) + return Task.FromResult(new FileStreamResult(GetContentStream(context, filePath), new MediaTypeHeaderValue(contentType) { Encoding = contentType == "text/plain" ? Encoding.UTF8 : null })); @@ -53,7 +54,7 @@ namespace Sonarr.Http.Frontend.Mappers return Task.FromResult(null); } - protected virtual Stream GetContentStream(string filePath) + protected virtual Stream GetContentStream(HttpContext context, string filePath) { return File.OpenRead(filePath); } diff --git a/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs index c79d16464..b1bf41ef7 100644 --- a/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs @@ -1,4 +1,5 @@ using System.IO; +using Microsoft.AspNetCore.Http; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; @@ -27,7 +28,7 @@ namespace Sonarr.Http.Frontend.Mappers return FilePath; } - protected override Stream GetContentStream(string filePath) + protected override Stream GetContentStream(HttpContext context, string filePath) { var text = GetFileText(); diff --git a/src/Sonarr.Http/Frontend/StaticResourceController.cs b/src/Sonarr.Http/Frontend/StaticResourceController.cs index 49bc495b7..508f9ceec 100644 --- a/src/Sonarr.Http/Frontend/StaticResourceController.cs +++ b/src/Sonarr.Http/Frontend/StaticResourceController.cs @@ -54,7 +54,7 @@ namespace Sonarr.Http.Frontend if (mapper != null) { - var result = await mapper.GetResponse(path); + var result = await mapper.GetResponse(Request.HttpContext, path); if (result != null) { diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj index c7ce3317c..c34f21318 100644 --- a/src/Sonarr.Http/Sonarr.Http.csproj +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -5,6 +5,7 @@ +