TCIS.Logging 1.0.9

dotnet add package TCIS.Logging --version 1.0.9
                    
NuGet\Install-Package TCIS.Logging -Version 1.0.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TCIS.Logging" Version="1.0.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TCIS.Logging" Version="1.0.9" />
                    
Directory.Packages.props
<PackageReference Include="TCIS.Logging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TCIS.Logging --version 1.0.9
                    
#r "nuget: TCIS.Logging, 1.0.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TCIS.Logging@1.0.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TCIS.Logging&version=1.0.9
                    
Install as a Cake Addin
#tool nuget:?package=TCIS.Logging&version=1.0.9
                    
Install as a Cake Tool

TCIS.Logging

The core library providing foundational abstractions and infrastructure for the Logging system across all TCIS applications.

Collector transport supports the existing ClientSecret mode and opt-in ClientCertificate mode with a provisioned certificate-store key and mandatory HTTPS. See device certificate configuration and migration.

🌟 Overview

TCIS.Logging serves as the standard logging framework, integrated with IWorkContext to capture user and trace information, while utilizing a Channel mechanism for high-performance, asynchronous (non-blocking) logging.

📦 Installation

Install the package via NuGet or .NET CLI:

dotnet add package TCIS.Logging

⚙️ Configuration (appsettings.json)

You can configure the queue limit for the Background Logging Process:

{
  "TLogSettings": {
    "MaxQueueSize": 10000,
    "SiteCode": "HP9"
  }
}

SiteCode (tuỳ chọn) là nhãn application.site_code dự phòng cho bản ghi không có WorkContext — consumer Kafka/Wolverine, hosted service, log của framework. Nó không nạp vào WorkContext và không ảnh hưởng cách ly dữ liệu; WorkContext.Tenant.SiteCode luôn thắng khi có giá trị. Pod chạy nhiều site thì không đặt. Biến môi trường tương ứng: TLogSettings__SiteCode.

Giới hạn dung lượng data.parameters / data.result

{
  "TLogSettings": {
    "ParameterSize": 2048,
    "ResponseSize": 1000,
    "OversizePreview": {
      "Enabled": true,
      "MaxArrayItems": 3
    }
  }
}
Khoá Mặc định Ý nghĩa
ParameterSize 2048 Trần byte của data.parameters, mỗi lời gọi. Vượt trần thì giữ preview đã mask; <= 0 dùng 2048.
ResponseSize 1000 Trần byte của data.result, gồm cả preview/summary; <= 0 dùng 2048.
OversizePreview.Enabled true Tự giữ preview khi payload vượt giới hạn; false giữ hành vi bỏ cả khối và ghi _warning như trước.
OversizePreview.MaxArrayItems 3 Giữ N item đầu tiên của mỗi mảng, kể cả mảng lồng. <= 0 dùng 3, tối đa 256.

Hai ngưỡng này độc lập nhau; hạ ParameterSize không ảnh hưởng ResponseSize.

Payload nhỏ giữ nguyên shape. Payload lớn được ghi dưới { "payload": ..., "_log_summary": ... }, giữ các field cùng item/object, không chuyển thành các mảng riêng theo field. Metadata ghi số item/field bị lược bỏ; nếu N item vẫn quá trần byte, preview tiếp tục rút gọn để giữ JSON hợp lệ trong giới hạn. Collection trả về ở gốc có trên 100 item chỉ được lấy N item đầu để log, không duyệt toàn bộ; khi đó ghi original_item_count thay vì đoán original_size_bytes. Whitelist và ResultFilterMaxItems vẫn có hiệu lực. ResponseSummary.Enabled=true tiếp tục dùng chế độ summary ưu tiên item lỗi đã có; overflow preview mặc định giữ thứ tự đầu vào.

Trong kết quả tìm kiếm Elasticsearch, phần fields luôn trả giá trị lá thành mảng, kể cả scalar. Để xem cấu trúc object/mảng gốc và quan hệ field của từng item, xem _source, không ghép item theo vị trí trong fields.

Chỉ ghi một số field của data.result (whitelist)

Mặc định data.result ghi cả đối tượng trả về. Muốn chỉ giữ vài field:

{
  "TLogSettings": {
    "ResultIncludeFields": ["id", "code", "cd"],
    "ResultIncludeFieldsByMethod": {
      "OrderService": ["id", "status"],
      "OrderService.Get": ["id"],
      "Repo`1.Get": ["*"]
    },
    "ResultFilterMaxItems": 1000
  }
}
Khoá Mặc định Ý nghĩa
ResultIncludeFields [] Whitelist toàn cục theo pattern tên field. Rỗng, chỉ có mục trắng hoặc chứa mục "*" = ghi hết sau masking. Pattern không rỗng nhưng chuẩn hóa thành rỗng (ví dụ "_") vẫn fail-closed.
ResultIncludeFieldsByMethod {} Ghi đè theo lớp hiện thực hoặc Class.Method; chấp nhận pattern như *Service, OrderService.Get*. Ưu tiên method chính xác > method pattern > class chính xác > class pattern > global. Pattern cụ thể hơn được ưu tiên; nếu bằng nhau, so khóa theo thứ tự ordinal, không phụ thuộc thứ tự cấu hình. Giá trị rỗng/["*"] cho phép mọi field của rule đó.
ResultFilterMaxItems 1000 Khi lọc đang áp và result là collection có Count vượt ngưỡng thì không serialize, ghi _warning "Filter skipped" (chốt chặn CPU/bộ nhớ).

Quy tắc khớp:

  • Duyệt vét cạn mọi tầng object/list lồng nhau tới tận lá (object → list → object → list…), với N phần tử bất kỳ; không dừng ở tầng 1 hay ở phần tử đầu.
  • Tên field so qua chuẩn hoá: user_id, userId, UserId tương đương, không phân biệt hoa thường.
  • Pattern dùng * cho không hoặc nhiều ký tự: id khớp đúng id/Id, *id khớp hậu tố (order_id), id* khớp tiền tố (id_code), *id* khớp chuỗi ở giữa (middle_id_extra). Có thể kết hợp order*id. Đây là pattern tên, không phải regex, JSONPath hoặc cú pháp SQL; % và ? không có ý nghĩa wildcard.
  • Field khớp được giữ nguyên cả giá trị: nếu nó là object/list thì giữ nguyên cả cây bên trong.
  • Field không khớp mà chứa object/list thì đi sâu tiếp; nhánh không còn field nào bị bỏ. Phần tử list không có field nào khớp bị bỏ khỏi list; phần tử scalar trong list bị bỏ.
  • Che dữ liệu nhạy cảm luôn thắng: password/token… vẫn thành *** kể cả khi nằm trong field được giữ.
  • Lọc chạy trước ResponseSize và trước ResponseSummary (whitelist chạy trước; ResponseSummary.ExcludedFields là blacklist chạy sau, áp trên phần đã giữ). Không còn field nào khớp thì data.result bỏ trống.
  • Chỉ áp cho TLogInterceptor (AOP, package TCIS.Logging.Autofac). TLogWriter, ControllerLoggingFilter, TGrpcClientInterceptor/TGrpcServerInterceptor chưa lọc.
  • Kết quả được cache theo phương thức của lớp hiện thực lúc gọi lần đầu; đổi cấu hình lúc đang chạy không có hiệu lực với phương thức đã gọi.

Pattern dùng chung cho include/exclude, class và function

Quy tắc trung tính áp dụng cho từng danh sách: không khai báo, [], chỉ có mục trắng hoặc có mục "*" thì danh sách đó không giới hạn log. Với exclude, ["*"] không loại gì; ["*", "OrderService"] cũng bỏ qua danh sách exclude đó. *id* là pattern lọc bình thường, khác với mục "*" đứng độc lập. Các danh sách exclude khác và masking vẫn có hiệu lực.

{
  "TLogSettings": {
    "ResultIncludeFields": ["*id", "code*"],
    "ResultIncludeFieldsByMethod": {
      "OrderService.Get*": ["id", "status"],
      "*Repository": ["*id"]
    },
    "ExcludeClasses": ["*Health*"],
    "ExcludeMethods": ["GetCache*", "*.Map*"],
    "ExcludeAssemblies": ["Legacy.*"],
    "ResponseSummary": {
      "Enabled": true,
      "ExcludedFields": ["*token*", "private*"]
    },
    "FailureCapture": {
      "InputIncludeFields": ["*id", "code*"],
      "ExcludedFields": ["*token*"]
    }
  }
}

Các field include/exclude của capture và summary dùng cùng matcher; field bị exclude không được đưa lại vào giá trị/độ dài trong validation evidence. Exclude class/function vẫn chỉ áp dụng ở các boundary vốn đã hỗ trợ policy đó (AOP, gRPC, owner của capture); không tự tắt mọi access log HTTP. Class/function/assembly khớp không phân biệt hoa thường nhưng không bỏ _/- như tên field. Function có thể viết riêng Get* hoặc kèm class OrderService.Get*; assembly dùng được cả dạng có/không .dll.

AopSettings.IncludedSuffixes/ExcludedSuffixes và Pluggable logging cũng hỗ trợ pattern class. Giá trị thường như Service giữ nghĩa hậu tố cũ; Service* khớp đầu tên class. Rỗng/* bỏ giới hạn của danh sách tương ứng trong namespace đã chọn. Logger/capture infrastructure và collection không bị proxy. TargetNamespaces, DiRegistrationSuffixes, các field ưu tiên/phân loại của summary, SensitiveProps và các selector boundary FailureCapture.*.Names giữ nguyên contract riêng; các selector capture rỗng vẫn không capture.

Số điện thoại và mã nghiệp vụ

Masking theo nội dung nhận diện số điện thoại Việt Nam đứng độc lập (0…/+84…). Dãy số nằm liền trong mã chữ–số, ví dụ TCI0912340001, được giữ nguyên; quy tắc áp dụng chung, không phụ thuộc site, topic hay tên field. Field nhạy cảm như phone, mobile vẫn được che theo tên. Chuỗi trong JSON lồng và sau escape xuống dòng/tab vẫn được kiểm tra số điện thoại.

Field ra [MASKING_FAILED]

Bước che theo nội dung (JWT, Bearer, email, số điện thoại…) dùng regex có timeout 1 giây đo bằng đồng hồ thật. Khi che một field thất bại, field đó bị thay bằng [MASKING_FAILED] (fail-closed: thà mất giá trị còn hơn lộ dữ liệu). Timeout được thử lại một lần: tiến trình bị treo tạm thời (GC, CPU throttle, thread pool đói) làm cả chuỗi ngắn cũng timeout, lần thử lại sẽ qua; regex thật sự chạy quá lâu sẽ timeout lần nữa và vẫn fail-closed. Gặp [MASKING_FAILED] thì tìm bản ghi WARN [log-record-masker] Mask: masking failed cùng service để xem loại lỗi.

data.result ra _warning: [ERROR: Invalid JSON after masking]

Result được serialize thành JSON rồi che bằng regex trên chuỗi đã serialize, sau đó parse lại. Một string chứa JSON (vd ApplicationConfigDto.value của gRPC GetByKey) có " lồng thành " và xuống dòng thành \n; các regex che phải nhận ra các escape này. Trước đây regex email nuốt u0022/n làm phần local rồi thay bằng *, để lại \* — escape không hợp lệ — nên cả result bị thay bằng _warning này; cùng lúc regex che theo tên khóa không nhận " nên Password/ApiKey trong JSON lồng không được che. Cả hai đã sửa (test EscapedJsonContentMaskingTests). Còn gặp _warning này thì có thêm một regex mới tạo ra escape hỏng: thêm ca vào test đó.

Chẩn đoán treo (LogInvocationStart, SlowCallThreshold)

Log AOP (TLogInterceptor) chỉ ghi khi Proceed() trả về (trong finally). Một lời gọi treo — chờ DB, lock, HTTP không timeout — vì thế không để lại bản ghi nào. Hai tuỳ chọn trong TLogSettings bù chỗ này:

{
  "TLogSettings": {
    "LogInvocationStart": true,
    "InvocationStartLevel": "INFO",
    "SlowCallThreshold": "00:00:30"
  }
}
Khoá Mặc định Ý nghĩa
LogInvocationStart false Ghi bản ghi "{Class}.{Method} started" trước Proceed(): không có data, event.outcome: "unknown", cùng event.id với bản ghi kết thúc.
InvocationStartLevel "DEBUG" Mức của bản ghi started. Mặc định DEBUG sẽ bị admission loại khi min level là INFO (mặc định của lib) — hoặc hạ min level bằng Dynamic Log Level, hoặc đặt "INFO" khi cần điều tra mà không muốn hạ cả hệ thống.
InvocationSuccessLevel "INFO" Mức của bản ghi kết thúc một lời gọi thành công. Lỗi vẫn ghi WARN/ERROR theo loại, cảnh báo chậm vẫn WARN. Đặt "DEBUG" cho host gọi lặp cùng một việc (worker quét lại hàng chờ): bản ghi thành công dưới min level không được dựng, nên tiết kiệm cả serialize. Phải là TRACE…FATAL — sai thì khởi động lỗi (ValidateOnStart).
InvocationResultLevelSelector null Option đăng ký bằng code: dự án tự đọc result gốc và chọn level cao hơn cho chính bản ghi AOP kết thúc. Không tự nhận diện IsSuccess/error_code; không đổi chính sách exception.
SlowCallThreshold null (tắt) Lời gọi chạy quá ngưỡng mà chưa kết thúc ⇒ một bản ghi WARN "{Class}.{Method} still running after {ms}ms", event.outcome: "unknown", event.duration = ms đã trôi, cùng event.id với bản ghi kết thúc. <= 0 cũng là tắt.

Để nâng level theo DTO của dự án, đăng ký sau/bên cạnh cấu hình logging bằng PostConfigure:

using Microsoft.Extensions.DependencyInjection;
using TCIS.Logging.Models;

builder.Services.PostConfigure<TLoggingOptions>(options =>
{
    options.InvocationResultLevelSelector = result => result switch
    {
        SyncSmartHubResponseDto { IsSuccess: false } => "ERROR",
        _ => null
    };
});

SyncSmartHubResponseDto ở ví dụ trên là DTO của project sử dụng; thư viện không phụ thuộc DTO này. Handler tiếp tục return result như cũ; rule chỉ nâng level trên bản ghi hoàn tất hiện có, không sinh thêm một log riêng. Không đăng ký rule thì result nghiệp vụ lỗi vẫn giữ hành vi mặc định. Rule nhận result gốc, kể cả property có [JsonIgnore]. null, level không hợp lệ hoặc thấp hơn InvocationSuccessLevel giữ level cũ; callback ném lỗi cũng không làm hỏng xử lý nghiệp vụ. Các mức hợp lệ là TRACE, DEBUG, INFO, WARN, ERROR, FATAL (chuẩn hóa hoa/thường/khoảng trắng). Rule được gọi một lần trên completion bình thường, trước minimum-level admission; minimum level vẫn có hiệu lực với mức sau nâng. Với void/Task không có result, callback nhận null. Viết callback nhanh, không side effect, an toàn khi chạy đồng thời. Exception vẫn đi theo classifier hiện tại; option không tự đổi event.outcome, loại event hay tạo exception/error metadata. Option chỉ áp dụng TLogInterceptor (Autofac AOP); không đổi level của log do framework/HTTP/EventBus tự phát. Không thể khai báo delegate này bằng biểu thức trong appsettings.json.

Cơ chế và giới hạn:

  • Một sweeper dùng chung (một Timer, chu kỳ 1 giây, tạo lười) quét các lời gọi đang chạy — độ phân giải ~1 giây, không phải mili giây; không tạo Timer cho từng lời gọi. WARN bắn đúng một lần, không lặp theo bội số ngưỡng.
  • Ngữ cảnh (WorkContext, Activity) được chụp trên thread gọi lúc bắt đầu nên WARN vẫn mang đúng transaction/user dù sweeper chạy ở thread khác. WARN không bắn sau khi hàm đã hoàn tất; hàm xong trước ngưỡng thì không có WARN.
  • Hàm chạy lâu một cách hợp lệ (job, export) sẽ sinh WARN: loại chúng bằng ExcludeClasses/ExcludeMethods (đường bị loại không đăng ký sổ đang chạy). Khuyến nghị bật SlowCallThreshold cho API đồng bộ, không bật toàn cục mù quáng.
  • Cách đọc log: thấy WARN ... still running mà chưa có bản ghi kết thúc cùng event.id ⇒ lời gọi đã vào hàm và đang treo. Phạm vi: cả ba đường — hàm đồng bộ, Task, Task<T>; chỉ TLogInterceptor (controller/gRPC không có).
  • Giới hạn khi thread pool cạn: sweeper chạy trên thread pool. Đúng lúc thread pool bị bão hoà (thường đi kèm treo) callback có thể bị trễ hoặc không chạy, khi đó không có WARN — không có WARN chưa chứng minh là không treo.
  • event.id dùng chung cho started/WARN/kết thúc. Nếu pipeline phía sau (Filebeat/Logstash/ES ingest) đặt _id hoặc dedupe theo event.id, bản ghi kết thúc sẽ đè bản ghi treo. Thư viện không dùng event.id làm khoá chống trùng (dùng RecordId riêng cho spool/biên nhận). Đã kiểm trên Kibana UAT ngày 21/10/2026 (data view tcis-logs*, gồm các service eBerth): _id khác event.id ở mọi bản ghi lấy mẫu, nên ở đó không bị đè. Kết quả này phụ thuộc cấu hình pipeline của từng môi trường — môi trường khác (production, cụm khác) thì kiểm lại bằng cách so _id với event.id của một bản ghi trong Discover (tab JSON), hoặc bật thử một service rồi tìm theo event.id: thấy 2 bản ghi (started + kết thúc) là không bị đè.

Điều tra nhanh một service nghi bị treo ở Proceed()

Cấu hình đọc lúc khởi động (IOptions): đổi xong phải restart service đó. Mỗi service có appsettings riêng nên chỉ bật cho service đang nghi ngờ.

{
  "TLogSettings": {
    "SlowCallThreshold": "00:00:10"
  }
}
  1. Bước 1 — chỉ cần SlowCallThreshold (ghi WARN nên không phải hạ min level, chi phí thấp). Thấy WARN ... still running mà chưa có bản ghi kết thúc cùng event.id ⇒ lời gọi đã vào hàm và đang treo; kết luận "API chưa được gọi" là sai. Hàm chạy lâu hợp lệ gây ồn thì thêm vào ExcludeMethods/ExcludeClasses.
  2. Bước 2 — nếu cần biết mọi lời gọi có vào hàm hay không, thêm "LogInvocationStart": true và "InvocationStartLevel": "INFO" (hoặc giữ DEBUG và hạ min level bằng Dynamic Log Level). Nặng hơn: mỗi lời gọi được log thêm một bản ghi, chỉ bật trong lúc điều tra rồi tắt.
  3. Điều tra xong: bỏ hai khoá trên và restart.

🚀 Usage

In Program.cs, add the Logging service to the DI container:

using TCIS.Logging.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Method 1: Read from IConfiguration (default section: "TLogSettings")
builder.Services.AddTLogging(builder.Configuration);

// Method 2: Configure via Action
builder.Services.AddTLogging(options => 
{
    options.MaxQueueSize = 5000;
});

// (Optional) Add Console Sink to display formatted logs in Development
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddTCISConsoleSink();
}

Ngữ cảnh của phạm vi chẩn đoán trên bản ghi lỗi (labels)

TExceptionLogScope bỏ trùng theo tầng: một lỗi chỉ báo một lần, ở tầng thấp nhất (ví dụ port được bọc AOP); bản ghi của tầng ngoài bị bỏ. Muốn bản ghi còn lại vẫn cho biết nó thuộc tầng ngoài nào, tầng ngoài mở phạm vi mang ngữ cảnh:

using var scope = TExceptionLogScope.BeginOperation(new Dictionary<string, string>
{
    ["pipeline_step"] = "CheckRulesStep",
});

Mọi bản ghi lỗi ghi bên trong — ở bất kỳ độ sâu nào — nhận ngữ cảnh của mọi phạm vi bao nó làm ECS labels (phạm vi trong thắng khi trùng khoá; nhãn người gọi tự đặt không bị ghi đè). Không mở phạm vi mang ngữ cảnh ⇒ không đổi gì. Bản ghi thành công không đổi. PipelineEngine dùng cơ chế này cho từng Step (labels.pipeline_step, pipeline_step_key, pipeline_site_code) — RFC-0007.

💡 Basic Example

Use ILogWriter (do not use ILogger directly from Microsoft Logging to comply with TCIS rules) in your services:

using TCIS.Logging.Abstractions.Abstractions;

public class MyService
{
    private readonly ILogWriter _logger;
    private const string Provider = "my-service";

    public MyService(ILogWriter logger)
    {
        _logger = logger;
    }

    public async Task DoWorkAsync()
    {
        _logger.LogInformation(
            $"[{Provider}] DoWorkAsync: Processing data...", 
            nameof(DoWorkAsync), 
            new { Step = 1, Data = "Sample" }
        );
    }
}

Gửi log về Log Collector

AddTLogCollector (namespace TCIS.Logging.Extensions) thêm hai thành phần, độc lập với nhau:

  1. Outbox (CollectorOutboxSink) — mỗi bản ghi (mọi level) được ghi một dòng NDJSON xuống đĩa; đây là ranh giới bền.
  2. Shipper (CollectorShipper, hosted service) — đọc outbox theo lô và POST {Endpoint}/api/logs/{TenantId}/{Group}/batch với header X-Client-Id, X-Client-Secret (URL phân biệt hoa thường). Không chạy trên UI/request thread.
services.AddTLogCollector(configuration);                       // section "LogCollector"
services.AddTLogCollector(o => { o.Endpoint = new Uri("https://logcollector.example/"); /* ... */ });
{
  "LogCollector": {
    "Endpoint": "https://logcollector.example/",
    "TenantId": "<tenant-id>",
    "Group": "<group>",
    "ClientId": "<client-id>",
    "ClientSecret": "<client-secret>"
  }
}
Khoá (LogCollectorOptions) Mặc định Ghi chú
Endpoint, TenantId, Group, ClientId, ClientSecret — Bộ 5 khoá gửi. Vắng cả 5 ⇒ shipper chờ, không báo lỗi. Điền một phần hoặc sai ⇒ một bản ghi CONFIG_LOG_COLLECTOR* cục bộ, shipper dừng tới khi đúng. Nếu một phần đến từ DB, đừng để sót trong appsettings: bỏ hẳn section LogCollector và nạp đủ qua ConfigureCollector, hoặc chấp nhận một bản ghi CONFIG lúc khởi động
AllowInsecureHttp false http:// chỉ hợp lệ khi bật. Secret đi trong header ⇒ chỉ dùng cho POC, chuyển https:// khi collector có TLS
OutboxDirectory host chọn Desktop: %LOCALAPPDATA%\TCIS\{ServiceName}\outbox. Chốt lúc khởi động
OutboxMaxBytes 50 MiB Đầy ⇒ bỏ bản ghi cũ nhất, mức thấp nhất trước; ERROR/FATAL giữ tới cùng. Chốt lúc khởi động
MaxBatchItems / MaxBatchBytes / MaxRecordBytes 1000 / 4 MiB / 64 KiB Giới hạn lô và từng bản ghi
RequestTimeout / MaxBackoff / AuthFailureRetry 30 s / 5 phút / 15 phút RequestTimeout tối đa 10 phút (lớn hơn ⇒ CONFIG_LOG_COLLECTOR). Backoff lũy thừa có jitter với 429/503/lỗi mạng, tôn trọng Retry-After (xem dưới); 401/403 dừng gửi tới chu kỳ thử lại AuthFailureRetry (không theo Retry-After)
ShutdownSendBudget 3 s Ngân sách gửi nốt khi host dừng (mọi loại host — xem "Dừng host" dưới). 0 ⇒ không gửi lúc dừng
BatchLinger 10 s Gom lô: khi outbox đã gửi hết, bản ghi mới chờ tối đa chừng này rồi mới POST. 0 ⇒ gửi ngay như trước. Âm / > 1 giờ ⇒ CONFIG_LOG_COLLECTOR
MinBatchItems 200 Đủ chừng này bản ghi chờ ⇒ POST ngay, không chờ hết BatchLinger. 0 ⇒ bỏ ngưỡng số lượng. Âm ⇒ CONFIG_LOG_COLLECTOR
MinBatchInterval 5 s Khoảng cách tối thiểu giữa hai POST khi đang xả backlog. 0 ⇒ gửi liền như trước. Âm / > 1 giờ ⇒ CONFIG_LOG_COLLECTOR

Giảm tải collector khi nhiều máy cùng gửi

Collector có hàng đợi bounded (đầy ⇒ 429). Ba cơ chế ở shipper giảm số request mà không đổi thứ tự, checkpoint theo byte-offset hay at-least-once:

  • Gom lô (BatchLinger, MinBatchItems). Máy ít log không còn POST 1–2 bản ghi mỗi 2 s: sau khi outbox đã gửi hết, lần POST kế tiếp chỉ xảy ra khi bản ghi chờ đầu tiên đã đợi đủ BatchLinger, hoặc đủ MinBatchItems bản ghi, hoặc byte chờ đạt 80 % MaxBatchBytes. Số liệu "chờ bao lâu / bao nhiêu" do outbox đếm lúc ghi (chính xác cho bản ghi của process hiện tại; segment của process/phiên khác trong cùng thư mục được nhặt bằng một lần đọc outbox mỗi BatchLinger). Backlog có sẵn lúc khởi động (phiên trước, sau sự cố) gửi ngay.
  • ERROR/FATAL là khẩn. Có bản ghi ERROR/FATAL được ghi vào outbox (qua ILogWriter hay ILogger/Serilog) ⇒ POST trong ~2 s như trước; lô gồm cả các bản ghi đứng trước nó (thứ tự outbox giữ nguyên).
  • Dừng host / crash bỏ qua gom lô. Host dừng thì shipper gửi nốt ngay trong ShutdownSendBudget, không chờ linger, không giãn nhịp (chi tiết ở "Dừng host" dưới). Đường crash đồng bộ chỉ đưa bản ghi xuống đĩa (fsync); bản ghi FATAL là khẩn nên shipper gửi ngay nếu process còn sống, nếu không thì gửi ngay ở lần khởi động sau (backlog).
  • Giãn nhịp xả backlog (MinBatchInterval). Sau một lô 202 mà outbox vẫn còn, shipper chờ MinBatchInterval rồi mới claim lô kế tiếp — sau sự cố, cả đội máy xả backlog lô 1 000 bản ghi không còn bắn liên tục. Với {siteCode}, các đoạn khác đích trong cùng một lô đã claim vẫn gửi liền; chỉ giãn giữa các lần claim.
  • Retry-After. Phản hồi đang đi backoff (429, 503, 5xx, 202 từ chối một phần…) có Retry-After (số giây hoặc HTTP-date) ⇒ chờ max(backoff lũy thừa, Retry-After × (1 + 0–20 %)) — jitter chỉ lệch lên để không thử lại sớm hơn yêu cầu mà cả đội vẫn không dồn về một mốc; phần Retry-After kẹp trần 30 phút. Giá trị hỏng/âm/đã qua ⇒ backoff thường. Mỗi lần có Retry-After để lại một dòng chẩn đoán shipper (không có secret).

Dừng host: gửi nốt outbox

Shipper là hosted service: khi host dừng (generic host / ASP.NET Core / worker, IIS qua TLoggingHost.RunFramework, desktop qua Dispose), StopAsync của nó:

  1. dừng vòng lặp nền;
  2. chờ pipeline xả vào outbox mọi bản ghi đã vào hàng đợi tới lúc đó (vd. ghi trong ApplicationStopping) — tối đa nửa ngân sách. Host dừng hosted service theo thứ tự ngược đăng ký; processor của TCIS.Logging thường dừng sau shipper (adapter ASP.NET Core và host IIS/desktop luôn đặt nó đầu danh sách), nên lúc shipper dừng hàng đợi có thể vẫn còn bản ghi;
  3. gửi nốt phần còn lại của outbox trong ngân sách còn lại, bỏ qua BatchLinger/MinBatchInterval.

Tổng thời gian ≤ ShutdownSendBudget và dừng ngay khi host hết ShutdownTimeout (token của host bị huỷ); không bao giờ ném. Chưa cấu hình gửi, hoặc đang bị chặn (401/403, Retry-After) ⇒ không chờ, không gửi. Collector chết/treo ⇒ trả về trong ngân sách. Phần chưa gửi nằm yên trên đĩa, gửi ở lần chạy sau, kèm một dòng chẩn đoán [CollectorShipper] Stop: ….

Giới hạn: bản ghi sinh sau lúc shipper dừng — log lúc dừng của hosted service dừng sau nó (đăng ký trước nó), request ASP.NET Core còn đang xử lý khi shipper đã dừng, dòng "stopped" của chính processor — nằm lại outbox cho lần chạy sau. Host desktop gửi thêm một lần sau khi mọi hosted service đã dừng (chung ngân sách); generic host/IIS thì không. Muốn log lúc dừng của hosted service app cũng được gửi nốt: đăng ký theo thứ tự AddTLogging → AddTLogCollector → AddHostedService của app (IIS: gọi AddTLogCollector trong callback của RunFramework là đã đúng thứ tự này). Container / pod có ổ tạm (outbox mất khi xoá container): đặt OutboxDirectory lên volume bền, hoặc giảm BatchLinger (vd. 00:00:02) để lượng bản ghi đang gom lô lúc dừng nhỏ.

Phía gateway (Kong trước collector), nên bật thêm plugin rate-limiting theo consumer (mỗi client collector một consumer) trả 429 kèm Retry-After: một máy hỏng/vòng lặp log không chiếm hết hàng đợi collector của cả đội, còn shipper tự lùi đúng thời gian gateway yêu cầu.

Đích theo site của từng bản ghi — {siteCode}

TenantId và Group nhận placeholder {siteCode} (không phân biệt hoa thường; toàn bộ hoặc một phần giá trị, vd "desktop-{siteCode}"). Ví dụ client collector riêng của TTOS — POST {Endpoint}/api/logs/ttos/{siteCode}/batch:

"LogCollector": { "Endpoint": "http://collector-host", "TenantId": "ttos", "Group": "{siteCode}", "ClientId": "ttos", "ClientSecret": "" }

(http:// cần thêm "AllowInsecureHttp": true; secret không bao giờ ghi vào tài liệu/repo.)

  • Giá trị thay = custom.application.site_code của chính bản ghi trong outbox (bản ghi có thể nằm từ phiên trước, nên không dùng WorkContext lúc gửi). Bản ghi không có site / rỗng ⇒ TLogSettings:SiteCode hiện hành ⇒ "unknown". Ký tự không hợp lệ trên URL được escape (Uri.EscapeDataString). Collector so khớp tenant/group phân biệt hoa thường.
  • Một lô HTTP chỉ chứa các bản ghi liên tiếp cùng đích tính từ checkpoint; gặp bản ghi đích khác thì đoạn kết thúc, đoạn sau gửi đích mới — không sắp xếp lại, không bỏ qua nên thứ tự và at-least-once giữ nguyên. Site xen kẽ ⇒ lô nhỏ hơn.
  • Placeholder khác ({foo}) hoặc ngoặc nhọn lẻ ⇒ CONFIG_LOG_COLLECTOR_PLACEHOLDER, shipper chờ tới khi sửa.
  • Không có placeholder ⇒ hành vi cũ, một URL tĩnh.
  • Collector trả 403 khi client đã xác thực nhưng tenant/group không có trong Tenants hoặc Clients:Allowed của nó ⇒ một WARN SEC_LOG_COLLECTOR_DESTINATION_DENIED nêu đích (vd 'ttos/HP9'), một lần cho mỗi đích. Gửi dừng toàn bộ AuthFailureRetry rồi thử lại (bản ghi đích khác phía sau cũng chờ — không bỏ, không chuyển đi); cấp quyền đích đó phía collector là gửi tiếp. Mỗi site TTOS có thể chọn phải có trong Tenants/Allowed của client.

Cấu hình có thể đến muộn: ICollectorConfiguration.Configure(o => …) (lấy từ DI) đè lên section LogCollector; app WinForms dùng TLog.ConfigureCollector (cùng đường này). ICollectorShipperStatus (Sent, Dropped, LastStatusCode, AuthBlocked, ClockSkewMs, Configured) cho chẩn đoán.

Đảm bảo và đánh đổi cần biết:

  • At-least-once. Checkpoint chỉ tiến sau khi collector xác nhận ⇒ lỗi giữa chừng có thể gửi lặp, không mất.
  • Trần outbox tính theo từng process. Nhiều process cùng ghi một thư mục chung có thể cùng vượt trần tạm thời cho tới lần đếm lại kế tiếp; mỗi process chỉ ghi segment của mình, không ghi đè nhau.
  • Nhiều writer chung một thư mục outbox chạy đúng trên cả Windows lẫn Linux/container. Writer giữ khoá độc quyền {segment}.writer suốt đời segment; segment còn writer sống không bao giờ bị process/instance khác xoá hay viết lại khi dọn chỗ. Writer crash để lại file khoá không ai giữ ⇒ coi là đã chết, segment được gửi rồi dọn cùng khoá. Segment được gửi cũ trước theo mốc tạo ghi trong tên — outbox-{session}-{seq}.{yyyyMMddTHHmmssfffffffZ}.ndjson — không theo thời gian file của hệ thống (Linux không có birth time tin được); segment tên cũ outbox-{session}-{seq}.ndjson vẫn được gửi.
  • Mất điện. Mỗi lần ghi chỉ Flush(false) (đã vào cache hệ điều hành — process sập hay Environment.Exit không mất); fsync chạy định kỳ ≤ 1 s, nên mất điện có thể mất tối đa chừng đó dòng cuối. File log cục bộ giữ bản ghi riêng.
  • Throughput bị giới hạn bởi pipeline lõi (~150–200 bản ghi/s mỗi process); hàng đợi hấp thụ đợt tăng đột biến.
  • Gửi bị chặn thì có dấu vết trong file log. 401 (sai/thiếu credentials, client bị khoá) ⇒ một WARN CONFIG_LOG_COLLECTOR_AUTH mỗi đợt bị chặn; 403 (đích không được phép) ⇒ một WARN SEC_LOG_COLLECTOR_DESTINATION_DENIED mỗi đích; không tới được collector liên tục > 10 phút ⇒ một WARN INFRA_LOG_COLLECTOR_UNREACHABLE cho tới khi gửi lại được (cả hai qua pipeline: file cục bộ + outbox). Chi tiết từng lần lỗi chỉ ở chẩn đoán khẩn cấp (stderr; host desktop: logs\diagnostics-*.log).

Capture khi xử lý lỗi

Xem cấu hình FailureCapture và giới hạn để bổ sung input/output đã mask vào log lỗi của queue, HTTP server và HTTP client. Tính năng mặc định tắt và kết hợp các bộ lọc hiện có.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (6)

Showing the top 5 NuGet packages that depend on TCIS.Logging:

Package Downloads
TCIS.AspNetCore

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. ASP.NET Core integration for TCIS Framework.

TCIS.Http

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. HTTP client extensions and utilities for TCIS Framework.

TCIS.Http.Grpc

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. gRPC client and server integration for TCIS Framework.

TCIS.Logging.Autofac

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. Autofac integration for TCIS Logging.

TCIS.Logging.Serilog

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. Serilog integration for TCIS Logging.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.9 117 11/5/2026
1.0.9-rc.2 66 11/5/2026
1.0.9-rc.1 92 11/4/2026
1.0.8 291 11/3/2026
1.0.8-rc.11 127 10/30/2026
1.0.8-rc.10 119 10/30/2026
1.0.8-rc.9 123 10/30/2026
1.0.8-rc.8 121 10/29/2026
1.0.8-rc.7 135 10/27/2026
1.0.8-rc.6 129 10/27/2026
1.0.8-rc.5 124 10/23/2026
1.0.8-rc.4 130 10/23/2026
1.0.8-rc.3 125 10/23/2026
1.0.8-rc.2 126 10/22/2026
1.0.8-rc.1 127 10/22/2026
1.0.7 276 10/22/2026
1.0.6 373 10/20/2026
1.0.6-rc.1 115 10/20/2026
1.0.5 389 10/19/2026
1.0.4 340 10/13/2026
Loading failed