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:
@@ -11,10 +11,12 @@ namespace Novelly.Api.Beats;
|
||||
/// Beats are a chapter's outline: a flat, ordered table rather than a tree. Everything
|
||||
/// here is scoped to one chapter.
|
||||
/// </summary>
|
||||
public class BeatService(INovelDbContext db, TagService tags)
|
||||
public class BeatService(INovelDbContext db, TagService tags, ILogger<BeatService> logger)
|
||||
{
|
||||
public async Task<IReadOnlyList<BeatDto>> ListAsync(Guid chapterId, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Listing beats for chapter {ChapterId}", chapterId);
|
||||
|
||||
var beats = await Query()
|
||||
.Where(b => b.ChapterId == chapterId)
|
||||
.OrderBy(b => b.SortOrder)
|
||||
@@ -23,8 +25,11 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
return [.. beats.Select(b => b.ToDto())];
|
||||
}
|
||||
|
||||
public async Task<BeatDto> GetAsync(Guid id, CancellationToken ct = default) =>
|
||||
(await FindAsync(id, ct)).ToDto();
|
||||
public async Task<BeatDto> GetAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Getting beat {BeatId}", id);
|
||||
return (await FindAsync(id, ct)).ToDto();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every beat this character appears in, in manuscript order. This is the character
|
||||
@@ -34,8 +39,11 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
public async Task<IReadOnlyList<CharacterBeatDto>> ListForCharacterAsync(
|
||||
Guid characterId, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Listing beats for character {CharacterId}", characterId);
|
||||
|
||||
if (!await db.Characters.AnyAsync(c => c.Id == characterId, ct))
|
||||
{
|
||||
logger.LogWarning("Character {CharacterId} not found", characterId);
|
||||
throw new NotFoundException(nameof(Character), characterId);
|
||||
}
|
||||
|
||||
@@ -66,8 +74,14 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
|
||||
public async Task<BeatDto> CreateAsync(Guid chapterId, CreateBeatRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == chapterId, ct)
|
||||
?? throw new NotFoundException(nameof(Chapter), chapterId);
|
||||
logger.LogInformation("Creating beat {Title} for chapter {ChapterId}", request.Title, chapterId);
|
||||
|
||||
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == chapterId, ct);
|
||||
if (chapter is null)
|
||||
{
|
||||
logger.LogWarning("Chapter {ChapterId} not found", chapterId);
|
||||
throw new NotFoundException(nameof(Chapter), chapterId);
|
||||
}
|
||||
|
||||
await ValidateReferencesAsync(chapter, request.CharacterId, request.SceneId, ct);
|
||||
|
||||
@@ -94,9 +108,15 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
|
||||
public async Task<BeatDto> UpdateAsync(Guid id, UpdateBeatRequest request, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Updating beat {BeatId}", id);
|
||||
|
||||
var beat = await FindAsync(id, ct);
|
||||
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == beat.ChapterId, ct)
|
||||
?? throw new NotFoundException(nameof(Chapter), beat.ChapterId);
|
||||
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == beat.ChapterId, ct);
|
||||
if (chapter is null)
|
||||
{
|
||||
logger.LogWarning("Chapter {ChapterId} not found", beat.ChapterId);
|
||||
throw new NotFoundException(nameof(Chapter), beat.ChapterId);
|
||||
}
|
||||
|
||||
await ValidateReferencesAsync(chapter, request.CharacterId, request.SceneId, ct);
|
||||
|
||||
@@ -119,6 +139,8 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
|
||||
public async Task DeleteAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Deleting beat {BeatId}", id);
|
||||
|
||||
var beat = await FindAsync(id, ct);
|
||||
db.Beats.Remove(beat);
|
||||
await db.SaveChangesAsync(ct);
|
||||
@@ -131,11 +153,14 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
public async Task<IReadOnlyList<BeatDto>> ReorderAsync(
|
||||
Guid chapterId, ReorderBeatsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Reordering {Count} beats for chapter {ChapterId}", request.BeatIds.Count, chapterId);
|
||||
|
||||
var beats = await db.Beats.Where(b => b.ChapterId == chapterId).ToListAsync(ct);
|
||||
|
||||
var missing = request.BeatIds.Where(id => beats.All(b => b.Id != id)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
logger.LogWarning("Reorder for chapter {ChapterId} referenced missing beat {BeatId}", chapterId, missing[0]);
|
||||
throw new NotFoundException(nameof(Beat), missing[0]);
|
||||
}
|
||||
|
||||
@@ -159,6 +184,8 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
private async Task ValidateReferencesAsync(
|
||||
Chapter chapter, Guid? characterId, Guid? sceneId, CancellationToken ct)
|
||||
{
|
||||
logger.LogDebug("Validating beat references for chapter {ChapterId}: character {CharacterId}, scene {SceneId}", chapter.Id, characterId, sceneId);
|
||||
|
||||
if (characterId is { } cid)
|
||||
{
|
||||
var belongs = await db.Characters
|
||||
@@ -166,6 +193,7 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
|
||||
if (!belongs)
|
||||
{
|
||||
logger.LogWarning("Rejected beat reference: character {CharacterId} does not belong to project {ProjectId}", cid, chapter.ProjectId);
|
||||
throw new InvalidOperationException(
|
||||
"A beat's character must belong to the same project as its chapter.");
|
||||
}
|
||||
@@ -177,6 +205,7 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
|
||||
if (!belongs)
|
||||
{
|
||||
logger.LogWarning("Rejected beat reference: scene {SceneId} does not belong to chapter {ChapterId}", sid, chapter.Id);
|
||||
throw new InvalidOperationException(
|
||||
"A beat can only be grouped under a scene in the same chapter.");
|
||||
}
|
||||
@@ -185,6 +214,8 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
|
||||
private async Task<int> NextSortOrderAsync(Guid chapterId, CancellationToken ct)
|
||||
{
|
||||
logger.LogDebug("Computing next sort order for chapter {ChapterId}", chapterId);
|
||||
|
||||
var max = await db.Beats
|
||||
.Where(b => b.ChapterId == chapterId)
|
||||
.MaxAsync(b => (int?)b.SortOrder, ct);
|
||||
@@ -198,7 +229,18 @@ public class BeatService(INovelDbContext db, TagService tags)
|
||||
.Include(b => b.Scene)
|
||||
.Include(b => b.Tags);
|
||||
|
||||
private async Task<Beat> FindAsync(Guid id, CancellationToken ct) =>
|
||||
await Query().FirstOrDefaultAsync(b => b.Id == id, ct)
|
||||
?? throw new NotFoundException(nameof(Beat), id);
|
||||
private async Task<Beat> FindAsync(Guid id, CancellationToken ct)
|
||||
{
|
||||
logger.LogDebug("Finding beat {BeatId}", id);
|
||||
|
||||
var beat = await Query().FirstOrDefaultAsync(b => b.Id == id, ct);
|
||||
if (beat is null)
|
||||
{
|
||||
logger.LogWarning("Beat {BeatId} not found", id);
|
||||
throw new NotFoundException(nameof(Beat), id);
|
||||
}
|
||||
|
||||
logger.LogDebug("Found beat {BeatId}", id);
|
||||
return beat;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user