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:
@@ -1,10 +1,12 @@
|
||||
using Novelly.Api.Common;
|
||||
|
||||
namespace Novelly.Api.Chapters;
|
||||
|
||||
public static class ChapterEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapChapterEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var projectScoped = app.MapGroup("/api/projects/{projectId:guid}/chapters").WithTags("Chapters");
|
||||
var projectScoped = app.MapGroup("/api/projects/{projectId:guid}/chapters").WithTags("Chapters").AddEndpointFilter<RequestLoggingEndpointFilter>();
|
||||
|
||||
projectScoped.MapGet("/", async (Guid projectId, ChapterService service, CancellationToken ct) =>
|
||||
Results.Ok(await service.ListAsync(projectId, ct)))
|
||||
@@ -18,7 +20,7 @@ public static class ChapterEndpoints
|
||||
})
|
||||
.WithSummary("Add a chapter.");
|
||||
|
||||
var chapters = app.MapGroup("/api/chapters").WithTags("Chapters");
|
||||
var chapters = app.MapGroup("/api/chapters").WithTags("Chapters").AddEndpointFilter<RequestLoggingEndpointFilter>();
|
||||
|
||||
chapters.MapGet("/{id:guid}", async (Guid id, ChapterService service, CancellationToken ct) =>
|
||||
Results.Ok(await service.GetAsync(id, ct)))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user