Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 22 additions & 39 deletions src/Application/EventHandlers/UserCreatedEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ namespace Application.EventHandlers;
public sealed class UserCreatedEventHandler(
IEmailService emailService,
ITokenGenerationService tokenService,
ITracingService tracingService,
ILogger<UserCreatedEventHandler> logger)
: IDomainEventHandler<UserCreatedEvent>
{
Expand All @@ -31,48 +30,32 @@ public async Task Handle(DomainEventNotification<UserCreatedEvent> notification,
return;
}

using var activity = tracingService.StartActivity(
"UserCreatedEventHandler.SendVerificationEmail",
System.Diagnostics.ActivityKind.Consumer);

if (activity != null)
try
{
tracingService.AddTags(activity,
("user.id", @event.UserId.ToString()),
("user.email", @event.Email),
("event.type", "UserCreatedEvent"));

try
{
logger.LogInformation(
"Sending email verification to user {UserId} at {Email}",
@event.UserId,
@event.Email);

// Generate email confirmation token using Identity
var token = await tokenService.GenerateEmailConfirmationTokenAsync(@event.UserId);

await emailService.SendEmailVerificationAsync(
@event.UserId,
token,
@event.Email,
@event.FullName,
cancellationToken);
logger.LogInformation(
"Sending email verification to user {UserId} at {Email}",
@event.UserId,
@event.Email);

logger.LogInformation(
"Verification email sent successfully to {Email}",
@event.Email);
// Generate email confirmation token using Identity
var token = await tokenService.GenerateEmailConfirmationTokenAsync(@event.UserId);

activity.SetStatus(System.Diagnostics.ActivityStatusCode.Ok);
}
catch (Exception ex)
{
logger.LogError(ex,
"Failed to send verification email to {Email}",
@event.Email);
await emailService.SendEmailVerificationAsync(
@event.UserId,
token,
@event.Email,
@event.FullName,
cancellationToken);

activity.SetStatus(System.Diagnostics.ActivityStatusCode.Error, ex.Message);
}
logger.LogInformation(
"Verification email sent successfully to {Email}",
@event.Email);
}
catch (Exception ex)
{
logger.LogError(ex,
"Failed to send verification email to {Email}",
@event.Email);
}
}
}
4 changes: 3 additions & 1 deletion src/Infrastructure/Services/Email/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ public async Task SendEmailVerificationAsync(
CancellationToken cancellationToken = default)
{
var subject = "Verify Your Email - Legal Assistant";
var verificationLink = $"{_appSettings.BaseUrl}/api/v1/auth/verify-email?userId={userId}&token={token}";
// URL-encode the token to handle special characters in the verification link
var encodedToken = WebUtility.UrlEncode(token);
var verificationLink = $"{_appSettings.BaseUrl}/api/v1/auth/verify-email?userId={userId}&token={encodedToken}";
var body = _templateService.GetEmailVerificationTemplate(fullName, verificationLink);

await SendEmailAsync(email, subject, body, cancellationToken);
Expand Down
103 changes: 59 additions & 44 deletions src/Web.Api/Extensions/OpenTelemetryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using Infrastructure.Services.Tracing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

Expand Down Expand Up @@ -41,34 +43,50 @@ public static IServiceCollection AddOpenTelemetryTracing(
$"Version: {telemetrySettings.ServiceVersion}, ProjectId: {projectId}, " +
$"IsCloudRun: {isCloudRun}, K_SERVICE: {serviceName}, SamplingRatio: {telemetrySettings.GoogleCloudTrace.SamplingRatio}");

// Cấu hình OpenTelemetry Tracing
services.AddOpenTelemetry()
.WithTracing(builder =>
// Resource builder dùng chung cho cả Tracing và Logging
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(
serviceName: telemetrySettings.ServiceName,
serviceVersion: telemetrySettings.ServiceVersion,
serviceInstanceId: serviceRevision ?? Environment.MachineName)
.AddAttributes(new Dictionary<string, object>
{
builder
// Resource: service name, version, và GCP-specific attributes
.SetResourceBuilder(
ResourceBuilder.CreateDefault()
.AddService(
serviceName: telemetrySettings.ServiceName,
serviceVersion: telemetrySettings.ServiceVersion,
serviceInstanceId: serviceRevision ?? Environment.MachineName)
.AddAttributes(new Dictionary<string, object>
{
// GCP Project ID
["cloud.provider"] = "gcp",
["cloud.platform"] = isCloudRun ? "gcp_cloud_run" : "unknown",
["gcp.project_id"] = projectId ?? string.Empty,
// GCP Project ID
["cloud.provider"] = "gcp",
["cloud.platform"] = isCloudRun ? "gcp_cloud_run" : "unknown",
["gcp.project_id"] = projectId ?? string.Empty,

// Cloud Run specific attributes
["faas.name"] = serviceName ?? telemetrySettings.ServiceName,
["faas.version"] = serviceRevision ?? telemetrySettings.ServiceVersion,
// Cloud Run specific attributes
["faas.name"] = serviceName ?? telemetrySettings.ServiceName,
["faas.version"] = serviceRevision ?? telemetrySettings.ServiceVersion,

// Service namespace để GCP grouping
["service.namespace"] = "legal-assistant",
}
.Where(kvp => !string.IsNullOrEmpty(kvp.Value?.ToString())))
)
// Service namespace để GCP grouping
["service.namespace"] = "legal-assistant",
}
.Where(kvp => !string.IsNullOrEmpty(kvp.Value?.ToString())));

// OTLP endpoint dùng chung
var otlpEndpoint = telemetrySettings.GoogleCloudTrace.OtlpEndpoint
?? "http://localhost:4317";

// Cấu hình OpenTelemetry với cả Tracing và Logging
var openTelemetry = services.AddOpenTelemetry();

// Configure resource
openTelemetry.ConfigureResource(resource =>
{
foreach (var attr in resourceBuilder.Build().Attributes)
{
resource.AddAttributes(new[] { attr });
}
});

// Configure Tracing
openTelemetry.WithTracing(builder =>
{
builder
// Resource: service name, version, và GCP-specific attributes
.SetResourceBuilder(resourceBuilder)
// QUAN TRỌNG: Entity Framework Core instrumentation PHẢI được add TRƯỚC ASP.NET Core
// để đảm bảo EF spans được link với HTTP spans
.AddEntityFrameworkCoreInstrumentation(options =>
Expand Down Expand Up @@ -147,28 +165,25 @@ public static IServiceCollection AddOpenTelemetryTracing(
// Sampling
.SetSampler(new TraceIdRatioBasedSampler(telemetrySettings.GoogleCloudTrace.SamplingRatio));

// OTLP Exporter với Google Cloud authentication
builder.AddOtlpExporter(otlpOptions =>
{
var endpoint = telemetrySettings.GoogleCloudTrace.OtlpEndpoint
?? "https://cloudtrace.googleapis.com";

otlpOptions.Endpoint = new Uri(endpoint);
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
// OTLP Exporter với Google Cloud authentication
builder.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri(otlpEndpoint);
otlpOptions.Protocol = OtlpExportProtocol.Grpc;

// Trên Cloud Run, Application Default Credentials tự động được sử dụng
// cho gRPC authentication với Google Cloud APIs
Console.WriteLine($"[OpenTelemetry] OTLP Exporter configured - Endpoint: {endpoint}, Protocol: gRPC");
});
// Trên Cloud Run, Application Default Credentials tự động được sử dụng
// cho gRPC authentication với Google Cloud APIs
Console.WriteLine($"[OpenTelemetry] Tracing OTLP Exporter configured - Endpoint: {otlpEndpoint}, Protocol: gRPC");
});

if (!isCloudRun && telemetrySettings.GoogleCloudTrace.EnableConsoleExporter)
{
builder.AddConsoleExporter();
Console.WriteLine("[OpenTelemetry] Console Exporter enabled for local development");
}
if (!isCloudRun && telemetrySettings.GoogleCloudTrace.EnableConsoleExporter)
{
builder.AddConsoleExporter();
Console.WriteLine("[OpenTelemetry] Tracing Console Exporter enabled for local development");
}

Console.WriteLine("[OpenTelemetry] Tracing configuration completed");
});
Console.WriteLine("[OpenTelemetry] Tracing configuration completed");
});

return services;
}
Expand Down
16 changes: 16 additions & 0 deletions src/Web.Api/Extensions/SerilogExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ public static class SerilogExtensions
/// </summary>
public static void ConfigureSerilog()
{
// Đọc OTLP endpoint từ environment hoặc sử dụng default
var otlpEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT")
?? "http://localhost:4317";

Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
Expand All @@ -31,6 +35,18 @@ public static void ConfigureSerilog()
retainedFileCountLimit: 30,
fileSizeLimitBytes: 10_485_760, // 10MB
formatProvider: CultureInfo.InvariantCulture)
// Thêm OpenTelemetry sink để gửi logs đến Aspire Dashboard
.WriteTo.OpenTelemetry(options =>
{
options.Endpoint = otlpEndpoint;
options.Protocol = Serilog.Sinks.OpenTelemetry.OtlpProtocol.Grpc;
options.ResourceAttributes = new Dictionary<string, object>
{
["service.name"] = "LegalAssistant.AppService",
["service.version"] = "1.0.0",
["service.namespace"] = "legal-assistant"
};
})
.CreateLogger();
}

Expand Down
1 change: 1 addition & 0 deletions src/Web.Api/Web.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.21" />

<!-- OpenTelemetry packages -->
Expand Down