49 / 73

Logging va Observability

Servis ishlab turgan serverga to'g'ridan-to'g'ri debugger ulab bo'lmaydigan holatlarda, loglar, metrikalar va tracelar uning ichida nimalar yuz berayotganini tushunishning yagona ishonchli vositasiga aylanadi. Go 1.21 versiyasida standart kutubxonaga kiritilgan log/slog paketi tuzilmali (structured) logging'ni rasman til darajasiga olib chiqdi: endilikda oddiy key-value loglarni hosil qilish uchun ekotizimdagi tashqi kutubxonalarga (Zap, Zerolog) tayanish shart emas. slog paketining sintaksisini o'rganishdan ko'ra muhimroq narsa — "tuzilmali log" aslida nima berishini va logging chegarasi qayerda tugab, uning o'rnini metrikalar va tracing egallashi kerakligini to'g'ri tushunishdir.

Tuzilmali logging: oddiy jumlalar emas, kalit-qiymat maydonlari

Eski uslubdagi logging odam o'qishi uchun mo'ljallangan jumlalarni formatlaydi va keyinchalik kimdir uni o'qib, kerakli tafsilotlarni ajratib olishiga umid qiladi:

example.go
log.Printf("user %d logged in from %s at %s", userID, ip, timestamp)

Bu terminalda log faylini ko'z bilan kuzatayotgan dasturchi uchun tushunarli bo'lishi mumkin, ammo avtomatlashtirilgan tahlil tizimlari (mashinalar) uchun mutlaqo noqulaydir: userIDni ajratib olish uchun murakkab regex yozish va hech kim logdagi so'zlarni o'zgartirib yubormasligiga umid qilish kerak bo'ladi. Tuzilmali logging buni teskarisiga o'zgartiradi: xabar qisqa va o'zgarmas satr (label) bo'lib qoladi, barcha o'zgaruvchilar esa alohida kalit-qiymat maydonlariga aylanadi:

example.go
slog.Info("user logged in",
	"user_id", userID,
	"ip", ip,
)

Asosiy foyda log satrining o'zida emas, balki keyinchalik ularni tahlil qilishda namoyon bo'ladi: log agregatorlari (Loki, Elasticsearch, Datadog va h.k.) user_id maydonini alohida indekslaydi va "4218-foydalanuvchining barcha harakatlarini ko'rsat" degan so'rovni hech qanday regexsiz bir zumda bajaradi. Chunki ma'lumot dastlabdanoq matn ichiga ko'milgan so'z emas, aniq tuzilmali ma'lumot bo'lgan.

Tuzilmali logging'ning maqsadi log terminalda qanday ko'rinishida emas, keyinchalik mashina u bilan nima qila olishida.

slog bir xil tuzilmali ma'lumot uchun ikkita standart formatni taqdim etadi: lokal ishlab chiqish jarayonida terminalda qulay o'qiladigan key=value ko'rinishi uchun slog.NewTextHandler va production muhiti uchun mo'ljallangan mashina o'qiy oladigan JSON formati uchun slog.NewJSONHandler:

example.go
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("request handled",
	slog.String("method", "GET"),
	slog.String("path", "/api/users"),
	slog.Int("status", 200),
	slog.Duration("elapsed", 45*time.Millisecond),
)

Erkin juftliklar o'rniga qat'iy tipli konstruktorlardan (slog.String, slog.Int, slog.Duration) foydalanish kodni biroz uzaytirsa-da, xavfli adashishlarning oldini oladi. Argumentlar soni ko'p bo'lganda bitta parametr tushib qolsa, erkin juftliklar kalit va qiymat o'rnini almashtirib yuborishi mumkin. Tipli konstruktorlar esa har bir atributning chegarasini qat'iy belgilaydi va bunday xatoliklarni kompilyatsiya bosqichidayoq aniqlaydi.

Log darajalari — bu qat'iy filtr, shunchaki tavsiya emas

slogda to'rtta asosiy daraja mavjud — Debug, Info, Warn, Error. Ularning asosiy vazifasi: minimal darajani bir marta belgilaysiz, undan pastdagi barcha loglar hatto formatlanmasdanoq filtrlab tashlanadi:

example.go
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
	Level: slog.LevelInfo, // Debug darajasidagi chaqiruvlar resurs sarflamasdan o'tkazib yuboriladi
})
logger := slog.New(handler)

Bu Debug darajasidagi loglarni kodda bemalol qoldirish imkonini beradi: normal holatda ular tizimni og'irlashtirmaydi, avariya paytida darajani pasaytirish orqali esa production'da barcha tafsilotlarni darhol ko'rish mumkin. Asosiy qoida: kundalik rejimda talab etilmaydigan mayda texnik qadamlar uchun Debug, tizimdagi muhim biznes hodisalar uchun Info (foydalanuvchi ro'yxatdan o'tdi, to'lov qabul qilindi), ogohlantiruvchi noxushliklar uchun Warn (ikkinchi urinishda muvaffaqiyatli bo'ldi, eskirgan API chaqirildi) va inson aralashuvini talab qiluvchi haqiqiy muammolar uchungina Error darajasini tanlang. Dastur bemalol o'zi yengib o'tadigan oddiy holatlarni (masalan, qidiruvda "foydalanuvchi topilmadi") Error darajasida chiqarish dasturchilarni haqiqiy avariyalarga beparvo bo'lishga o'rgatadi.

Testlarda yoki jim turishi kerak bo'lgan kutubxonalarda loggingni butunlay o'chirish uchun Go 1.24 da slog.DiscardHandler qo'shildi: u nafaqat chiqishni bekor qiladi, balki formatlash va xotira allokatsiyasi ishlarini ham bajarmaydi. Aynan shu jihati bilan u NewTextHandler yoki NewJSONHandler ni io.Discard ga yo'naltirishdan farq qiladi — ular natijani tashlab yuborishdan oldin har bir yozuvni baribir formatlab chiqadi:

example.go
logger := slog.New(slog.DiscardHandler) // chiqish yo'q, formatlash xarajati yo'q

Bitta logni bir vaqtda bir nechta manzilga (masalan, konsolga va log faylga) yuborish kerak bo'lganda Go 1.26 dagi slog.NewMultiHandler qo'l keladi:

example.go
logger := slog.New(slog.NewMultiHandler(
	slog.NewTextHandler(os.Stdout, nil),
	slog.NewJSONHandler(shippingPipe, nil),
))

So'rov kontekstini (context) loglarga ulash

InfoContext, ErrorContext metodlari loglarga so'rov doirasidagi qiymatlarni — ayniqsa request_id yoki trace_idni — context.Contextdan avtomatik olib biriktirish imkonini beradi:

example.go
func handleRequest(ctx context.Context) {
	slog.InfoContext(ctx, "processing request")
	processData(ctx)
}

Agar ma'lumotlar qo'lingizda allaqachon tayyor bo'lsa, slog.Logger.With(...) metodi yordamida o'zgarmas atributlarga ega yangi logger yaratish eng sodda usuldir:

example.go
reqLogger := slog.With("request_id", requestID)
reqLogger.Info("started")   // request_id avtomatik kiradi
reqLogger.Info("completed") // bu xabarga ham qo'shiladi

Loglarga hech qachon yozilmasligi kerak bo'lgan ma'lumotlar

Tuzilmali logging maxfiy ma'lumotlarning tasodifan fosh bo'lish xavfini oshiradi, chunki password yoki token kaliti username yonida tabiiy ko'rinadi:

example.go
// Hech qachon bunday qilmang — bu ma'lumotlar loglar bazasida ochiq saqlanib qoladi!
slog.Info("login attempt", "username", user, "password", pass)

Xavfsizlik qoidasi: maxfiy qiymatlarning o'zini emas, ularning identifikatorlarini loglang — emailni emas, user_idni; tokenning o'zini emas, uning faqat so'nggi 4 belgisini yoki shunchaki mavjudlik faktini.

Logging qayerda ojiz qoladi: Metrikalar va Tracing

Loglar "nima sodir bo'ldi va qaysi ketma-ketlikda?" degan savolga mukammal javob beradi. Biroq "soatiga nechta so'rov kelyapti?", "o'rtacha kechikish (latency) qancha?" yoki "o'nlab mikroservislar zanjirida aynan qaysi biri so'rovni kechiktiryapti?" degan savollarga loglar orqali javob topish nihoyatda samarasizdir.

Metrikalar — vaqt bo'yicha hisoblanadigan sonli ko'rsatkichlar: so'rovlar soni, xatoliklar foizi, kechikish persentillari (p95, p99). Dashboard va alertlar (ogohlantirishlar) aynan metrikalar asosida ishlaydi, chunki "p99 kechikish besh daqiqa davomida 500ms dan oshdi" — bu aniq belgilangan va hisoblash arzon bo'lgan alert sharti; "sekin so'rovlarni loglardan grep qilish" esa bunday emas. Eng oddiy holatlar uchun sync/atomic bilan qo'lda yozilgan hisoblagich kifoya qiladi:

example.go
var requestCount atomic.Int64

func handler(w http.ResponseWriter, r *http.Request) {
	requestCount.Add(1)
	// ...
}

Ammo histogramma (kechikish persentillari uchun), label (metrikani endpoint yoki status kodi bo'yicha ajratish) yoki dashboard vositasi o'qiy oladigan (scrape) format kerak bo'lgan zahoti haqiqiy metrikalar kutubxonasiga o'ting — Go ekotizimidagi de-fakto standart bu Prometheus'ning client_golang paketi:

example.go
var requestDuration = prometheus.NewHistogramVec(
	prometheus.HistogramOpts{
		Name: "http_request_duration_seconds",
		Help: "HTTP request duration in seconds",
	},
	[]string{"method", "path", "status"},
)

func init() {
	prometheus.MustRegister(requestDuration)
}

Tracing — bitta aniq sekin yoki xatoga uchragan so'rovning o'nlab servislar bo'ylab o'tgan butun yo'lini (chaqiruvlar daraxtini) ko'rsatib beradi. Har bir oraliq amal alohida span (vaqt oralig'i) sifatida qayd etiladi. Go tilida bunga OpenTelemetry (OTel) orqali erishiladi:

example.go
tracer := otel.Tracer("myapp")
ctx, span := tracer.Start(ctx, "processOrder")
defer span.End()

span.SetAttributes(attribute.String("order_id", orderID))

ctx, dbSpan := tracer.Start(ctx, "database.query")
// ... ma'lumotlar bazasiga so'rov ...
dbSpan.End()

Katta hajmdagi taqsimlangan tracing tizimlaridan tashqari, Go 1.25 da standart kutubxonaga runtime/trace.FlightRecorder kiritildi. U samolyotning qora qutisi kabi faqat so'nggi bir necha soniyalik trace ma'lumotlarini (goroutine almashinuvlari, GC pauzalari, bloklanishlar) xotiradagi halqasimon buferda saqlab turadi va faqat avariya yoki haddan tashqari sekinlashuv yuz bergandagina diskka to'kib beradi:

example.go
fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{})
fr.Start()
defer fr.Stop()

// Xatolik yuz berganda (sekin so'rov, alert):
var buf bytes.Buffer
fr.WriteTo(&buf) // Go runtime faoliyatining so'nggi bir necha soniyasi

Bu uchta vosita bir-birini inkor etmaydi, balki to'ldiradi:

  • Loglar — "nima bo'ldi?",
  • Metrikalar — "tizim holati qanday va qanchalik tez ishlayapti?",
  • Tracelar — "bitta so'rov aynan qaysi oraliqlarda kechikdi?"

Loglar "nima bo'ldi", metrikalar "qancha va qanchalik tez", tracelar esa "bitta so'rov qayerdan o'tdi" degan savolga javob beradi — vositani savolga qarab tanlang.

Observability vositalari ishlayotgan tizimni tahlil qilishga yordam beradi. Keyingi bobda esa kodni umuman ishga tushirmasdan turib xatolar va zaifliklarni aniqlash imkonini beruvchi statik tahlil vositalarini (go vet, staticcheck, govulncheck) ko'rib chiqamiz.