Browse Source

fix slow db and api logging so it also works in scanner project

pull/2747/head
Jason Dove 7 months ago
parent
commit
d1b994a6c0
No known key found for this signature in database
  1. 5
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  2. 21
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  3. 16
      ErsatzTV.Infrastructure/Data/TvContext.cs
  4. 3
      ErsatzTV.Infrastructure/SlowApiHandler.cs
  5. 10
      ErsatzTV.Infrastructure/SlowQueryInterceptor.cs
  6. 6
      ErsatzTV.Scanner/Program.cs
  7. 18
      ErsatzTV/Startup.cs

5
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -88,6 +88,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<JellyfinShow> maybeExisting = await dbContext.JellyfinShows Option<JellyfinShow> maybeExisting = await dbContext.JellyfinShows
.TagWithCallSite()
.Include(m => m.LibraryPath) .Include(m => m.LibraryPath)
.ThenInclude(lp => lp.Library) .ThenInclude(lp => lp.Library)
.Include(m => m.ShowMetadata) .Include(m => m.ShowMetadata)
@ -128,6 +129,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<JellyfinSeason> maybeExisting = await dbContext.JellyfinSeasons Option<JellyfinSeason> maybeExisting = await dbContext.JellyfinSeasons
.TagWithCallSite()
.Include(m => m.LibraryPath) .Include(m => m.LibraryPath)
.Include(m => m.SeasonMetadata) .Include(m => m.SeasonMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
@ -158,6 +160,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes Option<JellyfinEpisode> maybeExisting = await dbContext.JellyfinEpisodes
.TagWithCallSite()
.Include(m => m.LibraryPath) .Include(m => m.LibraryPath)
.ThenInclude(lp => lp.Library) .ThenInclude(lp => lp.Library)
.Include(m => m.MediaVersions) .Include(m => m.MediaVersions)
@ -511,6 +514,8 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<JellyfinShow> maybeShow = await dbContext.JellyfinShows Option<JellyfinShow> maybeShow = await dbContext.JellyfinShows
.AsNoTracking()
.TagWithCallSite()
.Where(s => s.Id == showId) .Where(s => s.Id == showId)
.Where(s => s.LibraryPath.LibraryId == libraryId) .Where(s => s.LibraryPath.LibraryId == libraryId)
.Include(s => s.ShowMetadata) .Include(s => s.ShowMetadata)

21
ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs

@ -15,6 +15,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
var baseItem = await dbContext.MediaItems var baseItem = await dbContext.MediaItems
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
if (baseItem is null) if (baseItem is null)
@ -27,60 +28,70 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
case Movie: case Movie:
return await dbContext.Movies return await dbContext.Movies
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case Episode: case Episode:
return await dbContext.Episodes return await dbContext.Episodes
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case Season: case Season:
return await dbContext.Seasons return await dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case Show: case Show:
return await dbContext.Shows return await dbContext.Shows
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case MusicVideo: case MusicVideo:
return await dbContext.MusicVideos return await dbContext.MusicVideos
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case Artist: case Artist:
return await dbContext.Artists return await dbContext.Artists
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case OtherVideo: case OtherVideo:
return await dbContext.OtherVideos return await dbContext.OtherVideos
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case Song: case Song:
return await dbContext.Songs return await dbContext.Songs
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case Image: case Image:
return await dbContext.Images return await dbContext.Images
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
case RemoteStream: case RemoteStream:
return await dbContext.RemoteStreams return await dbContext.RemoteStreams
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken); .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
@ -225,6 +236,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Movie> movies = dbContext.Movies ConfiguredCancelableAsyncEnumerable<Movie> movies = dbContext.Movies
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -242,6 +254,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Show> shows = dbContext.Shows ConfiguredCancelableAsyncEnumerable<Show> shows = dbContext.Shows
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -259,6 +272,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Season> seasons = dbContext.Seasons ConfiguredCancelableAsyncEnumerable<Season> seasons = dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -276,6 +290,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Episode> episodes = dbContext.Episodes ConfiguredCancelableAsyncEnumerable<Episode> episodes = dbContext.Episodes
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -294,6 +309,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<MusicVideo> musicVideos = dbContext.MusicVideos ConfiguredCancelableAsyncEnumerable<MusicVideo> musicVideos = dbContext.MusicVideos
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -311,6 +327,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Artist> artists = dbContext.Artists ConfiguredCancelableAsyncEnumerable<Artist> artists = dbContext.Artists
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -329,6 +346,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<OtherVideo> otherVideos = dbContext.OtherVideos ConfiguredCancelableAsyncEnumerable<OtherVideo> otherVideos = dbContext.OtherVideos
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -346,6 +364,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Song> songs = dbContext.Songs ConfiguredCancelableAsyncEnumerable<Song> songs = dbContext.Songs
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -363,6 +382,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<Image> images = dbContext.Images ConfiguredCancelableAsyncEnumerable<Image> images = dbContext.Images
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()
@ -381,6 +401,7 @@ public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : I
ConfiguredCancelableAsyncEnumerable<RemoteStream> remoteStreams = dbContext.RemoteStreams ConfiguredCancelableAsyncEnumerable<RemoteStream> remoteStreams = dbContext.RemoteStreams
.AsNoTracking() .AsNoTracking()
.TagWithCallSite()
.IncludeForSearch() .IncludeForSearch()
.AsSplitQuery() .AsSplitQuery()
.AsAsyncEnumerable() .AsAsyncEnumerable()

16
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -10,10 +10,17 @@ namespace ErsatzTV.Infrastructure.Data;
public class TvContext : DbContext public class TvContext : DbContext
{ {
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly SlowQueryInterceptor _slowQueryInterceptor;
public TvContext(DbContextOptions<TvContext> options, ILoggerFactory loggerFactory) public TvContext(
: base(options) => DbContextOptions<TvContext> options,
ILoggerFactory loggerFactory,
SlowQueryInterceptor slowQueryInterceptor)
: base(options)
{
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_slowQueryInterceptor = slowQueryInterceptor;
}
public static string LastInsertedRowId { get; set; } = "last_insert_rowid()"; public static string LastInsertedRowId { get; set; } = "last_insert_rowid()";
public static string CaseInsensitiveCollation { get; set; } = "NOCASE"; public static string CaseInsensitiveCollation { get; set; } = "NOCASE";
@ -119,8 +126,11 @@ public class TvContext : DbContext
public DbSet<Subtitle> Subtitles { get; set; } public DbSet<Subtitle> Subtitles { get; set; }
public DbSet<GraphicsElement> GraphicsElements { get; set; } public DbSet<GraphicsElement> GraphicsElements { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(_loggerFactory); optionsBuilder.UseLoggerFactory(_loggerFactory);
optionsBuilder.AddInterceptors(_slowQueryInterceptor);
}
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {

3
ErsatzTV/SlowApiHandler.cs → ErsatzTV.Infrastructure/SlowApiHandler.cs

@ -1,6 +1,7 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using Microsoft.Extensions.Logging;
namespace ErsatzTV; namespace ErsatzTV.Infrastructure;
using System.Diagnostics; using System.Diagnostics;

10
ErsatzTV/SlowQueryInterceptor.cs → ErsatzTV.Infrastructure/SlowQueryInterceptor.cs

@ -1,9 +1,11 @@
using System.Data.Common; using System.Data.Common;
using ErsatzTV.Core;
using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
namespace ErsatzTV; namespace ErsatzTV.Infrastructure;
public class SlowQueryInterceptor(int threshold) : DbCommandInterceptor public class SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger) : DbCommandInterceptor
{ {
public override ValueTask<DbDataReader> ReaderExecutedAsync( public override ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command, DbCommand command,
@ -11,9 +13,9 @@ public class SlowQueryInterceptor(int threshold) : DbCommandInterceptor
DbDataReader result, DbDataReader result,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (eventData.Duration.TotalMilliseconds > threshold) if (SystemEnvironment.SlowDbMs > 0 && eventData.Duration.TotalMilliseconds > SystemEnvironment.SlowDbMs)
{ {
Serilog.Log.Logger.Debug( logger.LogDebug(
"[SLOW QUERY] ({Milliseconds}ms): {Command}", "[SLOW QUERY] ({Milliseconds}ms): {Command}",
eventData.Duration.TotalMilliseconds, eventData.Duration.TotalMilliseconds,
command.CommandText); command.CommandText);

6
ErsatzTV.Scanner/Program.cs

@ -19,6 +19,7 @@ using ErsatzTV.Core.Plex;
using ErsatzTV.Core.Search; using ErsatzTV.Core.Search;
using ErsatzTV.FFmpeg.Capabilities; using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Emby; using ErsatzTV.Infrastructure.Emby;
@ -171,6 +172,8 @@ public class Program
services.AddHttpClient(); services.AddHttpClient();
services.AddHttpClient("RefitCustomClient").AddHttpMessageHandler<SlowApiHandler>();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>(); services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<IMetadataRepository, MetadataRepository>(); services.AddScoped<IMetadataRepository, MetadataRepository>();
services.AddScoped<IMediaSourceRepository, MediaSourceRepository>(); services.AddScoped<IMediaSourceRepository, MediaSourceRepository>();
@ -256,6 +259,9 @@ public class Program
services.AddSingleton<IFileSystem, RealFileSystem>(); services.AddSingleton<IFileSystem, RealFileSystem>();
services.AddTransient<SlowApiHandler>();
services.AddTransient<SlowQueryInterceptor>();
services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>()); services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<Worker>());
services.AddMemoryCache(); services.AddMemoryCache();

18
ErsatzTV/Startup.cs

@ -51,6 +51,7 @@ using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Filters; using ErsatzTV.Filters;
using ErsatzTV.Formatters; using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Database; using ErsatzTV.Infrastructure.Database;
@ -408,12 +409,6 @@ public class Startup
var sqliteConnectionString = $"Data Source={FileSystemLayout.DatabasePath};foreign keys=true;"; var sqliteConnectionString = $"Data Source={FileSystemLayout.DatabasePath};foreign keys=true;";
string mySqlConnectionString = Configuration.GetValue<string>("MySql:ConnectionString"); string mySqlConnectionString = Configuration.GetValue<string>("MySql:ConnectionString");
SlowQueryInterceptor interceptor = null;
if (SystemEnvironment.SlowDbMs.HasValue)
{
interceptor = new SlowQueryInterceptor(SystemEnvironment.SlowDbMs.Value);
}
services.AddDbContext<TvContext>( services.AddDbContext<TvContext>(
options => options =>
{ {
@ -444,11 +439,6 @@ public class Startup
} }
); );
} }
if (interceptor != null)
{
options.AddInterceptors(interceptor);
}
}, },
ServiceLifetime.Scoped, ServiceLifetime.Scoped,
ServiceLifetime.Singleton); ServiceLifetime.Singleton);
@ -478,11 +468,6 @@ public class Startup
} }
); );
} }
if (interceptor != null)
{
options.AddInterceptors(interceptor);
}
}); });
if (databaseProvider == Provider.Sqlite.Name) if (databaseProvider == Provider.Sqlite.Name)
@ -870,6 +855,7 @@ public class Startup
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>)); // services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
services.AddTransient<SlowApiHandler>(); services.AddTransient<SlowApiHandler>();
services.AddTransient<SlowQueryInterceptor>();
// run-once/blocking startup services // run-once/blocking startup services
services.AddHostedService<EndpointValidatorService>(); services.AddHostedService<EndpointValidatorService>();

Loading…
Cancel
Save