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
38 changes: 22 additions & 16 deletions src/Infrastructure/Repositories/ConversationRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public async Task<PaginatedResult<ConversationSummary>> GetHistoriesAsync(Guid u

var searchTerm = request.SearchTerm.Trim();

// Logic WHERE dùng chung cho cả đếm và lấy dữ liệu
// f_unaccent(): Bỏ dấu cả dữ liệu lẫn từ khóa (IMMUTABLE wrapper của unaccent)
// websearch_to_tsquery: Xử lý input an toàn (vd: "A or B" sẽ không lỗi)
// to_tsvector('simple', ...): Full-text search không stemming (phù hợp tiếng Việt)
Expand All @@ -52,25 +51,35 @@ AND to_tsvector('simple', f_unaccent(m.""Content"")) @@ websearch_to_tsquery('si

// Query lấy dữ liệu với pagination
var sql = $@"
SELECT DISTINCT c.""Id"", c.""Title"", c.""UpdatedAt""
SELECT DISTINCT c.""Id"", c.""Title"", c.""UpdatedAt"", c.""IsStarred""
FROM ""Conversations"" c
{whereClause}
ORDER BY c.""UpdatedAt"" DESC
OFFSET @offset LIMIT @limit";

// Query đếm tổng số - Execute trực tiếp để tránh EF Core wrap query
NpgsqlParameter[] CreateCommonParameters() =>
[
new NpgsqlParameter("@userId", userId),
new NpgsqlParameter("@searchTerm", searchTerm),
new NpgsqlParameter("@isStarred", NpgsqlTypes.NpgsqlDbType.Boolean)
{
Value = request.IsStarred.HasValue ? request.IsStarred.Value : DBNull.Value
}
];

var countSql = $@"
SELECT COUNT(DISTINCT c.""Id"")
FROM ""Conversations"" c
{whereClause}";

// Execute count query using raw connection
using var connection = _context.Database.GetDbConnection();
await using var command = connection.CreateCommand();
command.CommandText = countSql;
command.Parameters.Add(new NpgsqlParameter("@userId", userId));
command.Parameters.Add(new NpgsqlParameter("@searchTerm", searchTerm));
command.Parameters.Add(new NpgsqlParameter("@isStarred", request.IsStarred));

foreach (var param in CreateCommonParameters())
{
command.Parameters.Add(param);
}

if (connection.State != ConnectionState.Open)
{
Expand All @@ -81,17 +90,14 @@ SELECT COUNT(DISTINCT c.""Id"")
var totalCount = Convert.ToInt32(result, CultureInfo.InvariantCulture);

// Get paginated results
// IgnoreQueryFilters(): Bỏ qua query filter của EF Core (vì đã filter trong SQL rồi)
var conversations = await _context.Conversations
.FromSqlRaw(sql,
[
new NpgsqlParameter("@userId", userId),
new NpgsqlParameter("@searchTerm", searchTerm),
new NpgsqlParameter("@isStarred", request.IsStarred),
new NpgsqlParameter("@offset", request.Skip),
new NpgsqlParameter("@limit", request.Take)
])
.IgnoreQueryFilters() // ← QUAN TRỌNG: Bỏ qua WHERE NOT (d."IsDeleted")
[
.. CreateCommonParameters(),
new NpgsqlParameter("@offset", request.Skip),
new NpgsqlParameter("@limit", request.Take)
])
.IgnoreQueryFilters() // ← QUAN TRỌNG: Bỏ qua WHERE NOT (d."IsDeleted"), tránh lỗi vì sql không trả về dữ liệu deleted
.Select(c => new ConversationSummary
{
Id = c.Id,
Expand Down
104 changes: 44 additions & 60 deletions src/Web.Api/Extensions/OpenTelemetryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Infrastructure.Configuration;
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 @@ -43,50 +40,34 @@ public static IServiceCollection AddOpenTelemetryTracing(
$"Version: {telemetrySettings.ServiceVersion}, ProjectId: {projectId}, " +
$"IsCloudRun: {isCloudRun}, K_SERVICE: {serviceName}, SamplingRatio: {telemetrySettings.GoogleCloudTrace.SamplingRatio}");

// 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>
// Cấu hình OpenTelemetry Tracing
services.AddOpenTelemetry()
.WithTracing(builder =>
{
// 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,

// 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();
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,

// Configure resource
openTelemetry.ConfigureResource(resource =>
{
foreach (var attr in resourceBuilder.Build().Attributes)
{
resource.AddAttributes(new[] { attr });
}
});
// Cloud Run specific attributes
["faas.name"] = serviceName ?? telemetrySettings.ServiceName,
["faas.version"] = serviceRevision ?? telemetrySettings.ServiceVersion,

// Configure Tracing
openTelemetry.WithTracing(builder =>
{
builder
// Resource: service name, version, và GCP-specific attributes
.SetResourceBuilder(resourceBuilder)
// Service namespace để GCP grouping
["service.namespace"] = "legal-assistant",
}
.Where(kvp => !string.IsNullOrEmpty(kvp.Value?.ToString())))
)
// 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 @@ -165,25 +146,28 @@ public static IServiceCollection AddOpenTelemetryTracing(
// Sampling
.SetSampler(new TraceIdRatioBasedSampler(telemetrySettings.GoogleCloudTrace.SamplingRatio));

// OTLP Exporter với Google Cloud authentication
builder.AddOtlpExporter(otlpOptions =>
{
otlpOptions.Endpoint = new Uri(otlpEndpoint);
otlpOptions.Protocol = OtlpExportProtocol.Grpc;
// OTLP Exporter với Google Cloud authentication
builder.AddOtlpExporter(otlpOptions =>
{
var endpoint = telemetrySettings.GoogleCloudTrace.OtlpEndpoint
?? "https://cloudtrace.googleapis.com";

// 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");
});
otlpOptions.Endpoint = new Uri(endpoint);
otlpOptions.Protocol = OtlpExportProtocol.Grpc;

if (!isCloudRun && telemetrySettings.GoogleCloudTrace.EnableConsoleExporter)
{
builder.AddConsoleExporter();
Console.WriteLine("[OpenTelemetry] Tracing Console Exporter enabled for local development");
}
// 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");
});

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

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

return services;
}
Expand Down
3 changes: 1 addition & 2 deletions src/Web.Api/Middleware/GlobalExceptionMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,13 @@ private static (int StatusCode, string Message, object ErrorDetails) GetExceptio
}
),

// Default case - Don't leak internal details to client
_ => (
(int)HttpStatusCode.InternalServerError,
"An unexpected error occurred while processing your request",
new
{
Type = "InternalServerError",
Details = "Please contact support if the problem persists"
Details = exception.InnerException?.Message ?? exception.Message
}
)
};
Expand Down