Add Serilog console logging across the API

Information at endpoint and service-method boundaries, Debug in deeper
helpers, Warning before expected/recoverable failures (not-found,
validation, agent tool errors), Error on caught exceptions. Serilog
wraps the exception handler so request-completion logs report the
resolved status code rather than the raw exception. Never logs prose
bodies or the Anthropic API key.
This commit is contained in:
James Wampler
2026-08-06 12:11:20 -07:00
parent 4f396bb5f9
commit 04917fa09e
34 changed files with 790 additions and 147 deletions
+36 -8
View File
@@ -6,10 +6,12 @@ using Novelly.Api.Tags;
namespace Novelly.Api.Chapters;
public class ChapterService(INovelDbContext db, TagService tags)
public class ChapterService(INovelDbContext db, TagService tags, ILogger<ChapterService> logger)
{
public async Task<IReadOnlyList<ChapterSummaryDto>> ListAsync(Guid projectId, CancellationToken ct = default)
{
logger.LogInformation("Listing chapters for project {ProjectId}", projectId);
var chapters = await db.Chapters
.Include(c => c.PovCharacter)
.Include(c => c.Beats)
@@ -22,13 +24,19 @@ public class ChapterService(INovelDbContext db, TagService tags)
return [.. chapters.Select(c => c.ToSummaryDto())];
}
public async Task<ChapterDto> GetAsync(Guid id, CancellationToken ct = default) =>
(await FindAsync(id, ct)).ToDto();
public async Task<ChapterDto> GetAsync(Guid id, CancellationToken ct = default)
{
logger.LogInformation("Getting chapter {ChapterId}", id);
return (await FindAsync(id, ct)).ToDto();
}
public async Task<ChapterDto> CreateAsync(Guid projectId, CreateChapterRequest request, CancellationToken ct = default)
{
logger.LogInformation("Creating chapter {Title} for project {ProjectId}", request.Title, projectId);
if (!await db.Projects.AnyAsync(p => p.Id == projectId, ct))
{
logger.LogWarning("Project {ProjectId} not found", projectId);
throw new NotFoundException(nameof(Project), projectId);
}
@@ -57,6 +65,8 @@ public class ChapterService(INovelDbContext db, TagService tags)
public async Task<ChapterDto> UpdateAsync(Guid id, UpdateChapterRequest request, CancellationToken ct = default)
{
logger.LogInformation("Updating chapter {ChapterId}", id);
var chapter = await FindAsync(id, ct);
chapter.Title = Patch.Apply(chapter.Title, request.Title) ?? chapter.Title;
@@ -80,6 +90,8 @@ public class ChapterService(INovelDbContext db, TagService tags)
public async Task DeleteAsync(Guid id, CancellationToken ct = default)
{
logger.LogInformation("Deleting chapter {ChapterId}", id);
var chapter = await FindAsync(id, ct);
db.Chapters.Remove(chapter);
await db.SaveChangesAsync(ct);
@@ -87,21 +99,37 @@ public class ChapterService(INovelDbContext db, TagService tags)
private async Task<int> NextChapterNumberAsync(Guid projectId, CancellationToken ct)
{
logger.LogDebug("Computing next chapter number for project {ProjectId}", projectId);
var max = await db.Chapters
.Where(c => c.ProjectId == projectId)
.MaxAsync(c => (int?)c.Number, ct);
return (max ?? 0) + 1;
var next = (max ?? 0) + 1;
logger.LogDebug("Next chapter number for project {ProjectId} is {Number}", projectId, next);
return next;
}
private async Task<Chapter> FindAsync(Guid id, CancellationToken ct) =>
await db.Chapters
private async Task<Chapter> FindAsync(Guid id, CancellationToken ct)
{
logger.LogDebug("Finding chapter {ChapterId}", id);
var chapter = await db.Chapters
.Include(c => c.PovCharacter)
.Include(c => c.Beats).ThenInclude(b => b.Character)
.Include(c => c.Beats).ThenInclude(b => b.Scene)
.Include(c => c.Beats).ThenInclude(b => b.Tags)
.Include(c => c.Scenes).ThenInclude(s => s.PovCharacter)
.Include(c => c.Tags)
.FirstOrDefaultAsync(c => c.Id == id, ct)
?? throw new NotFoundException(nameof(Chapter), id);
.FirstOrDefaultAsync(c => c.Id == id, ct);
if (chapter is null)
{
logger.LogWarning("Chapter {ChapterId} not found", id);
throw new NotFoundException(nameof(Chapter), id);
}
logger.LogDebug("Found chapter {ChapterId}", id);
return chapter;
}
}