Logging, distributed tracing, metrics, alertingLogging, distributed tracing, metrics, alerting
Service gửi trace và log qua OTLP tới OpenTelemetry Collector; collector đẩy metric vào Prometheus, trace vào Tempo, log vào Loki. Grafana đọc cả ba và nối chúng bằng trace_id; Prometheus đánh giá rule SLO và đẩy cảnh báo qua Alertmanager. Bật “Burn rate vượt ngưỡng” để xem luồng gọi on-call; bật “Nhãn user_id” để xem cardinality làm Prometheus hết bộ nhớ.Services send traces and logs over OTLP to the OpenTelemetry Collector, which pushes metrics to Prometheus, traces to Tempo and logs to Loki. Grafana reads all three and joins them by trace_id; Prometheus evaluates SLO rules and sends alerts through Alertmanager. Turn on “Burn-rate alert” to follow the page to on-call; turn on “user_id label” to see cardinality run Prometheus out of memory.
1Từ 14:02, một phần request checkout mất gần 2 giây. Checkout gọi order service, header traceparent đi kèm nên cả hai span thuộc cùng một trace.From 14:02, some checkout requests take almost 2 seconds. Checkout calls the order service with a traceparent header, so both spans belong to one trace.
Log dạng chuỗi tự do, không có định danh request, buộc người điều tra grep theo thời gian và đoán. Ghi log JSON một dòng (Logback + logstash-encoder, Go log/slog JSONHandler), luôn có trace_id, span_id, request_id, service, level. Micrometer Tracing đưa traceId vào MDC tự động.Free-form log strings with no request identifier force investigators to grep by timestamp and guess. Write one-line JSON logs (Logback with logstash-encoder, Go log/slog JSONHandler) that always include trace_id, span_id, request_id, service and level. Micrometer Tracing puts traceId into the MDC automatically.
Mỗi service tự tạo trace mới vì không đọc header từ upstream. Dùng W3C traceparent làm propagator chung, instrument cả HTTP client và server (otelhttp, Spring RestClient tự cấu hình), và truyền context qua Kafka bằng header của message.Each service starts a new trace because it never reads the upstream header. Use W3C traceparent as the shared propagator, instrument both HTTP client and server (otelhttp, an auto-configured Spring RestClient), and carry the context through Kafka in message headers.
Mỗi tổ hợp nhãn là một time series trong bộ nhớ Prometheus. user_id, order_id, URL thô hay email làm số series bùng nổ. Nhãn chỉ dùng tập giá trị nhỏ và cố định (route template, method, code); giá trị cao để vào span hoặc log. Đặt sample_limit và theo dõi prometheus_tsdb_head_series.Every label combination is a time series in Prometheus memory. user_id, order_id, raw URLs or emails make the series count explode. Labels take only small, fixed value sets (route template, method, code); high-cardinality values go into spans or logs. Set sample_limit and watch prometheus_tsdb_head_series.
Trung bình 120 ms có thể che 2% request mất 3 giây. Dùng histogram với bucket bám theo ngưỡng SLO (0,1 · 0,3 · 0,8 · 2 s) và tính p99 bằng histogram_quantile trên toàn cụm. Summary tính phân vị tại từng instance nên không cộng gộp được.A 120 ms average can hide 2% of requests taking 3 seconds. Use histograms with buckets aligned to the SLO thresholds (0.1, 0.3, 0.8, 2 s) and compute p99 with histogram_quantile across the cluster. Summaries compute quantiles per instance and cannot be aggregated.
CPU 85% hay một pod restart chưa chắc làm người dùng đau, nhưng vẫn gọi người dậy lúc 3 giờ sáng. Chỉ page khi ngân sách lỗi bị đốt nhanh (multi-window burn rate 14,4x trong 1 giờ, 6x trong 6 giờ); nguyên nhân để trên dashboard hoặc thành ticket.85% CPU or one pod restart may not hurt users, yet it wakes someone at 3 a.m. Page only when the error budget burns fast (multi-window burn rate of 14.4x over 1 hour, 6x over 6 hours); causes belong on dashboards or in tickets.
Giữ 100% trace ở 12k rps rất tốn. Head sampling (ví dụ 10%) quyết định ngay ở đầu nên rẻ nhưng có thể bỏ sót trace lỗi. Tail sampling ở collector chờ trace hoàn tất rồi giữ mọi trace lỗi hoặc chậm hơn 1 s, cộng 5% số còn lại.Keeping 100% of traces at 12k rps is expensive. Head sampling (say 10%) decides at the start, which is cheap but may drop error traces. Tail sampling in the collector waits for the trace to complete and keeps every error or trace slower than 1 s, plus 5% of the rest.
Gửi span đồng bộ hay ghi log chặn có thể làm request chậm khi collector có sự cố. SDK dùng BatchSpanProcessor với hàng đợi giới hạn, đầy thì bỏ span chứ không chặn; collector chạy dạng agent cạnh service với memory_limiter.Exporting spans synchronously or blocking on log writes can slow requests when the collector has trouble. The SDK uses a BatchSpanProcessor with a bounded queue that drops spans instead of blocking when full; the collector runs as an agent next to the service with a memory_limiter.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
log line (Loki) | ts, level, service, env, trace_id, span_id, request_id, msg, fields… | Nhãn Loki chỉ là service, env, level; trace_id là trường JSON, tra bằng | json, không làm nhãnLoki labels are only service, env and level; trace_id is a JSON field queried with | json, never a label |
http_server_request_duration_seconds | histogram · labels: service, route, method, code · buckets 0.05 0.1 0.3 0.8 2 5 | Nguồn của RED: rate từ _count, errors từ code=~"5..", duration từ histogram_quantileThe source of RED: rate from _count, errors from code=~"5..", duration from histogram_quantile |
span (Tempo) | trace_id, span_id, parent_span_id, name, service, start, duration, status, attributes | Thuộc tính giá trị cao như user.id, order.id đặt ở đây, không ở metricHigh-cardinality attributes such as user.id and order.id live here, not in metrics |
slo | service, sli, objective, window, fast_burn, slow_burn, runbook_url | Ví dụ checkout · success ratio · 99.9% · 30d · 14.4x/1h · 6x/6h; sinh ra recording rule và alertE.g. checkout · success ratio · 99.9% · 30d · 14.4x/1h · 6x/6h; generates recording rules and alerts |
Checkout gắn observation có nhãn ít giá trị cho metric và nhãn nhiều giá trị chỉ cho span; log tự mang traceId. Go làm tương tự bằng otelhttp, slog và histogram có exemplar; rule Prometheus cảnh báo theo burn rate.Checkout wraps the work in an observation with low-cardinality tags for metrics and high-cardinality ones for the span only; logs carry the traceId automatically. Go does the same with otelhttp, slog and a histogram with exemplars; a Prometheus rule alerts on burn rate.
@Slf4j
@RestController
@RequiredArgsConstructor
public class CheckoutController {
private final ObservationRegistry registry;
private final OrderClient orders; // RestClient from the builder: traceparent added for you
@PostMapping("/checkout")
public CheckoutResult checkout(@RequestBody CheckoutRequest req) {
return Observation.createNotStarted("checkout", registry)
.lowCardinalityKeyValue("payment.method", req.paymentMethod()) // metric tag + span
.highCardinalityKeyValue("cart.id", req.cartId()) // span only
.observe(() -> {
log.info("checkout started items={}", req.items().size()); // MDC has traceId, spanId
return orders.place(req);
});
}
}
@Configuration
class MetricsConfig {
@Bean
MeterFilter dropUserId() {
return MeterFilter.ignoreTags("user_id");
}
@Bean
MeterFilter capUriTag() {
// more than 100 distinct uri values: stop registering new meters
return MeterFilter.maximumAllowableTags("http.server.requests", "uri", 100, MeterFilter.deny());
}
@Bean
MeterFilter sloBuckets() {
return new MeterFilter() {
@Override
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
if (!id.getName().equals("http.server.requests")) return config;
return DistributionStatisticConfig.builder()
.serviceLevelObjectives(0.1e9, 0.3e9, 0.8e9, 2e9) // nanoseconds
.build().merge(config);
}
};
}
}var reqDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_server_request_duration_seconds",
Help: "HTTP request latency.",
Buckets: []float64{.05, .1, .3, .8, 2, 5},
}, []string{"route", "method", "code"}) // never user_id
var client = &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport), Timeout: 2 * time.Second}
func logger(ctx context.Context) *slog.Logger {
sc := trace.SpanContextFromContext(ctx)
return slog.Default().With("trace_id", sc.TraceID().String(), "span_id", sc.SpanID().String())
}
func instrument(route string, h http.HandlerFunc) http.Handler {
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, code: http.StatusOK}
h(rec, r)
traceID := trace.SpanContextFromContext(r.Context()).TraceID().String()
obs := reqDuration.WithLabelValues(route, r.Method, strconv.Itoa(rec.code))
obs.(prometheus.ExemplarObserver).ObserveWithExemplar(
time.Since(start).Seconds(), prometheus.Labels{"trace_id": traceID})
})
return otelhttp.NewHandler(inner, route) // reads traceparent, starts the server span
}
func checkout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
trace.SpanFromContext(ctx).SetAttributes(attribute.String("cart.id", r.URL.Query().Get("cart")))
logger(ctx).InfoContext(ctx, "checkout started")
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://order-service/orders", r.Body)
resp, err := client.Do(req) // injects traceparent into the outgoing request
if err != nil {
logger(ctx).ErrorContext(ctx, "order call failed", "err", err)
http.Error(w, "unavailable", http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
}
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
otel.SetTextMapPropagator(propagation.TraceContext{})
http.Handle("/checkout", instrument("/checkout", checkout))
http.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer,
promhttp.HandlerOpts{EnableOpenMetrics: true})) // exemplars need OpenMetrics
log.Fatal(http.ListenAndServe(":8080", nil))
}| Head samplingHead sampling | Tail samplingTail sampling | |
|---|---|---|
| Quyết định khiDecides | Ngay ở span đầu tiên, truyền qua cờ sampled trong traceparentAt the first span, carried by the sampled flag in traceparent | Sau khi trace hoàn tất, tại collectorAfter the trace completes, in the collector |
| Giữ trace lỗiKeeps error traces | Không đảm bảo, lỗi hiếm có thể bị bỏNot guaranteed; rare errors may be dropped | Có, theo luật: mọi lỗi, mọi trace chậm hơn 1 sYes, by policy: every error, every trace slower than 1 s |
| Chi phíCost | Rẻ, không cần bộ nhớ đệmCheap, no buffering | Collector phải giữ span trong bộ nhớ vài giây, cần định tuyến theo trace_idThe collector buffers spans for seconds and needs routing by trace_id |
| Chọn khiPick it when | Lưu lượng vừa phải, cần đơn giảnTraffic is moderate and simplicity matters | Lưu lượng lớn và cần mọi trace lỗi để điều traTraffic is high and every error trace is needed for investigations |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
Metric là con số tổng hợp, rẻ, giữ lâu, dùng để cảnh báo và vẽ xu hướng. Trace cho thấy một request đi qua những đâu và tốn bao lâu ở mỗi chặng. Log ghi chi tiết của một sự kiện. Luồng điều tra thường là metric báo động, trace khoanh vùng, log giải thích; trace_id nối ba thứ lại.Metrics are cheap aggregated numbers kept for a long time, used for alerts and trends. Traces show where one request went and how long each hop took. Logs record the details of one event. An investigation usually goes metric to raise the alarm, trace to narrow it down, log to explain it; trace_id ties them together.
Burn rate gắn cảnh báo với ngân sách lỗi: 14,4x trong 1 giờ nghĩa là mất 2% ngân sách tháng, đáng gọi người. Ngưỡng cố định như 1% trong 5 phút vừa ồn khi có đợt lỗi ngắn vừa bỏ sót lỗi âm ỉ. Dùng hai cửa sổ để vừa nhạy vừa tắt nhanh.Burn rate ties the alert to the error budget: 14.4x over 1 hour means 2% of the monthly budget is gone, which is worth a page. A fixed threshold like 1% over 5 minutes is both noisy on short blips and blind to slow leaks. Two windows make it both sensitive and quick to clear.
Producer ghi traceparent vào header của message, consumer đọc ra và tạo span mới liên kết (span link hoặc con của span producer). Instrumentation của OpenTelemetry cho Spring Kafka và franz-go/sarama làm việc này; với code tự viết thì dùng propagator Inject và Extract trên header.The producer writes traceparent into the message headers and the consumer extracts it to start a linked span (a span link or a child of the producer span). OpenTelemetry instrumentation for Spring Kafka and franz-go/sarama does this; hand-written code calls the propagator's Inject and Extract on the headers.
Cardinality. Xem prometheus_tsdb_head_series, trang TSDB Status để tìm metric và nhãn có nhiều series nhất, thường là nhãn chứa ID, URL thô hay pod name thay đổi liên tục. Bỏ nhãn bằng metric_relabel_configs hoặc ở collector, sửa code, đặt sample_limit.Cardinality. Check prometheus_tsdb_head_series and the TSDB Status page for the metrics and labels with the most series, usually labels holding IDs, raw URLs or constantly changing pod names. Drop the label with metric_relabel_configs or in the collector, fix the code, and set sample_limit.
Phím 1–4 để chọn đáp án, Enter để kiểm tra.Press 1–4 to choose an answer, Enter to check.
Ôn tập ngắt quãng (spaced repetition): thẻ được lặp lại sau 1 → 3 → 7 → 14 → 30 ngày để củng cố trí nhớ dài hạn.Spaced repetition: cards are repeated after 1 → 3 → 7 → 14 → 30 days to build long-term memory.