Golang ile Proje Çapında Global Loglama

Programlama

Merhabalar, bu yazımızda Golang’da proje çapında loglama işleminin nasıl yapılacağını anlatıyorum. Proje çapında loglama derken kastteiğim şey, projedeki her paketten global loglama değişkenini çağırarak log dosyasına not düşebilmek. Ayrıca Gin-Gonic ile yazdığımız REST API’mize gelen her isteği de yine bu yapı sayesinde loglayabiliyor ve gerekli durumlarda debug yapabiliyoruz.

Proje Yapısı

proje-yapisi-golang

Burada kullanacağımız paketler, logger ve middlewares paketleri.

  • Logger paketi, Go’nun varsayılan “log” paketi kullanılarak geliştirilen “logrus”u kullanıyor. Logger paketi sayesinde “logger.Log.Infoln(”Log mesajı”)” şeklinde logger’imizi herhangi bir paketten çağırabileceğiz.
  • Middlewares paketi ise Gin-Gonic ile yazdığımız REST API’ye gelen istekleri keserek istek bilgilerini alacak ve ardından bu bilgileri “api.log” dosyamıza yazacak.

 

Logger Paketini Hazırlayalım


logger/logger.go dosyasında aşağıdaki gibi paket tanımımızı oluşturup ardından kullanacağımız logrus ve godotenv paketlerini ve diğer built-in paketleri import edelim.

ve diğer built-in paketleri import edelim.

package logger

import (
	"io"
	logging "log"
	"os"
	"strconv"

	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
)

var (
	Log *logrus.Logger // share will all packages
)

Ayrıca, burada diğer paketlerden de erişerek kullanacağımız “Log” değişkenini global olarak tanımlayalım.

  • Global değişken tanımlama: Go’da bir değişkeni global olarak tanımlamak için paketin en tepesinde (herhangi bir fonksiyonun içine yazmadan) “var” belirteci ile tanımlarız. Değişken ve fonksiyonları public yapmak için ise ilk harfini büyük harf yaparız, eğer private yapmamız gerekirse ilk harfi küçük olmalıdır. Bu değişken proje çapında global olarak kullanacağımız için public yapıyoruz.

Artık fonksiyonlarımızı yazmaya başlayabiliriz. Burada kullanacağımız ilk fonksiyon init fonksiyonu. Init fonksiyonu, paketin varsayılan fonksiyonu olup doğrudan çalıştırılır. Herhangi bir konumdan onu çağırmanıza gerek kalmaz.

package logger

import (
	"io"
	logging "log"
	"os"
	"strconv"

	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
)

var (
	Log *logrus.Logger // share will all packages
)

func init() {
// The file needs to exist prior
	f, err := os.OpenFile("api.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)

	if err != nil {
		// Use go's logger, while we configure Logrus
		logging.Fatalf("error opening file: %v", err)
	}
}

Şuanki tanımlama ile “api.log” adında bir dosya oluşturup bu dosya için kendimize okuma, oluşturma ve yazma izinlerini verdik. Eğer dosya açılmazsa da Go’nun default log paketini kullanarak bir hata logunu ekrana basacağız.

 

Sırada ise logruspaketini projemize ekleyip konfigüre etmek var. Burada çıktıyı text formatında istediğim için TextFormatter’i kullandım. Ayrıca logları hem log dosyamıza hem de ekrana basacak şekilde konifgürasyonu tamamladım.

package logger

import (
	"io"
	logging "log"
	"os"
	"strconv"

	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
)

var (
	Log *logrus.Logger // share will all packages
)

func init() {
// The file needs to exist prior
	f, err := os.OpenFile("api.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)

	if err != nil {
		// Use go's logger, while we configure Logrus
		logging.Fatalf("error opening file: %v", err)
	}

// Configure Logrus
	Log = logrus.New()
	Log.Formatter = &logrus.TextFormatter{}
	Log.SetReportCaller(true)
	mw := io.MultiWriter(os.Stdout, f)
	Log.SetOutput(mw)

}

Şimdi ise sırada logger’imize son dokunuşları yapmak var. Aşağıda eklediğim kod bloğu ile de projemizde bulunan .env dosyasını okuyarak buradaki “DEBUG_MODE” değişkenine göre loglama işlemini debug ve üstü seviyeye getirdim.

💡
Eğer .env dosyası içerisinde “DEBUG_MODE=true” ifadesini kullanırsak loglama işlemine projede kullandığımız logrus.Log.Debugln(”debug mesajı”) logları da dahil edilecek. Aksi durumda Debugln logları dahil edilmeyip sadece Info ve üst seviye (Warning, Error vs.) loglar dahil edilecek.
package logger

import (
	"io"
	logging "log"
	"os"
	"strconv"

	"github.com/joho/godotenv"
	"github.com/sirupsen/logrus"
)

var (
	Log *logrus.Logger // share will all packages
)

func init() {
// The file needs to exist prior
	f, err := os.OpenFile("api.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)

	if err != nil {
		// Use go's logger, while we configure Logrus
		logging.Fatalf("error opening file: %v", err)
	}

// Configure Logrus
	Log = logrus.New()
	Log.Formatter = &logrus.TextFormatter{}
	Log.SetReportCaller(true)
	mw := io.MultiWriter(os.Stdout, f)
	Log.SetOutput(mw)

// Set the log level from .env file
	debug_mode := goDotEnvVariable("DEBUG_MODE")
	debug_mode_bool, _ := strconv.ParseBool(debug_mode)
	if debug_mode_bool == true {
		Log.Level = logrus.DebugLevel
		Log.Infoln("DEBUG_MODE=" + debug_mode)
	} else {
		Log.Level = logrus.InfoLevel
		Log.Infoln("DEBUG_MODE=" + debug_mode)
	}
}

HTTP İsteklerini Loglamak İçin Middleware Paketini Hazırlayalım

logger paketimiz artık kullanıma hazır ve diğer tüm paketlerden (main, controller vs.) çağrılabilir. Şimdi middlewares paketinde bulunan loggingMiddleware.go dosyasını konfigüre edelim.

package middlewares

import (
	"time"
	"github.com/rbozburun/rest_api/logger"
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

func LoggingMiddleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// Starting time request
		startTime := time.Now()

		// Processing request
		ctx.Next()

		// End Time request
		endTime := time.Now()

		// execution time
		latencyTime := endTime.Sub(startTime)

		// Request method
		reqMethod := ctx.Request.Method

		// Request route
		reqUri := ctx.Request.RequestURI

		// status code
		statusCode := ctx.Writer.Status()

		// Request IP
		clientIP := ctx.ClientIP()

		//Use global logger as middleware logger
		logger.Log.WithFields(log.Fields{
			"METHOD":    reqMethod,
			"URI":       reqUri,
			"STATUS":    statusCode,
			"LATENCY":   latencyTime,
			"CLIENT_IP": clientIP,
		}).Info("HTTP REQUEST")
		ctx.Next()
	}
}

Yukarıdaki kodda gin’in contextini alıp bu context içerisindeki client ip, method, uri, status ve latency bilgilerini loglaması için logger’a ilettik. Sırada son bir adım kaldı; Gin Framework’ünü bu middleware’i kullanacak şekilde yapılandırmak.

Ben gin’i main içerisinde başlattığım için middleware’i kullandırtan kodu da burada ekledim.

package main

import (
	"github.com/rbozburun/rest_api/config"
	"github.com/rbozburun/rest_apilogger"
	"github.com/rbozburun/rest_api/middlewares"
	"github.com/rbozburun/rest_api/routes"
	"github.com/gin-gonic/gin"
)

func main() {
  // Connect o db
	config.Connect()
	logger.Log.Debugln("API Main function started...")

	router := gin.Default()

	// Use middleware
	router.Use(gin.Recovery())
	router.Use(middlewares.LoggingMiddleware())

	// Init UserRoute router
	routes.UserRoute(router)
	logger.Log.Debugln("UserRoute initiliazed.")

	// Init Login router
	routes.LoginRoute(router)
	logger.Log.Debugln("LoginRoute initiliazed.")

	// Start the API service
	router.Run("0.0.0.0:8080")
}

Artık herşey hazır, logger’i diğer paketlerinizde import ederek “logger.Log.<logLevel(”msg”)>” şeklinde çağırıp kullanabilirsiniz. UserController içerisindeki örnek bir kullanımını aşağıya bırakıyorum.

package controller

import (
	"crypto/sha256"
	"fmt"
	"net/http"
	"strconv"

	"github.com/rbozburun/rest_api/config"
	"github.com/rbozburun/rest_api/logger"
	"github.com/rbozburun/rest_api/models"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

// POST /api/v1/user
func CreateUser(ctx *gin.Context) {
	logger.Log.Debugln("CreateUser controller called.")

	var user models.User
	ctx.ShouldBindJSON(&user)
	token := uuid.Must(uuid.NewRandom()).String()

	// Convert the password of the user to sha256 hash & set
	if user.Password != "" {
		pwdHash := sha256.Sum256([]byte(user.Password))
		// String reference of created hash
		pwdHash_asStr := fmt.Sprintf("%x", pwdHash[:])
		user.Password = pwdHash_asStr
	} else {
		// Throw error
		ctx.JSON(http.StatusBadRequest, gin.H{"Error": "Provide password!"})
		logger.Log.Debugln("User password is empty.")
		return
	}

	// Convert created access to token to sha256
	hashed_token := sha256.Sum256([]byte(token))
	// String reference of created hash
	tokenhash_asStr := fmt.Sprintf("%x", hashed_token[:])

	// Set the user's token as hashed access token (string reference)
	user.AccessToken = tokenhash_asStr

	if err := config.DB.Create(&user).Error; err != nil {
		// Throw error
		logger.Log.Debugln("User creation error. Probable there is smth with database.")
		ctx.JSON(http.StatusBadRequest, gin.H{"Error": err.Error()})
		return
	} else {
		// Create user succesfuly and respond with access token (non-hashed)
		ctx.JSON(http.StatusCreated, gin.H{"token": token})
		logger.Log.Infoln("A new user created.")
	}

}

Örnek bir log çıktısı aşağıdaki gibidir.

time="2023-04-21T16:55:55+03:00" level=info msg="DEBUG_MODE=false" func=github.com/testuser/main_rest_api/logger.init.0 file="C:/Users/ResulBozburun/Desktop/Personal/testuser_Main_API/api/logger/logger.go:54"
time="2023-04-21T16:56:13+03:00" level=info msg="Access token provided." func=github.com/testuser/main_rest_api/controller.Login file="C:/Users/ResulBozburun/Desktop/Personal/testuser_Main_API/api/controller/login.go:34"
time="2023-04-21T16:56:13+03:00" level=info msg="HTTP REQUEST" func=github.com/testuser/main_rest_api/middlewares.LoggingMiddleware.func1 file="C:/Users/ResulBozburun/Desktop/Personal/testuser_Main_API/api/middlewares/loggingMiddleware.go:44" CLIENT_IP=127.0.0.1 LATENCY=2.6564ms METHOD=POST STATUS=200 URI=/api/1.0/login
time="2023-04-21T16:56:24+03:00" level=info msg="Access token provided." func=github.com/testuser/main_rest_api/controller.Login file="C:/Users/ResulBozburun/Desktop/Personal/testuser_Main_API/api/controller/login.go:34"
time="2023-04-21T16:56:24+03:00" level=info msg="HTTP REQUEST" func=github.com/testuser/main_rest_api/middlewares.LoggingMiddleware.func1 file="C:/Users/ResulBozburun/Desktop/Personal/testuser_Main_API/api/middlewares/loggingMiddleware.go:44" CLIENT_IP=127.0.0.1 LATENCY=1.8069ms METHOD=POST STATUS=200 URI=/api/1.0/login

 

Bir cevap yazın