Libraries & Ecosystem · کتابخانه‌ها و اکوسیستم سنیورSenior ~52 دقیقه مطالعه~46 min read

Observability: Micrometer، Prometheus و OpenTelemetryObservability: Micrometer, Prometheus & OpenTelemetry

از صفر یاد می‌گیری چرا مانیتورینگ کافی نیست و observability چیست، سه ستون متریک/لاگ/ترِیس، چطور Micrometer نقش نمای واحد متریک را بازی می‌کند (Counter/Gauge/Timer/DistributionSummary)، Prometheus چطور scrape و هیستوگرام و صدک می‌سازد، Grafana و PromQL، مفاهیم tracing توزیع‌شده و OpenTelemetry، متدهای RED و USE، دام cardinality و SLI/SLO — با تشبیه، کد اجراشدنی و سؤالات مصاحبه.Learn from scratch why monitoring isn't observability, the three pillars of metrics/logs/traces, how Micrometer acts as a single metrics facade (Counter/Gauge/Timer/DistributionSummary), how Prometheus scrapes and builds histograms and percentiles, Grafana and PromQL, distributed tracing concepts and OpenTelemetry, the RED and USE methods, the cardinality trap, and SLI/SLO — with analogies, runnable code, and interview questions.


ساعت دو بامداد است. سرویسِ سفارشِ تو «کُند» شده — نه از کار افتاده، فقط کُند. بعضی کاربران خطا می‌گیرند، بعضی نه. کدی که دیروز کار می‌کرد امروز هم دقیقاً همان است. حالا این سؤال را جواب بده: مشکل کجاست؟ پایگاه‌داده؟ یک سرویسِ پایین‌دستی؟ یک صف که پُر شده؟ یک کاربر که ورودی عجیب فرستاده؟ اگر تنها ابزارت این باشد که به سرور SSH بزنی و لاگ‌ها را چشمی بخوانی، تا صبح آن‌جا نشسته‌ای. سیستمی که بتوانی از بیرون، فقط با نگاه کردن به خروجی‌هایش، بفهمی درونش چه می‌گذرد، سیستمی است که observability دارد. این فصل دقیقاً دربارهٔ ساختنِ همان توانایی است.

مانیتورینگ داشبورد ماشین است، observability اتاق تشخیص پزشک

یک ماشین چند چراغِ هشدار روی داشبورد دارد: چراغِ روغن، چراغِ دما، چراغِ بنزین. اینها مانیتورینگ هستند — سؤال‌های از پیش تعیین‌شده‌ای که جوابشان بله/خیر است: «آیا دما بالاست؟». مفید، اما محدود؛ فقط به سؤال‌هایی جواب می‌دهند که سازنده از قبل پیش‌بینی کرده. حالا یک بیمارِ ناخوش را تصور کن که وارد اتاق تشخیص می‌شود. پزشک از قبل نمی‌داند مشکل چیست؛ او آزمایش خون، فشار، ضربان، تصویربرداری و شرح‌حال را کنار هم می‌گذارد تا به سؤالی جواب دهد که هرگز از قبل پیش‌بینی نشده بود. این observability است: توانِ پرسیدنِ سؤال‌های جدید و پیش‌بینی‌نشده دربارهٔ سیستم، بدون اینکه مجبور باشی کدِ جدید deploy کنی. مانیتورینگ می‌گوید «چیزی خراب است»؛ observability می‌گوید «چرا».

این فصل قلبِ عملیاتیِ هر سیستمِ توزیع‌شدهٔ جدی است. مثل همیشه، اول با تشبیه مفهوم را می‌گیریم، بعد اسمِ فنی‌اش را یاد می‌گیریم، و بعد به کدِ واقعی وصلش می‌کنیم. نگران واژه‌های تازه نباش؛ هر کدام را همان‌جا که برای اولین بار می‌آید معنا می‌کنم.

نقشهٔ راه این فصل
  1. سه ستونِ observability: متریک، لاگ، ترِیس — هر کدام به چه سؤالی جواب می‌دهند.
  2. Micrometer به‌عنوان «SLF4J برای متریک‌ها»: چرا یک نما (facade) لازم داریم.
  3. چهار نوع سنجه (meter): Counter، Gauge، Timer و DistributionSummary — کِی کدام.
  4. Tag‌ها و ابعاد (dimensions) — چطور یک متریک را برش بزنیم و چرا اینجا خطرِ cardinality کمین کرده.
  5. Prometheus: مدلِ pull، فرمتِ exposition، و اینکه هیستوگرام چطور صدک (percentile) می‌سازد.
  6. PromQL و Grafana — از دادهٔ خام تا نمودار و alert.
  7. Tracing توزیع‌شده: span، trace، و انتشارِ context بین سرویس‌ها.
  8. OpenTelemetry — استانداردِ واحدِ صنعت برای هر سه سیگنال.
  9. متدهای RED و USE و چهار سیگنالِ طلایی — چه چیزی را اندازه بگیریم.
  10. SLI/SLO/SLA و بودجهٔ خطا — زبانِ قابل‌اتکا بودن.
  11. دام‌ها، best practiceها و یک بخشِ کاملِ پرسش‌وپاسخ مصاحبه.

بخش صفر — چند واژه که باید قبل از شروع «حسشان کنی»

سه واژه در کلِ فصل تکرار می‌شوند. بگذار همین حالا با تشبیه در ذهنت بکارمشان تا بعداً گیر نکنی.

  • تلمتری (telemetry): «داده‌ای که سیستم دربارهٔ خودش تولید و به بیرون می‌فرستد». ریشه‌اش یونانی است: tele یعنی «از دور» و metron یعنی «اندازه». یعنی «اندازه‌گیری از راه دور». هر متریک، لاگ و ترِیس یک قطعه تلمتری است.
  • instrumentation (ابزارگذاری): «کاری که می‌کنی تا کدت تلمتری تولید کند» — مثل کار گذاشتنِ حسگر روی یک موتور. وقتی یک خط counter.increment() اضافه می‌کنی، داری کدت را instrument می‌کنی.
  • cardinality (کاردینالیتی): «تعداد مقدارهای متمایزی که یک چیز می‌تواند بگیرد». جنسیت کاردینالیتیِ پایین دارد (دو-سه مقدار)؛ شناسهٔ کاربر کاردینالیتیِ بسیار بالا دارد (میلیون‌ها مقدار). این واژه به‌ظاهر بی‌خطر، در ادامه قهرمانِ خطرناک‌ترین دامِ فصل است.

بخش یک — سه ستونِ Observability

اطلاعاتی که سیستم دربارهٔ خودش می‌دهد سه جنسِ متفاوت دارد. به‌شان می‌گویند سه ستونِ observability. هر ستون به یک نوع سؤالِ متفاوت جواب می‌دهد، و اشتباهِ رایجِ تازه‌کارها این است که با یک ستون سراغِ سؤالِ ستونِ دیگر می‌روند.

کتابِ حساب، دفترچهٔ خاطرات و ردِ پا در برف

تصور کن می‌خواهی بفهمی در یک خانه دیشب چه گذشت. سه منبع داری. اول کتابِ حسابِ خانه: فقط عددها — «۱۲ لیوان آب مصرف شد، ۳ بار درِ یخچال باز شد». فشرده، ارزان، خوب برای دیدنِ روند، اما نمی‌گوید چرا. این متریک است. دوم دفترچهٔ خاطراتِ ساکنان: هر کس نوشته «ساعت ۹ مهمان آمد، ساعت ۱۰ برق رفت». پُرجزئیات، متنی، خوب برای فهمیدنِ یک لحظهٔ خاص. این لاگ است. سوم ردِ پا در برف: می‌توانی مسیرِ یک نفر را از درِ ورودی تا آشپزخانه تا اتاق دنبال کنی و ببینی هر قدم چقدر طول کشید. این ترِیس است — سفرِ یک درخواست از میان کلِ سیستم.

بیایید دقیق‌تر تعریف کنیم:

ستون چیست به چه سؤالی جواب می‌دهد هزینهٔ ذخیره نمونه
Metrics (متریک) عددهای جمع‌شونده در طولِ زمان (time series) «چند تا؟ چقدر سریع؟ روند به کدام سو؟» بسیار کم (فشرده) نرخ درخواست، درصد خطا، مصرف حافظه
Logs (لاگ) رویدادهای گسسته و متنی با تایم‌استمپ «در آن لحظهٔ خاص دقیقاً چه شد؟» زیاد (پُرحجم) «کاربر ۴۲ وارد شد»، stack traceِ یک خطا
Traces (ترِیس) مسیرِ یک درخواست از میان چند سرویس «کجای این سفر کُند/خراب شد؟» متوسط (اغلب نمونه‌برداری‌شده) درخواستی که از API به DB به cache رفت
ستون‌ها مکمل‌اند، نه جایگزین

یک تیمِ بالغ هر سه را با هم می‌بافد. جریانِ کارِ واقعی چنین است: متریک به تو هشدار می‌دهد که «نرخِ خطا بالا رفت» (چه چیزی خراب است)؛ ترِیس نشانت می‌دهد که «کدام سرویس و کدام مرحله» کُند/خراب شده (کجا)؛ و لاگ آن لحظهٔ دقیق را با جزئیات نشانت می‌دهد (چرا). متریک ارزان و همیشه‌روشن است، پس اول از آن می‌فهمی. لاگ گران است، پس آن را برای عمیق‌شدن نگه می‌داری. اگر فقط یکی داشته باشی، همیشه نیمی از داستان را از دست می‌دهی.

در بقیهٔ فصل روی متریک تمرکزِ اصلی داریم (چون بیشترین ابزارِ اکوسیستمِ جاوا آنجاست)، بعد سراغِ tracing و OpenTelemetry می‌رویم. لاگ‌گیری فصلِ جداگانهٔ خودش را دارد.


بخش دو — Micrometer: «SLF4J برای متریک‌ها»

فرض کن تصمیم گرفتی متریک جمع کنی. اما به کدام سیستم بفرستی؟ Prometheus؟ Datadog؟ CloudWatch؟ Graphite؟ New Relic؟ هر کدام API و فرمتِ خودش را دارد. اگر مستقیم به API یکی از آنها کد بزنی، روزی که شرکت تصمیم بگیرد مهاجرت کند، باید هزار جای کد را دست بزنی.

مترجمِ همزمان در سازمان ملل

تصور کن یک سخنران در سازمان ملل فارسی حرف می‌زند و شنونده‌ها به انگلیسی، عربی، چینی و فرانسه گوش می‌دهند. سخنران فقط یک بار و به یک زبان حرف می‌زند؛ اتاقکِ مترجمانِ همزمان همان یک جمله را به هر زبانی که لازم است برمی‌گردانند. Micrometer همان اتاقکِ مترجم است: کدِ تو یک بار و به «زبانِ Micrometer» متریک تولید می‌کند، و Micrometer پشتِ صحنه همان را به زبانِ Prometheus یا Datadog یا هر back-endِ دیگری ترجمه می‌کند. کدت را عوض نمی‌کنی؛ فقط وابستگیِ (dependency) مربوط به آن back-end را عوض می‌کنی.

اگر فصلِ لاگ‌گیری را خوانده باشی، این الگو برایت آشناست: SLF4J یک نما (facade) روی Logback و Log4j2 بود. Micrometer دقیقاً همان نقش را برای متریک‌ها بازی می‌کند — یک نمای بی‌طرفِ فروشنده (vendor-neutral). خودِ تیمِ Micrometer با همین جملهٔ «SLF4J for metrics» معرفی‌اش می‌کند.

دو مفهومِ کلیدیِ Micrometer:

  • Meter (سنجه): واحدِ اندازه‌گیری — یک شمارنده، یک عقربه، یک زمان‌سنج. هر «چیزی که اندازه می‌گیری» یک meter است.
  • MeterRegistry (ثبت‌گاهِ سنجه‌ها): کارخانه و انبارِ همهٔ meterها. تو meterها را از registry می‌سازی، و نوعِ registry تعیین می‌کند خروجی به کدام سیستم برود. یک PrometheusMeterRegistry خروجی را برای Prometheus آماده می‌کند؛ یک SimpleMeterRegistry فقط در حافظه نگه می‌دارد (خوب برای تست).

در Spring Boot تقریباً هیچ‌کدام از اینها را دستی نمی‌سازی — auto-configuration یک registry سراسری برایت می‌سازد. کافی است این وابستگی‌ها را اضافه کنی:

<!-- Actuator اندپوینت‌های مدیریتی از جمله متریک را فراهم می‌کند -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- این وابستگی «زبانِ خروجی» را Prometheus می‌کند -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

فقط با وجودِ همین وابستگیِ دوم، Spring Boot یک اندپوینتِ /actuator/prometheus می‌سازد که Prometheus می‌تواند از آن متریک بخواند. اگر فردا خواستی به Datadog بروی، وابستگیِ micrometer-registry-prometheus را با micrometer-registry-datadog عوض می‌کنی و کدِ instrumentationِ تو دست‌نخورده می‌ماند. این کلِ ارزشِ نما است.

نکتهٔ نسخه‌ها

Micrometer پروژهٔ مستقلی زیرِ چترِ Spring است و مرتب نسخه می‌دهد (شاخهٔ ۱.x). دو نوع registry برای Prometheus وجود دارد: micrometer-registry-prometheus که روی کلاینتِ جدیدِ Prometheus (نسخهٔ ۱.x) سوار است، و نسخهٔ قدیمیِ -simpleclient که روی کلاینتِ legacy (نسخهٔ ۰.x) کار می‌کند. برای پروژهٔ جدید همان اولی را بردار. اگر از Spring Boot 3 استفاده می‌کنی، مدیریتِ نسخه (BOM) خودش سازگارترین نسخهٔ Micrometer را برایت می‌آورد؛ لازم نیست نسخه را دستی بنویسی.


بخش سه — چهار نوع سنجه (Meter) که باید بلد باشی

Micrometer چند نوع meter دارد، اما چهار تا ستونِ اصلی‌اند. تفاوتشان را با یک تشبیه بگیر، بعد کدش را ببین.

کنتور برق، درجهٔ باک بنزین، کرنومتر و ترازوی آشپزخانه

چهار جنس اندازه‌گیری داری. کنتورِ برق فقط بالا می‌رود و هرگز پایین نمی‌آید — این Counter است. درجهٔ باکِ بنزین بالا و پایین می‌رود و یک عددِ لحظه‌ای را نشان می‌دهد — این Gauge است. کرنومتر مدت‌زمانِ یک رویداد را می‌گیرد و بعد هم می‌گوید «چند بار گرفتی و مجموعش چقدر شد» — این Timer است. و ترازوی آشپزخانه که وزنِ چیزهای متفاوت را می‌سنجی و توزیعِ آن اندازه‌ها برایت مهم است — این DistributionSummary است.

۱) Counter — فقط بالا می‌رود

یک شمارنده که صرفاً افزایشی (monotonically increasing) است؛ یعنی هرگز کم نمی‌شود، فقط زیاد می‌شود یا صفر می‌ماند (تا زمانِ ری‌استارتِ برنامه). برای شمردنِ «چند بار یک اتفاق افتاد» به کار می‌رود: تعدادِ درخواست، تعدادِ خطا، تعدادِ سفارشِ ثبت‌شده.

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;

public class OrderService {
    private final Counter ordersPlaced;

    public OrderService(MeterRegistry registry) {
        this.ordersPlaced = Counter.builder("orders.placed")   // نامِ متریک
                .description("تعداد کل سفارش‌های ثبت‌شده")
                .tag("channel", "web")                          // یک بُعد (dimension)
                .register(registry);
    }

    public void placeOrder(Order order) {
        // ... منطق ثبت سفارش ...
        ordersPlaced.increment();   // یک واحد بالا برو
    }
}
هرگز نرخ (rate) را خودت ذخیره نکن

وسوسه می‌شوی که یک متریکِ «درخواست بر ثانیه» بسازی و خودت نرخ را حساب کنی. این کار غلط است. فلسفهٔ Counter این است که تو فقط عددِ خامِ مجموع را ذخیره کنی، و محاسبهٔ نرخ را به سیستمِ query (مثل PromQL با تابعِ rate()) بسپاری. چرا؟ چون نرخ به بازهٔ زمانی وابسته است و اگر خودت آن را از قبل محاسبه و ذخیره کنی، دیگر نمی‌توانی بعداً بازه را عوض کنی. مجموعِ خام همه‌چیز را نگه می‌دارد؛ نرخ یک دیدگاه از آن است. اصل: خام ذخیره کن، در زمانِ خواندن جمع/مشتق بگیر.

۲) Gauge — عقربه‌ای که بالا و پایین می‌رود

یک مقدارِ لحظه‌ای که در هر لحظه می‌تواند بالا یا پایین باشد: تعدادِ آیتم‌های یک صف، تعدادِ اتصالِ فعالِ دیتابیس، درصدِ مصرفِ حافظه. نکتهٔ مهم: gauge را مشاهده می‌کنی، نه اینکه دستی مقدارش را ست کنی. تو به Micrometer یک شیء و یک تابع می‌دهی، و Micrometer هر بار که خواست متریک را بخواند، آن تابع را صدا می‌زند تا مقدارِ فعلی را بگیرد.

import io.micrometer.core.instrument.Gauge;
import java.util.Queue;

Queue<Task> pending = ...;

Gauge.builder("queue.size", pending, Queue::size)  // شیء + تابعی که مقدار را می‌دهد
        .description("تعداد کارهای منتظر در صف")
        .register(registry);
دامِ نگه‌داشتِ مرجعِ قوی (memory leak)

Micrometer عمداً به شیءِ داده‌شده به gauge یک ارجاعِ ضعیف (weak reference) نگه می‌دارد — یعنی اگر بقیهٔ برنامه دیگر آن شیء را نخواهند، garbage collector می‌تواند آن را جمع کند و gauge مقدارِ NaN برمی‌گرداند. تازه‌کارها اغلب یک شیءِ موقت را به gauge می‌دهند، آن شیء جمع می‌شود و gauge «می‌میرد». اطمینان حاصل کن که خودِ برنامه هم به آن شیء ارجاعِ زنده‌ای دارد. و هرگز gauge را روی متغیّری بساز که مدام با یک شیءِ جدید جایگزین می‌شود؛ gauge به همان شیءِ اولیه چسبیده می‌ماند.

۳) Timer — کرنومتر برای رویدادهای کوتاه

مهم‌ترین meter برای مانیتورینگِ کارایی. یک Timer هم‌زمان دو چیز را ثبت می‌کند: تعدادِ دفعاتی که رویداد رخ داد و مجموعِ مدت‌زمان‌ها. از این دو، سیستمِ query می‌تواند میانگین، توان عملیاتی (throughput) و — اگر histogram روشن باشد — صدک‌ها را بسازد.

import io.micrometer.core.instrument.Timer;

Timer requestTimer = Timer.builder("http.server.requests")
        .description("مدت پردازش درخواست")
        .tag("uri", "/checkout")
        .publishPercentileHistogram()   // سطل‌های هیستوگرام را منتشر کن (برای صدک سمت سرور)
        .register(registry);

// روش ۱: با یک بلاکِ کد
String result = requestTimer.record(() -> handleCheckout());

// روش ۲: دستی با Timer.Sample برای وقتی شروع و پایان جدا هستند
Timer.Sample sample = Timer.start(registry);
try {
    handleCheckout();
} finally {
    sample.stop(requestTimer);   // مدت را از لحظهٔ start تا اینجا ثبت می‌کند
}

در Spring، به‌جای نوشتنِ دستیِ همهٔ اینها، می‌توانی از annotationِ @Timed استفاده کنی تا یک متد را خودکار زمان‌سنجی کنی:

import io.micrometer.core.annotation.Timed;

@Timed(value = "checkout.duration", description = "مدت فرآیند تسویه", percentiles = {0.5, 0.95, 0.99})
public Receipt checkout(Cart cart) {
    // ...
}

۴) DistributionSummary — توزیعِ اندازه‌ها (نه زمان)

درست مثل Timer است، اما برای اندازه‌گیریِ چیزی که واحدش زمان نیست: حجمِ payloadِ یک درخواست به بایت، تعدادِ آیتم‌های یک سبدِ خرید، اندازهٔ یک batch. همان امکاناتِ توزیع و صدک را دارد.

import io.micrometer.core.instrument.DistributionSummary;

DistributionSummary payloadSize = DistributionSummary.builder("http.request.size")
        .baseUnit("bytes")
        .publishPercentiles(0.5, 0.95, 0.99)   // صدک‌های سمتِ کلاینت را حساب کن
        .register(registry);

payloadSize.record(requestBody.length);
یک meterِ کمترشناخته اما پرکاربرد: LongTaskTimer

Timer فقط بعد از پایان رویداد مدت را ثبت می‌کند. اما اگر یک کار همین الان در حال اجراست و می‌خواهی بدانی «چند تا کارِ طولانی هم‌زمان در جریان‌اند و هرکدام چقدر است که شروع شده»، به LongTaskTimer نیاز داری. مثال: یک batchِ سنگین که یک ساعت طول می‌کشد؛ با Timer معمولی تا ساعتِ بعد هیچ چیزی نمی‌بینی، اما LongTaskTimer همین حالا نشانت می‌دهد که کاری در جریان است و چند دقیقه از آن گذشته. برای jobهای طولانی حیاتی است.


بخش چهار — Tagها: از متریکِ تخت تا متریکِ چندبُعدی

تا اینجا متریک‌هایمان یک عددِ ساده بودند. اما قدرتِ واقعیِ سیستم‌های مدرن در ابعاد (dimensions) است. یک tag (که Prometheus به آن label می‌گوید) یک جفتِ کلید-مقدار است که به متریک می‌چسبد و اجازه می‌دهد بعداً آن را برش بزنی.

برچسب روی جعبه‌های انبار

تصور کن یک انبار داری با هزاران جعبه. اگر روی همه فقط بنویسی «کالا»، فقط می‌توانی بگویی «کلاً چند جعبه داریم». اما اگر روی هر جعبه چند برچسب بزنی — «شهر: تهران، دسته: لوازم‌خانگی، وضعیت: ارسال‌شده» — حالا می‌توانی هر سؤالی بپرسی: «چند جعبهٔ لوازم‌خانگی در تهران که هنوز ارسال نشده؟». tagها روی متریک دقیقاً همین برچسب‌ها هستند: یک متریکِ واحدِ http.server.requests را با tagهای method، uri و status می‌توانی از هر زاویه‌ای ببُری.

// یک متریک، اما چند بُعد — همان اسم با ترکیب‌های مختلفِ tag
registry.counter("http.server.requests",
        "method", request.getMethod(),      // GET / POST / ...
        "uri", request.getUri(),            // /checkout / /cart / ...
        "status", response.getStatus())     // 200 / 404 / 500 ...
    .increment();

هر ترکیبِ یکتای مقدارِ tagها یک time series جداگانه می‌سازد. یعنی {method=GET, uri=/cart, status=200} یک سری است و {method=POST, uri=/cart, status=500} سریِ دیگری. و اینجا دقیقاً همان دامی است که در بخشِ صفر هشدارش را دادم.

انفجارِ Cardinality — گران‌ترین اشتباهِ observability

تعدادِ کلِ time seriesها برابر است با حاصل‌ضربِ تعدادِ مقدارهای ممکنِ هر tag. اگر method سه مقدار و status پنج مقدار داشته باشد، ۱۵ سری داری — عالی. اما اگر روی همان متریک یک tag با userId بگذاری و یک میلیون کاربر داشته باشی، ناگهان میلیون‌ها سری داری و حافظهٔ Prometheus منفجر می‌شود، query‌ها کُند می‌شوند و هزینه سر به فلک می‌کشد. این را cardinality explosion می‌گویند. قانونِ طلایی: هرگز چیزی با کاردینالیتیِ نامحدود یا خیلی بالا را به‌عنوان tag نگذار — نه userId، نه email، نه یک URLِ خام که شناسه دارد (/order/12345)، نه پیامِ خطای آزاد. اینها را در لاگ یا ترِیس بگذار، نه در tagِ متریک. tag برای چیزهای کم‌مقدار و محدود است: متد، وضعیت، نامِ مسیرِ الگو (/order/{id}، نه /order/12345).

چرا این‌قدر مهم است که در مصاحبه بارها می‌پرسند

متریک‌ها ارزان‌اند به‌شرطی که کاردینالیتیشان مهار شده باشد. مدلِ ذخیره‌سازیِ Prometheus هر time series یکتا را جداگانه در حافظه نگه می‌دارد. یک tagِ پرکاردینالیتی می‌تواند ظرفِ چند ساعت کلِ کلاسترِ مانیتورینگ را از پا دربیاورد — پدیده‌ای که به آن «cardinality bomb» می‌گویند. تشخیصِ اینکه چه چیزی tagِ خوب است و چه چیزی نیست، مرزِ بینِ کسی است که «کد نوشته» و کسی که «سیستمِ production را نگه داشته».


بخش پنج — Prometheus: مدلِ Pull و ساختِ صدک

حالا متریک تولید می‌کنیم؛ اما چه کسی آنها را جمع و ذخیره می‌کند؟ رایج‌ترین جواب در دنیای cloud-native، Prometheus است — یک سیستمِ متن‌بازِ پایگاه‌دادهٔ سری‌زمانی (time-series database) که زیرِ چترِ CNCF توسعه می‌یابد.

پستچی که سراغِ خانه‌ها می‌رود، نه خانه‌ها که نامه می‌فرستند

دو مدلِ جمع‌آوریِ داده وجود دارد. در مدلِ push، هر برنامه خودش داده‌اش را «هُل می‌دهد» به سمتِ سرورِ مرکزی — مثل اینکه هر خانه نامه‌اش را به پست بیندازد. در مدلِ pull، یک جمع‌کنندهٔ مرکزی خودش دوره‌ای سراغِ هر برنامه می‌رود و می‌گوید «متریک‌هایت را بده» — مثل پستچی‌ای که خودش در می‌زند و نامه را می‌گیرد. Prometheus مدلِ pull را انتخاب کرده: هر چند ثانیه یک‌بار سراغِ اندپوینتِ /actuator/prometheusِ برنامه‌ات می‌رود و همه‌چیز را می‌خواند. به این کار scraping (خراشیدن/برداشت) می‌گویند.

مزیتِ pull این است که Prometheus خودش می‌داند چه چیزی را باید scrape کند (service discovery)، و اگر برنامه‌ای جواب ندهد، همان «جواب‌ندادن» خودش یک سیگنالِ سلامت است (آن instance «down» است). یک کانفیگِ ساده:

# prometheus.yml — به Prometheus می‌گوید کجا و هر چند وقت scrape کند
global:
  scrape_interval: 15s        # هر ۱۵ ثانیه یک‌بار برداشت کن

scrape_configs:
  - job_name: 'order-service'
    metrics_path: '/actuator/prometheus'   # همان اندپوینتی که Micrometer ساخت
    static_configs:
      - targets: ['order-service:8080']

وقتی Prometheus آن اندپوینت را می‌خواند، یک متنِ ساده می‌بیند (به آن exposition format می‌گویند). چیزی شبیه این:

# HELP http_server_requests_seconds  مدت پردازش درخواست
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{uri="/checkout",le="0.05"} 24
http_server_requests_seconds_bucket{uri="/checkout",le="0.1"}  57
http_server_requests_seconds_bucket{uri="/checkout",le="0.5"}  92
http_server_requests_seconds_bucket{uri="/checkout",le="+Inf"} 100
http_server_requests_seconds_count{uri="/checkout"} 100
http_server_requests_seconds_sum{uri="/checkout"}   12.4
چرا اسمِ متریک عوض شد؟

حواست باشد نامی که در جاوا نوشتیم http.server.requests بود اما اینجا شد http_server_requests_seconds. این کارِ Micrometer است: هر registry نام‌ها را به قراردادِ back-endِ خودش ترجمه می‌کند. Prometheus نقطه را نمی‌پسندد و به زیرخط تبدیل می‌کند، واحدِ پایه (seconds) را به نام می‌چسباند، و به شمارنده‌ها پسوندِ _total اضافه می‌کند. این دقیقاً همان «مترجمِ همزمان» است که در بخشِ Micrometer گفتیم — تو یک بار به زبانِ Micrometer نوشتی، خروجی به زبانِ Prometheus ترجمه شد.

هیستوگرام: چطور از تک‌تکِ زمان‌ها، صدک می‌سازیم

اینجا مهم‌ترین ایدهٔ فنیِ Prometheus را باید بفهمی: چطور بدونِ نگه‌داشتنِ تک‌تکِ اندازه‌ها، می‌شود صدکِ ۹۹ام را حساب کرد؟

سطل‌های اندازهٔ کفش در یک کفش‌فروشی

تصور کن می‌خواهی توزیعِ سایزِ پای مشتری‌های یک کفش‌فروشی را بفهمی، اما نمی‌توانی سایزِ دقیقِ تک‌تکِ ده‌هزار مشتری را ذخیره کنی. راهِ هوشمندانه: چند سطل بگذار — «تا سایز ۳۸»، «تا سایز ۴۰»، «تا سایز ۴۲»، «تا هر سایزی». هر مشتری که آمد، شمارندهٔ همهٔ سطل‌هایی که سایزش «کوچک‌تر یا مساویِ» آنهاست را یکی زیاد کن. حالا فقط چند عددِ شمارنده داری، اما می‌توانی تخمین بزنی که مثلاً «۹۵٪ مشتری‌ها سایزشان زیرِ ۴۱ است». این دقیقاً کاری است که histogram در Prometheus می‌کند.

به آن le در خروجیِ بالا دقت کن: مخففِ less than or equal (کوچک‌تر یا مساوی). هر خطِ _bucket{le="0.1"} 57 یعنی «۵۷ درخواست، مدتشان کوچک‌تر یا مساویِ ۰.۱ ثانیه بود». اینها تجمعی (cumulative) هستند: سطلِ بزرگ‌تر شاملِ همهٔ کوچک‌ترها هم هست. حالا سیستمِ query می‌تواند با درون‌یابیِ بینِ سطل‌ها، صدک را تخمین بزند. در Prometheus این کار را تابعِ histogram_quantile انجام می‌دهد:

# صدکِ ۹۵ام مدتِ درخواست‌ها در ۵ دقیقهٔ گذشته
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))
هیستوگرام (سمت‌سرور) در برابر summary (سمت‌کلاینت) — تفاوتی که خیلی‌ها اشتباه می‌کنند

دو راه برای صدک داری و تفاوتشان حیاتی است. Histogram: برنامه فقط سطل‌های خام را می‌فرستد و محاسبهٔ صدک روی سرورِ Prometheus انجام می‌شود. مزیتِ بزرگ: می‌توانی صدکِ چند instance را با هم جمع (aggregate) کنی — چون سطل‌ها جمع‌پذیرند. Summary: برنامه خودش صدک‌ها را سمتِ کلاینت حساب می‌کند و عددِ آماده می‌فرستد. مشکل: صدک‌ها جمع‌پذیر نیستند — نمی‌توانی صدکِ ۹۹امِ سه سرور را با میانگین‌گرفتن به‌دست بیاوری (این کار از نظرِ ریاضی غلط است). قانون: در سیستمِ توزیع‌شده که چند instance داری، تقریباً همیشه histogram بردار، نه summary. در Micrometer، publishPercentileHistogram() سطل‌ها (histogram) را می‌دهد و publishPercentiles(...) صدکِ سمتِ کلاینت (summary) را.

هیستوگرام‌های native — نسلِ بعد

هیستوگرامِ کلاسیک یک ضعف دارد: باید سطل‌ها را از قبل دستی تعریف کنی و اگر بد بچینی‌شان دقت پایین می‌آید. Prometheus قابلیتِ تازه‌ای به نامِ native histograms (که قبلاً «sparse histograms» نامیده می‌شد) اضافه کرده که سطل‌ها را به‌صورتِ نمایی و خودکار می‌سازد؛ دقتِ بسیار بالاتر با حافظهٔ کمتر و بدونِ نیاز به تنظیمِ دستیِ سطل‌ها. هنوز نسبتاً جدید است و در حالِ بلوغ، اما جهتِ حرکتِ صنعت همین است. اگر در مصاحبه اسمش را ببری، نشان می‌دهی که جریان را دنبال می‌کنی.


بخش شش — PromQL و Grafana: از عدد تا تصمیم

دادهٔ خام به‌تنهایی بی‌فایده است؛ باید بتوانی از آن سؤال بپرسی. زبانِ query‌ زدن در Prometheus PromQL نام دارد. چند نمونهٔ پرتکرار که باید در انگشتانت باشد:

# نرخِ درخواست بر ثانیه (RED: Rate) — از یک counter مشتق می‌گیریم
sum(rate(http_server_requests_seconds_count[5m])) by (uri)

# درصدِ خطا (RED: Errors) — نسبتِ 5xx به کل
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
  /
sum(rate(http_server_requests_seconds_count[5m]))

# صدکِ ۹۹امِ تأخیر (RED: Duration)
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le))

به rate(...[5m]) دقت کن: چون counter فقط بالا می‌رود، عددِ خامش بی‌معناست؛ rate مشتقِ آن را در پنجرهٔ ۵ دقیقه‌ای می‌گیرد و به «بر ثانیه» تبدیل می‌کند. این همان چیزی است که در بخشِ Counter گفتم: خام ذخیره کن، در زمانِ query نرخ بگیر.

Prometheus آشپزخانه است، Grafana میزِ سِروِ غذا

Prometheus عالی است در جمع‌آوری و ذخیره و query، اما رابطِ کاربریِ زیبایی برای دیدنِ روندها ندارد. Grafana ابزارِ متن‌بازِ داشبورد و بصری‌سازی است که به Prometheus (و ده‌ها منبعِ دیگر) وصل می‌شود و آن اعداد را به نمودار، عقربه و جدولِ زنده تبدیل می‌کند. تقسیمِ کار: Prometheus آشپز است که غذا (داده) را می‌پزد و نگه می‌دارد؛ Grafana پیشخدمتی است که آن را زیبا روی میز می‌چیند تا انسان بتواند ببیند و تصمیم بگیرد. علاوه بر نمودار، هر دو می‌توانند alert بسازند: «اگر صدکِ ۹۹ام از ۵۰۰ms گذشت، به تیم پیام بده».


بخش هفت — Tracing توزیع‌شده: دنبال‌کردنِ ردِ یک درخواست

متریک‌ها به تو می‌گویند «۵٪ درخواست‌ها کُندند»، اما در یک معماریِ میکروسرویس که یک درخواستِ کاربر از ده سرویسِ مختلف عبور می‌کند، متریک نمی‌گوید کدام سرویس و کدام مرحله مقصر است. اینجا ستونِ سوم وارد می‌شود: distributed tracing.

ردیابیِ بستهٔ پستی بین‌المللی

یک بستهٔ پستی از تهران به توکیو می‌فرستی. صفحهٔ ردیابی نشانت می‌دهد: «تهران — تحویل به مرکز (۱۰ دقیقه)» ← «فرودگاه امام — گمرک (۳ ساعت!)» ← «توکیو — مرکز توزیع (۲۰ دقیقه)» ← «تحویل». حالا می‌بینی که کلِ تأخیر مالِ گمرک بود، نه پرواز. کلِ سفر یک trace است؛ هر مرحله (هر ایستگاه) یک span است. هر span می‌داند کِی شروع شد، کِی تمام شد، و به کدام مرحلهٔ والد تعلق دارد. distributed tracing دقیقاً همین صفحهٔ ردیابی را برای یک درخواستِ نرم‌افزاری می‌سازد.

بیایید واژه‌ها را دقیق کنیم:

  • Span (اسپن): یک واحدِ کارِ نام‌دار با زمانِ شروع و پایان — مثلاً «کوئری به دیتابیس» یا «فراخوانیِ سرویسِ پرداخت». هر span می‌تواند attribute (کلید-مقدارهای توصیفی)، event (لحظه‌های نقطه‌ای درونش) و یک status (موفق/ناموفق) داشته باشد.
  • Trace (ترِیس): مجموعهٔ همهٔ spanهای یک درخواست که به‌صورتِ یک درختِ والد-فرزند به هم وصل‌اند. ریشهٔ درخت اولین span است (مثلاً ورودِ درخواست به API gateway).
  • Trace context / انتشار (propagation): برای اینکه spanهای سرویس‌های مختلف بفهمند همه مالِ یک traceاند، هر سرویس هنگامِ صدا زدنِ سرویسِ بعدی، یک شناسهٔ مشترک را در هدرِ HTTP پاس می‌دهد. استانداردِ صنعتیِ این کار W3C Trace Context است که از یک هدر به نامِ traceparent استفاده می‌کند.

هدرِ traceparent چیزی شبیهِ این است و ساختارِ ساده اما دقیقی دارد:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             ^  ^                                ^                ^
          نسخه   trace-id (۱۶ بایت)          span-id (۸ بایت)   flags
چرا propagation قلبِ tracing است

جادوی tracing در همین «پاس دادنِ شناسهٔ مشترک» است. سرویسِ A یک trace-id می‌سازد؛ وقتی سرویسِ B را صدا می‌زند، همان trace-id را در هدرِ traceparent می‌گذارد؛ سرویسِ B هم برای صدا زدنِ C همان کار را می‌کند. در انتها، همهٔ spanها با یک trace-id مشترک به یک back-end (مثل Jaeger یا Tempo) فرستاده می‌شوند و آنجا با هم به یک درختِ واحد بازسازی می‌شوند. اگر حتی یک سرویس این هدر را پاس ندهد، زنجیره می‌شکند و trace نصفه‌کاره می‌ماند. این رایج‌ترین علتِ «trace ناقص» در production است.

نمونه‌برداری (sampling) — چرا همهٔ traceها را نگه نمی‌داریم

اگر برای هر تک‌درخواست یک traceِ کامل ذخیره کنی، حجمِ داده کمرشکن می‌شود. برای همین tracing معمولاً sampled است: فقط درصدی از traceها (مثلاً ۱٪ یا ۱۰٪) کامل نگه داشته می‌شوند. دو راهبردِ اصلی: head-based sampling که در همان ابتدا تصمیم می‌گیرد این trace نگه داشته شود یا نه (ساده و ارزان)، و tail-based sampling که تا پایانِ trace صبر می‌کند تا تصمیم بگیرد — مثلاً «همهٔ traceهایی که خطا داشتند یا کُند بودند را نگه دار» (باهوش‌تر اما گران‌تر، چون باید همه را موقتاً بافر کند). این تفاوت سؤالِ رایجِ مصاحبه است.


بخش هشت — OpenTelemetry: استانداردِ واحدِ صنعت

تا اینجا هر سیگنال ابزار و استانداردِ خودش را داشت. تاریخاً این هرج‌ومرج بود: Zipkin و Jaeger و OpenTracing و OpenCensus هر کدام API خودشان. مشکل: اگر کدت را با API یک فروشنده instrument می‌کردی، به آن قفل می‌شدی (vendor lock-in).

USB-C برای تلمتری

یادت هست هر گوشی یک شارژرِ متفاوت داشت؟ آیفون یک جور، هر اندروید یک جور، و کابل‌ها با هم ناسازگار. بعد USB-C آمد و شد استانداردِ واحد: یک درگاه برای همه‌چیز. OpenTelemetry (که کوتاه‌شده‌اش OTel است) دقیقاً USB-Cِ دنیای observability است — یک API و پروتکلِ واحد و بی‌طرفِ فروشنده برای هر سه سیگنال (metrics, logs, traces). کدت را یک بار با OTel instrument می‌کنی، و بعد می‌توانی خروجی را به هر back-endی (Jaeger، Prometheus، Grafana، Datadog، ...) بفرستی — بدونِ عوض‌کردنِ کد.

OpenTelemetry پروژه‌ای زیرِ چترِ CNCF است (حاصلِ ادغامِ OpenTracing و OpenCensus) و امروز به استانداردِ واقعیِ صنعت تبدیل شده. اجزای کلیدی‌اش:

  • API: رابطی که کدت با آن instrument می‌شود (span می‌سازد، متریک ثبت می‌کند). سبک و بدونِ وابستگیِ سنگین.
  • SDK: پیاده‌سازیِ واقعیِ API — نمونه‌برداری، پردازش، و صادر کردن (export).
  • OTLP (OpenTelemetry Protocol): پروتکلِ واحدی که تلمتری را از برنامه به back-end می‌فرستد. زبانِ مشترکِ همه.
  • Collector: یک سرویسِ واسط که تلمتری را از برنامه‌ها می‌گیرد، پردازش/فیلتر/غنی می‌کند، و به یک یا چند back-end می‌فرستد. مثلِ یک هابِ مرکزی.
  • Semantic Conventions (قراردادهای معنایی): توافقی استاندارد بر سرِ نام‌ها. مثلاً همه بر سرِ اینکه متدِ HTTP در attributeی به نامِ http.request.method ذخیره شود توافق کرده‌اند. این یعنی داشبوردها و ابزارها بینِ سازمان‌ها قابلِ‌حمل می‌شوند.
وضعیتِ بلوغِ سیگنال‌ها در OTel

همهٔ سیگنال‌ها هم‌زمان بالغ نشدند. Tracing اولین سیگنالی بود که به پایداری (stable) رسید و کاملاً production-ready است. Metrics هم پایدار شده. Logs دیرتر آمد و حول‌وحوشِ همان سطح پایدار شده. یک سیگنالِ چهارم، Profiles (نمایهٔ مصرفِ CPU/حافظه)، تازه‌واردِ در حالِ توسعه است. کتابخانه‌های جاوای OTel در شاخهٔ ۱.x به‌سرعت نسخه می‌دهند و artifactهای «stable» گارانتیِ سازگاریِ رو به عقب دارند، در حالی که قراردادهای آزمایشی در artifactهایی با پسوندِ -alpha جدا نگه داشته می‌شوند تا کدِ تولیدی به چیزِ ناپایدار وصل نشود.

دو راهِ instrument کردن با OTel در جاوا

راهِ اول — Java agent (بدونِ تغییرِ کد): OpenTelemetry یک Java agent دارد که هنگامِ اجرا به JVM می‌چسبد و ده‌ها کتابخانهٔ رایج (Spring، JDBC، Kafka، HTTP client...) را خودکار instrument می‌کند. کافی است یک flag به اجرا اضافه کنی:

java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.service.name=order-service \
     -Dotel.exporter.otlp.endpoint=http://collector:4317 \
     -jar order-service.jar

راهِ دوم — instrumentation دستی با API: وقتی می‌خواهی span‌های سفارشی برای منطقِ کسب‌وکارِ خودت بسازی:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

Tracer tracer = openTelemetry.getTracer("order-service");

Span span = tracer.spanBuilder("validate-inventory").startSpan();
try (var scope = span.makeCurrent()) {
    span.setAttribute("order.id", orderId);      // attribute توصیفی
    boolean ok = inventory.check(orderId);
    span.setAttribute("inventory.available", ok);
} catch (Exception e) {
    span.recordException(e);                      // خطا را روی span ثبت کن
    throw e;
} finally {
    span.end();                                   // بدونِ این، span هرگز بسته نمی‌شود!
}

پُلِ Micrometer و OpenTelemetry: Observation API

اینجا زیبایی معماریِ Spring Boot 3 آشکار می‌شود. Micrometer یک Observation API معرفی کرد که یک لایهٔ instrumentation است که تو یک بار می‌نویسی و هم‌زمان متریک و ترِیس (و کمکِ همبستگیِ لاگ) تولید می‌کند.

یک بار عکس بگیر، همه‌جا چاپش کن

قبلاً برای متریک یک بار کد می‌نوشتی، برای trace بارِ دیگر — دو بار کارِ تکراری برای یک رویداد. Observation API مثلِ این است که یک بار از یک صحنه عکس بگیری و بعد همان عکس را هم در آلبوم (متریک) بچسبانی، هم قاب کنی (trace)، هم در روزنامه چاپ کنی (لاگ). یک instrumentation، چند خروجی.

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;

Observation.createNotStarted("order.checkout", observationRegistry)
    .lowCardinalityKeyValue("channel", "web")    // به‌عنوان tagِ متریک هم می‌رود (کم‌کاردینال)
    .highCardinalityKeyValue("orderId", orderId) // فقط به‌عنوان attributeِ span (پرکاردینال)
    .observe(() -> processCheckout(order));
نبوغِ تفکیکِ low/high cardinality در Observation API

به دو متدِ بالا دقت کن — این طراحی دقیقاً درسِ بخشِ cardinality را در خودِ API نهادینه کرده. lowCardinalityKeyValue هم به متریک می‌رود هم به trace (چون کم‌کاردینال است و متریک را منفجر نمی‌کند). اما highCardinalityKeyValue فقط به span/trace می‌رود و هرگز به‌عنوان tagِ متریک استفاده نمی‌شود. این یعنی API خودش تو را از انفجارِ cardinality محافظت می‌کند: orderId را به‌درستی در trace می‌گذارد (جایی که پرکاردینالی مشکلی نیست) اما از متریک دورش نگه می‌دارد. طراحیِ فوق‌العاده‌ای که فلسفهٔ سه ستون را در یک API جمع کرده.


بخش نُه — چه چیزی را اندازه بگیریم؟ RED، USE و سیگنال‌های طلایی

instrument کردن آسان است؛ دانستنِ اینکه چه چیزی ارزشِ اندازه‌گیری دارد هنر است. سه چارچوبِ استاندارد در صنعت وجود دارد که باید بلد باشی.

معاینهٔ پزشک: نبض، فشار، دما

پزشک برای ارزیابیِ سریعِ هر بیمار همیشه سراغِ چند علامتِ حیاتیِ ثابت می‌رود: نبض، فشار، دما، تنفس. مجبور نیست هر بار همه‌چیز را از نو اختراع کند؛ یک چک‌لیستِ استاندارد دارد. RED و USE هم دقیقاً همان چک‌لیست‌های استانداردِ «علائمِ حیاتیِ سیستم» هستند — به‌جای اینکه سردرگم باشی چه اندازه بگیری، از یک الگوی اثبات‌شده پیروی می‌کنی.

چارچوب برای چه چیزی سه/چهار عددی که می‌گیری ابداع‌کننده
RED سرویس‌های درخواست‌محور (API، وب) Rate (نرخِ درخواست)، Errors (نرخِ خطا)، Duration (تأخیر) Tom Wilkie
USE منابع (CPU، دیسک، حافظه، صف) Utilization (بهره‌وری)، Saturation (اشباع/صف)، Errors (خطا) Brendan Gregg
Four Golden Signals سرویس‌ها (کتابِ SRE گوگل) Latency، Traffic، Errors، Saturation Google SRE
  • RED برای هر چیزی که «درخواست» می‌گیرد عالی است. سه عددِ ساده که ۹۰٪ سلامتِ یک API را نشان می‌دهند. همان سه query PromQL که در بخشِ شش نوشتیم دقیقاً RED بودند.
  • USE برای منابع است، نه سرویس‌ها. برای هر منبع بپرس: چقدر مشغول است (utilization)؟ چقدر کارِ منتظر دارد (saturation، مثلِ طولِ صف)؟ چند خطا داد؟
  • Four Golden Signals نسخهٔ گوگلی است؛ عملاً ترکیبی از RED و USE با تأکید بر latency.
چطور از این‌ها در مصاحبه استفاده کنی

اگر مصاحبه‌گر بپرسد «یک سرویسِ جدید ساختی، چه چیزی مانیتور می‌کنی؟»، جوابِ حرفه‌ای این نیست که رندوم چند متریک نام ببری. بگو: «برای خودِ سرویس RED را می‌گیرم — نرخ، خطا، تأخیر (با صدکِ ۹۵ و ۹۹، نه میانگین)؛ و برای منابعِ زیرین USE را — بهره‌وریِ CPU، اشباعِ thread pool و صفِ دیتابیس». این جواب نشان می‌دهد چارچوبِ ذهنیِ ساختاریافته داری، نه فهرستِ تصادفی.

میانگین دروغ می‌گوید؛ همیشه صدک بگیر

یک خطای کلاسیک: مانیتور کردنِ میانگینِ تأخیر. میانگین کاربرانِ بدبخت را پنهان می‌کند. اگر ۹۹ درخواست ۱۰ms و یک درخواست ۵ ثانیه طول بکشد، میانگین حدودِ ۶۰ms است — کاملاً سالم به‌نظر می‌رسد، در حالی که یک کاربر ۵ ثانیه منتظر مانده. همیشه صدک بگیر: p50 (میانه، کاربرِ معمولی)، p95 و p99 (کاربرانِ بدشانس). قانون: میانگین برای تأخیر تقریباً همیشه بی‌فایده یا گمراه‌کننده است. برای همین بود که این‌قدر روی هیستوگرام و histogram_quantile تأکید کردیم.


بخش ده — SLI، SLO و SLA: زبانِ قابل‌اتکا بودن

آخرین قطعهٔ پازل: چطور دربارهٔ «چقدر باید سیستم قابل‌اتکا باشد» به‌شکلِ عددی و توافقی حرف بزنیم. سه سرواژهٔ نزدیک اما متفاوت که مدام قاطی می‌شوند.

سرعت‌سنج، هدفِ شخصی، و قرارداد با کارفرما

تصور کن رانندهٔ حرفه‌ای هستی. سرعت‌سنجِ ماشین عددِ واقعیِ لحظه‌ای را نشان می‌دهد — این SLI است (Indicator، شاخصِ اندازه‌گیری‌شده). هدفی که برای خودت گذاشته‌ای («سعی می‌کنم زیرِ ۴ ساعت برسم») — این SLO است (Objective، هدفِ داخلی). و قراردادی که با کارفرما بسته‌ای («اگر دیرتر از ۵ ساعت برسم، جریمه می‌شوم») — این SLA است (Agreement، توافقِ رسمی با پیامدِ حقوقی/مالی). ترتیبِ سخت‌گیری: SLI اندازه می‌گیرد، SLO هدفِ درونیِ سخت‌گیرانه‌تر است، و SLA سقفِ بیرونیِ نرم‌تر با جریمه.

بیایید دقیق تعریف کنیم:

  • SLI (Service Level Indicator): یک عددِ اندازه‌گیری‌شده دربارهٔ کیفیتِ سرویس. مثال: «درصدِ درخواست‌هایی که در کمتر از ۳۰۰ms و بدونِ خطا پاسخ گرفتند». این چیزی است که واقعاً اندازه می‌گیری — و همان‌طور که دیدی، از متریک‌ها می‌آید.
  • SLO (Service Level Objective): یک هدفِ داخلی روی آن SLI. مثال: «۹۹.۹٪ درخواست‌ها باید موفق باشند، در پنجرهٔ ۳۰ روزه». این خطِ قرمزِ تیمِ توسعه است.
  • SLA (Service Level Agreement): یک قراردادِ رسمی با مشتری که اگر نقض شود پیامدِ مالی/حقوقی دارد (مثلِ برگشتِ پول). SLA همیشه از SLO سست‌تر است تا حاشیهٔ امن داشته باشی.
بودجهٔ خطا (Error Budget) — پُلِ بینِ پایداری و سرعتِ توسعه

از SLO یک مفهومِ فوق‌العاده کاربردی بیرون می‌آید. اگر SLO تو «۹۹.۹٪ موفقیت» است، پس اجازه داری ۰.۱٪ خطا داشته باشی. به آن ۰.۱٪ می‌گویند بودجهٔ خطا (error budget) — سهمیهٔ مجازِ خرابی. این نبوغِ فرهنگِ SRE گوگل است: تا وقتی بودجهٔ خطا مصرف نشده، تیم می‌تواند سریع feature بدهد و ریسک کند؛ اما اگر بودجه ته کشید (خرابیِ زیاد)، همه‌چیز متوقف می‌شود و تمرکز فقط روی پایداری می‌رود. بودجهٔ خطا دعوای همیشگیِ «پایداری در برابر سرعت» را از بحثِ سلیقه‌ای به یک عددِ عینی تبدیل می‌کند. اگر این را در مصاحبه توضیح دهی، بلافاصله چند سطح بالاتر دیده می‌شوی.

۹۹.۹٪ یعنی چقدر خرابیِ مجاز؟

تعدادِ نُه‌ها را حس کن: «سه نُه» (۹۹.۹٪) یعنی حدودِ ۴۳ دقیقه خرابیِ مجاز در ماه. «چهار نُه» (۹۹.۹۹٪) یعنی حدودِ ۴.۳ دقیقه در ماه. «پنج نُه» (۹۹.۹۹۹٪) یعنی حدودِ ۲۶ ثانیه در ماه — بسیار گران و سخت. هر نُهِ اضافه هزینه را چند برابر می‌کند، پس SLO را واقع‌بینانه انتخاب کن؛ «پنج نُه» برای یک وبلاگ اتلافِ منابع است.


بخش یازده — دام‌ها و best practiceها

بگذار مهم‌ترین تله‌ها را یک‌جا جمع کنم — همان‌هایی که در مصاحبه یا کدِ واقعی گاز می‌گیرند:

  • انفجارِ cardinality: هرگز userId، ایمیل، URLِ خام یا پیامِ خطای آزاد را به‌عنوان tagِ متریک نگذار. اینها را در لاگ/ترِیس بگذار. tag فقط برای مقدارهای محدود و شمارش‌پذیر.
  • میانگین به‌جای صدک: میانگینِ تأخیر کاربرانِ کُند را پنهان می‌کند. p95/p99 بگیر.
  • نرخِ از پیش‌محاسبه‌شده: counter را خام ذخیره کن و نرخ را با rate() در query بگیر؛ خودت نرخ نساز.
  • summary در سیستمِ توزیع‌شده: صدکِ سمتِ‌کلاینت (summary) جمع‌پذیر نیست؛ برای چند instance از histogram استفاده کن.
  • span که بسته نمی‌شود: فراموش کردنِ span.end() در finally، span را برای همیشه باز می‌گذارد و trace را خراب می‌کند.
  • شکستنِ زنجیرهٔ propagation: اگر یک سرویس هدرِ traceparent را پاس ندهد، trace نصفه می‌ماند.
  • gauge روی شیءِ موقت: ارجاعِ ضعیفِ gauge باعث می‌شود اگر شیء garbage شود، gauge بمیرد و NaN بدهد.
  • قفل‌شدن به فروشنده: مستقیم به API یک vendor کد نزن؛ از نمای Micrometer/OpenTelemetry استفاده کن تا مهاجرت ممکن بماند.

best practiceها:

  • کدت را یک بار با OpenTelemetry یا Micrometer Observation API instrument کن تا هم متریک هم trace بگیری.
  • نامِ متریک‌ها را طبقِ قراردادِ semantic conventionsِ OTel بگذار تا داشبوردها قابلِ‌حمل باشند.
  • سه ستون را با هم ببند: از متریک هشدار بگیر، با trace محل را پیدا کن، با لاگ جزئیات را ببین.
  • SLO تعریف کن و روی error budget مانیتور بگذار، نه فقط روی «سرور بالا/پایین».
  • برای پروژهٔ جدید، OTLP و Collector را از روزِ اول بگذار تا back-end قابلِ‌تعویض باشد.

سؤالات مصاحبه

حالا وقتِ محک زدن است. اول خودت به هر سؤال جواب بده، بعد جواب را باز کن.

۱) تفاوتِ monitoring و observability چیست؟

monitoring یعنی جمع‌آوری و هشدار روی مجموعه‌ای از متریک‌های از پیش تعریف‌شده — به سؤال‌هایی که از قبل می‌دانستی جواب می‌دهد («آیا CPU بالای ۸۰٪ است؟»). observability یعنی سیستم آن‌قدر داده دربارهٔ خودش بیرون می‌دهد که بتوانی سؤال‌های پیش‌بینی‌نشده و جدید بپرسی، بدونِ اینکه لازم باشد کدِ جدید deploy کنی. جملهٔ کلیدی: «monitoring می‌گوید چیزی خراب است؛ observability می‌گوید چرا». observability اَبَرمجموعهٔ monitoring است.

۲) سه ستونِ observability را نام ببر و بگو هر کدام به چه سؤالی جواب می‌دهد.

Metrics (اعدادِ جمع‌شونده در زمان): «چقدر؟ چند تا؟ روند به کدام سو؟» — ارزان و همیشه‌روشن. Logs (رویدادهای گسستهٔ متنی): «در آن لحظهٔ خاص دقیقاً چه شد؟» — پُرجزئیات و گران. Traces (مسیرِ یک درخواست بینِ سرویس‌ها): «کجای این سفر کُند/خراب شد؟». نکتهٔ ارشد: این سه مکمل‌اند؛ جریانِ کار این است که متریک هشدار می‌دهد، trace محل را نشان می‌دهد، لاگ جزئیات را.

۳) Micrometer چیست و چرا به آن «SLF4J برای متریک‌ها» می‌گویند؟

Micrometer یک نمای (facade) بی‌طرفِ فروشنده برای instrument کردنِ متریک‌ها در JVM است. کدت را یک بار به «زبانِ Micrometer» می‌نویسی و با عوض‌کردنِ فقط یک وابستگیِ registry (مثلاً prometheus به datadog)، خروجی به back-endِ دیگری می‌رود بدونِ تغییرِ کدِ instrumentation. دقیقاً همان الگوی SLF4J است که یک facade روی Logback/Log4j2 بود — به همین دلیل تیمِ Micrometer خودش را «SLF4J for metrics» معرفی می‌کند.

۴) چهار نوعِ اصلیِ meter را نام ببر و تفاوتِ Counter و Gauge را دقیق بگو.

Counter (فقط بالا می‌رود، برای شمردنِ رخداد)، Gauge (مقدارِ لحظه‌ای که بالا/پایین می‌رود، مثلِ طولِ صف)، Timer (مدتِ رویدادهای کوتاه + تعداد)، DistributionSummary (توزیعِ اندازه‌های غیرزمانی مثلِ حجمِ payload). تفاوتِ کلیدیِ Counter و Gauge: Counter صرفاً افزایشی است و مقدارِ خامش را با rate() به نرخ تبدیل می‌کنی؛ Gauge یک اسنپ‌شاتِ لحظه‌ای است که Micrometer با صدا زدنِ یک تابع می‌خواند و مقدارش می‌تواند کم شود. اشتباهِ رایج: چیزی که فقط بالا می‌رود را gauge کردن، یا برعکس.

۵) (مهم‌ترین سؤال) انفجارِ cardinality چیست و چطور از آن جلوگیری می‌کنی؟

هر ترکیبِ یکتای مقدارِ tagها یک time series جدا می‌سازد، و تعدادِ کلِ سری‌ها حاصل‌ضربِ کاردینالیتیِ tagهاست. اگر یک tag با مقدارهای نامحدود (مثلِ userId با میلیون‌ها مقدار) بگذاری، میلیون‌ها سری ساخته می‌شود، حافظهٔ Prometheus منفجر می‌شود و کلاسترِ مانیتورینگ از پا درمی‌آید — «cardinality bomb». راهِ جلوگیری: tag را فقط برای مقدارهای محدود و شمارش‌پذیر بگذار (متد، وضعیت، الگوی مسیرِ /order/{id} نه /order/12345). مقدارهای پرکاردینال (userId، email، خطای آزاد) را در لاگ یا trace بگذار، نه در tagِ متریک.

۶) تفاوتِ مدلِ pull و push در جمع‌آوریِ متریک چیست و Prometheus کدام را انتخاب کرده؟

در push برنامه خودش داده را به سرورِ مرکزی می‌فرستد؛ در pull جمع‌کنندهٔ مرکزی خودش دوره‌ای سراغِ برنامه می‌رود و متریک را می‌خواند (scrape). Prometheus مدلِ pull را انتخاب کرده و از اندپوینتِ برنامه (مثلِ /actuator/prometheus) می‌خواند. مزیت‌ها: خودِ Prometheus می‌داند چه چیزی را باید scrape کند (service discovery)، و اگر برنامه‌ای جواب ندهد همان «down بودن» یک سیگنالِ سلامتِ رایگان است. برای jobهای کوتاه‌عمر که فرصتِ scrape شدن ندارند، Prometheus یک Pushgateway هم دارد.

۷) چطور Prometheus بدونِ ذخیرهٔ تک‌تکِ اندازه‌ها صدکِ ۹۹ام را حساب می‌کند؟

با histogram. برنامه چند سطل (bucket) تجمعی می‌سازد؛ هر سطل با label‌ای به نامِ le (کوچک‌تر یا مساوی) می‌گوید «چند مشاهده کمتر یا مساویِ این مرز بودند». چون سطل‌ها تجمعی‌اند، تابعِ histogram_quantile(0.99, ...) در PromQL با درون‌یابیِ بینِ سطل‌ها صدک را روی سرور تخمین می‌زند. نتیجه: فقط چند عددِ شمارنده ذخیره می‌شود اما صدکِ کلِ توزیع تخمین‌پذیر است. دقتش به چیدمانِ سطل‌ها وابسته است (که native histograms این محدودیت را برطرف می‌کند).

۸) تفاوتِ histogram و summary در Prometheus چیست و کِی کدام را می‌گیری؟

Histogram: برنامه سطل‌های خام را می‌فرستد و محاسبهٔ صدک روی سرورِ Prometheus انجام می‌شود؛ چون سطل‌ها جمع‌پذیرند، می‌توانی صدکِ چند instance را با هم aggregate کنی. Summary: برنامه خودش صدک‌ها را سمتِ کلاینت حساب می‌کند و عددِ آماده می‌فرستد؛ اما این صدک‌ها جمع‌پذیر نیستند — میانگین‌گرفتنِ p99ِ سه سرور از نظرِ ریاضی غلط است. پس در هر سیستمِ توزیع‌شده با چند instance، histogram بگیر. summary فقط وقتی منطقی است که یک instance داری و به دقتِ صدکِ سمتِ‌کلاینت نیاز داری.

۹) span و trace را تعریف کن و بگو trace context چطور بینِ سرویس‌ها منتقل می‌شود.

span یک واحدِ کارِ نام‌دار با زمانِ شروع/پایان است (مثلِ یک کوئری به DB) که می‌تواند attribute، event و status داشته باشد. trace مجموعهٔ همهٔ spanهای یک درخواست است که به‌صورتِ یک درختِ والد-فرزند به هم وصل‌اند. انتقالِ context: هر سرویس هنگامِ صدا زدنِ سرویسِ بعدی یک شناسهٔ مشترک (trace-id) را در هدرِ HTTP پاس می‌دهد؛ استانداردِ صنعتی W3C Trace Context با هدرِ traceparent است (فرمت: نسخه-traceid-spanid-flags). اگر حتی یک سرویس این هدر را پاس ندهد، زنجیره می‌شکند و trace ناقص می‌ماند.

۱۰) تفاوتِ head-based و tail-based sampling چیست؟

چون ذخیرهٔ همهٔ traceها گران است، فقط درصدی نگه داشته می‌شود. head-based: تصمیمِ «نگه داریم یا نه» در همان ابتدای trace گرفته می‌شود (ساده و ارزان، اما ممکن است traceهای جالب مثلِ خطاها را از دست بدهی). tail-based: تا پایانِ trace صبر می‌کنی، بعد تصمیم می‌گیری — مثلاً «همهٔ traceهای دارای خطا یا کُند را نگه دار» (باهوش‌تر چون traceهای مهم را می‌گیرد، اما گران‌تر چون باید همه را موقتاً بافر کنی و معمولاً در Collector انجام می‌شود).

۱۱) OpenTelemetry چیست و چه مشکلی را حل کرد؟

OpenTelemetry (OTel) یک استانداردِ واحد و بی‌طرفِ فروشنده زیرِ چترِ CNCF برای هر سه سیگنال (metrics/logs/traces) است، حاصلِ ادغامِ OpenTracing و OpenCensus. مشکلی که حل کرد: قبلاً هر ابزار (Zipkin, Jaeger, ...) API خودش را داشت و instrument کردنِ کد با یکی، تو را به آن قفل می‌کرد (vendor lock-in). با OTel کدت را یک بار instrument می‌کنی و با پروتکلِ واحدِ OTLP خروجی را به هر back-endی می‌فرستی. اجزا: API، SDK، OTLP، Collector و semantic conventions (توافق بر سرِ نامِ استانداردِ attributeها).

۱۲) RED و USE چیستند و هر کدام کجا به کار می‌روند؟

دو چارچوبِ استاندارد برای «چه چیزی را اندازه بگیریم». RED (Rate, Errors, Duration) برای سرویس‌های درخواست‌محور مثلِ API است — سه عددی که سلامتِ یک سرویس را نشان می‌دهند. USE (Utilization, Saturation, Errors) برای منابع است مثلِ CPU، دیسک، thread pool و صف — برای هر منبع می‌پرسی چقدر مشغول، چقدر کارِ منتظر، چند خطا. یک نسخهٔ سومِ نزدیک، Four Golden Signalsِ گوگل (Latency, Traffic, Errors, Saturation) است. جوابِ حرفه‌ای در مصاحبه: RED برای سرویس، USE برای منابعِ زیرین.

۱۳) چرا مانیتور کردنِ میانگینِ تأخیر خطرناک است؟

چون میانگین کاربرانِ بدشانس را پنهان می‌کند. اگر ۹۹ درخواست ۱۰ms و یکی ۵ ثانیه باشد، میانگین حدودِ ۶۰ms و ظاهراً سالم است، در حالی که یک کاربر ۵ ثانیه رنج کشیده. راه‌حل: همیشه صدک بگیر — p50 (کاربرِ معمولی)، p95 و p99 (کاربرانِ دُم توزیع). دقیقاً به همین دلیل روی histogram و histogram_quantile تأکید می‌شود؛ صدک تجربهٔ واقعیِ بدترین کاربران را نشان می‌دهد، نه یک میانگینِ فریبنده.

۱۴) تفاوتِ SLI، SLO و SLA را با مثال بگو و error budget چیست.

SLI یک عددِ اندازه‌گیری‌شده دربارهٔ کیفیت است (مثلاً «درصدِ درخواست‌های موفق زیرِ ۳۰۰ms»). SLO یک هدفِ داخلی روی آن SLI است (مثلاً «۹۹.۹٪ در ۳۰ روز»). SLA یک قراردادِ رسمی با مشتری است که نقضش پیامدِ مالی/حقوقی دارد و همیشه از SLO سست‌تر است. Error budget: متممِ SLO؛ اگر SLO «۹۹.۹٪» است، «۰.۱٪» بودجهٔ خطای مجاز توست. تا وقتی بودجه مصرف نشده تیم می‌تواند سریع feature بدهد؛ اگر ته کشید، تمرکز روی پایداری می‌رود. این «پایداری در برابر سرعت» را از بحثِ سلیقه‌ای به یک عددِ عینی تبدیل می‌کند.

۱۵) Observation API در Micrometer چه می‌کند و تفکیکِ low/high cardinality در آن چرا مهم است؟

Observation API اجازه می‌دهد یک رویداد را یک بار instrument کنی و هم‌زمان متریک و trace (و کمکِ همبستگیِ لاگ) تولید کنی — به‌جای دو بار کارِ تکراری. تفکیکِ کاردینالیتی نبوغِ طراحی‌اش است: lowCardinalityKeyValue هم به متریک هم به trace می‌رود (چون کم‌مقدار است و متریک را منفجر نمی‌کند)، اما highCardinalityKeyValue فقط به span/trace می‌رود و هرگز tagِ متریک نمی‌شود. یعنی API خودش تو را از انفجارِ cardinality محافظت می‌کند: orderId را در trace می‌گذارد اما از متریک دور نگه می‌دارد.


در یک نگاه
  • Observability توانِ پرسیدنِ سؤال‌های پیش‌بینی‌نشده دربارهٔ سیستم است؛ monitoring می‌گوید «چیزی خراب است»، observability می‌گوید «چرا». سه ستون: metrics (چقدر؟ ارزان، همیشه‌روشن)، logs (آن لحظه چه شد؟ گران)، traces (کجای سفر؟).
  • Micrometer نمای بی‌طرفِ فروشنده برای متریک‌هاست — «SLF4J برای متریک». چهار meterِ اصلی: Counter (فقط بالا)، Gauge (لحظه‌ای)، Timer (مدت + تعداد)، DistributionSummary (توزیعِ اندازه). با عوض‌کردنِ یک registry، خروجی به هر back-endی می‌رود.
  • Tag/label متریک را چندبُعدی می‌کند اما دامِ مرگبار cardinality explosion را می‌آورد: هرگز userId/email/URL خام را tag نکن.
  • Prometheus با مدلِ pull از /actuator/prometheus scrape می‌کند؛ histogram با سطل‌های le و histogram_quantile صدک را سمتِ سرور می‌سازد (جمع‌پذیر، برخلافِ summary). PromQL با rate() نرخ می‌گیرد، Grafana آن را بصری می‌کند.
  • Tracing: هر درخواست یک trace (درختِ spanهاست)؛ context با هدرِ traceparent (W3C) بین سرویس‌ها منتقل می‌شود؛ sampling حجم را مهار می‌کند.
  • OpenTelemetry استانداردِ واحدِ CNCF (USB-Cِ تلمتری) با API/SDK/OTLP/Collector/semantic conventions است؛ vendor lock-in را می‌کُشد. Observation API یک instrumentation → متریک + trace.
  • چه اندازه بگیریم: RED برای سرویس، USE برای منابع؛ همیشه صدک نه میانگین. SLI/SLO/SLA و error budget زبانِ عددیِ قابل‌اتکا بودن‌اند.

It's 2 a.m. Your order service has gone "slow" — not down, just slow. Some users get errors, some don't. The code that worked yesterday is byte-for-byte the same today. Now answer this: where is the problem? The database? A downstream service? A queue that filled up? One user sending weird input? If your only tool is to SSH into a box and eyeball log files, you'll be there until dawn. A system you can understand from the outside — figuring out what's happening inside just by looking at what it emits — is a system that has observability. This chapter is about building exactly that capability.

Monitoring is a car's dashboard; observability is a doctor's diagnostic room

A car has a few warning lights: oil, temperature, fuel. That's monitoring — a fixed set of pre-baked yes/no questions ("is the temperature high?"). Useful, but limited to questions the manufacturer anticipated. Now picture a sick patient walking into a diagnostic room. The doctor doesn't know the problem in advance; they combine blood work, blood pressure, pulse, imaging, and history to answer a question that was never pre-planned. That's observability: the power to ask new, unanticipated questions about a system without shipping new code. Monitoring says "something is broken"; observability says "why."

This chapter is the operational heart of every serious distributed system. As always, we grab the concept with an analogy first, then learn its technical name, then wire it to real code. Don't fear the new words — I'll define each one the moment it first appears.

Roadmap for this chapter
  1. The three pillars of observability: metrics, logs, traces — and which question each answers.
  2. Micrometer as "SLF4J for metrics": why we need a facade.
  3. The four meter types: Counter, Gauge, Timer, DistributionSummary — when to use which.
  4. Tags and dimensions — how to slice a metric, and why the cardinality trap lurks here.
  5. Prometheus: the pull model, the exposition format, and how histograms build percentiles.
  6. PromQL and Grafana — from raw data to charts and alerts.
  7. Distributed tracing: span, trace, and context propagation across services.
  8. OpenTelemetry — the industry's single standard for all three signals.
  9. The RED and USE methods and the Four Golden Signals — what to measure.
  10. SLI/SLO/SLA and the error budget — the language of reliability.
  11. Pitfalls, best practices, and a full interview Q&A section.

Part 0 — a few words you must "feel" before we start

Three words recur throughout this chapter. Let me plant them with analogies now so you don't get stuck later.

  • Telemetry: "the data a system produces and sends out about itself." Its Greek roots are tele ("far") and metron ("measure") — literally "measuring from a distance." Every metric, log, and trace is a piece of telemetry.
  • Instrumentation: "the work you do so your code produces telemetry" — like fitting sensors onto an engine. When you add a counter.increment() line, you are instrumenting your code.
  • Cardinality: "the number of distinct values something can take." Gender has low cardinality (a couple of values); a user ID has very high cardinality (millions). This innocent-looking word will later star in the chapter's most dangerous trap.

Part 1 — The three pillars of observability

The information a system emits about itself comes in three different flavors, called the three pillars of observability. Each answers a different kind of question, and the classic beginner mistake is reaching for one pillar to answer another pillar's question.

The ledger, the diary, and the footprints in snow

Imagine you want to reconstruct what happened in a house last night. You have three sources. First, the household ledger: just numbers — "12 glasses of water used, fridge opened 3 times." Compact, cheap, great for spotting trends, but it never says why. That's a metric. Second, the residents' diaries: someone wrote "guest arrived at 9, power went out at 10." Detailed, textual, great for understanding a specific moment. That's a log. Third, footprints in the snow: you can follow one person from the front door to the kitchen to a room, and see how long each step took. That's a trace — one request's journey through the whole system.

Let's define them precisely:

Pillar What it is Question it answers Storage cost Example
Metrics Numbers aggregated over time (time series) "How many? How fast? Which way is the trend?" Very low (compact) Request rate, error %, memory usage
Logs Discrete, timestamped text events "What exactly happened at that moment?" High (voluminous) "User 42 logged in", an error's stack trace
Traces One request's path across services "Where in the journey did it slow/break?" Medium (usually sampled) A request that went API → DB → cache
The pillars are complementary, not interchangeable

A mature team weaves all three together. The real workflow: a metric alerts you that "error rate went up" (what is broken); a trace shows you "which service and which step" slowed or failed (where); and a log shows you that exact moment in detail (why). Metrics are cheap and always-on, so you learn from them first. Logs are expensive, so you save them for going deep. With only one pillar, you always lose half the story.

For the rest of the chapter we focus mainly on metrics (that's where most of the Java ecosystem's tooling lives), then move to tracing and OpenTelemetry. Logging has its own dedicated chapter.


Part 2 — Micrometer: "SLF4J for metrics"

Say you've decided to collect metrics. But send them where? Prometheus? Datadog? CloudWatch? Graphite? New Relic? Each has its own API and format. If you code directly against one vendor's API, the day the company decides to migrate, you're touching a thousand places in your code.

The simultaneous interpreter at the UN

Picture a speaker at the UN talking in Persian while listeners follow along in English, Arabic, Chinese, and French. The speaker talks once, in one language; the booth of simultaneous interpreters translates that same sentence into whatever language is needed. Micrometer is that interpreter booth: your code produces metrics once, in "Micrometer language," and Micrometer translates behind the scenes into Prometheus, or Datadog, or any other back-end. You don't change your code; you just swap the dependency for that back-end.

If you've read the logging chapter this pattern is familiar: SLF4J was a facade over Logback and Log4j2. Micrometer plays exactly that role for metrics — a vendor-neutral facade. The Micrometer team itself introduces it with the phrase "SLF4J for metrics."

Two key Micrometer concepts:

  • Meter: the unit of measurement — a counter, a gauge, a timer. Anything "you measure" is a meter.
  • MeterRegistry: the factory and warehouse for all meters. You create meters from the registry, and the type of registry decides where output goes. A PrometheusMeterRegistry prepares output for Prometheus; a SimpleMeterRegistry just holds it in memory (great for tests).

In Spring Boot you almost never build these by hand — auto-configuration creates a global registry for you. Just add these dependencies:

<!-- Actuator exposes management endpoints, including metrics -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- This dependency makes the "output language" Prometheus -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

With just that second dependency present, Spring Boot exposes an /actuator/prometheus endpoint that Prometheus can read metrics from. If tomorrow you move to Datadog, you swap micrometer-registry-prometheus for micrometer-registry-datadog and your instrumentation code stays untouched. That's the whole value of a facade.

A note on versions

Micrometer is an independent project under the Spring umbrella and releases regularly (the 1.x line). There are two Prometheus registries: micrometer-registry-prometheus, which sits on the newer Prometheus Java client (1.x), and the legacy -simpleclient variant on the older client (0.x). For a new project pick the former. If you use Spring Boot 3, its dependency management (BOM) brings a compatible Micrometer version automatically; you don't hand-write the version.


Part 3 — The four meter types you must know

Micrometer has several meters, but four are the main pillars. Grab their difference with an analogy, then see the code.

An electricity meter, a fuel gauge, a stopwatch, and a kitchen scale

There are four flavors of measurement. An electricity meter only ever goes up, never down — that's a Counter. A fuel gauge goes up and down and shows an instantaneous value — that's a Gauge. A stopwatch times an event and then tells you "how many times you timed and the total" — that's a Timer. And a kitchen scale, where you weigh various things and care about the distribution of those weights — that's a DistributionSummary.

1) Counter — only goes up

A monotonically increasing counter; it never decreases, only rises or holds (until the app restarts). Use it to count "how many times something happened": request count, error count, orders placed.

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;

public class OrderService {
    private final Counter ordersPlaced;

    public OrderService(MeterRegistry registry) {
        this.ordersPlaced = Counter.builder("orders.placed")   // metric name
                .description("Total number of orders placed")
                .tag("channel", "web")                          // a dimension
                .register(registry);
    }

    public void placeOrder(Order order) {
        // ... order-placing logic ...
        ordersPlaced.increment();   // go up by one
    }
}
Never store a rate yourself

You'll be tempted to build a "requests per second" metric and compute the rate yourself. That's wrong. The whole philosophy of a Counter is that you store only the raw cumulative total, and leave rate computation to the query system (like PromQL's rate()). Why? Because a rate depends on the time window, and if you pre-compute and store it, you can never change the window later. The raw total keeps everything; a rate is a view of it. Rule: store raw, aggregate/differentiate at read time.

2) Gauge — a needle that goes up and down

An instantaneous value that at any moment can be higher or lower: items in a queue, active DB connections, memory usage percent. Key point: you observe a gauge, you don't set its value by hand. You hand Micrometer an object and a function, and whenever Micrometer wants to read the metric, it calls that function to get the current value.

import io.micrometer.core.instrument.Gauge;
import java.util.Queue;

Queue<Task> pending = ...;

Gauge.builder("queue.size", pending, Queue::size)  // object + a function giving the value
        .description("Number of tasks waiting in the queue")
        .register(registry);
The strong-reference / memory-leak trap

Micrometer deliberately holds a weak reference to the object you give a gauge — meaning if the rest of the program no longer wants that object, the garbage collector can reclaim it and the gauge returns NaN. Beginners often hand a temporary object to a gauge, the object gets collected, and the gauge "dies." Make sure your program keeps a live reference to that object. And never build a gauge on a variable that keeps getting replaced with a new object; the gauge stays stuck to the original one.

3) Timer — a stopwatch for short events

The single most important meter for performance monitoring. A Timer records two things at once: how many times the event happened and the sum of all durations. From these two, the query system can derive average, throughput, and — if a histogram is enabled — percentiles.

import io.micrometer.core.instrument.Timer;

Timer requestTimer = Timer.builder("http.server.requests")
        .description("Request processing time")
        .tag("uri", "/checkout")
        .publishPercentileHistogram()   // publish histogram buckets (server-side percentiles)
        .register(registry);

// Style 1: with a block of code
String result = requestTimer.record(() -> handleCheckout());

// Style 2: manual, with Timer.Sample when start and stop are separate
Timer.Sample sample = Timer.start(registry);
try {
    handleCheckout();
} finally {
    sample.stop(requestTimer);   // records duration from start to here
}

In Spring, instead of writing all this by hand, you can use the @Timed annotation to time a method automatically:

import io.micrometer.core.annotation.Timed;

@Timed(value = "checkout.duration", description = "Checkout process duration", percentiles = {0.5, 0.95, 0.99})
public Receipt checkout(Cart cart) {
    // ...
}

4) DistributionSummary — the distribution of sizes (not time)

Just like a Timer, but for measuring something whose unit isn't time: a request payload's size in bytes, the number of items in a cart, the size of a batch. Same distribution and percentile capabilities.

import io.micrometer.core.instrument.DistributionSummary;

DistributionSummary payloadSize = DistributionSummary.builder("http.request.size")
        .baseUnit("bytes")
        .publishPercentiles(0.5, 0.95, 0.99)   // compute client-side percentiles
        .register(registry);

payloadSize.record(requestBody.length);
A lesser-known but handy meter: LongTaskTimer

A Timer records a duration only after the event ends. But if a task is running right now and you want to know "how many long-running tasks are in flight and how long each has been going," you need a LongTaskTimer. Example: a heavy batch that takes an hour; with a plain Timer you see nothing until the next hour, but a LongTaskTimer shows you right now that a task is in progress and how many minutes have elapsed. Crucial for long-running jobs.


Part 4 — Tags: from a flat metric to a multi-dimensional one

So far our metrics have been a single plain number. But the real power of modern systems is in dimensions. A tag (Prometheus calls it a label) is a key-value pair attached to a metric that lets you slice it later.

Labels on warehouse boxes

Imagine a warehouse with thousands of boxes. If they all just say "goods," all you can say is "how many boxes total." But if each box carries a few labels — "city: Tehran, category: appliances, status: shipped" — now you can ask any question: "how many appliance boxes in Tehran not yet shipped?" Tags on a metric are exactly those labels: a single http.server.requests metric, with tags method, uri, and status, can be sliced from any angle.

// One metric, many dimensions — same name with different tag combinations
registry.counter("http.server.requests",
        "method", request.getMethod(),      // GET / POST / ...
        "uri", request.getUri(),            // /checkout / /cart / ...
        "status", response.getStatus())     // 200 / 404 / 500 ...
    .increment();

Every unique combination of tag values creates a separate time series. So {method=GET, uri=/cart, status=200} is one series and {method=POST, uri=/cart, status=500} is another. And this is exactly the trap I warned about back in Part 0.

Cardinality explosion — the costliest mistake in observability

The total number of time series equals the product of the possible values of each tag. If method has three values and status five, you have 15 series — great. But if you add a userId tag to that same metric and you have a million users, you suddenly have millions of series, Prometheus's memory explodes, queries slow to a crawl, and cost skyrockets. This is cardinality explosion. Golden rule: never put anything with unbounded or very high cardinality as a tag — not userId, not email, not a raw URL with an ID in it (/order/12345), not a free-form error message. Put those in a log or a trace, not a metric tag. Tags are for low-value, bounded things: method, status, a templated route name (/order/{id}, not /order/12345).

Why this comes up in interviews again and again

Metrics are cheap only if their cardinality is controlled. Prometheus's storage model keeps each unique time series separately in memory. One high-cardinality tag can take down an entire monitoring cluster within hours — a phenomenon called a "cardinality bomb." Recognizing what makes a good tag versus a bad one is the line between someone who "wrote code" and someone who "kept a production system alive."


Part 5 — Prometheus: the pull model and building percentiles

Now we produce metrics; but who collects and stores them? The most common answer in the cloud-native world is Prometheus — an open-source time-series database developed under the CNCF umbrella.

The postal carrier who visits houses, rather than houses that mail letters

There are two models for collecting data. In the push model, each app "pushes" its data toward a central server — like every house dropping its letter into the mail. In the pull model, a central collector periodically visits each app and says "give me your metrics" — like a postal carrier who knocks and takes the letter. Prometheus chose the pull model: every few seconds it visits your app's /actuator/prometheus endpoint and reads everything. This is called scraping.

The advantage of pull is that Prometheus itself knows what to scrape (service discovery), and if an app doesn't respond, that "not responding" is itself a health signal (that instance is "down"). A simple config:

# prometheus.yml — tells Prometheus where and how often to scrape
global:
  scrape_interval: 15s        # scrape every 15 seconds

scrape_configs:
  - job_name: 'order-service'
    metrics_path: '/actuator/prometheus'   # the endpoint Micrometer created
    static_configs:
      - targets: ['order-service:8080']

When Prometheus reads that endpoint, it sees plain text (called the exposition format), something like:

# HELP http_server_requests_seconds  Request processing time
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{uri="/checkout",le="0.05"} 24
http_server_requests_seconds_bucket{uri="/checkout",le="0.1"}  57
http_server_requests_seconds_bucket{uri="/checkout",le="0.5"}  92
http_server_requests_seconds_bucket{uri="/checkout",le="+Inf"} 100
http_server_requests_seconds_count{uri="/checkout"} 100
http_server_requests_seconds_sum{uri="/checkout"}   12.4
Why did the metric name change?

Notice the name we wrote in Java was http.server.requests but here it became http_server_requests_seconds. That's Micrometer's doing: each registry translates names into its back-end's convention. Prometheus dislikes dots and converts them to underscores, appends the base unit (seconds) to the name, and adds a _total suffix to counters. This is exactly the "simultaneous interpreter" from the Micrometer part — you wrote once in Micrometer language, the output was translated into Prometheus language.

Histograms: how we build percentiles from individual timings

Here you must understand Prometheus's most important technical idea: how can you compute the 99th percentile without keeping every individual measurement?

Shoe-size buckets in a shoe store

Imagine you want to understand the distribution of customers' foot sizes at a shoe store, but you can't store the exact size of each of ten thousand customers. The clever way: set up a few buckets — "up to size 38," "up to size 40," "up to size 42," "up to any size." For each customer who arrives, increment the counter of every bucket whose size is "greater than or equal to" theirs. Now you have just a few counters, but you can estimate that, say, "95% of customers are under size 41." That's exactly what a histogram does in Prometheus.

Notice the le in the output above: short for less than or equal to. Each line _bucket{le="0.1"} 57 means "57 requests had a duration less than or equal to 0.1 seconds." These are cumulative: a bigger bucket includes all smaller ones too. Now the query system can estimate a percentile by interpolating between buckets. In Prometheus that's done by the histogram_quantile function:

# 95th percentile of request duration over the last 5 minutes
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))
Histogram (server-side) vs summary (client-side) — a difference many get wrong

You have two ways to get percentiles and their difference is critical. Histogram: the app sends only raw buckets and the percentile is computed on the Prometheus server. Big advantage: you can aggregate percentiles across multiple instances — because buckets are additive. Summary: the app computes percentiles itself client-side and sends ready numbers. The problem: percentiles are not additive — you can't get the 99th percentile of three servers by averaging them (that's mathematically wrong). Rule: in a distributed system with multiple instances, almost always pick histogram, not summary. In Micrometer, publishPercentileHistogram() gives the buckets (histogram) and publishPercentiles(...) gives client-side percentiles (summary).

Native histograms — the next generation

The classic histogram has one weakness: you must define the buckets manually up front, and if you pick them badly accuracy suffers. Prometheus added a newer capability called native histograms (formerly "sparse histograms") that builds buckets exponentially and automatically; much higher accuracy with less memory and no manual bucket tuning. It's still relatively new and maturing, but it's the direction the industry is heading. Mentioning it in an interview shows you follow the ecosystem.


Part 6 — PromQL and Grafana: from numbers to decisions

Raw data alone is useless; you must be able to ask questions of it. Prometheus's query language is PromQL. A few frequent examples you should have at your fingertips:

# Requests per second (RED: Rate) — derived from a counter
sum(rate(http_server_requests_seconds_count[5m])) by (uri)

# Error percentage (RED: Errors) — ratio of 5xx to total
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
  /
sum(rate(http_server_requests_seconds_count[5m]))

# 99th percentile latency (RED: Duration)
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le))

Notice rate(...[5m]): because a counter only goes up, its raw value is meaningless; rate takes its derivative over the 5-minute window and turns it into "per second." This is exactly what I said in the Counter part: store raw, compute the rate at query time.

Prometheus is the kitchen, Grafana is the serving table

Prometheus is excellent at collecting, storing, and querying, but it has no beautiful UI for viewing trends. Grafana is the open-source dashboard-and-visualization tool that connects to Prometheus (and dozens of other sources) and turns those numbers into live charts, gauges, and tables. Division of labor: Prometheus is the chef who cooks (data) and keeps it; Grafana is the waiter who arranges it beautifully on the table so a human can see and decide. Besides charts, both can build alerts: "if the 99th percentile exceeds 500ms, page the team."


Part 7 — Distributed tracing: following one request's trail

Metrics tell you "5% of requests are slow," but in a microservice architecture where a single user request passes through ten different services, a metric doesn't tell you which service and which step is to blame. Here the third pillar enters: distributed tracing.

Tracking an international parcel

You mail a parcel from Tehran to Tokyo. The tracking page shows you: "Tehran — handed to hub (10 min)" → "IKA airport — customs (3 hours!)" → "Tokyo — distribution center (20 min)" → "delivered." Now you can see the whole delay was customs, not the flight. The entire journey is a trace; each stage (each stop) is a span. Each span knows when it started, when it ended, and which parent stage it belongs to. Distributed tracing builds exactly that tracking page for a software request.

Let's sharpen the words:

  • Span: a named unit of work with a start and end time — e.g. "database query" or "call the payment service." Each span can have attributes (descriptive key-values), events (point-in-time moments within it), and a status (ok/error).
  • Trace: the collection of all spans of one request, connected as a tree of parent-child. The root of the tree is the first span (e.g. the request entering the API gateway).
  • Trace context / propagation: so that spans from different services know they all belong to one trace, each service passes a shared identifier in an HTTP header when calling the next service. The industry standard for this is W3C Trace Context, which uses a header called traceparent.

The traceparent header looks like this and has a simple but precise structure:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             ^  ^                                ^                ^
          version  trace-id (16 bytes)          span-id (8 bytes)  flags
Why propagation is the heart of tracing

The magic of tracing is exactly this "passing a shared identifier." Service A creates a trace-id; when it calls service B, it puts that same trace-id in the traceparent header; B does the same when calling C. In the end, all spans with one shared trace-id are sent to a back-end (like Jaeger or Tempo) and there they're reassembled into a single tree. If even one service fails to pass the header, the chain breaks and the trace is left incomplete. This is the most common cause of "broken traces" in production.

Sampling — why we don't keep every trace

If you store a full trace for every single request, the data volume becomes crushing. So tracing is usually sampled: only a percentage of traces (say 1% or 10%) are kept in full. Two main strategies: head-based sampling decides at the very start whether to keep a trace (simple and cheap), and tail-based sampling waits until the trace is complete to decide — e.g. "keep all traces that had an error or were slow" (smarter but more expensive, because it must buffer everything temporarily). This distinction is a common interview question.


Part 8 — OpenTelemetry: the industry's single standard

Until now each signal had its own tool and standard. Historically this was chaos: Zipkin and Jaeger and OpenTracing and OpenCensus each had their own API. The problem: if you instrumented your code with one vendor's API, you were locked to it (vendor lock-in).

USB-C for telemetry

Remember when every phone had a different charger? iPhone one way, every Android another, cables incompatible. Then USB-C arrived and became the single standard: one port for everything. OpenTelemetry (shortened to OTel) is exactly the USB-C of the observability world — one vendor-neutral API and protocol for all three signals (metrics, logs, traces). You instrument your code once with OTel, then you can send the output to any back-end (Jaeger, Prometheus, Grafana, Datadog, ...) — without changing code.

OpenTelemetry is a project under the CNCF umbrella (the result of merging OpenTracing and OpenCensus) and has become the industry's de-facto standard today. Its key components:

  • API: the interface your code instruments against (creates spans, records metrics). Lightweight, no heavy dependencies.
  • SDK: the actual implementation of the API — sampling, processing, and exporting.
  • OTLP (OpenTelemetry Protocol): the single protocol that sends telemetry from your app to a back-end. Everyone's shared language.
  • Collector: an intermediary service that receives telemetry from apps, processes/filters/enriches it, and forwards it to one or more back-ends. Like a central hub.
  • Semantic Conventions: a standard agreement on names. For example, everyone agreed the HTTP method is stored in an attribute called http.request.method. This means dashboards and tools become portable across organizations.
The maturity status of OTel's signals

Not all signals matured at once. Tracing was the first signal to reach stable and is fully production-ready. Metrics is stable too. Logs arrived later and stabilized around the same level. A fourth signal, Profiles (CPU/memory usage profiling), is a newcomer still in development. The OTel Java libraries release rapidly on the 1.x line, and "stable" artifacts carry strong backward-compatibility guarantees, while experimental conventions are kept apart in artifacts with an -alpha suffix so production code doesn't bind to something unstable.

Two ways to instrument with OTel in Java

Way 1 — the Java agent (no code changes): OpenTelemetry ships a Java agent that attaches to the JVM at runtime and automatically instruments dozens of common libraries (Spring, JDBC, Kafka, HTTP clients...). Just add a flag to the run:

java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.service.name=order-service \
     -Dotel.exporter.otlp.endpoint=http://collector:4317 \
     -jar order-service.jar

Way 2 — manual instrumentation with the API: when you want custom spans for your own business logic:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;

Tracer tracer = openTelemetry.getTracer("order-service");

Span span = tracer.spanBuilder("validate-inventory").startSpan();
try (var scope = span.makeCurrent()) {
    span.setAttribute("order.id", orderId);      // descriptive attribute
    boolean ok = inventory.check(orderId);
    span.setAttribute("inventory.available", ok);
} catch (Exception e) {
    span.recordException(e);                      // record the error on the span
    throw e;
} finally {
    span.end();                                   // without this, the span never closes!
}

The bridge between Micrometer and OpenTelemetry: the Observation API

Here the beauty of Spring Boot 3's architecture becomes clear. Micrometer introduced an Observation API — an instrumentation layer you write once that simultaneously produces metrics and traces (and helps with log correlation).

Take one photo, print it everywhere

Before, you wrote code once for a metric and again for a trace — duplicate work for one event. The Observation API is like taking one photo of a scene and then pasting that same photo into an album (metric), framing it (trace), and printing it in a newspaper (log). One instrumentation, many outputs.

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;

Observation.createNotStarted("order.checkout", observationRegistry)
    .lowCardinalityKeyValue("channel", "web")    // goes to the metric tag too (low-cardinality)
    .highCardinalityKeyValue("orderId", orderId) // only a span attribute (high-cardinality)
    .observe(() -> processCheckout(order));
The genius of the low/high cardinality split in the Observation API

Look at those two methods above — this design bakes the cardinality lesson directly into the API itself. lowCardinalityKeyValue goes to both the metric and the trace (because it's low-cardinality and won't blow up the metric). But highCardinalityKeyValue goes only to the span/trace and is never used as a metric tag. This means the API itself protects you from cardinality explosion: it correctly puts orderId in the trace (where high cardinality is fine) while keeping it out of the metric. A brilliant design that condenses the three-pillar philosophy into one API.


Part 9 — What should you measure? RED, USE, and the Golden Signals

Instrumenting is easy; knowing what's worth measuring is the art. There are three standard industry frameworks you should know.

A doctor's exam: pulse, blood pressure, temperature

To quickly assess any patient, a doctor always reaches for a fixed set of vital signs: pulse, blood pressure, temperature, respiration. They don't reinvent everything each time; they have a standard checklist. RED and USE are exactly those standard checklists of "system vital signs" — instead of being lost about what to measure, you follow a proven pattern.

Framework For what The three/four numbers you take Coined by
RED Request-driven services (API, web) Rate, Errors, Duration (latency) Tom Wilkie
USE Resources (CPU, disk, memory, queue) Utilization, Saturation (queue), Errors Brendan Gregg
Four Golden Signals Services (Google SRE book) Latency, Traffic, Errors, Saturation Google SRE
  • RED is great for anything that takes a "request." Three simple numbers that reveal 90% of an API's health. Those same three PromQL queries in Part 6 were exactly RED.
  • USE is for resources, not services. For each resource ask: how busy is it (utilization)? How much work is waiting (saturation, like queue length)? How many errors did it produce?
  • Four Golden Signals is Google's version; effectively a blend of RED and USE with an emphasis on latency.
How to use these in an interview

If the interviewer asks "you built a new service, what do you monitor?", the professional answer is not to name a few random metrics. Say: "For the service itself I take RED — rate, errors, latency (at p95 and p99, not the average); and for the underlying resources I take USE — CPU utilization, thread-pool saturation, and DB queue." This answer shows you have a structured mental framework, not a random list.

The average lies; always take percentiles

A classic mistake: monitoring the average latency. The average hides the unlucky users. If 99 requests take 10ms and one takes 5 seconds, the average is about 60ms — looking perfectly healthy, while one user waited 5 seconds. Always take percentiles: p50 (the median, the typical user), p95 and p99 (the unlucky ones). Rule: for latency, the average is almost always useless or misleading. That's why we stressed histograms and histogram_quantile so much.


Part 10 — SLI, SLO, and SLA: the language of reliability

The last piece of the puzzle: how to talk about "how reliable should the system be" in a numeric, agreed-upon way. Three closely related but distinct acronyms that constantly get muddled.

The speedometer, the personal goal, and the contract with a client

Imagine you're a professional driver. The car's speedometer shows the actual, instantaneous number — that's the SLI (Indicator, the measured metric). The goal you set for yourself ("I'll try to arrive under 4 hours") — that's the SLO (Objective, the internal target). And the contract you signed with a client ("if I arrive later than 5 hours I'm penalized") — that's the SLA (Agreement, the formal commitment with legal/financial consequence). Order of strictness: SLI measures, SLO is the stricter internal target, and SLA is the looser external ceiling with a penalty.

Let's define them precisely:

  • SLI (Service Level Indicator): a measured number about service quality. Example: "percent of requests answered in under 300ms with no error." This is what you actually measure — and, as you saw, it comes from metrics.
  • SLO (Service Level Objective): an internal target on that SLI. Example: "99.9% of requests must succeed, over a 30-day window." This is the dev team's red line.
  • SLA (Service Level Agreement): a formal contract with a customer that, if breached, has financial/legal consequences (like refunds). An SLA is always looser than the SLO so you keep a safety margin.
The Error Budget — the bridge between stability and dev speed

From the SLO comes an extremely practical concept. If your SLO is "99.9% success," then you're allowed 0.1% failure. That 0.1% is called the error budget — the permitted failure quota. This is the genius of Google's SRE culture: as long as the error budget isn't spent, the team can ship features fast and take risks; but if the budget runs out (too much failure), everything halts and the focus shifts to stability alone. The error budget turns the eternal "stability vs. speed" fight from a matter of taste into an objective number. If you explain this in an interview, you're instantly seen a few levels higher.

How much downtime does 99.9% allow?

Feel the number of nines: "three nines" (99.9%) means about 43 minutes of allowed downtime per month. "Four nines" (99.99%) means about 4.3 minutes per month. "Five nines" (99.999%) means about 26 seconds per month — very expensive and hard. Each extra nine multiplies the cost, so pick your SLO realistically; "five nines" for a blog is a waste of resources.


Part 11 — Pitfalls and best practices

Let me gather the top traps in one place — the ones that bite in interviews or real code:

  • Cardinality explosion: never put userId, email, a raw URL, or a free-form error message as a metric tag. Put those in logs/traces. Tags are only for bounded, countable values.
  • Average instead of percentile: the average latency hides slow users. Take p95/p99.
  • Pre-computed rate: store the counter raw and derive the rate with rate() at query time; don't build a rate yourself.
  • Summary in a distributed system: client-side percentiles (summary) aren't additive; for multiple instances use histogram.
  • A span that never closes: forgetting span.end() in finally leaves the span open forever and corrupts the trace.
  • Breaking the propagation chain: if one service doesn't pass the traceparent header, the trace is left incomplete.
  • Gauge on a temporary object: the gauge's weak reference means if the object is garbage-collected, the gauge dies and returns NaN.
  • Vendor lock-in: don't code directly against one vendor's API; use the Micrometer/OpenTelemetry facade so migration stays possible.

Best practices:

  • Instrument your code once with OpenTelemetry or the Micrometer Observation API to get both metrics and traces.
  • Name metrics per OTel's semantic conventions so dashboards are portable.
  • Weave the three pillars: alert from a metric, locate with a trace, see details in a log.
  • Define SLOs and monitor the error budget, not just "server up/down."
  • For a new project, put OTLP and a Collector in place from day one so the back-end is swappable.

Interview Questions

Now it's time to test yourself. Answer each question yourself first, then open the answer.

1) What is the difference between monitoring and observability?

Monitoring means collecting and alerting on a set of pre-defined metrics — answering questions you already knew ("is CPU over 80%?"). Observability means the system emits enough data about itself that you can ask unanticipated, new questions without deploying new code. The key line: "monitoring says something is broken; observability says why." Observability is a superset of monitoring.

2) Name the three pillars of observability and say which question each answers.

Metrics (numbers aggregated over time): "how much? how many? which way is the trend?" — cheap and always-on. Logs (discrete text events): "what exactly happened at that specific moment?" — detailed and expensive. Traces (one request's path across services): "where in the journey did it slow/break?" Senior note: the three are complementary; the workflow is that a metric alerts, a trace localizes, a log details.

3) What is Micrometer and why is it called "SLF4J for metrics"?

Micrometer is a vendor-neutral facade for instrumenting metrics on the JVM. You write your code once in "Micrometer language," and by swapping only a registry dependency (e.g. prometheus to datadog), output goes to a different back-end without changing instrumentation code. It's exactly the SLF4J pattern, which was a facade over Logback/Log4j2 — which is why the Micrometer team introduces it as "SLF4J for metrics."

4) Name the four main meter types and precisely state the difference between Counter and Gauge.

Counter (only goes up, for counting events), Gauge (an instantaneous value that goes up/down, like queue length), Timer (duration of short events + count), DistributionSummary (distribution of non-time sizes like payload size). Key Counter vs Gauge difference: a Counter is monotonically increasing and you turn its raw value into a rate with rate(); a Gauge is an instantaneous snapshot that Micrometer reads by calling a function, and its value can decrease. Common mistake: making something that only goes up a gauge, or vice versa.

5) (The most important question) What is cardinality explosion and how do you prevent it?

Every unique combination of tag values creates a separate time series, and the total number of series is the product of the tags' cardinalities. If you add a tag with unbounded values (like userId with millions of values), millions of series are created, Prometheus's memory explodes, and the monitoring cluster falls over — a "cardinality bomb." Prevention: use tags only for bounded, countable values (method, status, the route template /order/{id} not /order/12345). Put high-cardinality values (userId, email, free-form errors) in logs or traces, not metric tags.

6) What's the difference between the pull and push models for metric collection, and which did Prometheus choose?

In push the app sends data to a central server itself; in pull a central collector periodically visits the app and reads (scrapes) the metrics. Prometheus chose pull, reading from the app's endpoint (e.g. /actuator/prometheus). Advantages: Prometheus itself knows what to scrape (service discovery), and if an app doesn't respond, that "down-ness" is a free health signal. For short-lived jobs that don't get a chance to be scraped, Prometheus also has a Pushgateway.

7) How does Prometheus compute the 99th percentile without storing every measurement?

With a histogram. The app maintains a few cumulative buckets; each bucket, with a label called le (less than or equal), says "how many observations were less than or equal to this boundary." Because the buckets are cumulative, the histogram_quantile(0.99, ...) function in PromQL estimates the percentile server-side by interpolating between buckets. Result: only a few counters are stored but the whole distribution's percentile is estimable. Its accuracy depends on bucket layout (which native histograms remove as a limitation).

8) What's the difference between histogram and summary in Prometheus and when do you use which?

Histogram: the app sends raw buckets and the percentile is computed on the Prometheus server; since buckets are additive, you can aggregate percentiles across multiple instances. Summary: the app computes percentiles itself client-side and sends ready numbers; but those percentiles are not additive — averaging the p99 of three servers is mathematically wrong. So in any distributed system with multiple instances, use a histogram. Summary only makes sense when you have one instance and need client-side percentile accuracy.

9) Define span and trace and explain how trace context propagates between services.

A span is a named unit of work with a start/end time (like a DB query) that can carry attributes, events, and a status. A trace is the collection of all spans of one request, connected as a parent-child tree. Context propagation: each service passes a shared identifier (trace-id) in an HTTP header when calling the next service; the industry standard is W3C Trace Context with the traceparent header (format: version-traceid-spanid-flags). If even one service fails to pass the header, the chain breaks and the trace is incomplete.

10) What's the difference between head-based and tail-based sampling?

Because storing all traces is expensive, only a fraction is kept. Head-based: the "keep or not" decision is made at the very start of the trace (simple and cheap, but you may miss interesting traces like errors). Tail-based: you wait until the trace completes, then decide — e.g. "keep all traces with an error or that were slow" (smarter because it catches the important ones, but more expensive since it must buffer everything temporarily, and is usually done in the Collector).

11) What is OpenTelemetry and what problem did it solve?

OpenTelemetry (OTel) is a single, vendor-neutral standard under the CNCF umbrella for all three signals (metrics/logs/traces), the result of merging OpenTracing and OpenCensus. The problem it solved: previously each tool (Zipkin, Jaeger, ...) had its own API, and instrumenting your code with one locked you to it (vendor lock-in). With OTel you instrument your code once and, via the single OTLP protocol, send output to any back-end. Components: API, SDK, OTLP, Collector, and semantic conventions (agreement on standard attribute names).

12) What are RED and USE and where does each apply?

Two standard frameworks for "what to measure." RED (Rate, Errors, Duration) is for request-driven services like an API — three numbers that show a service's health. USE (Utilization, Saturation, Errors) is for resources like CPU, disk, thread pool, and queue — for each resource you ask how busy, how much waiting work, how many errors. A close third is Google's Four Golden Signals (Latency, Traffic, Errors, Saturation). The professional interview answer: RED for the service, USE for the underlying resources.

13) Why is monitoring the average latency dangerous?

Because the average hides unlucky users. If 99 requests take 10ms and one takes 5 seconds, the average is about 60ms and looks healthy, while one user suffered 5 seconds. Solution: always take percentiles — p50 (the typical user), p95 and p99 (the tail users). This is exactly why histograms and histogram_quantile are emphasized; a percentile shows the real experience of the worst-off users, not a deceptive average.

14) Explain the difference between SLI, SLO, and SLA with an example, and what an error budget is.

SLI is a measured number about quality (e.g. "percent of successful requests under 300ms"). SLO is an internal target on that SLI (e.g. "99.9% over 30 days"). SLA is a formal contract with a customer whose breach has financial/legal consequences, and it's always looser than the SLO. Error budget: the complement of the SLO; if the SLO is "99.9%," then "0.1%" is your allowed failure budget. While the budget isn't spent the team can ship features fast; if it runs out, focus shifts to stability. This turns "stability vs. speed" from a matter of taste into an objective number.

15) What does the Observation API in Micrometer do, and why is its low/high cardinality split important?

The Observation API lets you instrument an event once and simultaneously produce a metric and a trace (plus log correlation) — instead of duplicate work. The cardinality split is its design genius: lowCardinalityKeyValue goes to both the metric and the trace (because it's low-value and won't blow up the metric), but highCardinalityKeyValue goes only to the span/trace and never becomes a metric tag. So the API itself protects you from cardinality explosion: it puts orderId in the trace but keeps it out of the metric.


In a nutshell
  • Observability is the power to ask unanticipated questions about a system; monitoring says "something is broken," observability says "why." Three pillars: metrics (how much? cheap, always-on), logs (what happened at that moment? expensive), traces (where in the journey?).
  • Micrometer is the vendor-neutral facade for metrics — "SLF4J for metrics." Four main meters: Counter (only up), Gauge (instantaneous), Timer (duration + count), DistributionSummary (size distribution). Swap one registry and output goes to any back-end.
  • Tags/labels make a metric multi-dimensional but bring the deadly cardinality explosion trap: never tag with userId/email/raw URL.
  • Prometheus scrapes from /actuator/prometheus with a pull model; a histogram with le buckets and histogram_quantile builds percentiles server-side (additive, unlike summary). PromQL derives rates with rate(), Grafana visualizes it.
  • Tracing: each request is a trace (a tree of spans); context propagates between services via the traceparent (W3C) header; sampling controls volume.
  • OpenTelemetry is the single CNCF standard (the USB-C of telemetry) with API/SDK/OTLP/Collector/semantic conventions; it kills vendor lock-in. The Observation API means one instrumentation → metric + trace.
  • What to measure: RED for the service, USE for resources; always percentiles, not the average. SLI/SLO/SLA and the error budget are the numeric language of reliability.