Irbisa · Шпаргалка
Observability и эксплуатация
slog, Prometheus, OpenTelemetry, пробы, SLO, разбор всплеска p99
Логи, метрики и трейсы: на какой вопрос отвечает только каждый из них?
Что такое структурированное логирование, как его делает
log/slogи какой уровень лога для чего использовать?// wrong: the fields are baked into the sentence, so every question is a grepfunc placeOld(id, tenant string, err error) { log.Printf("order %s failed for tenant %s: %v", id, tenant, err)} var level = new(slog.LevelVar) // Leveler: flip at runtime, no redeploy…Найдите баг: неудачные списания попадают в лог строками
ERROR, но поиск по полюerrих не находит.func (s *Server) handleCharge(w http.ResponseWriter, r *http.Request) { orderID := r.PathValue("id") log := s.log.With("request_id", requestID(r)) if err := s.payments.Charge(r.Context(), orderID); err != nil { log.ErrorContext(r.Context(), "charge failed", "order_id", orderID, err) http.Error(w, "payment failed", http.StatusBadGateway) return } log.InfoContext(r.Context(), "charge ok", "order_id", orderID) w.WriteHeader(http.StatusNoContent)}Как Prometheus забирает метрики из Go-сервиса и какие метрики рантайма
client_golangотдаёт без единой строки вашего кода?func newMetrics() (*prometheus.Registry, *prometheus.CounterVec) { reg := prometheus.NewRegistry() // empty: nothing is implicit here reg.MustRegister( collectors.NewGoCollector( collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsScheduler),…Чем отличаются counter, gauge, histogram и summary и какой тип брать для задержки запросов?
var ( requests = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "http_requests_total", Help: "Requests by route and status class.", }, []string{"route", "status"})…Что такое трейс, спан и trace id и как trace id переходит из одного сервиса в другой?
func initTracing(ctx context.Context) (func(context.Context) error, error) { exp, err := otlptracegrpc.New(ctx) // endpoint from OTEL_EXPORTER_OTLP_ENDPOINT if err != nil { return nil, err } tp := sdktrace.NewTracerProvider(…Что такое SLI, SLO и SLA и что такое бюджет ошибок?
Как расшифровываются RED и USE и какой из двух наборов инструментировать первым на HTTP-сервисе, а какой на воркере очереди?
Хендлеры повторяют
"request_id", ridв каждом вызовеslog, а в строках из пакетов поглубже его нет. Как это исправить и в чём ловушка с логгером вContext?type ctxKey struct{} func withLogger(ctx context.Context, l *slog.Logger) context.Context { return context.WithValue(ctx, ctxKey{}, l)}…slog.Info("auth ok", "user", u)положил живой bearer-токен в хранилище логов, а эта структура логируется из десятка мест. Где это чинится один раз?type Token string // Resolved by the handler at output time, everywhere this value is logged.func (Token) LogValue() slog.Value { return slog.StringValue("REDACTED") } type User struct {…Один неудачный поход в базу даёт пять строк
ERRORи пять инкрементов счётчика ошибок, по одному на слой. Какое здесь правило и где место единственной строке лога?// wrong: logged here, and again by every caller abovefunc (r *Repo) load(ctx context.Context, id string) (Order, error) { var o Order if err := r.db.QueryRowContext(ctx, q, id).Scan(&o.ID); err != nil { slog.ErrorContext(ctx, "query failed", "err", err) return Order{}, err…Этот хендлер обслуживает первый запрос и падает на каждом следующем. Что происходит и где нужно создавать счётчик?
func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) { orders := prometheus.NewCounter(prometheus.CounterOpts{ Name: "orders_created_total", Help: "Orders created.", }) prometheus.MustRegister(orders) if err := s.create(r.Context(), r); err != nil { http.Error(w, "create failed", http.StatusInternalServerError) return } orders.Inc() w.WriteHeader(http.StatusCreated)}Как выбрать бакеты гистограммы для задержки запросов и почему верхний бакет по умолчанию искажает p99?
var latency = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request_duration_seconds", // Boundaries around the SLO (300ms) and out past the timeout (10s), // so the p99 does not saturate at the last bucket. Buckets: []float64{.005, .01, .025, .05, .1, .2, .3, .5, 1, 2.5, 5, 10, 30},…Найдите баг: гистограмма задержек checkout кладёт каждый запрос в самый нижний бакет, включая те, на которых клиенты отваливаются по таймауту.
var checkoutDuration = promauto.NewHistogram(prometheus.HistogramOpts{ Name: "checkout_duration_seconds", Help: "Checkout handler latency.", Buckets: []float64{.05, .1, .3, .5, 1, 2.5, 5, 10},}) func (s *Server) handleCheckout(w http.ResponseWriter, r *http.Request) { start := time.Now() defer checkoutDuration.Observe(time.Since(start).Seconds()) if err := s.checkout(r.Context(), r); err != nil { http.Error(w, "checkout failed", http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated)}Запрос вернул 500, а его спан в трейсе зелёный. Что код не сделал и чем
span.RecordErrorотличается отspan.SetStatus?var tracer = otel.Tracer("github.com/acme/orders") func (s *Service) Load(ctx context.Context, id string) (Order, error) { // Attributes at Start are visible to the sampler; later ones are not. ctx, span := tracer.Start(ctx, "OrderService.Load", trace.WithAttributes(attribute.String("order.id", id)))…В трейсе спан запроса из репозитория висит соседом
GetOrder, а не его ребёнком, а неудачные поиски не появляются вовсе. Почему?func (s *Service) GetOrder(ctx context.Context, id string) (Order, error) { _, span := tracer.Start(ctx, "GetOrder") o, err := s.repo.Find(ctx, id) // repo starts its own "db.query" span from ctx if err != nil { return Order{}, fmt.Errorf("get order %s: %w", id, err) } span.End() return o, nil}Оба сервиса работают с SDK OpenTelemetry, но нижний заводит совершенно новый трейс на каждый запрос, который присылает гейтвей. Чего не хватает?
func initPropagation() { // Without this the SDK propagates nothing at all. otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, // W3C traceparent / tracestate propagation.Baggage{}, ))…Что решает каждая из проб (liveness, readiness и startup) и что каждая из них должна проверять в Go-сервисе?
type Probes struct { started atomic.Bool // set once config, caches and pools are ready draining atomic.Bool // set on SIGTERM lastTick atomic.Int64 // unix nanos, set at start and bumped by the worker loop}…Каждый роллинг-деплой теряет несколько запросов, хотя сервис вызывает
srv.ShutdownпоSIGTERM. Что в этот момент делает Kubernetes и как это переждать?Лейбл
user_idнаhttp_requests_totalоставил Prometheus с четырьмя миллионами рядов и запросами, падающими по таймауту. Почему один лейбл так дорог и как его ограничить?func metrics(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) route := r.Pattern // set by the mux while routing: read it after next…Двадцатисекундное моргание базы превратилось в четырёхминутный простой сервиса, чья readiness-проба пингует эту базу. Откуда взялись остальные три с половиной минуты?
Вы сэмплируете 1% трейсов на входе (head-based), и девятисекундного оформления заказа клиента в бэкенде нет. На что отвечает 1% и что изменил бы tail-сэмплинг?
func provider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { return sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp), // ParentBased: only a root flips the coin; downstream follows traceparent. sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.01))), )…В 14:05 p99 вырос втрое при ровном CPU и без деплоев. Как в проде дойти от дашборда до медленного места в коде и что должно быть готово заранее для
pprof?// Importing this package registers its handlers on http.DefaultServeMux from// its init(), which is exactly why the public server must never use that mux.import "net/http/pprof" func startDebug() *http.Server { mux := http.NewServeMux() // our own mux, registered explicitly…Вас будят в три ночи алерты
CPU > 80%иp99 > 500ms, которые гаснут раньше, чем кто-то посмотрит, а настоящая двухчасовая деградация не разбудила никого. Как переделать алертинг?groups: - name: orders-slo rules: - record: orders:error_ratio:rate5m expr: | sum(rate(http_requests_total{job="orders",status="5xx"}[5m]))…