Files
tailscale-custom/.agent/uploads/1773926334916_audit_log_flow.md
T
huanld 2fb067ecbf
checklocks / checklocks (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
natlab-integrationtest / natlab-integrationtest (push) Has been cancelled
CI / gomod-cache (push) Has been cancelled
CI / race-root-integration (1/4) (push) Has been cancelled
CI / race-root-integration (2/4) (push) Has been cancelled
CI / race-root-integration (3/4) (push) Has been cancelled
CI / race-root-integration (4/4) (push) Has been cancelled
CI / test (-race, amd64, 1/3) (push) Has been cancelled
CI / test (-race, amd64, 2/3) (push) Has been cancelled
CI / test (-race, amd64, 3/3) (push) Has been cancelled
CI / test (386) (push) Has been cancelled
CI / test (amd64) (push) Has been cancelled
CI / Windows (benchmarks) (push) Has been cancelled
CI / Windows (1/2) (push) Has been cancelled
CI / Windows (2/2) (push) Has been cancelled
CI / macos (push) Has been cancelled
CI / privileged (push) Has been cancelled
CI / vm (push) Has been cancelled
CI / cross (386, linux) (push) Has been cancelled
CI / cross (amd64, darwin) (push) Has been cancelled
CI / cross (amd64, freebsd) (push) Has been cancelled
CI / cross (amd64, openbsd) (push) Has been cancelled
CI / cross (amd64, windows) (push) Has been cancelled
CI / cross (arm, 5, linux) (push) Has been cancelled
CI / cross (arm, 7, linux) (push) Has been cancelled
CI / cross (arm64, darwin) (push) Has been cancelled
CI / cross (arm64, linux) (push) Has been cancelled
CI / cross (arm64, windows) (push) Has been cancelled
CI / cross (loong64, linux) (push) Has been cancelled
CI / ios (push) Has been cancelled
CI / crossmin (amd64, illumos) (push) Has been cancelled
CI / crossmin (amd64, plan9) (push) Has been cancelled
CI / crossmin (amd64, solaris) (push) Has been cancelled
CI / crossmin (ppc64, aix) (push) Has been cancelled
CI / android (push) Has been cancelled
CI / wasm (push) Has been cancelled
CI / tailscale_go (push) Has been cancelled
CI / fuzz (push) Has been cancelled
CI / depaware (push) Has been cancelled
CI / go_generate (push) Has been cancelled
CI / make_tidy (push) Has been cancelled
CI / licenses (push) Has been cancelled
CI / staticcheck (macOS) (push) Has been cancelled
CI / staticcheck (Linux) (push) Has been cancelled
CI / staticcheck (Windows) (push) Has been cancelled
CI / staticcheck (Portable (1/4)) (push) Has been cancelled
CI / staticcheck (Portable (2/4)) (push) Has been cancelled
CI / staticcheck (Portable (3/4)) (push) Has been cancelled
CI / staticcheck (Portable (4/4)) (push) Has been cancelled
CI / notify_slack (push) Has been cancelled
CI / merge_blocker (push) Has been cancelled
CI / check_mergeability_strict (push) Has been cancelled
CI / check_mergeability (push) Has been cancelled
Dockerfile build / deploy (push) Has been cancelled
test installer.sh / test (curl, alpine:3.21) (push) Has been cancelled
test installer.sh / test (curl, alpine:edge) (push) Has been cancelled
test installer.sh / test (curl, alpine:latest) (push) Has been cancelled
test installer.sh / test (curl, amazonlinux:latest) (push) Has been cancelled
test installer.sh / test (curl, archlinux:latest) (push) Has been cancelled
test installer.sh / test (curl, debian:oldstable-slim) (push) Has been cancelled
test installer.sh / test (curl, debian:sid-slim) (push) Has been cancelled
test installer.sh / test (curl, debian:stable-slim, 1.80.0) (push) Has been cancelled
test installer.sh / test (curl, debian:testing-slim) (push) Has been cancelled
test installer.sh / test (curl, elementary/docker:stable) (push) Has been cancelled
test installer.sh / test (curl, elementary/docker:unstable) (push) Has been cancelled
test installer.sh / test (curl, fedora:latest, 1.80.0) (push) Has been cancelled
test installer.sh / test (curl, kalilinux/kali-dev) (push) Has been cancelled
test installer.sh / test (curl, kalilinux/kali-rolling) (push) Has been cancelled
test installer.sh / test (curl, opensuse/leap:latest) (push) Has been cancelled
test installer.sh / test (curl, opensuse/tumbleweed:latest) (push) Has been cancelled
test installer.sh / test (curl, oraclelinux:8) (push) Has been cancelled
test installer.sh / test (curl, oraclelinux:9) (push) Has been cancelled
test installer.sh / test (curl, parrotsec/core:latest) (push) Has been cancelled
test installer.sh / test (curl, rockylinux:8.7) (push) Has been cancelled
test installer.sh / test (curl, rockylinux:9) (push) Has been cancelled
test installer.sh / test (curl, ubuntu:20.04) (push) Has been cancelled
test installer.sh / test (curl, ubuntu:22.04) (push) Has been cancelled
test installer.sh / test (curl, ubuntu:24.04, 1.80.0) (push) Has been cancelled
test installer.sh / test (wget, debian:oldstable-slim) (push) Has been cancelled
test installer.sh / test (wget, debian:sid-slim) (push) Has been cancelled
update-flake / update-flake (push) Has been cancelled
tailscale.com/cmd/vet / vet (push) Has been cancelled
test installer.sh / notify-slack (push) Has been cancelled
feat: security hardening, production roadmap, admin panel v1
Client security fixes (cmd/tailscale-tray/main.go):
- SSRF protection in Add Server dialog (validateControlURL): reject
  private/loopback/link-local/cloud-metadata IPs via DNS resolution
- RCE gate on AuthURL/BrowseToURL exec paths (validateAuthURL)
- Sanitized URL logging (sanitizeURLForLog drops query auth tokens)
- Error handling on exec.Command with user-facing showError()

Admin panel security (web-admin):
- Bcrypt password hashing (replaces SHA256)
- Rate limiting: 5 failed logins → 15-min lockout
- Session + login attempt cleanup goroutine (hourly)
- url.QueryEscape / encodeURIComponent for all API params
- Fail-hard startup when no TLS and non-loopback bind
- ADMIN_PASSWORD required (no default), password min 12 chars
- Username regex whitelist

Installer hardening (Setup.wxs):
- util:PermissionEx restricts SCM access: only Administrators +
  SYSTEM can start/stop/reconfigure service. Authenticated Users
  limited to QueryStatus/QueryConfig/Interrogate
- Vital="yes" on ServiceInstall

Docs & roadmap:
- PRODUCTION_ROADMAP.md: 5-milestone plan (security + features +
  distribution + ops) with granular tasks, effort, done-when
- CLIENT_SECURITY_AUDIT.md, SECURITY_FIXES.md, DEPLOYMENT.md
- AI assistant rules (.cursorrules, .antigravityrules, etc.)

Build & distribution:
- build-msi.ps1, deploy-and-sign.ps1, sign-release.ps1
- redeploy.ps1, tray-deploy.ps1, test-msi.ps1
- installer/msi/ alternative WXS setup
- Restored .github/workflows/ removed in mirror cleanup

.gitignore hardened: *.pfx, *.p12, *.key, *.pem, .env*
2026-04-22 15:18:11 +07:00

31 KiB

Audit Log Flow - Tài liệu chi tiết luồng hoạt động

Mục đích: Tài liệu này mô tả đầy đủ chi tiết luồng ghi Audit Log trong hệ thống INS, đủ để tái tạo lại chức năng tương tự.


1. Tổng quan kiến trúc

Hệ thống Audit Log sử dụng kiến trúc event-driven với RabbitMQ làm message broker, tách biệt hoàn toàn việc ghi log khỏi business logic.

graph TD
    subgraph "TIER 1: Frontend (Blazor WASM)"
        A["INS_AuditLogger.razor<br/>(DXComponents)"]
        B["LogService.cs<br/>(HTTP Client)"]
    end

    subgraph "TIER 2: Module Backend"
        C["Module Controller<br/>(REST API /api/logging/audit)"]
        D["gRPC Client<br/>(INS.LoggingController)"]
    end

    subgraph "TIER 3: INS.WASM.AUTH Server"
        E["CorrelationIdMiddleware"]
        F["ActivityLoggingMiddleware"]
        G["LoggingGrpcService"]
        H["AuditLogPublisher"]
        I["IRabbitMQService.SendAsync()"]
    end

    subgraph "Message Broker"
        J["RabbitMQ<br/>Exchange: ins.logs.exchange<br/>(topic)"]
        K["Queue: ins.server.auditlog.queue<br/>Binding: logs.audit.#"]
    end

    subgraph "Consumer Layer"
        L["AuditLogConsumerService<br/>(BackgroundService)"]
        M["BaseRabbitMqConsumerService<br/>(lazy init + retry)"]
    end

    subgraph "Database"
        N["SQL Server<br/>[Log].[AuditLogs]"]
    end

    A --> B
    B -->|"POST /api/logging/audit"| C
    C -->|"gRPC"| D
    D -->|"gRPC SendAuditLog()"| G
    G --> I
    H --> I
    F --> I
    I -->|"Publish"| J
    J -->|"Route: logs.audit.*"| K
    K -->|"Consume"| L
    L --> M
    L -->|"EF Core INSERT"| N
    E -->|"Inject CorrelationId"| G
    E -->|"Inject CorrelationId"| H

3 Đường vào (Entry Points) cho Audit Log

# Entry Point Routing Key Khi nào dùng
1 gRPC từ Module (LoggingGrpcService) logs.audit.server Module Frontend gọi qua LogService → Module Controller → gRPC
2 AuditLogPublisher (direct call) logs.audit.data Backend code gọi trực tiếp khi có data change
3 REST API (TestRabbitMQController) logs.audit.server Test/debug endpoint

2. Chi tiết từng thành phần

2.1. Frontend Blazor: INS_AuditLogger.razor

Location: lib/INS.DXComponents/Logging/INS_AuditLogger.razor

Chức năng: Component ẩn (không render UI), inject vào Blazor page để ghi audit log cho thao tác CRUD.

Cách sử dụng trong page:

<INS_AuditLogger @ref="auditLogger" />

@code {
    private INS_AuditLogger auditLogger = default!;
    
    // Khi INSERT
    await auditLogger.LogInsertAsync("TableName", "rowId", newObject);
    
    // Khi UPDATE
    await auditLogger.LogUpdateAsync("TableName", "rowId", oldObject, newObject);
    
    // Khi DELETE
    await auditLogger.LogDeleteAsync("TableName", "rowId", oldObject);
    
    // Custom operation
    await auditLogger.LogCustomAsync("TableName", "APPROVE", "rowId", oldObj, newObj);
}

Nội bộ hoạt động:

  1. OnInitializedAsync() → Lấy UserIdUsername từ AuthenticationStateProvider
  2. Serialize old/new data thành JSON bằng System.Text.Json.JsonSerializer
  3. Gọi LogService.AuditAsync() với các tham số

Dependencies: LogService, AuthenticationStateProvider

DI Registration:

services.AddScoped<INS_AuditLogger>();

2.2. LogService (HTTP Client - 3-tier Bridge)

Location: lib/INS.DXComponents/Logging/LogService.cs

Chức năng: Bridge giữa Frontend và Module Backend, gửi log qua HTTP POST.

Flow: Frontend → LogService → HTTP POST → Module Controller → gRPC → INS.Server

public class LogService
{
    private readonly IHttpClientFactory _httpClientFactory;

    // Sử dụng "AuthenticatedClient" - tự động gắn Access Token
    private HttpClient CreateClient() => _httpClientFactory.CreateClient("AuthenticatedClient");

    public async Task<bool> AuditAsync(
        string tableName,
        string operation,
        string? rowId = null,
        string? oldValue = null,
        string? newValue = null,
        string? userId = null,
        string? username = null)
    {
        var dto = new RabbitMQAuditLogDto
        {
            TableName = tableName,
            RowId = rowId ?? string.Empty,
            Operation = operation,
            OldData = oldValue,
            NewData = newValue,
            ChangedBy = username ?? userId ?? "System",
            Timestamp = DateTime.UtcNow
        };

        using var client = CreateClient();
        var response = await client.PostAsJsonAsync("/api/logging/audit", dto);
        return response.IsSuccessStatusCode;
    }
}

Đặc điểm quan trọng:

  • Sử dụng IHttpClientFactory với named client "AuthenticatedClient" (tự gắn JWT token)
  • Swallow exceptions (return false) → Log failure KHÔNG break UI flow
  • POST tới /api/logging/audit trên Module Backend

DI Registration:

// Extension method
services.AddINSLogging(); // → AddScoped<LogService>()

2.3. gRPC Proto Definition

Location: INS.WASM.AUTH/src/INS.Backend/Protos/logging.proto

syntax = "proto3";
option csharp_namespace = "INS.LoggingController";
import "google/protobuf/timestamp.proto";
package logging;

service LoggingService {
  rpc SendAuditLog (AuditLogRequest) returns (LogResponse);
  rpc SendAppLog (AppLogRequest) returns (LogResponse);
  rpc SendActivityLog (ActivityLogRequest) returns (LogResponse);
}

message AuditLogRequest {
  string table_name = 1;
  string row_id = 2;
  string operation = 3;      // INSERT, UPDATE, DELETE
  string old_data = 4;       // JSON
  string new_data = 5;       // JSON
  string changed_by = 6;
  google.protobuf.Timestamp timestamp = 7;
  string user_id = 8;
  repeated string roles = 9;
}

message LogResponse {
  bool success = 1;
  string message = 2;
}

NuGet package: INS.LoggingController (v1.0.4) - chứa generated gRPC client/server code


2.4. LoggingGrpcService (gRPC → RabbitMQ Bridge)

Location: INS.WASM.AUTH/src/INS.Backend/Services/Grpc/LoggingGrpcService.cs

Chức năng: Nhận gRPC call từ modules, chuyển đổi sang RabbitMQAuditLogDto, publish lên RabbitMQ.

public class LoggingGrpcService : LoggingService.LoggingServiceBase
{
    private readonly IRabbitMQService _rabbitMQService;

    public override async Task<LogResponse> SendAuditLog(
        AuditLogRequest request, ServerCallContext context)
    {
        // Map Protobuf → DTO
        var dto = new RabbitMQAuditLogDto
        {
            TableName = request.TableName,
            RowId = request.RowId,
            Operation = request.Operation,
            OldData = request.OldData,
            NewData = request.NewData,
            ChangedBy = request.ChangedBy,
            Timestamp = request.Timestamp.ToDateTime()
        };

        // Publish tới RabbitMQ
        await _rabbitMQService.SendAsync(
            exchangeName: "ins.logs.exchange",
            routingKey: "logs.audit.server",
            message: dto
        );

        return new LogResponse { Success = true, Message = "Audit log sent successfully" };
    }
}

Routing Key: logs.audit.server (fixed) Exchange: ins.logs.exchange (topic exchange)


2.5. AuditLogPublisher (Direct Publisher)

Location: INS.WASM.AUTH/src/INS.Backend/Services/AuditLogPublisher.cs

Chức năng: Publisher trực tiếp cho backend code, tự động enrich message với request context và generate rollback SQL.

Interface:

public interface IAuditLogPublisher
{
    // Simplified API - auto-enrich từ request context
    Task PublishAsync(
        string schemaName,
        string tableName,
        string rowId,
        string operation,        // INSERT, UPDATE, DELETE
        string? oldData,         // JSON
        string? newData,         // JSON
        string? primaryKeyColumns = null,  // JSON array: ["Id"] hoặc ["Code", "Version"]
        string? functionName = null);

    // Full control API
    Task PublishAsync(AuditLogMessage message);
}

Luồng xử lý chi tiết:

PublishAsync(params) 
  │
  ├── 1. Generate Rollback SQL
  │       └── IRollbackSqlGenerator.GenerateRollbackSql()
  │
  ├── 2. Detect Changed Columns (nếu UPDATE)
  │       └── IRollbackSqlGenerator.GetChangedColumns(oldData, newData)
  │
  ├── 3. Build AuditLogMessage (enrich từ IRequestContextAccessor)
  │       ├── WHO: UserId, Username, IpAddress, SessionId, MachineName
  │       ├── WHAT: Schema, Table, RowId, Operation, OldData, NewData, ChangedColumns
  │       ├── WHEN: Timestamp (UtcNow)
  │       ├── HOW TO ROLLBACK: RollbackSql
  │       └── WHERE FROM: ModuleId, CorrelationId, ParentCorrelationId, RequestUrl, HttpMethod
  │
  ├── 4. Kiểm tra Enabled
  │       └── RabbitMQ:Enabled && RabbitMQ:AuditLog:Enabled
  │
  └── 5. Publish lên RabbitMQ
          └── IRabbitMQService.SendAsync(exchangeName, routingKey, message)

Configuration (appsettings.json):

{
  "RabbitMQ": {
    "Enabled": true,
    "AuditLog": {
      "Enabled": true,
      "ExchangeName": "ins.logs.exchange",
      "PublishRoutingKey": "logs.audit.data"
    }
  },
  "ModuleRegistration": {
    "ModuleId": "INS.Server"
  }
}

Dependencies:

Service Lifetime Chức năng
IRabbitMQService? Singleton Publish message (optional - nullable)
IRequestContextAccessor Scoped Lấy context từ HTTP request hiện tại
IRollbackSqlGenerator Singleton Generate rollback SQL + detect changed columns
IConfiguration Singleton Đọc config

DI Registration:

builder.Services.AddScoped<IRequestContextAccessor, RequestContextAccessor>();
builder.Services.AddSingleton<IRollbackSqlGenerator, RollbackSqlGenerator>();
builder.Services.AddScoped<IAuditLogPublisher, AuditLogPublisher>();

Error handling: Swallow exceptions - audit log failure KHÔNG break business flow:

catch (Exception ex)
{
    _logger.LogError(ex, "Failed to publish AuditLog for {Table}/{Operation}", ...);
    // Don't throw - audit log failure shouldn't break the main flow
}

2.6. CorrelationIdMiddleware (Distributed Tracing)

Location: INS.WASM.AUTH/src/INS.Backend/Middleware/CorrelationIdMiddleware.cs

Chức năng: Inject/generate CorrelationId cho mỗi HTTP request, cho phép trace toàn bộ luồng từ request → audit log → database.

HTTP Headers sử dụng:

Header Mục đích Auto-generate?
X-Correlation-ID Unique ID cho request Guid.NewGuid() nếu không có
X-Parent-Correlation-ID ID của request cha (chained calls)
X-Trace-Span-ID Span ID cho distributed tracing 16-char substring
X-Module-ID Module nào gọi request
X-Sequence-Number Thứ tự trong chain

Luồng xử lý:

  1. Đọc/generate CorrelationId từ request header
  2. Populate IRequestContextAccessor (scoped service)
  3. Store vào HttpContext.Items
  4. Gắn CorrelationId vào response header
  5. Gọi _next(context) → pipeline tiếp tục

Middleware registration order:

app.UseCorrelationId();        // ← PHẢI trước các middleware khác
app.UseActivityLogging();      // Activity log middleware
// ... other middleware

2.7. IRequestContextAccessor (Scoped Context)

Location: INS.WASM.AUTH/src/INS.Backend/Middleware/IRequestContextAccessor.cs

Chức năng: Scoped service chứa thông tin request hiện tại, được populate bởi CorrelationIdMiddlewareActivityLoggingMiddleware.

public interface IRequestContextAccessor
{
    // Tracing
    string? CorrelationId { get; set; }
    string? ParentCorrelationId { get; set; }
    string? TraceSpanId { get; set; }
    string? ModuleId { get; set; }
    int SequenceNumber { get; set; }

    // Request
    string? RequestUrl { get; set; }
    string? HttpMethod { get; set; }
    string? RequestBody { get; set; }
    string? ResponseBody { get; set; }
    string? IpAddress { get; set; }
    string? UserAgent { get; set; }

    // User
    string? UserId { get; set; }
    string? Username { get; set; }
    string? SessionId { get; set; }

    // Timing
    DateTime RequestStartTime { get; set; }
    long GetDurationMs();

    // System
    string? GetSystemState();
}

Lifetime: Scoped → mỗi HTTP request có một instance riêng, đảm bảo data isolation.


2.8. RollbackSqlGenerator

Location: INS.WASM.AUTH/src/INS.Backend/Services/RollbackSqlGenerator.cs

Chức năng: Tự động generate SQL để rollback thay đổi dựa trên operation type.

Logic:

Operation Rollback SQL Input cần
INSERT DELETE FROM [schema].[table] WHERE [pk] = [value] PK columns + newData
UPDATE UPDATE [schema].[table] SET [cols] = [old_values] WHERE [pk] = [value] PK columns + oldData
DELETE INSERT INTO [schema].[table] ([cols]) VALUES ([old_values]) oldData

GetChangedColumns(): So sánh oldData và newData (JSON), trả về danh sách columns đã thay đổi.

WHERE clause strategy (ưu tiên):

  1. Parse rowId as JSON object → {"Id": 123, "Code": "ABC"}[Id] = 123 AND [Code] = N'ABC'
  2. Dùng primaryKeyColumns + data → extract PK values từ data
  3. Fallback: [Id] = rowId (int/Guid/string detection)

Value formatting:

  • nullNULL
  • true/false1/0
  • number → literal
  • string → N'escaped_value' (SQL injection protection via ''')

2.9. Shared DTO: RabbitMQAuditLogDto

Location: lib/INS.Shared/Models/RabbitMQLogDtos.cs

public class RabbitMQAuditLogDto
{
    public string TableName { get; set; } = string.Empty;
    public string RowId { get; set; } = string.Empty;
    public string Operation { get; set; } = string.Empty; // INSERT, UPDATE, DELETE
    public string? OldData { get; set; }     // JSON
    public string? NewData { get; set; }     // JSON
    public string? ChangedBy { get; set; }
    public string? DeviceName { get; set; }
    public DateTime? Timestamp { get; set; }
    public string? CorrelationId { get; set; }
}

Important

RabbitMQAuditLogDto (lightweight, dùng cho gRPC path) khác với AuditLogMessage (full, dùng cho AuditLogPublisher). Consumer phải handle cả 2 format.


2.10. AuditLogMessage (Full Message Model)

Location: INS.Backend/Models/RabbitMQLogModels.cs (cả INS.Server và INS.WASM.AUTH)

public class AuditLogMessage
{
    // WHO
    public string? UserId { get; set; }
    public string? ChangedBy { get; set; }
    public string? IpAddress { get; set; }
    public string? SessionId { get; set; }
    public string? DeviceName { get; set; }

    // WHAT
    public string SchemaName { get; set; }
    public string TableName { get; set; }
    public string RowId { get; set; }
    public string? PrimaryKeyColumns { get; set; }
    public string Operation { get; set; }     // INSERT, UPDATE, DELETE
    public string? OldData { get; set; }      // JSON
    public string? NewData { get; set; }      // JSON
    public string? ChangedColumns { get; set; }

    // WHEN
    public DateTime? Timestamp { get; set; }

    // HOW TO ROLLBACK
    public string? RollbackSql { get; set; }

    // TRACING
    public string? ModuleId { get; set; }
    public string? CorrelationId { get; set; }
    public string? ParentCorrelationId { get; set; }
    public string? RequestUrl { get; set; }
    public string? HttpMethod { get; set; }
    public string? RequestBody { get; set; }
    public string? FunctionName { get; set; }
}

2.11. RabbitMQ Configuration

Exchange: ins.logs.exchange (type: topic)

Routing Keys:

Publisher Routing Key Mô tả
LoggingGrpcService.SendAuditLog() logs.audit.server Từ module gRPC
AuditLogPublisher.PublishAsync() logs.audit.data Từ backend direct call
ActivityLoggingMiddleware logs.activity Auto HTTP tracking

Queue Binding: ins.server.auditlog.queue bind với pattern logs.audit.# → match cả logs.audit.serverlogs.audit.data

Core Library: INS.RabbitMQ (v1.2.1, RabbitMQ.Client 7.2.0)

  • Interface: IRabbitMQService
  • Methods: SendAsync(exchange, routingKey, message), CreateConsumer<T>()

2.12. BaseRabbitMqConsumerService (Consumer Infrastructure)

Location: INS.WASM.AUTH/src/INS.Backend/Services/Consumers/BaseRabbitMqConsumerService.cs

Chức năng: Abstract base class cho tất cả consumer, kế thừa BackgroundService.

Đặc điểm kiến trúc:

  • Lazy initialization: Không connect RabbitMQ tại startup, đợi 5s rồi mới connect
  • Auto-retry: Nếu connect fail, retry mỗi RetryIntervalSeconds (default: 30s)
  • Fluent API: Sử dụng IRabbitMQService.CreateConsumer<T>() fluent builder
public abstract class BaseRabbitMqConsumerService<TMessage> : BackgroundService 
    where TMessage : class
{
    protected readonly string QueueName;
    protected readonly string ExchangeName;
    protected readonly string RoutingKey;
    protected readonly ushort PrefetchCount;  // default: 10
    protected readonly bool Enabled;
    protected readonly int RetryIntervalSeconds;  // default: 30

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (!Enabled) return;

        await Task.Delay(5_000);  // Đợi app khởi động

        while (!stoppingToken.IsCancellationRequested)
        {
            if (!_isConnected)
            {
                await TryConnectAsync(stoppingToken);
            }

            if (!_isConnected)
            {
                await Task.Delay(RetryIntervalSeconds * 1000);
            }
            else
            {
                await Task.Delay(60_000);  // Health check mỗi 1 phút
            }
        }
    }

    private async Task TryConnectAsync(CancellationToken ct)
    {
        // Fluent API registration
        await RabbitMQService.CreateConsumer<TMessage>()
            .WithQueue(QueueName)
            .WithExchange(ExchangeName, RoutingKey)
            .WithPrefetchCount(PrefetchCount)
            .WithHandler(async message => await ProcessMessageAsync(message))
            .RegisterAsync();
    }

    // Subclass implement: return true = ACK, false = NACK (requeue)
    protected abstract Task<bool> ProcessMessageAsync(TMessage message);
}

Config từ appsettings (section name truyền qua constructor):

{
  "RabbitMQ": {
    "Enabled": true,
    "RetryIntervalSeconds": 30,
    "AuditLog": {
      "Enabled": true,
      "QueueName": "ins.server.auditlog.queue",
      "ExchangeName": "ins.logs.exchange",
      "RoutingKey": "logs.audit.#",
      "PrefetchCount": 10
    }
  }
}

2.13. AuditLogConsumerService (Write to DB)

Location: INS.WASM.AUTH/src/INS.Backend/Services/Consumers/AuditLogConsumerService.cs

Chức năng: Consume AuditLogMessage từ RabbitMQ và INSERT vào [Log].[audit_log] table.

public class AuditLogConsumerService : BaseRabbitMqConsumerService<AuditLogMessage>
{
    public AuditLogConsumerService(
        IServiceProvider serviceProvider,
        IConfiguration configuration,
        ILogger<AuditLogConsumerService> logger)
        : base(serviceProvider, configuration, logger, "AuditLog")  // config section
    { }

    protected override async Task<bool> ProcessMessageAsync(AuditLogMessage message)
    {
        // 1. Validate required fields
        if (string.IsNullOrEmpty(message.TableName) || 
            string.IsNullOrEmpty(message.RowId) || 
            string.IsNullOrEmpty(message.Operation))
        {
            return true;  // ACK invalid message (skip, không requeue)
        }

        // 2. Tạo scope mới cho DbContext (scoped service)
        using var scope = ServiceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<AuthDbContext>();

        // 3. Map message → Entity
        var auditLog = new AuditLog
        {
            // WHO
            UserId = message.UserId,
            ChangedBy = message.ChangedBy,
            DeviceName = message.DeviceName,
            IpAddress = message.IpAddress,
            SessionId = message.SessionId,

            // WHAT
            SchemaName = message.SchemaName,
            TableName = message.TableName,
            RowId = message.RowId,
            PrimaryKeyColumns = message.PrimaryKeyColumns,
            Operation = message.Operation,
            OldData = message.OldData,
            NewData = message.NewData,
            ChangedColumns = message.ChangedColumns,

            // WHEN
            ChangedAt = message.Timestamp ?? DateTime.UtcNow,

            // HOW TO ROLLBACK
            RollbackSql = message.RollbackSql,
            RollbackStatus = "NotRolledBack",

            // WHERE FROM
            ModuleId = message.ModuleId,
            CorrelationId = message.CorrelationId,
            ParentCorrelationId = message.ParentCorrelationId,
            RequestUrl = message.RequestUrl,
            HttpMethod = message.HttpMethod,
            RequestBody = message.RequestBody,
            FunctionName = message.FunctionName
        };

        // 4. Insert vào database
        dbContext.AuditLogs.Add(auditLog);
        await dbContext.SaveChangesAsync();

        return true;  // ACK
    }
}

ACK/NACK strategy:

  • return true (ACK) → Message processed, remove from queue
  • return true cho invalid message → Skip, không requeue vĩnh viễn
  • return false (NACK) → Exception xảy ra, requeue message

DI: Consumer là BackgroundService, registered as HostedService:

builder.Services.AddHostedService<AuditLogConsumerService>();

2.14. Database Entity: AuditLog

Location: INS.WASM.AUTH/src/INS.Backend/Data/LogModels/AuditLog.cs

Table: [log].[audit_log]

[Table("audit_log", Schema = "log")]
public class AuditLog
{
    [Key]
    [Column("audit_id")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long AuditId { get; set; }

    // WHO
    [Column("user_id"), MaxLength(255)]
    public string? UserId { get; set; }

    [Column("changed_by"), MaxLength(255)]
    public string? ChangedBy { get; set; }

    [Column("device_name"), MaxLength(255)]
    public string? DeviceName { get; set; }

    [Column("ip_address"), MaxLength(45)]
    public string? IpAddress { get; set; }

    [Column("session_id"), MaxLength(255)]
    public string? SessionId { get; set; }

    // WHAT
    [Column("schema_name"), MaxLength(128)]
    public string? SchemaName { get; set; }

    [Required, Column("table_name"), MaxLength(255)]
    public string TableName { get; set; }

    [Required, Column("row_id")]
    public string RowId { get; set; }

    [Column("primary_key_columns"), MaxLength(500)]
    public string? PrimaryKeyColumns { get; set; }

    [Required, Column("operation"), MaxLength(10)]
    public string Operation { get; set; }

    [Column("old_data")]
    public string? OldData { get; set; }

    [Column("new_data")]
    public string? NewData { get; set; }

    [Column("changed_columns")]
    public string? ChangedColumns { get; set; }

    // WHEN
    [Required, Column("changed_at")]
    public DateTime ChangedAt { get; set; }

    // HOW TO ROLLBACK
    [Column("rollback_sql")]
    public string? RollbackSql { get; set; }

    [Column("rollback_status"), MaxLength(20)]
    public string? RollbackStatus { get; set; }  // NotRolledBack, SUCCESS, FAILED

    [Column("rollback_at")]
    public DateTime? RollbackAt { get; set; }

    [Column("rollback_by"), MaxLength(255)]
    public string? RollbackBy { get; set; }

    // WHERE FROM
    [Column("module_id"), MaxLength(100)]
    public string? ModuleId { get; set; }

    [Column("correlation_id"), MaxLength(255)]
    public string? CorrelationId { get; set; }

    [Column("parent_correlation_id"), MaxLength(255)]
    public string? ParentCorrelationId { get; set; }

    [Column("request_url"), MaxLength(2048)]
    public string? RequestUrl { get; set; }

    [Column("http_method"), MaxLength(10)]
    public string? HttpMethod { get; set; }

    [Column("request_body")]
    public string? RequestBody { get; set; }

    [Column("function_name"), MaxLength(255)]
    public string? FunctionName { get; set; }
}

Database Indexes (từ migration SQL):

CREATE INDEX [idx_audit_log_changed_at]     ON [Log].[AuditLogs] ([changed_at]);
CREATE INDEX [idx_audit_log_changed_by]     ON [Log].[AuditLogs] ([changed_by]);
CREATE INDEX [idx_audit_log_correlation_id] ON [Log].[AuditLogs] ([correlation_id]);
CREATE INDEX [idx_audit_log_table_name]     ON [Log].[AuditLogs] ([table_name]);
CREATE INDEX [idx_audit_log_table_time]     ON [Log].[AuditLogs] ([table_name], [changed_at]);

3. Full Flow: Từ UI Click đến Database

Flow 1: Frontend Blazor gọi audit (3-tier path)

1. User click "Save" trên Blazor page
2. Page code gọi: await auditLogger.LogUpdateAsync("Users", "123", oldUser, newUser)
3. INS_AuditLogger:
   ├── Serialize oldUser/newUser → JSON
   └── Gọi LogService.AuditAsync(tableName, operation, rowId, oldJson, newJson, userId, username)
4. LogService:
   ├── Tạo RabbitMQAuditLogDto
   └── POST /api/logging/audit → Module Backend Controller
5. Module Backend Controller:
   ├── Nhận DTO
   └── Gọi gRPC SendAuditLog() tới INS.WASM.AUTH
6. LoggingGrpcService (INS.WASM.AUTH):
   ├── Map AuditLogRequest → RabbitMQAuditLogDto
   └── IRabbitMQService.SendAsync("ins.logs.exchange", "logs.audit.server", dto)
7. RabbitMQ:
   ├── Nhận message tại exchange "ins.logs.exchange"
   └── Route theo key "logs.audit.server" → match "logs.audit.#" → queue "ins.server.auditlog.queue"
8. AuditLogConsumerService (BackgroundService):
   ├── Consume message từ queue
   ├── Validate required fields
   ├── Map AuditLogMessage → AuditLog entity
   └── dbContext.AuditLogs.Add() + SaveChangesAsync()
9. SQL Server: INSERT INTO [log].[audit_log] (...)

Flow 2: Backend direct publish (enriched path)

1. Backend API handler thực hiện data change
2. Code gọi: await _auditLogPublisher.PublishAsync("dbo", "Users", "123", "UPDATE", oldJson, newJson, "[\"Id\"]", "UpdateUser")
3. AuditLogPublisher:
   ├── Generate rollback SQL: UPDATE [dbo].[Users] SET [...] WHERE [Id] = 123
   ├── Detect changed columns: ["Name", "Email"]
   ├── Enrich từ IRequestContextAccessor:
   │    ├── UserId, Username, IpAddress, SessionId
   │    ├── CorrelationId (từ CorrelationIdMiddleware)
   │    ├── RequestUrl, HttpMethod
   │    └── ModuleId
   └── IRabbitMQService.SendAsync("ins.logs.exchange", "logs.audit.data", message)
4. RabbitMQ → Queue → Consumer → Database (giống Flow 1 step 7-9)

4. Module Extension Pattern (CDE Example)

CDE module có hệ thống audit log riêng biệt (domain-specific), KHÁC với system-level audit log ở trên:

CDEAuditController

Location: INS.CDE/src/INS.CDE.Backend/Controllers/CDE/CDEAuditController.cs

  • Table: CDE có bảng CDEActivityLogs riêng (không dùng chung [Log].[audit_log])
  • Entity fields: ProjectId, DocumentId, EntityType, EntityId, EntityCode, Action, Details, IPAddress, UserAgent
  • Endpoints:
    • GET /api/cde/audit/logs - Lấy logs với filters (project, document, user, action, date range)
    • GET /api/cde/audit/documents/{id}/logs - Logs theo document
    • GET /api/cde/audit/stats - Statistics cho dashboard

Module Config

// ModuleOptions.cs
public bool EnableAuditLogging { get; set; } = true;

Note

Modules có thể: (1) Sử dụng system-level audit log qua LogService/INS_AuditLogger → ghi vào [Log].[audit_log], HOẶC (2) Có bảng audit log riêng (như CDE) cho domain-specific tracking.


5. DI Registration Summary

INS.WASM.AUTH/Program.cs

// Infrastructure
builder.Services.AddScoped<IRequestContextAccessor, RequestContextAccessor>();
builder.Services.AddSingleton<IRollbackSqlGenerator, RollbackSqlGenerator>();

// Publisher
builder.Services.AddScoped<IAuditLogPublisher, AuditLogPublisher>();

// Consumers (BackgroundServices)
builder.Services.AddHostedService<AuditLogConsumerService>();
builder.Services.AddHostedService<AppLogConsumerService>();
builder.Services.AddHostedService<ActivityLogConsumerService>();

Middleware Pipeline Order

app.UseCorrelationId();        // 1. Generate/inject CorrelationId
app.UseActivityLogging();      // 2. Auto-capture HTTP → Activity Log
// ... Authentication, Authorization
// ... Controllers, gRPC endpoints

Frontend (Blazor WASM)

services.AddINSLogging();      // → AddScoped<LogService>()
services.AddScoped<INS_AuditLogger>();

6. Checklist để tái tạo chức năng

  • Shared Models: Tạo RabbitMQAuditLogDtoAuditLogMessage
  • Proto file: Định nghĩa logging.proto với AuditLogRequest message
  • gRPC Service: Implement LoggingGrpcService nhận request, map → DTO, publish RabbitMQ
  • RabbitMQ Core: Setup IRabbitMQService với SendAsync()CreateConsumer<T>() fluent API
  • CorrelationIdMiddleware: Inject/generate X-Correlation-ID mỗi request
  • IRequestContextAccessor: Scoped service chứa request context (user, IP, correlation, URL)
  • RollbackSqlGenerator: Generate rollback SQL dựa trên operation type
  • AuditLogPublisher: Publisher enriches message từ context, generate rollback, publish RabbitMQ
  • BaseRabbitMqConsumerService: Abstract BackgroundService với lazy init + retry logic
  • AuditLogConsumerService: Consume message → validate → map → EF Core INSERT
  • AuditLog Entity: EF Core entity map tới [log].[audit_log] table
  • Database Schema: Create table với indexes trên changed_at, changed_by, correlation_id, table_name
  • LogService: Frontend HTTP client bridge (POST → Module Controller → gRPC)
  • INS_AuditLogger: Blazor component wrapper (serialize old/new → LogService)
  • DI Registration: Register tất cả services với correct lifetimes
  • Configuration: appsettings.json cho RabbitMQ exchange/queue/routing keys