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:
@@ -13,10 +13,12 @@ namespace Novelly.Api.Characters;
|
||||
/// on a supporting character. Demoting someone should not delete work, and a character
|
||||
/// who turns out to matter gets promoted after the arc is already sketched.
|
||||
/// </remarks>
|
||||
public class CharacterArcService(INovelDbContext db)
|
||||
public class CharacterArcService(INovelDbContext db, ILogger<CharacterArcService> logger)
|
||||
{
|
||||
public async Task<IReadOnlyList<ArcStageDto>> ListAsync(Guid characterId, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Listing arc stages for character {CharacterId}", characterId);
|
||||
|
||||
var stages = await Query()
|
||||
.Where(s => s.CharacterId == characterId)
|
||||
.OrderBy(s => s.SortOrder)
|
||||
@@ -25,14 +27,23 @@ public class CharacterArcService(INovelDbContext db)
|
||||
return [.. stages.Select(s => s.ToDto())];
|
||||
}
|
||||
|
||||
public async Task<ArcStageDto> GetAsync(Guid id, CancellationToken ct = default) =>
|
||||
(await FindAsync(id, ct)).ToDto();
|
||||
public async Task<ArcStageDto> GetAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Getting arc stage {ArcStageId}", id);
|
||||
return (await FindAsync(id, ct)).ToDto();
|
||||
}
|
||||
|
||||
public async Task<ArcStageDto> CreateAsync(
|
||||
Guid characterId, CreateArcStageRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var character = await db.Characters.FirstOrDefaultAsync(c => c.Id == characterId, ct)
|
||||
?? throw new NotFoundException(nameof(Character), characterId);
|
||||
logger.LogInformation("Creating arc stage {Title} for character {CharacterId}", request.Title, characterId);
|
||||
|
||||
var character = await db.Characters.FirstOrDefaultAsync(c => c.Id == characterId, ct);
|
||||
if (character is null)
|
||||
{
|
||||
logger.LogWarning("Character {CharacterId} not found", characterId);
|
||||
throw new NotFoundException(nameof(Character), characterId);
|
||||
}
|
||||
|
||||
await EnsureChapterIsInSameProjectAsync(character, request.ChapterId, ct);
|
||||
|
||||
@@ -53,10 +64,16 @@ public class CharacterArcService(INovelDbContext db)
|
||||
public async Task<ArcStageDto> UpdateAsync(
|
||||
Guid id, UpdateArcStageRequest request, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Updating arc stage {ArcStageId}", id);
|
||||
|
||||
var stage = await FindAsync(id, ct);
|
||||
|
||||
var character = await db.Characters.FirstOrDefaultAsync(c => c.Id == stage.CharacterId, ct)
|
||||
?? throw new NotFoundException(nameof(Character), stage.CharacterId);
|
||||
var character = await db.Characters.FirstOrDefaultAsync(c => c.Id == stage.CharacterId, ct);
|
||||
if (character is null)
|
||||
{
|
||||
logger.LogWarning("Character {CharacterId} not found", stage.CharacterId);
|
||||
throw new NotFoundException(nameof(Character), stage.CharacterId);
|
||||
}
|
||||
|
||||
await EnsureChapterIsInSameProjectAsync(character, request.ChapterId, ct);
|
||||
|
||||
@@ -72,6 +89,8 @@ public class CharacterArcService(INovelDbContext db)
|
||||
|
||||
public async Task DeleteAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Deleting arc stage {ArcStageId}", id);
|
||||
|
||||
var stage = await FindAsync(id, ct);
|
||||
db.CharacterArcStages.Remove(stage);
|
||||
await db.SaveChangesAsync(ct);
|
||||
@@ -84,6 +103,8 @@ public class CharacterArcService(INovelDbContext db)
|
||||
public async Task<IReadOnlyList<ArcStageDto>> ReorderAsync(
|
||||
Guid characterId, ReorderArcStagesRequest request, CancellationToken ct = default)
|
||||
{
|
||||
logger.LogInformation("Reordering {Count} arc stages for character {CharacterId}", request.StageIds.Count, characterId);
|
||||
|
||||
var stages = await db.CharacterArcStages
|
||||
.Where(s => s.CharacterId == characterId)
|
||||
.ToListAsync(ct);
|
||||
@@ -91,6 +112,7 @@ public class CharacterArcService(INovelDbContext db)
|
||||
var missing = request.StageIds.Where(id => stages.All(s => s.Id != id)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
logger.LogWarning("Reorder for character {CharacterId} referenced missing arc stage {ArcStageId}", characterId, missing[0]);
|
||||
throw new NotFoundException(nameof(CharacterArcStage), missing[0]);
|
||||
}
|
||||
|
||||
@@ -117,10 +139,13 @@ public class CharacterArcService(INovelDbContext db)
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogDebug("Checking chapter {ChapterId} belongs to project {ProjectId}", id, character.ProjectId);
|
||||
|
||||
var belongs = await db.Chapters.AnyAsync(c => c.Id == id && c.ProjectId == character.ProjectId, ct);
|
||||
|
||||
if (!belongs)
|
||||
{
|
||||
logger.LogWarning("Rejected arc stage: chapter {ChapterId} does not belong to project {ProjectId}", id, character.ProjectId);
|
||||
throw new InvalidOperationException(
|
||||
"An arc stage can only point at a chapter in the same project as its character.");
|
||||
}
|
||||
@@ -128,6 +153,8 @@ public class CharacterArcService(INovelDbContext db)
|
||||
|
||||
private async Task<int> NextSortOrderAsync(Guid characterId, CancellationToken ct)
|
||||
{
|
||||
logger.LogDebug("Computing next sort order for character {CharacterId}", characterId);
|
||||
|
||||
var max = await db.CharacterArcStages
|
||||
.Where(s => s.CharacterId == characterId)
|
||||
.MaxAsync(s => (int?)s.SortOrder, ct);
|
||||
@@ -137,7 +164,18 @@ public class CharacterArcService(INovelDbContext db)
|
||||
|
||||
private IQueryable<CharacterArcStage> Query() => db.CharacterArcStages.Include(s => s.Chapter);
|
||||
|
||||
private async Task<CharacterArcStage> FindAsync(Guid id, CancellationToken ct) =>
|
||||
await Query().FirstOrDefaultAsync(s => s.Id == id, ct)
|
||||
?? throw new NotFoundException(nameof(CharacterArcStage), id);
|
||||
private async Task<CharacterArcStage> FindAsync(Guid id, CancellationToken ct)
|
||||
{
|
||||
logger.LogDebug("Finding arc stage {ArcStageId}", id);
|
||||
|
||||
var stage = await Query().FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||
if (stage is null)
|
||||
{
|
||||
logger.LogWarning("CharacterArcStage {ArcStageId} not found", id);
|
||||
throw new NotFoundException(nameof(CharacterArcStage), id);
|
||||
}
|
||||
|
||||
logger.LogDebug("Found arc stage {ArcStageId}", id);
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user