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
+44 -8
View File
@@ -28,15 +28,21 @@ public class NovelAgentService(
private readonly AgentOptions _options = options.Value;
public async Task<IReadOnlyList<ConversationSummaryDto>> ListConversationsAsync(
Guid projectId, CancellationToken ct = default) =>
await db.Conversations
Guid projectId, CancellationToken ct = default)
{
logger.LogInformation("Listing agent conversations for project {ProjectId}", projectId);
return await db.Conversations
.Where(c => c.ProjectId == projectId)
.OrderByDescending(c => c.UpdatedAt)
.Select(c => new ConversationSummaryDto(c.Id, c.ProjectId, c.Title, c.Messages.Count, c.UpdatedAt))
.ToListAsync(ct);
}
public async Task<ConversationDto> GetConversationAsync(Guid conversationId, CancellationToken ct = default)
{
logger.LogInformation("Getting agent conversation {ConversationId}", conversationId);
var conversation = await LoadConversationAsync(conversationId, ct);
return new ConversationDto(
@@ -49,6 +55,8 @@ public class NovelAgentService(
public async Task DeleteConversationAsync(Guid conversationId, CancellationToken ct = default)
{
logger.LogInformation("Deleting agent conversation {ConversationId}", conversationId);
var conversation = await LoadConversationAsync(conversationId, ct);
db.Conversations.Remove(conversation);
await db.SaveChangesAsync(ct);
@@ -61,6 +69,10 @@ public class NovelAgentService(
public async Task<AgentTurnDto> SendMessageAsync(
Guid projectId, SendAgentMessageRequest request, CancellationToken ct = default)
{
logger.LogInformation(
"Sending agent message for project {ProjectId}, conversation {ConversationId}, message length {MessageLength}",
projectId, request.ConversationId, request.Message.Length);
var conversation = request.ConversationId is { } id
? await LoadConversationAsync(id, ct)
: await StartConversationAsync(projectId, request.Message, ct);
@@ -77,6 +89,8 @@ public class NovelAgentService(
for (var iteration = 0; iteration < _options.MaxIterations; iteration++)
{
logger.LogDebug("Agent iteration {Iteration} for project {ProjectId}", iteration, projectId);
var response = await model.CompleteAsync(systemPrompt, transcript, toolset.Definitions, ct);
foreach (var block in response.Content.OfType<AgentTextBlock>())
@@ -142,6 +156,8 @@ public class NovelAgentService(
private async Task<AgentMessage> AppendMessageAsync(
AgentConversation conversation, AgentRole role, string content, string? toolCallsJson, CancellationToken ct)
{
logger.LogDebug("Appending {Role} message to conversation {ConversationId}, content length {ContentLength}", role, conversation.Id, content.Length);
var message = new AgentMessage
{
ConversationId = conversation.Id,
@@ -170,8 +186,11 @@ public class NovelAgentService(
private async Task<AgentConversation> StartConversationAsync(
Guid projectId, string firstMessage, CancellationToken ct)
{
logger.LogDebug("Starting new agent conversation for project {ProjectId}", projectId);
if (!await db.Projects.AnyAsync(p => p.Id == projectId, ct))
{
logger.LogWarning("Project {ProjectId} not found", projectId);
throw new NotFoundException(nameof(Project), projectId);
}
@@ -185,11 +204,22 @@ public class NovelAgentService(
return conversation;
}
private async Task<AgentConversation> LoadConversationAsync(Guid conversationId, CancellationToken ct) =>
await db.Conversations
private async Task<AgentConversation> LoadConversationAsync(Guid conversationId, CancellationToken ct)
{
logger.LogDebug("Loading agent conversation {ConversationId}", conversationId);
var conversation = await db.Conversations
.Include(c => c.Messages)
.FirstOrDefaultAsync(c => c.Id == conversationId, ct)
?? throw new NotFoundException(nameof(AgentConversation), conversationId);
.FirstOrDefaultAsync(c => c.Id == conversationId, ct);
if (conversation is null)
{
logger.LogWarning("AgentConversation {ConversationId} not found", conversationId);
throw new NotFoundException(nameof(AgentConversation), conversationId);
}
return conversation;
}
/// <summary>
/// Replays the stored conversation as plain text turns. Tool calls are not replayed —
@@ -208,8 +238,14 @@ public class NovelAgentService(
private async Task<string> BuildSystemPromptAsync(Guid projectId, CancellationToken ct)
{
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == projectId, ct)
?? throw new NotFoundException(nameof(Project), projectId);
logger.LogDebug("Building system prompt for project {ProjectId}", projectId);
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == projectId, ct);
if (project is null)
{
logger.LogWarning("Project {ProjectId} not found", projectId);
throw new NotFoundException(nameof(Project), projectId);
}
var brief = new StringBuilder();
brief.AppendLine($"Title: {project.Title}");