Go'nun standart kütüphanesinde iki loglama paketi vardır: ilk sürümden beri bulunan basit log paketi ve Go 1.21 ile eklenen yapılandırılmış loglama paketi log/slog. log serbest metin satırları yazar; slog ise her kaydı bir zaman, bir seviye, bir mesaj ve anahtar-değer çiftlerinden oluşan bir yapı olarak üretir ve bunu key=value metni ya da satır başına bir JSON nesnesi olarak yazdırabilir. Yeni bir servis yazıyorsanız loglarınızın makinece aranıp filtrelenebilmesi için slog kullanmak mantıklı bir başlangıçtır.

Bu yazıda önce log paketinin bayraklarını ve önekini, ardından slog ile TextHandler ve JSONHandler kullanımını, seviyeleri ve çalışırken seviye değiştirmeyi, With ve gruplarla bağlam eklemeyi, LogValuer ile gizli değerleri maskelemeyi ve iki paketin birbirine nasıl bağlandığını anlatıyoruz. Örneklerde çıktı her çalıştırmada aynı olsun diye zaman alanını ReplaceAttr ile çıkarıyoruz; gerçek uygulamada bu alanı korumanız gerekir. Biçim fiilleri için Go fmt ve Printf yazısına bakabilirsiniz.

log paketi: hızlı ve basit

log paketi: hızlı ve basit

log paketindeki hazır standart logger standart hataya (stderr) yazar ve her satırın başına tarih ile saati ekler. Mesaj satır sonuyla bitmiyorsa logger kendisi ekler. Satır başında neyin görüneceğini bayraklar belirler: Ldate, Ltime, Lmicroseconds, Llongfile, Lshortfile, LUTC ve Lmsgprefix. Varsayılan LstdFlags, tarih ve saatin birleşimidir. Lmsgprefix dışında bu öğelerin sırası ve biçimi değiştirilemez. log.New ile çıktı hedefi, öneki ve bayrakları farklı ayrı bir logger oluşturabilirsiniz.

go
package main

import (
	"log"
	"os"
)

func main() {
	// Standart logger varsayılan olarak stderr'e tarih ve saatle yazar.
	// Çıktı tekrarlanabilir olsun diye bayrakları kapatıp stdout'a yönlendiriyoruz.
	log.SetOutput(os.Stdout)
	log.SetFlags(0)
	log.SetPrefix("[uygulama] ")
	log.Println("başladı")
	log.Printf("%d kayıt yüklendi", 42)

	// Ayrı bir logger: önek mesajın hemen önüne gelsin, dosya:satır eklensin.
	db := log.New(os.Stdout, "db: ", log.Lmsgprefix|log.Lshortfile)
	db.Print("bağlantı açıldı")
}

// Çıktı:
// [uygulama] başladı
// [uygulama] 42 kayıt yüklendi
// main.go:19: db: bağlantı açıldı

Bu pakette dikkat edilmesi gereken iki fonksiyon ailesi vardır. Fatal, Fatalf ve Fatalln mesajı yazdıktan sonra os.Exit(1) çağırır; os belgesine göre Exit programı hemen sonlandırır ve ertelenmiş (defer) fonksiyonlar çalışmaz. Panic ailesi ise mesajı yazıp panic çağırır. Dolayısıyla bir kütüphane fonksiyonunun içinde log.Fatal kullanmak, çağıranın dosya kapatma ya da temizlik yapmasını engeller; hatayı döndürüp kararı main fonksiyonuna bırakmak daha doğrudur.

slog ile ilk yapılandırılmış log

slog paketinde bir Logger, kayıtları üretir ve her kaydı bir Handler değerine verir; kaydın nasıl biçimleneceğine ve nereye yazılacağına handler karar verir. Paket iki hazır handler ile gelir: NewTextHandler boşlukla ayrılmış key=value çiftleri, NewJSONHandler satır başına bir JSON nesnesi yazar. Mesajdan sonra verdiğiniz argümanlar sırayla anahtar ve değer olarak eşleşir; slog.Int, slog.String, slog.Bool, slog.Any gibi kurucularla doğrudan Attr değerleri de verebilirsiniz.

go
package main

import (
	"errors"
	"log/slog"
	"os"
	"time"
)

// zamaniSil, örnek çıktısı her çalıştırmada aynı olsun diye "time" alanını atar.
func zamaniSil(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		return slog.Attr{}
	}
	return a
}

func main() {
	opts := &slog.HandlerOptions{ReplaceAttr: zamaniSil}

	metin := slog.New(slog.NewTextHandler(os.Stdout, opts))
	metin.Info("istek tamamlandı", "yol", "/api/urun", "durum", 200, "sure", 35*time.Millisecond)
	metin.Warn("disk dolmak üzere", slog.Int("yuzde", 91))
	metin.Error("ödeme başarısız", "err", errors.New("kart reddedildi"))

	js := slog.New(slog.NewJSONHandler(os.Stdout, opts))
	js.Info("istek tamamlandı", "yol", "/api/urun", "durum", 200, "sure", 35*time.Millisecond)
	js.Error("ödeme başarısız", "err", errors.New("kart reddedildi"))
}

// Çıktı:
// level=INFO msg="istek tamamlandı" yol=/api/urun durum=200 sure=35ms
// level=WARN msg="disk dolmak üzere" yuzde=91
// level=ERROR msg="ödeme başarısız" err="kart reddedildi"
// {"level":"INFO","msg":"istek tamamlandı","yol":"/api/urun","durum":200,"sure":35000000}
// {"level":"ERROR","msg":"ödeme başarısız","err":"kart reddedildi"}

Çıktıdaki farklara bakın. TextHandler, boşluk, tırnak, eşittir işareti ya da yazdırılamayan karakter içeren anahtar ve değerleri strconv.Quote ile tırnaklar; bu yüzden msg="istek tamamlandı" tırnaklı, yol=/api/urun tırnaksızdır. Süre değeri metinde 35ms, JSON'da ise nanosaniye cinsinden tamsayı olarak görünür. error tipindeki değerler her iki handler'da da Error() metoduyla string'e çevrilir; JSONHandler belgesine göre bu özel işlem yalnızca doğrudan Attr değeri olan hatalar için geçerlidir, bir struct ya da slice içine gömülmüş hatalar için yapılmaz. JSONHandler değerleri SetEscapeHTML(false) ayarlı bir json.Encoder gibi biçimlendirir, bu yüzden < ve & gibi karakterler kaçış dizisine çevrilmeden yazılır. Bir değer JSON'a çevrilemezse Handle hata döndürmez; hata mesajı string olarak satıra yazılır. Her iki handler da her kayıt için io.Writer üzerinde tek bir Write çağrısı yapar.

Zaman alanı normalde TextHandler'da milisaniye hassasiyetli RFC3339 biçiminde, JSONHandler'da json.Marshal biçiminde yazılır. ReplaceAttr fonksiyonu yerleşik time, level, source ve msg anahtarları dahil grup olmayan her özniteliği yazılmadan önce değiştirme şansı verir; sıfır Attr döndürürseniz o öznitelik atılır. Anahtar adlarını değiştirmek, zamanı farklı bir biçime çevirmek ya da kişisel veriyi temizlemek için bu kancayı kullanırsınız.

Seviyeler ve LevelVar ile dinamik ayar

Seviyeler ve LevelVar ile dinamik ayar

slog seviyeleri tamsayıdır: LevelDebug -4, LevelInfo 0, LevelWarn 4, LevelError 8. Belgeler bu sayıların neden seçildiğini açıklar: varsayılan seviyenin int sıfır değeri olan Info olması istenmiş, aradaki 4'lük boşluklar ise başka sistemlerin ara seviyelerine yer bırakmak içindir. Handler'ın en düşük seviyesini HandlerOptions.Level ile verirsiniz; nil ise Info kabul edilir. Buraya sabit bir Level verirseniz seviye handler'ın ömrü boyunca sabit kalır; *slog.LevelVar verirseniz program çalışırken değiştirilebilir ve LevelVar birden fazla goroutine tarafından güvenle okunup yazılabilir.

go
package main

import (
	"context"
	"fmt"
	"log/slog"
	"os"
)

func zamaniSil(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		return slog.Attr{}
	}
	return a
}

func main() {
	seviye := new(slog.LevelVar) // sıfır değeri Info
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level:       seviye,
		ReplaceAttr: zamaniSil,
	}))

	logger.Debug("görünmez") // Info altı: atılır
	logger.Info("görünür")

	seviye.Set(slog.LevelDebug) // çalışırken ayrıntılı moda geç
	logger.Debug("artık görünür", "deneme", 1)

	// Seviyeler tamsayıdır; adlandırılmış iki seviye arasındaki değerler böyle yazılır:
	logger.Log(context.Background(), slog.LevelInfo+2, "ara seviye")

	fmt.Println(slog.LevelDebug, int(slog.LevelDebug), slog.LevelError, int(slog.LevelError))
	fmt.Println(logger.Enabled(context.Background(), slog.LevelDebug))
}

// Çıktı:
// level=INFO msg=görünür
// level=DEBUG msg="artık görünür" deneme=1
// level=INFO+2 msg="ara seviye"
// DEBUG -4 ERROR 8
// true

İsimli iki seviye arasındaki bir değer, alttaki ismin sonuna fark eklenerek yazılır; örnekteki LevelInfo+2 değeri INFO+2 olarak göründü. logger.Enabled metodu bir seviyenin o anda yazılıp yazılmayacağını söyler. Bu, pahalı bir hesaplamayı yalnızca log gerçekten yazılacaksa yapmak için kullanılabilir; belgeler bunun için LogValuer arayüzünü de önerir.

With, Group ve WithGroup ile bağlam eklemek

Bir isteğe ait tüm log satırlarında aynı alanların (istek kimliği, kullanıcı, servis adı) bulunması istenir. Her çağrıda bu alanları tekrar yazmak yerine logger.With(...) ile bu öznitelikleri taşıyan yeni bir logger üretirsiniz. Belgelere göre yerleşik handler'lar With ile eklenen özniteliği yalnızca bir kez, With çağrısı sırasında biçimlendirir; bu yüzden ortak alanlar için hem okunaklı hem verimli bir yoldur. slog.Group birkaç özniteliği bir ad altında toplar, WithGroup ise logger'ın sonraki tüm anahtarlarını o grup adıyla nitelendirir.

go
package main

import (
	"log/slog"
	"os"
)

func zamaniSil(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		return slog.Attr{}
	}
	return a
}

func main() {
	opts := &slog.HandlerOptions{ReplaceAttr: zamaniSil}
	for _, h := range []slog.Handler{
		slog.NewTextHandler(os.Stdout, opts),
		slog.NewJSONHandler(os.Stdout, opts),
	} {
		base := slog.New(h).With("servis", "sepet") // her satıra eklenir
		base.Info("istek",
			slog.Group("http", "metot", "POST", "yol", "/sepet"),
			"kullanici", 7)

		odeme := base.WithGroup("odeme") // sonraki tüm anahtarlar "odeme" altında
		odeme.Info("tahsilat", "tutar", 149.90, "para", "TRY")
	}
}

// Çıktı:
// level=INFO msg=istek servis=sepet http.metot=POST http.yol=/sepet kullanici=7
// level=INFO msg=tahsilat servis=sepet odeme.tutar=149.9 odeme.para=TRY
// {"level":"INFO","msg":"istek","servis":"sepet","http":{"metot":"POST","yol":"/sepet"},"kullanici":7}
// {"level":"INFO","msg":"tahsilat","servis":"sepet","odeme":{"tutar":149.9,"para":"TRY"}}

Gruplar TextHandler'da noktalı anahtarlar (http.metot), JSONHandler'da iç içe nesneler olarak görünür. Büyük sistemlerde her alt sisteme kendi grup adını taşıyan bir logger vermek, farklı alt sistemlerin aynı anahtar adını (örneğin id) kullanmasından doğan karışıklığı önler. TextHandler belgesi bir uyarı da içerir: grup ve anahtar adlarının içinde nokta varsa a.b.c anahtarından grup yapısını geri çıkarmak mümkün değildir.

LogValuer, hatalı argümanlar ve log paketine köprü

Bir tip LogValuer arayüzünü (LogValue() slog.Value) uygularsa loga yazılırken bu metodun döndürdüğü değer kullanılır. Parola ya da erişim anahtarı gibi değerleri maskelemek, bir struct'ın alanlarını grup halinde göstermek veya pahalı bir hesaplamayı yalnızca log yazılacaksa yapmak için idealdir. slog.SetDefault ise üst düzey slog.Info gibi fonksiyonların kullandığı varsayılan logger'ı değiştirir ve aynı zamanda log paketinin varsayılan logger'ını da bu handler'a bağlar; eski log.Printf çağrıları yeniden yazılmadan yeni biçimde çıkar.

go
package main

import (
	"log"
	"log/slog"
	"os"
)

// Parola, loga yazılırken asla açık metin olarak görünmesin.
type Parola string

func (Parola) LogValue() slog.Value { return slog.StringValue("***") }

func zamaniSil(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		return slog.Attr{}
	}
	return a
}

func main() {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: zamaniSil}))
	slog.SetDefault(logger)

	slog.Info("giriş", "kullanici", "ayse", "parola", Parola("123456"))

	// Anahtarı unutulmuş değer: !BADKEY olarak işaretlenir (go vet de uyarır).
	slog.Info("eksik anahtar", "kullanici")

	// SetDefault sonrası eski log paketi çağrıları da aynı handler'dan geçer.
	log.Printf("eski kod %d satır yazdı", 3)
}

// Çıktı:
// level=INFO msg=giriş kullanici=ayse parola=***
// level=INFO msg="eksik anahtar" !BADKEY=kullanici
// level=INFO msg="eski kod 3 satır yazdı"

İkinci satırdaki !BADKEY işaretine dikkat edin. Logger.Log belgesine göre argüman bir Attr ise olduğu gibi kullanılır; string ise ve son argüman değilse bir sonraki argümanla birleşip çift oluşturur; diğer her durumda değer !BADKEY anahtarıyla yazılır. Bu hata derleyiciyi geçer ama Go 1.22 sürüm notlarına göre go vet artık anahtar konumunda string ya da Attr olmayan argümanları ve değeri eksik son anahtarı raporlar. Aynı dosya için vet çıktısı şuydu:

bash
$ go vet main.go
main.go:28:2: call to slog.Info missing a final value

go vet ve diğer araçlar için Go fmt, vet ve doc araçları yazısına bakın. Varsayılan handler kullanılırken (yani SetDefault çağrılmadan önce) Go 1.22 ile gelen slog.SetLogLoggerLevel, üst düzey slog çağrılarının hangi seviyeden itibaren log paketine aktarılacağını ayarlar; SetDefault sonrası ise log.Printf çağrılarının hangi seviyede kaydedileceğini belirler.

Bir web servisinde slog düzeni

Pratikte loglama yapılandırması main fonksiyonunda bir kez yapılır: handler seçilir (geliştirmede okunaklı TextHandler, log toplama sistemine giden üretim ortamında JSONHandler), seviye bir LevelVar ile ayarlanır ve slog.SetDefault ile varsayılan yapılır. Paket belgesindeki örnekler çıktıyı standart hataya (os.Stderr) yönlendirir; servis yöneticileri ve kapsayıcı çalıştırıcıları bu akışı toplayabilir. Ardından her gelen istek için istek kimliği, metot ve yol gibi alanları taşıyan bir alt logger With ile üretilir ve isteği işleyen fonksiyonlara parametre olarak ya da bağlam üzerinden aktarılır. Böylece bir hata satırına baktığınızda aynı isteğe ait diğer satırları tek bir alanla filtreleyebilirsiniz.

Testlerde log çıktısı genellikle gürültüdür. Go 1.24 ile gelen slog.DiscardHandler hiçbir zaman etkin olmadığı için slog.New(slog.DiscardHandler) ile hiçbir şey yazmayan bir logger elde edersiniz. Log çıktısının kendisini doğrulamak istiyorsanız bu yazıdaki örneklerde olduğu gibi handler'ı bir bytes.Buffer değerine yazdırıp zaman alanını ReplaceAttr ile çıkarabilirsiniz. Kendi handler'ınızı yazmak isterseniz paket belgesi ayrı bir handler yazma rehberine yönlendirir; Go 1.21 sürüm notlarına göre testing/slogtest paketi de slog.Handler uygulamalarını doğrulamaya yardımcı olur.

Hangi paketi ne zaman kullanmalı?

Hangi paketi ne zaman kullanmalı? Çıktı biçimi, Seviye, Anahtar-değer alanları, Dinamik seviye
Özellikloglog/slog
Çıktı biçimiSerbest metin satırıkey=value metin ya da JSON, özel handler
SeviyeYok (Fatal ve Panic dışında)Debug, Info, Warn, Error ve ara değerler
Anahtar-değer alanlarıYok, Printf ile metne gömülürVar, With ve Group ile
Dinamik seviyeYokLevelVar ile
Birden çok hedefio.MultiWriter ileGo 1.26 NewMultiHandler ile
Uygun olduğu yerKüçük araçlar, betiklerServisler, log toplama sistemleri

Go 1.24 sürüm notları, hiçbir zaman etkin olmayan ve tüm çıktıyı atan slog.DiscardHandler değerini; Go 1.25 notları GroupAttrs fonksiyonunu ve Record.Source metodunu; Go 1.26 notları ise verilen tüm handler'ları çağıran NewMultiHandler fonksiyonunu ekledi. MultiHandler en az bir handler etkinse etkindir ve kaydı yalnızca etkin handler'lara iletir. Böylece aynı kaydı hem konsola metin hem dosyaya JSON olarak yazmak için ek kod gerekmez.

Sık yapılan hatalar

  • Anahtar-değer çiftlerini eksik vermek: tek kalan argüman !BADKEY olarak yazılır. go vet çalıştırın ya da slog.String gibi Attr kurucularını kullanın.
  • Kütüphane kodunda log.Fatal: os.Exit ertelenmiş fonksiyonları çalıştırmaz; temizlik atlanır ve çağıran hatayı ele alamaz.
  • Argümanları ucuz sanmak: belgelere göre log çağrısının argümanları kayıt atılsa bile değerlendirilir. r.URL.String() sonucunu değil URL değerinin kendisini geçmek (handler String metodunu yalnızca kayıt etkinse çağırır) ya da LogValuer kullanmak gereksiz işi önler.
  • Gizli veriyi açık loglamak: parola, token ve kart bilgisi için LogValuer ya da ReplaceAttr ile maskeleme yapın.
  • slog.Info sarmalayan yardımcı yazmak: paket kaynak dosya bilgisini çağrı yığınından bulduğu için kendi Infof fonksiyonunuz kaynak olarak sarmalayıcının dosyasını gösterir. Belgeler doğru uygulamada program sayacının NewRecord fonksiyonuna verilmesini önerir.
  • Record kopyasını değiştirmek: özel handler yazarken Record değerini değiştirmeden önce Clone ile kopyalayın; basit kopya iç durumu paylaşır.

Sık Sorulan Sorular

slog çıktısına dosya adı ve satır numarası ekleyebilir miyim?

Evet, HandlerOptions.AddSource alanını true yapın. TextHandler kaynağı source=DOSYA:SATIR biçiminde, JSONHandler ise source anahtarı altında bir nesne olarak yazar.

Context alan log fonksiyonları ne işe yarar?

InfoContext, ErrorContext ve LogAttrs gibi fonksiyonlar bağlamı handler'a iletir. Yerleşik handler'lar bağlamı kullanmaz, ama özel bir handler bağlamdaki izleme kimliği gibi bilgileri satıra ekleyebilir. Belgeler, elinizde bir bağlam varsa onu çıktı metoduna vermenizi önerir; bağlamlar için Go context paketi yazısına bakın.

En verimli log çağrısı hangisi?

Belgelere göre Logger.LogAttrs, yalnızca Attr kabul ettiği için anahtar-değer çiftlerini ayrıştırma maliyetinden ve bellek ayırmadan kaçınabilir; aynı çıktıyı üretmenin en verimli yoludur.

Loglar hangi sırayla yazılır?

Yerleşik handler'lar Write çağrısından önce kilit alır ve her kaydı tek parça halinde yazar; ancak kayıtları zaman damgasına göre sıralamaz. Eşzamanlı goroutine'lerden gelen satırların sırası çalıştırmadan çalıştırmaya değişebilir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Go öğrenme rehberi · Go net/http web sunucusu · Go context paketi · Go fmt ve Printf · Go encoding/json · Go vet ve araçlar