فصل ۸ — مقدمهای بر Observability (مشاهدهپذیری)
Observability یعنی گردآوری، نمایش و تحلیل دادههای مربوط به رفتار یک برنامه. یک سامانهٔ عملیاتی باید بتواند رفتارهای نامطلوب مانند Unavailable شدن Service، خطاها و پاسخهای کند را آشکار کند و همراه آن اطلاعاتی فراهم کند که پیدا کردن علت مشکل را ممکن سازد؛ مانند Event Logهای دقیق، Metrics مربوط به مصرف منابع و Trace مسیر اجرای درخواستها.
این فصل با مثالهای ساده چهار موضوع را پوشش میدهد:
- مبانی Observability
- Logging
- Tracing و گردآوری Metrics
- Health Check
مبانی Observability و MELT
چهار ستون رایج Observability با سرواژهٔ MELT معرفی میشوند: Metrics، Events، Logs و Traces.
- Logs: ثبت اطلاعات دربارهٔ رخدادهایی که برنامه مشخص کرده است؛ برای مثال Exception یا دادهٔ Contextual که در Debug یک وضعیت خاص کمک میکند.
- Events: Actionهایی همراه با Metadata؛ برای مثال کاربری که فایلی با اندازهٔ مشخص Upload کرده است.
- Traces: نمایش مسیر تعامل کاربر با سامانه؛ برای نمونه یک HTTP Request که در ادامه به SQL Database یا Service خارجی وصل میشود و مدت هر Dependency را ثبت میکند.
- Metrics: اعداد تجمیعی برای ارزیابی سلامت کلی سامانه، مانند CPU و Memory Usage.
این فصل روی Logs، Traces و Metrics تمرکز میکند. ابزارهایی که این دادهها را جمع و تحلیل میکنند معمولاً در دستهٔ Application Performance Monitoring (APM) قرار میگیرند. APM برای مشاهدهٔ Real-time و تشخیص زودهنگام مشکل بهکار میرود.
نمونهٔ کتاب از Microsoft Azure Application Insights بهعنوان APM استفاده میکند. برای ادامه، یک Azure Account، یک Application Insights Workspace و Connection String آن لازم است.
Logging
Logging برای کشف خطاها و ثبت اطلاعات Contextual ضروری است. هر چیزی را میتوان Log کرد، اما اطلاعات حساس نباید در Log قرار گیرد. دادههای شخصی مانند شماره تلفن، ایمیل یا اطلاعات هویتی و نیز Secretهای برنامه مانند Password، Login و Connection String نمونههای اطلاعاتیاند که نباید ثبت شوند.
ASP.NET Core بهصورت Native رابط ILogger و Provider پیشفرض Console را در اختیار دارد.
شکل ۸-۱ — Logهای پیشفرض ASP.NET Core هنگام Startup
Log Levelها از کمترین تا بیشترین Severity عبارتاند از:
Trace = 0: جزئیات بسیار ریز و صرفاً اطلاعاتی.
Debug = 1: اطلاعات بیشتر برای تشخیص مشکل.
Information = 2: رخدادهای معمول و تأیید عملکرد طبیعی برنامه.
Warning = 3: وضعیت بالقوه مشکلساز که باید بررسی شود.
Error = 4: خطای واقعی مانند Exception رخداده در عملیات.
Critical = 5: خرابی جدی که ممکن است مانع اجرای کل برنامه شود؛ برای مثال عدم امکان اتصال حیاتی به Database.
در مثالها بیشتر از Information، Error و Critical استفاده میشود.
Serilog و Structured Logging
کتاب بهجای Provider پیشفرض، Serilog را پیکربندی میکند و همچنان از ILogger در کد استفاده میشود. Serilog Sinkهای مختلفی برای File، Console، Application Insights و مقصدهای دیگر دارد و از Structured Logging پشتیبانی میکند؛ یعنی متغیرهای Log را بهصورت Metadata مستقل ذخیره میکند، نه اینکه همهچیز فقط Plain Text باشد.
دو Package در API نصب میشوند:
Serilog.AspNetCoreSerilog.Sinks.ApplicationInsights
{
"Serilog": {
"Using": [
"Serilog.Sinks.ApplicationInsights"
],
"MinimumLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
},
"WriteTo": [
{
"Name": "ApplicationInsights",
"Args": {
"connectionString": "{YourApplicationInsightsConnectionString}",
"telemetryConverter": "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
}
}
],
"Enrich": [ "FromLogContext" ],
"Properties": {
"Application": "DemoAPI"
}
}
}
Listing 8-1 — پیکربندی Serilog در appsettings.json
Using Sink را مشخص میکند. حداقل Level پیشفرض Information است؛ بنابراین Information تا Critical ارسال میشوند. برای Logهای Framework، حداقل Level روی Warning گذاشته شده تا حجم نویز کاهش یابد. WriteTo مقصد و Connection String Application Insights را مشخص میکند و Enrich امکان افزودن Metadata بیشتر را فراهم میسازد.
Configuration با خط زیر به Host متصل میشود:
builder.Host.UseSerilog((context, configuration) =>
configuration.ReadFrom.Configuration(context.Configuration));
استفاده از ILogger
یک روش این است که مستقیم از app.Logger داخل Endpoint استفاده شود:
var app = builder.Build();
...
app.MapGet("/logging", () => {
app.Logger.LogInformation("/logging endpoint has been invoked.");
return Results.Ok();
});
...
app.Run();
Listing 8-2 — Logging با app.Logger
نویسنده این روش را از نظر Testability مناسب نمیداند، چون Lambda متغیری بیرونی را Capture میکند. روش بهتر تزریق ILogger<T> از DI است؛ T معمولاً کلاس مبدأ Log را مشخص میکند و Category آن را نیز تعیین میکند.
var app = builder.Build();
...
app.MapGet("/logging", (ILogger<Program> logger) => {
logger.LogInformation("/logging endpoint has been invoked.");
return Results.Ok();
});
...
app.Run();
Listing 8-3 — تزریق ILogger<T>
ILogger از انواع شناختهشدهٔ ASP.NET Core است و برای Inject شدن نیاز به ثبت Service اختصاصی ندارد.
Structured Logging و Scope
برای Structured Logging، مقدارهای متغیر باید بهصورت Placeholder نامدار در Message Template قرار گیرند. این کار به APM اجازه میدهد مقادیر را بهعنوان Property مستقل Index و نمایش دهد.
var app = builder.Build();
...
app.MapGet("/countries", async (
int? pageIndex,
int? pageSize,
ICountryMapper mapper,
ICountryService countryService,
ILogger<Program> logger) => {
var paging = new PagingDto
{
PageIndex = pageIndex.HasValue ? pageIndex.Value : 1,
PageSize = pageSize.HasValue ? pageSize.Value : 10
};
var countries = await countryService.GetAllAsync(paging);
using (logger.BeginScope(
"Getting countries with page index {pageIndex} and page size {pageSize}",
paging.PageIndex,
paging.PageSize))
{
logger.LogInformation(
"Received {count} countries from the query", countries.Count);
return Results.Ok(mapper.Map(countries));
}
});
...
app.Run();
Listing 8-4 — نمونهٔ Structured Logging
شکل ۸-۲ — پیدا کردن Log در Transaction Search برنامهٔ Application Insights
شکل ۸-۳ — جزئیات Structured Log و Custom Properties
در Application Insights، pageSize، pageIndex و count بهعنوان Custom Properties قابل مشاهدهاند. استفاده از Interpolation یا ساخت رشتهٔ نهایی پیش از Logging این ساختار را از بین میبرد و Log را کمقابلیتتر میکند.
Listing 8-5 — نمونهٔ نامناسبتر با تبدیل متغیرها به متن نهایی
شکل ۸-۴ — جزئیات Log بدون Structured Logging مؤثر
BeginScope چند Log مرتبط را در یک Context مشترک گروهبندی میکند. این کار خوانایی Trace یک عملیات را بالا میبرد، اما باید با دقت استفاده شود؛ اگر Exception در Scope رخ دهد، Logهای همان Scope نیز با همان Context ارسال خواهند شد.
Logging در Exception Handler
Handler سراسری خطا که در فصل ۵ ساخته شد اکنون ILogger<DefaultExceptionHandler> دریافت میکند تا Exception را پیش از نوشتن ProblemDetails ثبت کند.
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using System.Net;
namespace AspNetCore8MinimalApis.ExceptionHandlers;
public class DefaultExceptionHandler : IExceptionHandler
{
private readonly ILogger<DefaultExceptionHandler> _logger;
public DefaultExceptionHandler(ILogger<DefaultExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception,
"An unexpected error occurred and has been handled by the {DefaultExceptionHandler} handler",
nameof(DefaultExceptionHandler));
await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = (int)HttpStatusCode.InternalServerError,
Type = exception.GetType().Name,
Title = "An unexpected error occurred",
Detail = exception.Message,
Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"
});
return true;
}
}
Listing 8-6 — DefaultExceptionHandler همراه با ILogger
شکل ۸-۵ — پیدا کردن Exception در Application Insights
شکل ۸-۶ — جزئیات Log خطا و Exception ثبتشده
این الگو باعث میشود خطایی که به Client پاسخ استاندارد میدهد، همزمان جزئیات لازم را برای تیم فنی در APM ثبت کند.
Tracing و گردآوری Metrics
Tracing Dependencyهایی مانند SQL Database و گردآوری Metrics در Production برای تشخیص Bottleneckها و مشکلات Performance اهمیت زیادی دارد. توسعهدهنده ممکن است مسئول تفسیر نهایی همهٔ Telemetryها نباشد، اما باید بتواند جمعآوری آنها را فعال کند.
Package زیر در API نصب میشود:
Microsoft.ApplicationInsights.AspNetCore
و Telemetry با این خط ثبت میشود:
builder.Services.AddApplicationInsightsTelemetry();
Connection String نیز در Configuration قرار میگیرد:
"ApplicationInsights": {
"ConnectionString": "{YourConnectionString}"
}
پس از فعال شدن Telemetry، Application Insights بهصورت خودکار اطلاعات بیشتری ثبت میکند؛ از جمله HTTP Requestها، Dependencyهایی مانند SQL Query، مدت اجرای Request و مدت هر Dependency.
شکل ۸-۷ — نمونهٔ Telemetry شامل Request و Dependency
با انتخاب یک Dependency میتوان دید کدام HTTP Request آن را ایجاد کرده و زنجیرهٔ اجرا چگونه بوده است.
شکل ۸-۸ — ارتباط Telemetry بین Dependency و HTTP Request
Exceptionها نیز Metadata غنیتری مانند Endpoint مبدأ و مدت اجرا دریافت میکنند.
شکل ۸-۹ — Exception غنیشده با Metadata
در Overview میتوان Metrics عمومی مانند Failed Request Count و Server Response Time را مشاهده کرد.
شکل ۸-۱۰ — نمای کلی Metrics
بخش Live Metrics اطلاعات Real-timeتری مانند CPU و Memory Usage را نمایش میدهد.
شکل ۸-۱۱ — Metrics تفصیلی و زنده
امکان ساخت Metric و Trace سفارشی نیز وجود دارد، اما کتاب روی Telemetry پیشفرض تمرکز میکند.
Health Check
Health Check Endpointهایی هستند که وضعیت عملیاتی برنامه و Dependencyهای آن را گزارش میکنند. دو سؤال اصلی عبارتاند از:
- آیا برنامه Deploy شده و در حال اجراست؟
- آیا Service و Dependencyهای آن Healthy، Unhealthy یا Degraded هستند؟
دو نوع اصلی Health Check:
- Readiness: آیا برنامه آمادهٔ سرویسدهی است؟
- Liveness: آیا برنامه زنده و عملیاتی است؟
Liveness Health Check با SQL Server
برای بررسی SQL Server بستهٔ AspNetCore.HealthChecks.SqlServer نصب میشود. سپس Connection String با AddSqlServer به HealthChecks داده و Endpoint با MapHealthChecks منتشر میشود.
var builder = WebApplication.CreateBuilder(args);
...
var dbConnection = builder.Configuration.GetConnectionString("DemoDb");
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString: dbConnection);
...
var app = builder.Build();
app.MapHealthChecks("/health");
...
app.Run();
Listing 8-7 — پیکربندی GET /health برای SQL Server
Endpoint بر اساس امکان اتصال Database، Healthy یا Unhealthy برمیگرداند.
شکل ۸-۱۲ — پاسخ Healthy از GET /health
اگر چند Database بررسی شوند، برای تمایز هر Check نام جداگانه تعیین میشود.
var builder = WebApplication.CreateBuilder(args);
...
var dbConnection1 = builder.Configuration.GetConnectionString("DemoDb1");
var dbConnection2 = builder.Configuration.GetConnectionString("DemoDb2");
builder.Services.AddHealthChecks()
.AddSqlServer(name: "SQL1", connectionString: dbConnection1)
.AddSqlServer(name: "SQL2", connectionString: dbConnection2);
...
var app = builder.Build();
app.MapHealthChecks("/health");
...
app.Run();
Listing 8-8 — Health Check برای دو Database
Readiness Health Check سفارشی
Readiness معمولاً به منطق Application-specific نیاز دارد. برای مثال ممکن است برنامه پس از Startup چند ثانیه صرف Warm-up یا آمادهسازی Cache کند. نمونهٔ کتاب یک عملیات دهثانیهای مصنوعی را با Flag ثابت شبیهسازی میکند.
namespace AspNetCore8MinimalApis;
public static class Ready
{
public static bool IsReady { get; set; } = false;
}
Listing 8-9 — کلاس static نوع Ready
کلاس Health Check این Flag را میخواند:
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace AspNetCore8MinimalApis.Healthchecks;
public class ReadyHealthCheck : IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var result = Ready.IsReady
? HealthCheckResult.Healthy()
: HealthCheckResult.Unhealthy("Application not ready");
return Task.FromResult(result);
}
}
Listing 8-10 — کلاس ReadyHealthCheck
نمونهٔ Startup پس از ده ثانیه Flag را true میکند:
Task.Run(() => { Thread.Sleep(10000); Ready.IsReady = true; });
برای جدا نگهداشتن Readiness و Liveness، Checkها با Tag ثبت و هر Endpoint فقط Tag مربوط به خودش را انتخاب میکند.
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddHealthChecks()
.AddSqlServer(
name: "SQL1",
connectionString: dbConnection1,
tags: new[] { "live" })
.AddSqlServer(
name: "SQL2",
connectionString: dbConnection2,
tags: new[] { "live" })
.AddCheck<ReadyHealthCheck>(
"Readiness check",
tags: new[] { "ready" });
...
var app = builder.Build();
..
app.UseExceptionHandler(opt => { });
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = healthCheck => healthCheck.Tags.Contains("ready")
});
app.MapHealthChecks("/live", new HealthCheckOptions
{
Predicate = healthCheck => healthCheck.Tags.Contains("live")
});
...
app.Run();
Listing 8-11 — Endpointهای جداگانهٔ Readiness و Liveness
پیش از پایان ده ثانیه، /ready میتواند Unhealthy باشد حتی اگر /live Healthy باشد. پس از پایان Initialization، Readiness Healthy میشود. Invocation این Endpointها نیز مانند Requestهای دیگر توسط Application Insights Telemetry قابل ثبت است.
جمعبندی فصل
این فصل حداقل زیرساخت لازم برای Observability را معرفی کرد: MELT، Logging ساختاریافته با Serilog، ارسال Log به Application Insights، جمعآوری خودکار Trace و Metrics و ساخت Liveness/Readiness Health Check. ابزارهای APM دیگری مانند Jaeger و Grafana نیز در صنعت استفاده میشوند. فصل بعد به مدیریت دادههای حساس و Application Secretها میپردازد.
تصاویر منبع مرتبط با این بخش