فهرست مطالب
یک درخواست ساده مثل «بستن فاکتور» در سیستمی با چند سرویس، از دروازهٔ API میگذرد، به سرویس اصلی میرسد، آن سرویس از سرویس صورتحساب چیزی میپرسد و شاید پیامی در صف بگذارد. هر کدام لاگ خودشان را مینویسند. وقتی خطا رخ میدهد، در لاگ متمرکز صدها خط از همان ثانیه میبینید که مال دهها درخواست همزمان است. کدام خطها مال همان درخواست ناموفق است؟
جواب، یک شناسهٔ مشترک است که از اولین سرویس ساخته میشود و همراه درخواست به همهٔ سرویسهای بعدی میرود: trace_id. در این نوشته میگوییم این شناسه از کجا میآید، استاندارد W3C برای منتقل کردنش چیست، و چطور در ASP.NET Core، Java و Serilog آن را به همهٔ خطوط لاگ برسانیم.
trace_id و span_id چه هستند
در اصطلاح ردیابی توزیعشده، کل مسیر یک درخواست در همهٔ سرویسها یک trace است و هر بخش از کار (پردازش درخواست در یک سرویس، یک فراخوانی HTTP، یک کوئری) یک span. هر trace یک trace_id سیودو کاراکتری هگز دارد که در کل مسیر ثابت میماند، و هر span یک span_id شانزده کاراکتری دارد که در هر قدم عوض میشود.
برای کنار هم گذاشتن لاگها، trace_id مهمترین است: هر خطی که trace_id یکسان دارد، مال یک درخواست است. span_id برای وقتی است که بخواهید بدانید یک خط دقیقاً در کدام قدم نوشته شده. لازم نیست سیستم کامل ردیابی و نمودار span داشته باشید تا از trace_id در لاگها سود ببرید.
استاندارد W3C: سرآیند traceparent
پیشتر هر ابزار سرآیند خودش را داشت و سرویسهایی که با ابزار متفاوت ساخته شده بودند، شناسهٔ هم را نمیفهمیدند. استاندارد W3C Trace Context این را یکدست کرده است. سرآیند traceparent چهار بخش دارد:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
| | | |
version trace-id parent-id flags
هر سرویس وقتی درخواستی میگیرد، اگر traceparent داشته باشد همان trace-id را ادامه میدهد؛ اگر نداشته باشد trace تازهای میسازد. وقتی خودش سرویس دیگری را صدا میزند، سرآیند را با همان trace-id و span_id خودش بهعنوان parent-id میفرستد. بخش آخر (flags) میگوید trace نمونهبرداری شده یا نه؛ حتی اگر نشده باشد، شناسهها وجود دارند و در لاگ قابل استفادهاند. سرآیند اختیاری tracestate هم برای دادهٔ اختصاصی ابزارهاست.
ASP.NET Core: بیشترش آماده است
در .NET، ردیابی بر پایهٔ کلاس System.Diagnostics.Activity است. از .NET 5 به بعد قالب پیشفرض شناسهها W3C است، ASP.NET Core سرآیند traceparent ورودی را میخواند و HttpClient آن را به درخواستهای خروجی اضافه میکند. برای اینکه Activity برای هر درخواست قطعاً ساخته شود، سادهترین راه افزودن instrumentation در OpenTelemetry است، حتی اگر trace را به جایی نفرستید:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation());
builder.Logging.AddOpenTelemetry(o =>
{
o.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("clinic-api"));
o.AddOtlpExporter(e =>
{
e.Endpoint = new Uri("https://ingest.logmug.ir/v1/logs");
e.Protocol = OtlpExportProtocol.HttpProtobuf;
e.Headers = "x-logmug-key=lm_ingest_…";
});
});
با این پیکربندی، هر لاگی که با ILogger داخل یک درخواست نوشته شود، TraceId و SpanId همان Activity را با خودش دارد و بدون تغییر در کد برنامه به مقصد میرسد.
Java: عامل OpenTelemetry و MDC
در Java سادهترین راه، عامل جاوای OpenTelemetry است که بدون تغییر کد، فریمورکهای رایج HTTP را instrument میکند، traceparent را منتقل میکند و trace_id و span_id را در MDC قرار میدهد. اگر فقط لاگ میفرستید، صادرکنندهٔ trace و متریک را خاموش کنید:
java -javaagent:opentelemetry-javaagent.jar
-Dotel.service.name=billing
-Dotel.traces.exporter=none
-Dotel.metrics.exporter=none
-Dotel.logs.exporter=otlp
-jar billing.jar
نشانی و کلید مقصد با متغیرهای استاندارد OTEL_EXPORTER_OTLP_LOGS_ENDPOINT و OTEL_EXPORTER_OTLP_HEADERS تنظیم میشوند. اگر لاگ فایل یا کنسول هم دارید، شناسه را در الگوی Logback بیاورید تا آنجا هم دیده شود:
<pattern>%d{HH:mm:ss.SSS} %-5level [%X{trace_id}] %logger{36} - %msg%n</pattern>
Serilog: افزودن trace_id با enricher
نسخههای تازهٔ Serilog شناسههای Activity جاری را خودشان روی هر رخداد ثبت میکنند. ولی در برنامههای قدیمیتر، بهخصوص روی .NET Framework، یک enricher کوچک کار را تمام میکند:
using System.Diagnostics;
using Serilog.Core;
using Serilog.Events;
public class TraceIdEnricher : ILogEventEnricher
{
public void Enrich(LogEvent e, ILogEventPropertyFactory f)
{
var a = Activity.Current;
if (a == null) return;
e.AddPropertyIfAbsent(f.CreateProperty("trace_id", a.TraceId.ToHexString()));
e.AddPropertyIfAbsent(f.CreateProperty("span_id", a.SpanId.ToHexString()));
}
}
// هنگام راهاندازی برنامه
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = true;
Log.Logger = new LoggerConfiguration()
.Enrich.With<TraceIdEnricher>()
.WriteTo.Seq("https://ingest.logmug.ir", apiKey: "lm_ingest_…")
.CreateLogger();
دو خط تنظیم قالب مهم است: روی .NET Framework و .NET Core 3.x قالب پیشفرض Activity، W3C نیست و بدون آن شناسهها با سرویسهای دیگر همخوان نمیشوند.
جاهایی که زنجیره پاره میشود
اگر میبینید هر سرویس trace_id جدای خودش را دارد، معمولاً یکی از اینهاست:
- پروکسی یا دروازهای که سرآیند را حذف میکند. بعضی API gatewayها فقط فهرست مشخصی از سرآیندها را عبور میدهند؛
traceparentوtracestateرا به آن اضافه کنید. - صف پیام. سرآیند HTTP خودبهخود وارد پیام صف نمیشود. باید traceparent را در سرآیندهای پیام بگذارید و مصرفکننده آن را بخواند. instrumentation در OpenTelemetry برای برخی کلاینتها این کار را انجام میدهد، برای بقیه با
Propagators.DefaultTextMapPropagatorدستی انجام میشود. - کار پسزمینه. کاری که با
Task.Runبیانتظار یا در یک job زمانبندیشده اجرا میشود، ممکن است Activity والد نداشته باشد. برای jobها یک Activity تازه بسازید تا حداقل خطوط خود job کنار هم باشند. - کلاینت HTTP دستساز. اگر جایی بهجای HttpClient از کتابخانهٔ دیگری استفاده میشود، مطمئن شوید سرآیند را منتقل میکند.
trace_id را به کاربر و پشتیبانی هم برسانید
trace_id فقط برای برنامهنویس نیست. اگر آن را در سرآیند پاسخ برگردانید، فرانتاند میتواند در پیام خطا بهعنوان «کد پیگیری» نشانش دهد و ابزار رصد خطای مرورگر هم آن را کنار خطای شبکه ثبت کند:
app.Use(async (ctx, next) =>
{
ctx.Response.OnStarting(() =>
{
var id = Activity.Current?.TraceId.ToHexString();
if (id != null) ctx.Response.Headers["X-Trace-Id"] = id;
return Task.CompletedTask;
});
await next();
});
اگر فرانت روی دامنهٔ دیگری است، این سرآیند را در Access-Control-Expose-Headers هم بیاورید تا جاوااسکریپت بتواند آن را بخواند. دربارهٔ ثبت خطای شبکه در مرورگر، راهنمای رصد خطای جاوااسکریپت را ببینید.
در لاگماگ: یک کلیک تا همهٔ خطوط یک درخواست
وقتی لاگها trace_id داشته باشند، لاگماگ خطوطی را که trace_id یکسان دارند با یک کلیک کنار هم میآورد؛ از دروازهٔ API تا سرویس صورتحساب و صف پیام. از روی یک خطا میتوانید به «همهٔ لاگهای این درخواست» بروید یا به «همهٔ رخدادهای این خطا» که با اثرانگشت گروه شدهاند. OpenTelemetry و Serilog شناسه را خودشان میفرستند و در ارسال JSON مستقیم، trace_id را بهعنوان یک فیلد بفرستید؛ نمونهها در سایت لاگماگ آمده است.
صریح بگوییم: لاگماگ از trace_id فقط برای کنار هم گذاشتن خطوط استفاده میکند و نمودار زمانبندی span و متریک ندارد؛ APM نیست. اتصال مستقیم از گزارش باگماگ به لاگ بکاند همان درخواست هم در حال ساخت است و امروز در دسترس نیست.
پرسشهای پرتکرار
فرق trace_id با correlation id دستساز چیست؟
از نظر ایده هیچ؛ هر دو شناسهای مشترک برای یک درخواستاند. ولی trace_id استاندارد است: کتابخانهها، پروکسیها و ابزارها آن را میشناسند و خودشان منتقلش میکنند. شناسهٔ دستساز را باید در هر سرویس و هر کلاینت HTTP دستی منتقل کنید و دیر یا زود جایی جا میافتد.
آیا فرستادن traceparent از مرورگر امن است؟
شناسهها تصادفیاند و دادهٔ حساسی ندارند. ولی چون کاربر میتواند هر مقداری بفرستد، به آن برای تصمیم امنیتی تکیه نکنید. برخی تیمها در دروازهٔ API سرآیند ورودی از اینترنت را نادیده میگیرند و trace تازه میسازند.
نمونهبرداری trace روی لاگها اثر دارد؟
نه. حتی وقتی trace نمونهبرداری نشده و flags برابر 00 است، trace_id و span_id ساخته و منتقل میشوند و روی لاگها مینشینند. نمونهبرداری فقط تعیین میکند spanها صادر شوند یا نه.
جمعبندی
trace_id ارزانترین بهبودی است که میتوانید در لاگهای یک سیستم چندسرویسی بدهید. استاندارد W3C traceparent انتقالش را یکدست کرده، ASP.NET Core و عامل جاوای OpenTelemetry بیشتر کار را خودشان انجام میدهند و برای Serilog قدیمی هم یک enricher کوچک کافی است. بعد از آن، فقط باید مراقب جاهایی بود که زنجیره پاره میشود: پروکسیها، صفها و کارهای پسزمینه. اگر هنوز لاگهایتان متمرکز نیست، از نوشتهٔ لاگ متمرکز شروع کنید و برای دیدن نقش trace_id در یک حادثهٔ واقعی، چرخهٔ عمر یک خطا را بخوانید.