feat: add preprod observability foundation

This commit is contained in:
2026-05-08 15:45:31 -04:00
parent 1ca6ab7117
commit 8bcff96821
35 changed files with 1627 additions and 56 deletions

View File

@@ -1,6 +1,7 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.ContentItems.Data;
using Socialize.Api.Modules.ContentItems.Contracts;
@@ -37,7 +38,8 @@ internal class SubmitApprovalDecisionHandler(
AccessScopeService accessScopeService,
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
IContentItemActivityWriter activityWriter,
INotificationEventWriter notificationEventWriter)
INotificationEventWriter notificationEventWriter,
SocializeMetrics metrics)
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
{
public override void Configure()
@@ -157,6 +159,7 @@ internal class SubmitApprovalDecisionHandler(
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
ct);
}
metrics.RecordApprovalDecisionSubmitted(approval.WorkspaceId, normalizedDecision);
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
.Where(candidate => candidate.ApprovalRequestId == approval.Id)

View File

@@ -1,7 +1,10 @@
using Socialize.Api.Infrastructure.Observability;
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
internal sealed class CalendarImportBackgroundService(
IServiceScopeFactory scopeFactory,
SocializeMetrics metrics,
ILogger<CalendarImportBackgroundService> logger)
: BackgroundService
{
@@ -22,6 +25,7 @@ internal sealed class CalendarImportBackgroundService(
using IServiceScope scope = scopeFactory.CreateScope();
CalendarImportSyncService syncService = scope.ServiceProvider.GetRequiredService<CalendarImportSyncService>();
await syncService.RefreshDueSourcesAsync(stoppingToken);
metrics.RecordBackgroundJobRun(nameof(CalendarImportBackgroundService), true);
}
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
{
@@ -30,6 +34,7 @@ internal sealed class CalendarImportBackgroundService(
#pragma warning disable CA1031 // Background service should log and continue after unexpected sync failures.
catch (Exception ex)
{
metrics.RecordBackgroundJobRun(nameof(CalendarImportBackgroundService), false);
logger.LogError(ex, "Calendar import background sync failed.");
}
#pragma warning restore CA1031

View File

@@ -2,6 +2,7 @@ using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.ContentItems.Contracts;
using Socialize.Api.Modules.ContentItems.Data;
@@ -34,7 +35,8 @@ internal class CreateCommentHandler(
AccessScopeService accessScopeService,
IBlobStorage blobStorage,
IContentItemActivityWriter activityWriter,
INotificationEventWriter notificationEventWriter)
INotificationEventWriter notificationEventWriter,
SocializeMetrics metrics)
: Endpoint<CreateCommentRequest, CommentDto>
{
public override void Configure()
@@ -156,6 +158,7 @@ internal class CreateCommentHandler(
dbContext.Comments.Add(comment);
await dbContext.SaveChangesAsync(ct);
metrics.RecordCommentCreated(comment.WorkspaceId, comment.AttachmentBlobName is not null);
string? authorPortraitUrl = await dbContext.Users
.Where(candidate => candidate.Id == comment.AuthorUserId)

View File

@@ -1,6 +1,7 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.ContentItems.Contracts;
using Socialize.Api.Modules.Notifications.Contracts;
@@ -39,7 +40,8 @@ internal class CreateContentItemHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService,
IContentItemActivityWriter activityWriter,
INotificationEventWriter notificationEventWriter)
INotificationEventWriter notificationEventWriter,
SocializeMetrics metrics)
: Endpoint<CreateContentItemRequest, ContentItemDto>
{
public override void Configure()
@@ -123,6 +125,7 @@ internal class CreateContentItemHandler(
CreatedAt = DateTimeOffset.UtcNow,
});
await dbContext.SaveChangesAsync(ct);
metrics.RecordContentItemCreated(item.WorkspaceId);
await activityWriter.WriteAsync(
new ContentItemActivityWriteModel(

View File

@@ -1,5 +1,6 @@
using FastEndpoints;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Feedback.Contracts;
using Socialize.Api.Modules.Feedback.Data;
@@ -45,7 +46,8 @@ internal class SubmitFeedbackRequestValidator
internal class SubmitFeedbackHandler(
AppDbContext dbContext,
FeedbackNotificationService notificationService)
FeedbackNotificationService notificationService,
SocializeMetrics metrics)
: Endpoint<SubmitFeedbackRequest, FeedbackReportDto>
{
public override void Configure()
@@ -93,6 +95,7 @@ internal class SubmitFeedbackHandler(
dbContext.FeedbackReports.Add(report);
await notificationService.AddNewReportNotificationsAsync(report, ct);
await dbContext.SaveChangesAsync(ct);
metrics.RecordFeedbackSubmitted(report.Type.ToString(), report.WorkspaceId);
await SendAsync(report.ToDto(), StatusCodes.Status201Created, ct);
}

View File

@@ -1,5 +1,6 @@
using FastEndpoints;
using Microsoft.Extensions.Options;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Identity.Data;
using Socialize.Api.Modules.Identity.Configuration;
@@ -21,7 +22,8 @@ internal record LoginResponse(
internal class LoginHandler(
UserManager userManager,
IOptionsSnapshot<JwtOptions> jwtOptions,
AccessTokenFactory accessTokenFactory)
AccessTokenFactory accessTokenFactory,
SocializeMetrics metrics)
: Endpoint<LoginRequest, LoginResponse>
{
public override void Configure()
@@ -40,6 +42,7 @@ internal class LoginHandler(
user ??= await userManager.FindByNameAsync(request.Email);
if (user is null)
{
metrics.RecordLoginAttempt(false, "unknown_user");
await SendStringAsync(
"Invalid email or password",
401,
@@ -51,6 +54,7 @@ internal class LoginHandler(
bool isPasswordValid = await userManager.CheckPasswordAsync(user, request.Password);
if (!isPasswordValid)
{
metrics.RecordLoginAttempt(false, "invalid_password");
await SendStringAsync(
"Invalid email or password",
401,
@@ -61,6 +65,7 @@ internal class LoginHandler(
// Check if the email is confirmed
if (!user.EmailConfirmed)
{
metrics.RecordLoginAttempt(false, "email_unconfirmed");
await SendStringAsync(
"Email not verified. Please check your email for verification instructions.",
401,
@@ -76,6 +81,7 @@ internal class LoginHandler(
// Generate JWT token
string accessToken = await accessTokenFactory.CreateAsync(user);
metrics.RecordLoginAttempt(true, "success");
await SendOkAsync(
new LoginResponse(accessToken, user.RefreshToken),

View File

@@ -1,6 +1,7 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Organizations.Data;
using Socialize.Api.Modules.Organizations.Services;
@@ -21,7 +22,8 @@ internal class CreateOrganizationRequestValidator
}
internal class CreateOrganizationHandler(
AppDbContext dbContext)
AppDbContext dbContext,
SocializeMetrics metrics)
: Endpoint<CreateOrganizationRequest, OrganizationDto>
{
public override void Configure()
@@ -66,6 +68,7 @@ internal class CreateOrganizationHandler(
dbContext.Organizations.Add(organization);
dbContext.OrganizationMemberships.Add(ownerMembership);
await dbContext.SaveChangesAsync(ct);
metrics.RecordOrganizationCreated(organization.Id);
await SendAsync(
OrganizationDto.FromOrganization(

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Options;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Modules.ReleaseCommunications.Configuration;
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
@@ -6,6 +7,7 @@ namespace Socialize.Api.Modules.ReleaseCommunications.Services;
internal sealed class ReleaseUpdateEmailDigestBackgroundService(
IServiceScopeFactory scopeFactory,
IOptions<ReleaseCommunicationEmailOptions> options,
SocializeMetrics metrics,
ILogger<ReleaseUpdateEmailDigestBackgroundService> logger)
: BackgroundService
{
@@ -42,6 +44,7 @@ internal sealed class ReleaseUpdateEmailDigestBackgroundService(
TimeSpan.FromHours(options.Value.DigestIntervalHours),
force: false,
ct: stoppingToken);
metrics.RecordBackgroundJobRun(nameof(ReleaseUpdateEmailDigestBackgroundService), true);
if (sentCount > 0 && logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Sent {SentCount} release update digest emails.", sentCount);
@@ -54,6 +57,7 @@ internal sealed class ReleaseUpdateEmailDigestBackgroundService(
#pragma warning disable CA1031
catch (Exception ex)
{
metrics.RecordBackgroundJobRun(nameof(ReleaseUpdateEmailDigestBackgroundService), false);
logger.LogError(ex, "Release update digest service failed.");
}
#pragma warning restore CA1031

View File

@@ -1,6 +1,7 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Workspaces.Data;
@@ -24,7 +25,8 @@ internal class CreateWorkspaceRequestValidator
internal class CreateWorkspaceHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
AccessScopeService accessScopeService,
SocializeMetrics metrics)
: Endpoint<CreateWorkspaceRequest, WorkspaceDto>
{
public override void Configure()
@@ -65,6 +67,7 @@ internal class CreateWorkspaceHandler(
dbContext.Workspaces.Add(workspace);
await dbContext.SaveChangesAsync(ct);
metrics.RecordWorkspaceCreated(workspace.OrganizationId, workspace.Id);
WorkspaceDto dto = WorkspaceDto.FromWorkspace(workspace, []);

View File

@@ -1,6 +1,7 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Observability;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Identity.Contracts;
using Socialize.Api.Modules.Workspaces.Data;
@@ -31,7 +32,8 @@ internal class CreateWorkspaceInviteRequestValidator
internal class CreateWorkspaceInviteHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
AccessScopeService accessScopeService,
SocializeMetrics metrics)
: Endpoint<CreateWorkspaceInviteRequest, WorkspaceInviteDto>
{
public override void Configure()
@@ -91,6 +93,7 @@ internal class CreateWorkspaceInviteHandler(
dbContext.WorkspaceInvites.Add(invite);
await dbContext.SaveChangesAsync(ct);
metrics.RecordWorkspaceInviteCreated(invite.WorkspaceId, invite.Role);
await SendAsync(
new WorkspaceInviteDto(