OpenTelemetry به زبان ساده: لاگ، trace و یک شناسه برای کل درخواست

OpenTelemetry به زبان ساده: لاگ، trace و یک شناسه برای کل درخواست
در این مقاله می‌خوانید
  1. سه سیگنال: لاگ، trace و metric
  2. trace_id و span_id: رشته‌ای که خطوط را به هم می‌دوزد
  3. W3C traceparent: چطور شناسه از یک سرویس به سرویس بعد می‌رود
  4. SDK، Collector و OTLP: سه قطعه‌ای که باید بشناسید
  5. SDK: درون برنامهٔ شما
  6. OTLP: زبان مشترک
  7. Collector: ایستگاه میانی اختیاری
  8. یک نکتهٔ مهم پیش از کد: فقط لاگ
  9. ⁦.NET⁩: لاگ‌های ILogger با OTLP
  10. Java: عامل OTel بدون تغییر کد
  11. Python: ماژول logging و opentelemetry-instrument
  12. resource: نام سرویس، محیط و میزبان
  13. اشتباه‌هایی که بیشتر از همه دیده‌ام
  14. از trace_id تا جواب: کار با لاگ‌های به‌هم‌پیوسته
  15. منابع و مطالعهٔ بیشتر

اولین باری که یک سیستم را از یک برنامهٔ بزرگ به پنج سرویس کوچک شکستیم، فکر می‌کردم سخت‌ترین بخش کار، طراحی APIها خواهد بود. اشتباه می‌کردم. سخت‌ترین بخش، روزی بود که کاربری گفت «سفارشم ثبت نشد» و ما پنج پنجرهٔ ترمینال باز کردیم، در هر کدام دنبال زمان تقریبی گشتیم و سعی کردیم با چشم حدس بزنیم کدام خط در سرویس پرداخت به کدام خط در سرویس سفارش مربوط است. نیم ساعت طول کشید تا بفهمیم خطا اصلاً در سرویس سومی بوده که هیچ‌کدام به آن شک نکرده بودیم.

مشکل ما کمبود لاگ نبود؛ لاگ زیاد داشتیم. مشکل این بود که هیچ رشته‌ای خطوط یک درخواست را در سرویس‌های مختلف به هم وصل نمی‌کرد. OpenTelemetry دقیقاً برای همین ساخته شده — و البته برای خیلی چیزهای دیگر، که همین گستردگی باعث می‌شود بسیاری از تیم‌ها از دور به آن نگاه کنند و بترسند. در این مقاله سعی می‌کنم آن را از زاویهٔ کسی توضیح دهم که فقط می‌خواهد لاگ‌هایش معنی‌دار و به هم پیوسته باشند.

سه سیگنال: لاگ، trace و metric

OpenTelemetry (که کوتاهش می‌کنند OTel) یک استاندارد باز و مجموعه‌ای از کتابخانه‌هاست برای تولید و ارسال دادهٔ «مشاهده‌پذیری». این داده سه نوع اصلی دارد که در اصطلاح OTel به آن‌ها سیگنال می‌گویند:

  • لاگ (log): یک رویداد با زمان مشخص و متن یا ساختار — «پرداخت سفارش ۸۸۱۲ رد شد چون موجودی کافی نبود». لاگ به سؤال «دقیقاً چه اتفاقی افتاد؟» جواب می‌دهد.
  • trace: مسیر یک درخواست در سیستم. هر trace از چند span ساخته می‌شود؛ هر span یک تکه کار است با زمان شروع و پایان — مثلاً «فراخوانی سرویس پرداخت، ۳۴۰ میلی‌ثانیه». trace به سؤال «این درخواست از کجاها گذشت و وقتش کجا رفت؟» جواب می‌دهد.
  • metric: عددهای تجمیعی در طول زمان — تعداد درخواست در دقیقه، درصد خطا، صدک ۹۹ زمان پاسخ. metric به سؤال «وضع کلی چطور است و از کِی بد شد؟» جواب می‌دهد.

این سه رقیب هم نیستند و جای هم را نمی‌گیرند. metric معمولاً اولین جایی است که می‌فهمید چیزی خراب است، trace نشان می‌دهد کجا، و لاگ می‌گوید چرا. ولی نکته‌ای که در عمل خیلی مهم است و کمتر گفته می‌شود: لازم نیست هر سه را از روز اول داشته باشید. تیم‌هایی را دیده‌ام که ماه‌ها روی راه‌اندازی tracing کامل وقت گذاشتند و هنوز لاگ‌هایشان روی دیسک سرورها پخش بود. اگر فقط یکی را می‌توانید درست کنید، لاگ ساخت‌یافتهٔ متمرکز با یک شناسهٔ درخواست بیشترین بازده را در کوتاه‌ترین زمان دارد.

چیزی که OTel را برای لاگ ارزشمند می‌کند این است که حتی وقتی trace را جایی نمی‌فرستید، مفهوم trace را برای پیوند دادن لاگ‌ها به کار می‌گیرد. این همان بخشی است که بقیهٔ مقاله رویش تمرکز دارد.

trace_id و span_id: رشته‌ای که خطوط را به هم می‌دوزد

وقتی درخواستی وارد اولین سرویس می‌شود، یک trace_id برایش ساخته می‌شود: یک عدد تصادفی ۱۲۸ بیتی که به‌صورت ۳۲ نویسهٔ هگزادسیمال نوشته می‌شود. هر تکه کار درون آن درخواست یک span_id هشت‌بایتی (۱۶ نویسهٔ هگز) دارد. تا وقتی آن درخواست زنده است، هر سرویسی که درگیرش می‌شود همان trace_id را حمل می‌کند و برای کار خودش span_id تازه می‌سازد.

حالا اگر هر خط لاگ، trace_id و span_id جاری را با خودش داشته باشد، اتفاق مهمی می‌افتد: می‌توانید از روی یک خط خطا، همهٔ خطوط دیگرِ همان درخواست را — در همهٔ سرویس‌ها — بیرون بکشید. نه با حدس زمان، نه با grep روی شمارهٔ سفارش، بلکه با یک فیلتر دقیق روی یک مقدار یکتا.

یک نمونهٔ ساده. کاربر دکمهٔ «پرداخت» را می‌زند:

trace_id=4bf92f3577b34da6a3ce929d0e0e4736

[gateway]   span=00f067aa0ba902b7  INFO  POST /api/checkout
[orders]    span=a2fb4a1d1a96d312  INFO  Creating order for cart 5531
[payments]  span=b7ad6b7169203331  WARN  Bank gateway timeout, retrying (1/3)
[payments]  span=b7ad6b7169203331  ERROR Payment failed: gateway timeout
[orders]    span=a2fb4a1d1a96d312  ERROR Order 8812 rolled back

بدون trace_id، این پنج خط در سه جای مختلف و در میان هزاران خط دیگر گم‌اند. با آن، یک کوئری است. ارزش این را فقط کسی کامل می‌فهمد که یک بار نیمه‌شب، زیر فشار، خطوط را با زمان‌شان دستی جفت کرده باشد. تفاوت trace_id با «شناسهٔ همبستگی» (correlation id) دست‌ساز را جداگانه در مقالهٔ trace_id و correlation id باز کرده‌ام؛ خلاصه‌اش این است که trace_id استاندارد است و کتابخانه‌ها خودشان آن را می‌سازند و منتقل می‌کنند، و شما مجبور نیستید در هر نقطهٔ کد به یادش باشید.

W3C traceparent: چطور شناسه از یک سرویس به سرویس بعد می‌رود

trace_id فقط وقتی مفید است که از مرز سرویس‌ها عبور کند. برای این کار باید روی هر درخواست HTTP (یا پیام صف) جایی نوشته شود. سال‌ها هر ابزار هدر خودش را داشت — X-B3-TraceId در Zipkin، uber-trace-id در Jaeger، و هدرهای اختصاصی هر فروشنده — و نتیجه این بود که اگر دو سرویس با دو کتابخانهٔ مختلف ساخته شده بودند، زنجیره وسط راه پاره می‌شد.

استاندارد W3C Trace Context این را یکدست کرد. هستهٔ آن یک هدر است به نام traceparent:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             │  │                                │                │
             │  trace-id (32 hex)                parent-id (16)   flags
             version
  • 00 نسخهٔ قالب است.
  • بخش دوم trace_id است که در کل مسیر ثابت می‌ماند.
  • بخش سوم parent-id است: span_id همان spanی که این درخواست را فرستاده. سرویس گیرنده span خودش را به‌عنوان فرزند آن می‌سازد.
  • بخش آخر پرچم‌هاست؛ 01 یعنی این trace «نمونه‌برداری‌شده» (sampled) است و قرار است ثبت شود.

هدر دومی هم به نام tracestate هست برای دادهٔ اختصاصی فروشنده‌ها که معمولاً لازم نیست به آن دست بزنید. خبر خوب این است که در پلتفرم‌های امروزی تقریباً هیچ‌وقت لازم نیست این هدر را خودتان بخوانید یا بنویسید. در ⁦.NET⁩، کلاس Activity از ⁦.NET 5⁩ به بعد به‌طور پیش‌فرض قالب W3C را به کار می‌برد و HttpClient هدر را منتقل می‌کند؛ عامل جاوای OTel و کتابخانه‌های پایتون هم همین کار را برای چارچوب‌های رایج انجام می‌دهند. به این انتقال خودکار در اصطلاح OTel context propagation می‌گویند.

جایی که باید حواستان باشد، مرزهای غیر HTTP است. اگر سرویس A پیامی در RabbitMQ یا Kafka می‌گذارد و سرویس B آن را برمی‌دارد، trace_id فقط وقتی منتقل می‌شود که instrumentation آن کتابخانهٔ صف فعال باشد یا خودتان traceparent را در هدرهای پیام بگذارید. رایج‌ترین «پارگی زنجیره» که دیده‌ام همین‌جاست: درخواست HTTP به‌خوبی دنبال می‌شود و بعد در اولین صف، trace تازه‌ای شروع می‌شود و رابطه گم می‌شود. همین‌طور پروکسی‌ها و دروازه‌های API قدیمی که فقط هدرهای «شناخته‌شده» را عبور می‌دهند.

SDK، Collector و OTLP: سه قطعه‌ای که باید بشناسید

بیشتر سردرگمی‌هایی که دربارهٔ OTel می‌بینم از قاطی شدن این سه مفهوم می‌آید.

SDK: درون برنامهٔ شما

SDK کتابخانه‌ای است که داخل پروسهٔ برنامه اجرا می‌شود. کارش سه چیز است: ساختن و نگه‌داشتن context (همان trace_id و span_id جاری)، ضمیمه کردن آن به لاگ‌ها و spanها، و فرستادن داده به بیرون از طریق یک exporter. exporter تکه‌ای است که می‌داند داده را با چه پروتکلی و به کجا بفرستد. در کنار SDK معمولاً instrumentation هم نصب می‌کنید: کتابخانه‌هایی که بدون تغییر کد شما، برای چارچوب‌هایی مثل ⁦ASP.NET Core⁩، Spring یا Flask خودکار span می‌سازند.

یک نکتهٔ ظریف: در دنیای لاگ، OTel معمولاً نمی‌خواهد جای کتابخانهٔ لاگ شما را بگیرد. شما همچنان با ILogger یا SLF4J یا ماژول logging پایتون لاگ می‌نویسید؛ OTel یک «پل» (bridge) به آن وصل می‌کند که هر رکورد را با context جاری ترکیب و صادر می‌کند. این یعنی مهاجرت به OTel برای لاگ، بازنویسی کد نیست؛ پیکربندی است.

OTLP: زبان مشترک

OTLP (OpenTelemetry Protocol) پروتکل ارسال داده در OTel است. دو حالت انتقال دارد: gRPC (معمولاً روی درگاه 4317) و HTTP (معمولاً روی 4318) که روی HTTP بدنه می‌تواند protobuf یا JSON باشد. در OTLP/HTTP، هر سیگنال مسیر خودش را دارد: /v1/logs، /v1/traces و /v1/metrics. اهمیت OTLP این است که بین شما و مقصد قرارداد ثابتی می‌گذارد: اگر برنامه‌تان OTLP می‌فرستد، تعویض مقصد معمولاً یعنی عوض کردن یک نشانی و یک هدر، نه بازنویسی کد. این همان چیزی است که شما را از قفل شدن به یک فروشنده نجات می‌دهد.

Collector: ایستگاه میانی اختیاری

OpenTelemetry Collector یک برنامهٔ جداگانه است که داده را از یک طرف می‌گیرد (receiver)، پردازش می‌کند (processor) و به یک یا چند مقصد می‌فرستد (exporter). لازم نیست حتماً داشته باشیدش؛ برنامه می‌تواند مستقیم به مقصد OTLP بفرستد. ولی در چند حالت واقعاً ارزشش را دارد:

  • می‌خواهید کلید و نشانی مقصد فقط در یک جا باشد، نه در پیکربندی ده سرویس.
  • می‌خواهید پیش از خروج داده از شبکهٔ خودتان، فیلدهایی را حذف یا پنهان کنید.
  • منابعی دارید که OTLP نمی‌فرستند — فایل لاگ روی دیسک، syslog، یا قالب ابزارهای دیگر — و می‌خواهید همه را به یک قالب تبدیل کنید.
  • می‌خواهید وقتی مقصد لحظه‌ای در دسترس نیست، داده در یک صف محلی بماند.

نمونهٔ یک پیکربندی کوچک که لاگ را با OTLP از برنامه‌ها می‌گیرد، دسته‌بندی می‌کند و با OTLP/HTTP جلو می‌فرستد:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}

exporters:
  otlphttp:
    endpoint: https://ingest.logmug.ir
    headers:
      x-logmug-key: ${env:LOGMUG_INGEST_KEY}

service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]

exporter otlphttp خودش /v1/logs را به نشانی پایه اضافه می‌کند، پس همان نشانی پایه کافی است. کلید را هم از متغیر محیطی بخوانید، نه اینکه در فایل پیکربندی بنویسید و آن فایل در مخزن کد برود.

یک نکتهٔ مهم پیش از کد: فقط لاگ

بسیاری از مقصدها — از جمله LogMug — فقط سیگنال لاگ را می‌پذیرند و trace و metric را نه. این با حرف‌های بالا تناقض ندارد: برای اینکه لاگ‌ها trace_id داشته باشند، لازم نیست spanها را جایی ذخیره کنید؛ کافی است SDK درون برنامه trace را بسازد و منتقل کند. ساختن و صادر کردن دو کار جداست.

در عمل یعنی در پیکربندی، exporter لاگ را روی OTLP بگذارید و exporterهای trace و metric را خاموش کنید (none). اگر این کار را نکنید، SDK سعی می‌کند spanها را هم به همان نشانی بفرستد، خطا می‌گیرد و لاگ‌های داخلی exporter فضای شما را پر می‌کند. اگر روزی ابزار APM هم اضافه کردید، می‌توانید trace را به آن بفرستید و لاگ را به مقصد لاگ — OTel دقیقاً برای همین انعطاف ساخته شده.

⁦.NET⁩: لاگ‌های ILogger با OTLP

در ⁦ASP.NET Core⁩ نیازی به کتابخانهٔ لاگ تازه نیست؛ همان ILogger کار می‌کند. این پکیج‌ها را اضافه کنید:

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http

و در Program.cs:

using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddOpenTelemetry(o =>
{
    o.IncludeFormattedMessage = true; // متن رندرشده هم فرستاده شود
    o.IncludeScopes = true;           // مقادیر BeginScope به ویژگی تبدیل شوند
});

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r
        .AddService("orders-api")
        .AddAttributes(new Dictionary<string, object>
        {
            ["deployment.environment.name"] = builder.Environment.EnvironmentName
        }))
    // tracing فقط برای ساختن و منتقل کردن trace_id؛ exporter ندارد
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation())
    .WithLogging(l => l.AddOtlpExporter(o =>
    {
        o.Endpoint = new Uri("https://ingest.logmug.ir/v1/logs");
        o.Protocol = OtlpExportProtocol.HttpProtobuf;
        o.Headers = "x-logmug-key=" + builder.Configuration["LogMug:IngestKey"];
    }));

var app = builder.Build();

سه نکته که در این کد عمداً گذاشته‌ام:

  • وقتی Endpoint را در کد می‌دهید و پروتکل HTTP است، مسیر کامل /v1/logs را بنویسید. exporter ⁦.NET⁩ در این حالت مسیر را خودش اضافه نمی‌کند — برخلاف وقتی که نشانی را با متغیر محیطی OTEL_EXPORTER_OTLP_ENDPOINT می‌دهید. این یکی از رایج‌ترین دلایل «لاگ‌ها نمی‌رسند» است که دیده‌ام.
  • WithTracing بدون exporter یعنی spanها ساخته می‌شوند، trace_id روی هر لاگ می‌نشیند و هدر traceparent روی درخواست‌های خروجی HttpClient می‌رود، ولی spanها جایی فرستاده نمی‌شوند.
  • Headers رشته‌ای به شکل key=value است و چند هدر با کاما جدا می‌شوند. کلید را از پیکربندی یا متغیر محیطی بخوانید، نه از کد.

از اینجا به بعد هر logger.LogError(ex, "Payment failed for order {OrderId}", id) با trace_id، span_id، نام سرویس، ویژگی OrderId و جزئیات exception صادر می‌شود. جزئیات بیشتر ⁦ASP.NET Core⁩، از سطح‌ها تا ترکیب با Serilog، را در راهنمای جامع لاگ در ⁦ASP.NET Core⁩ نوشته‌ام و گام‌های اتصال عملی هم در آموزش اتصال ⁦ASP.NET Core⁩ با OpenTelemetry هست.

Java: عامل OTel بدون تغییر کد

در جاوا ساده‌ترین راه، Java agent رسمی OTel است: یک فایل JAR که هنگام اجرا به JVM می‌دهید و خودش Spring، کلاینت‌های HTTP، JDBC و کتابخانه‌های لاگ رایج (Logback و Log4j 2) را instrument می‌کند. لاگ‌هایی که با SLF4J می‌نویسید، با trace_id و span_id جاری صادر می‌شوند و نیازی به تغییر حتی یک خط کد نیست.

export OTEL_SERVICE_NAME=payments
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=production
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.logmug.ir
export OTEL_EXPORTER_OTLP_HEADERS=x-logmug-key=lm_ingest_xxxxxxxx
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=none
export OTEL_METRICS_EXPORTER=none

java -javaagent:/opt/otel/opentelemetry-javaagent.jar -jar payments.jar

اینجا نشانی پایه را بدون مسیر داده‌ام، چون وقتی endpoint عمومی با متغیر محیطی تنظیم می‌شود، SDK مسیر هر سیگنال (/v1/logs) را خودش اضافه می‌کند. همین متغیرها در همهٔ زبان‌ها یکسان‌اند؛ این یکی از نقاط قوت واقعی OTel است: یک‌بار یاد می‌گیرید و همه‌جا به کار می‌برید.

عامل جاوا trace_id را در MDC هم می‌گذارد (با کلیدهای trace_id و span_id)، پس اگر کنار OTLP لاگ متنی روی کنسول هم دارید، می‌توانید با %X{trace_id} در الگوی Logback آن را در خروجی متنی هم ببینید. جزئیات Spring Boot را در آموزش اتصال Java و Spring Boot آورده‌ام.

Python: ماژول logging و opentelemetry-instrument

در پایتون هم مسیر بی‌دردسر، instrumentation خودکار است:

pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-instrumentation-logging
opentelemetry-bootstrap -a install   # instrumentation چارچوب‌های نصب‌شده (Flask، Django، requests و ...)

export OTEL_SERVICE_NAME=notifications
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.logmug.ir
export OTEL_EXPORTER_OTLP_HEADERS=x-logmug-key=lm_ingest_xxxxxxxx
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=none
export OTEL_METRICS_EXPORTER=none
# در نسخه‌های پیش از 1.40 لازم است؛ در نسخه‌های تازه‌تر بی‌ضرر است
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true

opentelemetry-instrument python app.py

کد برنامه همان کد معمول است:

import logging

log = logging.getLogger("notifications")

def send_sms(order_id: int, template: str) -> None:
    log.info("Sending SMS for order %s", order_id, extra={"template": template})
    try:
        ...
    except TimeoutError:
        log.exception("SMS provider timed out for order %s", order_id)

هر رکورد logging با trace_id درخواست جاری صادر می‌شود و مقادیر extra ویژگی جداگانه می‌شوند. log.exception هم نوع، پیام و stack trace استثنا را در ویژگی‌های استاندارد exception.* می‌گذارد. اگر ترجیح می‌دهید همه‌چیز را در کد پیکربندی کنید، SDK پایتون LoggerProvider و BatchLogRecordProcessor و یک handler برای ماژول logging دارد؛ فقط حواستان باشد که بخش لاگ SDK پایتون هنوز زیر ماژول‌هایی با پیشوند زیرخط (_logs) است و مسیر import بعضی کلاس‌ها بین نسخه‌ها جابه‌جا شده — پیش از کپی کردن نمونه، مستندات نسخهٔ نصب‌شدهٔ خودتان را ببینید. برای ⁦Node.js⁩ و Go و بقیه، آموزش زبان‌های دیگر را ببینید.

resource: نام سرویس، محیط و میزبان

هر سیگنالی که SDK می‌فرستد، یک resource همراه دارد: مجموعه‌ای از ویژگی‌ها که می‌گوید این داده از کجا آمده. سه ویژگی که هرگز نباید خالی بمانند:

ویژگی معنی اگر خالی بماند
service.name نام منطقی سرویس مقدار پیش‌فرضی مثل unknown_service که همهٔ سرویس‌ها را یکی می‌کند
deployment.environment.name production، staging و … لاگ آزمایشی و واقعی قاطی می‌شوند
host.name نام ماشین یا پاد نمی‌فهمید مشکل فقط روی یک سرور است یا همه

نام ویژگی محیط در نسخه‌های قدیمی‌تر قرارداد معنایی OTel deployment.environment بود و در نسخه‌های تازه به deployment.environment.name تغییر کرده؛ بسیاری از ابزارها هر دو را می‌خوانند. host.name را هم بیشتر SDKها با resource detector خودکار پر می‌کنند، ولی پیش از اعتماد، یک بار در خروجی ببینید که واقعاً پر شده است.

یک توصیهٔ تجربی: نام سرویس را با تیم‌تان قرارداد کنید و ثابت نگه دارید. وقتی سرویسی یک هفته orders است و هفتهٔ بعد Orders.Api، هر فیلتر و هر لینکی که همکاران ذخیره کرده‌اند بی‌صدا خراب می‌شود.

اشتباه‌هایی که بیشتر از همه دیده‌ام

  • trace_id در لاگ هست ولی در هر سرویس متفاوت است. یعنی propagation کار نمی‌کند: یا یک سرویس instrumentation ندارد، یا یک پروکسی میانی هدر traceparent را حذف می‌کند، یا وسط مسیر صف هست. اول روی دو سرویس مجاور ببینید هدر واقعاً می‌رسد یا نه.
  • exporterهای trace و metric روشن مانده‌اند در حالی که مقصد فقط لاگ می‌پذیرد. نتیجه، خطاهای پی‌درپی exporter و ترافیک بیهوده است.
  • مسیر /v1/logs دوبار یا هیچ‌بار. قاعده: با متغیر محیطی عمومی، نشانی پایه؛ با تنظیم در کد یا متغیر مخصوص سیگنال (OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)، نشانی کامل.
  • لاگ‌ها در خاموش شدن برنامه گم می‌شوند. exporterها دسته‌ای می‌فرستند؛ اگر پروسه بی‌خبر کشته شود، آخرین دسته — که اغلب همان لاگ‌های مهم پیش از سقوط است — نمی‌رسد. در برنامه‌های کوتاه‌عمر (اسکریپت، job) پیش از خروج provider را shutdown یا flush کنید.
  • همه‌چیز در متن پیام. OTel قالب پیام را نجات نمی‌دهد. اگر شمارهٔ سفارش را وسط رشته چسبانده باشید، ویژگی جداگانه نمی‌شود و نمی‌توانید رویش فیلتر کنید.
  • شروع با بزرگ‌ترین پروژه. OTel را روی دو سرویسی که بیشترین تعامل را دارند راه بیندازید، زنجیره را ببینید و بعد گسترش دهید.

از trace_id تا جواب: کار با لاگ‌های به‌هم‌پیوسته

وقتی لاگ‌ها trace_id دارند و در یک جای متمرکز جمع شده‌اند، شیوهٔ عیب‌یابی عوض می‌شود. دیگر از «حدود ساعت ۱۰ بود» شروع نمی‌کنید؛ از یک خط خطا شروع می‌کنید، trace_id آن را برمی‌دارید و کل داستان درخواست را به ترتیب زمان می‌خوانید — از دروازهٔ API تا آخرین سرویس. این همان کاری است که در LogMug با «همهٔ لاگ‌های این درخواست» یک کلیک است؛ ولی هر سامانهٔ متمرکزی که روی trace_id فیلتر بدهد همین ارزش را دارد. اگر هنوز لاگ‌ها را روی سرورها نگه می‌دارید، اول مدیریت متمرکز لاگ را بخوانید؛ trace_id بدون جای مشترک، نصف ارزشش را دارد.

روش کامل عیب‌یابی با این ابزارها — از گزارش کاربر تا تأیید رفع مشکل — را در عیب‌یابی خطا در پروداکشن با لاگ گام‌به‌گام نوشته‌ام. خلاصهٔ حرف این مقاله اما یک جمله است: OTel را لازم نیست یک‌جا و کامل بپذیرید. با لاگ و trace_id شروع کنید؛ همین قدم کوچک، بیشترین فاصله را بین «نیم ساعت حدس» و «یک دقیقه جواب» پر می‌کند.

منابع و مطالعهٔ بیشتر

لاگ همهٔ سرویس‌هایتان را در یک جا جستجو کنید

C#، Java یا هر زبان دیگر — با چند خط پیکربندی وصل می‌شود. پلن رایگان کارت بانکی نمی‌خواهد.

شروع رایگان