آموزش OpenTelemetry در C# برای مبتدی‌ها

Open telemetry , c# مبتدی
parsakarimidev.ir

آموزش OpenTelemetry در C# برای مبتدی‌ها

Open telemetry , c#زبان برنامه‌نویسی سطح: مبتدی 1405/07/03

آموزش OpenTelemetry در C# برای مبتدی‌ها

⚡ خلاصه سریع — اصل کاری‌ها

این‌ها مهم‌ترین چیزهایی هستن که اگه هیچی ندونی، فقط اینا رو بدونی کافیه:

  • OpenTelemetry یک استاندارد باز برای جمع‌آوری Trace، Metric و Log از برنامه‌هاست.
  • Trace یعنی دنبال کردن مسیر یک درخواست از ورود تا خروج.
  • Span هر مرحله از کار توی مسیر درخواسته؛ مثل یک تابع یا یک call.
  • Activity در دات‌نت همان Span در OpenTelemetry است.
  • Exporter مشخص می‌کند اطلاعات کجا ارسال بشه؛ مثل Console یا Jaeger.
  • Instrumentation کتابخانه‌ای است که به‌صورت خودکار Activity می‌سازد.
  • ActivitySource سفارشی را حتماً با AddSource("...") ثبت کن.

۳ اشتباه رایج که باید ازشون پرهیز کنی:

  1. فقط پکیج اصلی OpenTelemetry را نصب نکنی؛ حتماً Instrumentation و Exporter هم لازم داری.
  2. ActivitySource سفارشی را فراموش نکنی یا با AddSource ثبت نکنی.
  3. Activity را با using مدیریت نکنی؛ در نتیجه Span بسته نمی‌شه.

اگر فقط ۵ دقیقه وقت داری، این‌ها رو یاد بگیر:

  1. یک ActivitySource بساز و با StartActivity یک Span شروع کن.
  2. از AddOpenTelemetry() در Program.cs برای راه‌اندازی استفاده کن.
  3. یک Exporter مثل AddConsoleExporter() اضافه کن تا Trace ها را ببینی.

۱. مقدمه

OpenTelemetry یا به‌صورت خلاصه OTel یک استاندارد باز است که به برنامه‌ها کمک می‌کند اطلاعات مشاهده‌پذیری (Observability) تولید کنند. یعنی بتوانی بفهمی داخل برنامه چه اتفاقی می‌افتد، مخصوصاً وقتی یک درخواست از چند سرویس عبور می‌کند.

سه نوع داده اصلی در OpenTelemetry داریم:

  • Trace: دنبال کردن مسیر کامل یک درخواست.
  • Metric: عددهایی مثل تعداد درخواست، زمان پاسخ، نرخ خطا.
  • Log: همان لاگ‌های معمولی.

چرا باید یادش بگیری؟

وقتی برنامه ساده است، شاید با Console.WriteLine بشه فهمید چه خبره. اما وقتی چند سرویس داری، یک درخواست ممکنه از ۵ سرویس عبور کنه. اون‌وقت دیگه نمی‌دونی مشکل کجاست. OpenTelemetry این دید را بهت می‌ده.

کجا استفاده می‌شه؟

  • API های ASP.NET Core
  • میکروسرویس‌ها
  • سیستم‌های ابری و کانتینری
  • برنامه‌هایی که با HTTP یا gRPC با هم صحبت می‌کنند

۲. پیش‌نیازها

قبل از شروع، این چیزها کافیه:

  • آشنایی ابتدایی با زبان C#
  • آشنایی با ASP.NET Core Minimal API
  • نصب بودن NET 8 SDK.
  • یک IDE مثل Visual Studio 2022 یا VS Code
  • اگر می‌خواهی Trace ها را فقط در Console ببینی، Docker لازم نداری. برای مثال واقعی با Jaeger، Docker اختیاری است.

برای بررسی نسخه dotnet:

dotnet --version

۳. نصب و راه‌اندازی

گام ۱: ساخت پروژه

dotnet new webapi -n TraceDemo -minimal
cd TraceDemo

گام ۲: نصب پکیج‌های لازم

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

هر پکیج چه کار می‌کند:

پکیج کار
OpenTelemetry.Extensions.Hosting راه‌اندازی OpenTelemetry در ASP.NET Core
OpenTelemetry.Instrumentation.AspNetCore ساخت خودکار Span برای درخواست‌های ورودی ASP.NET Core
OpenTelemetry.Instrumentation.Http ساخت خودکار Span برای خروجی‌های HttpClient
OpenTelemetry.Exporter.Console نمایش Trace ها در Console

گام ۳: اولین راه‌اندازی

فایل Program.cs را این‌طور تغییر بده:

using OpenTelemetry;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddHttpClientInstrumentation();
        tracing.AddConsoleExporter();
    });

var app = builder.Build();

app.MapGet("/", () => "Hello OpenTelemetry!");

app.Run();

توضیح کد

  • AddOpenTelemetry() سرویس OpenTelemetry را به برنامه اضافه می‌کند.
  • WithTracing یعنی فقط بخش Trace را فعال کن.
  • AddAspNetCoreInstrumentation() برای ساخت خودکار Span مربوط به درخواست‌های ASP.NET Core.
  • AddHttpClientInstrumentation() برای ساخت خودکار Span مربوط به HttpClient.
  • AddConsoleExporter() یعنی نتیجه Trace ها را در Console نمایش بده.

گام ۴: اجرا

dotnet run

سپس مرورگر را باز کن یا از دستور زیر استفاده کن:

curl http://localhost:5000/

در Console اطلاعاتی شبیه Trace ID و Span ID می‌بینی.


۴. مفاهیم پایه

Trace

یک Trace یعنی دنبال کردن کامل یک درخواست. مثلاً وقتی کاربر درخواست /orders می‌زند، ممکنه این مسیر طی بشه:

  1. ورود به API
  2. صدا زدن سرویس پرداخت
  3. ذخیره در دیتابیس

همه این مراحل در قالب یک Trace نمایش داده می‌شه.

Span

هر مرحله از Trace یک Span است. مثلاً:

  • POST /orders
  • PaymentService.ProcessPayment
  • Database.SaveOrder

هر Span می‌تونه attributes داشته باشه. مثلاً order.id = 123.

Activity

در دات‌نت، چیزی که OpenTelemetry به آن Span می‌گوید، با کلاس Activity نشان داده می‌شود. این کلاس در System.Diagnostics قرار دارد.

ActivitySource

برای ساخت Span سفارشی، باید یک ActivitySource بسازی:

using System.Diagnostics;

public class OrderService
{
    private static readonly ActivitySource ActivitySource = new("OrderService");

    public void CreateOrder()
    {
        using var activity = ActivitySource.StartActivity("CreateOrder");
        activity?.SetTag("order.id", 123);

        Console.WriteLine("سفارش ساخته شد");
    }
}

نکته خیلی مهم

اگه ActivitySource سفارشی داری، باید اسمش را در تنظیمات OpenTelemetry ثبت کنی:

tracing.AddSource("OrderService");

اگر این کار را نکنی، Activity های سفارشی تو Export نمی‌شوند.


۵. مثال‌های کد ساده

مثال ۱: فقط Trace خودکار با ASP.NET Core

using OpenTelemetry;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddConsoleExporter();
    });

var app = builder.Build();

app.MapGet("/hello", () => "سلام!");

app.Run();

توضیح:
هر درخواست به /hello یک Span خودکار با نام GET /hello می‌سازد و در Console نمایش می‌دهد.


مثال ۲: ساخت Span سفارشی

using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("MyApi");
        tracing.AddConsoleExporter();
    });

var app = builder.Build();

var activitySource = new ActivitySource("MyApi");

app.MapGet("/order", () =>
{
    using var activity = activitySource.StartActivity("CreateOrder");
    activity?.SetTag("order.id", 123);
    activity?.SetTag("customer.name", "Ali");

    return Results.Ok(new { id = 123, status = "created" });
});

app.Run();

توضیح:

  • new ActivitySource("MyApi") یک منبع ساخت Activity می‌سازد.
  • StartActivity("CreateOrder") یک Activity جدید شروع می‌کند.
  • SetTag اطلاعات اضافی به Activity اضافه می‌کند.
  • using var باعث می‌شود Activity به‌صورت خودکار بسته شود.
  • AddSource("MyApi") باعث می‌شود Activity های این منبع جمع‌آوری شوند.

مثال ۳: افزودن Event و Status

using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("MyApi");
        tracing.AddConsoleExporter();
    });

var app = builder.Build();

var activitySource = new ActivitySource("MyApi");

app.MapPost("/payment", () =>
{
    using var activity = activitySource.StartActivity("PaymentProcess");

    try
    {
        activity?.AddEvent(new ActivityEvent("PaymentStarted"));
        
        // شبیه‌سازی عملیات پرداخت
        Thread.Sleep(100);
        
        activity?.AddEvent(new ActivityEvent("PaymentCompleted"));
        return Results.Ok(new { status = "paid" });
    }
    catch (Exception ex)
    {
        activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
        throw;
    }
});

app.Run();

توضیح:

  • AddEvent یک رویداد زمانی داخل Span اضافه می‌کند.
  • SetStatus(ActivityStatusCode.Error, ...) وقتی خطا رخ بدهد، وضعیت Span را خطا می‌کند.
  • این اطلاعات کمک می‌کند بفهمی پرداخت در چه مرحله‌ای شکست خورده است.

مثال ۴: Span تودرتو

app.MapPost("/orders", () =>
{
    using var orderActivity = activitySource.StartActivity("CreateOrder");

    using (var paymentActivity = activitySource.StartActivity("ProcessPayment"))
    {
        paymentActivity?.SetTag("payment.amount", 25000);
    }

    using (var dbActivity = activitySource.StartActivity("SaveOrder"))
    {
        dbActivity?.SetTag("db.table", "orders");
    }

    return Results.Ok();
});

توضیح:

ProcessPayment و SaveOrder به‌صورت خودکار فرزند CreateOrder می‌شوند. چون هنگام ساخت، داخل همان Activity قبلی هستیم.


۶. مثال واقعی و کاربردی

حالا یک پروژه کوچک واقعی می‌سازیم و Trace ها را با Jaeger می‌بینیم.

گام ۱: اجرای Jaeger با Docker

docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  jaegertracing/all-in-one:latest

پس از اجرا، Jaeger UI روی آدرس زیر در دسترس است:

http://localhost:16686

گام ۲: نصب پکیج OTLP Exporter

dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

گام ۳: کد کامل پروژه

using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource.AddService("OrderApi"))
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddHttpClientInstrumentation();
        tracing.AddSource("OrderApi");
        tracing.AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:4317");
        });
    });

var app = builder.Build();

var activitySource = new ActivitySource("OrderApi");

app.MapPost("/orders", async (Order order) =>
{
    using var activity = activitySource.StartActivity("CreateOrder");
    activity?.SetTag("order.id", order.Id);
    activity?.SetTag("product.name", order.Product);

    var client = new HttpClient();
    var result = await client.GetStringAsync("https://jsonplaceholder.typicode.com/todos/1");

    return Results.Ok(new
    {
        orderId = order.Id,
        product = order.Product,
        externalData = result
    });
});

app.Run();

record Order(int Id, string Product);

توضیح کد

  • ConfigureResource نام سرویس را مشخص می‌کند تا در Jaeger قابل تشخیص باشد.
  • AddOtlpExporter اطلاعات را به Jaeger ارسال می‌کند.
  • وقتی /orders صدا زده می‌شود، هم Span سفارشی CreateOrder ثبت می‌شود و هم Span خودکار مربوط به خروجی HTTP به jsonplaceholder.
  • هر دو در یک Trace نمایش داده می‌شوند.

گام ۴: اجرا و تست

dotnet run

در Linux/Mac:

curl -X POST http://localhost:5000/orders \
  -H "Content-Type: application/json" \
  -d '{"id": 10, "product": "book"}'

در PowerShell:

Invoke-RestMethod -Method Post -Uri http://localhost:5000/orders `
  -ContentType "application/json" `
  -Body '{"id": 10, "product": "book"}'

حالا در Jaeger UI می‌توانی سرویس OrderApi را انتخاب کنی و Trace کامل این درخواست را ببینی.


۷. مباحث پیشرفته

Sampling

همیشه لازم نیست همه Trace ها ذخیره شوند. می‌توانی فقط درصدی از آن‌ها را نگه داری:

using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.SetSampler(new TraceIdRatioBasedSampler(0.1));
        tracing.AddAspNetCoreInstrumentation();
        tracing.AddConsoleExporter();
    });

این کد فقط ۱۰٪ Trace ها را نگه می‌دارد.

Metrics

برای جمع‌آوری Metric ها:

using OpenTelemetry;
using OpenTelemetry.Metrics;

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics.AddAspNetCoreInstrumentation();
        metrics.AddConsoleExporter();
    });

Logging

می‌توانی لاگ‌ها را هم به OpenTelemetry بدهی:

using OpenTelemetry.Logs;

builder.Logging.AddOpenTelemetry(logging =>
{
    logging.AddConsoleExporter();
});

Baggage

Baggage اطلاعات اضافی را همراه Trace بین سرویس‌ها منتقل می‌کند:

using System.Diagnostics;

Baggage.Current.SetBaggage("user.id", "42");

۸. اشتباهات رایج

  1. ثبت نکردن AddSource("...") برای ActivitySource سفارشی
    در نتیجه Activity های سفارشی تو Export نمی‌شوند.

  2. نصب نکردن پکیج Instrumentation
    فقط با نصب OpenTelemetry.Extensions.Hosting هیچ Span خودکاری ساخته نمی‌شود.

  3. عدم مدیریت عمر Activity
    اگر using var activity را فراموش کنی، ممکن است Activity بسته نشود و Trace ناقص بماند.

  4. مشخص نکردن service.name
    در Jaeger سرویس به صورت unknown_service نمایش داده می‌شود.

  5. استفاده از ActivitySource جدید در هر درخواست
    ActivitySource باید یک نمونه ثابت باشد، نه اینکه برای هر درخواست ساخته شود.

  6. اشتباه در پورت OTLP Exporter
    پورت پیش‌فرض gRPC معمولاً 4317 است، نه 16686.


۹. بهترین روش‌ها (Best Practices)

  • همیشه service.name را با ConfigureResource مشخص کن.
  • از یک ActivitySource ثابت و استاتیک در هر کلاس استفاده کن.
  • نام Activity ها را کوتاه و معنادار بگذار؛ مثل CreateOrder.
  • اطلاعات مهم را با SetTag اضافه کن، اما از داده‌های حساس یا با تنوع خیلی زیاد پرهیز کن.
  • برای خطاها از SetStatus(ActivityStatusCode.Error) استفاده کن.
  • در محیط Production از Sampling استفاده کن تا حجم داده‌ها کنترل شود.
  • رویدادهای مهم را با AddEvent ثبت کن، نه همه چیز را.
  • همزمان با Trace، Metric ها را هم جمع‌آوری کن.
  • از Semantic Conventions استفاده کن؛ مثلاً http.request.method به جای نام‌های دلخواه.

۱۰. منابع و ادامه مسیر

قدم بعدی

  • Metrics را با Prometheus و Grafana اجرا کن.
  • یاد بگیر چطور Baggage و Context Propagation در میکروسرویس‌ها کار می‌کند.
  • یک Exporter سفارشی برای نیاز خودت بنویس.

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.