pemrograman

05 Implementasi Singleton Logger Logrus Pada Golang

Pada kali ini kita akan mencoba untuk mengimplementasikan Singleton Design Pattern. Apa itu Singletone? Bisa kamu baca terlebih dahulu artikel disini. Agar lebih paham lagi jadi Singleton adalah desain standar perangkat lunak yang memberikan jaminan keberadaan hanya satu instance kelas, sambil mempertahankan titik akses global ke objeknya. Singleton adalah pola desain yang membatasi instantiation ke suatu objek, kita harus memastikan bahwa ini hanya terjadi sekali.

Ada 2 cara yang akan kita coba implementasikan Logrus diantaranya yaitu dengan langsung menggunakan Singleton Logrus sendiri dan membuat custom sendiri dikarenakan ada beberapa kebutuhan misalkan, set fields tersendiri atau kebutuhan yang lainnya.

Penggunaan Singletone Logrus

Nah pada kali ini kita akan coba membuat inisialisasi object dari Logrus sebagai Logger sehingga kita tidak perlu lagi membuat object logger sendiri. Jika kita menggunakan Singleton maka akan otomatis logger tersebut akan berubah. Secara default Logger singleton yang ada di Logrus itu menggunakan TextFormatter dan Info Level. Cara agar membuat Logger singletone kita akan langsung menggunakan package pada Logrus-nya saja.

Pertama akan buat fungsi unit test seperti dibawah ini.

func TestSingletone(t *testing.T) {
	// set formatter using JSON
	logrus.SetFormatter(&logrus.JSONFormatter{})
	// initiate log rus
	logrus.Info("info using Logrus singleton")
	logrus.Warn("warn using Logrus singleton")
	logrus.Error("error using Logrus singleton")
}

Maka ketika dijalankan akan tampil seperti dibawah ini.

{"level":"info","msg":"info using Logrus singleton","time":"2024-04-30T23:40:00+07:00"}
{"level":"warning","msg":"warn using Logrus singleton","time":"2024-04-30T23:40:00+07:00"}
{"level":"error","msg":"error using Logrus singleton","time":"2024-04-30T23:40:00+07:00"}

Membuat Custom Singleton untuk inisialisasi Logrus

Kita akan membuat Singleton untuk inisialisasi Logrus yang kita custom agar lebih mudah dan sesuai dengan kebutuhan kita. Pertama kita buat folder pkg/env ini digunakan untuk kebutuhan pengecekan environment yang diguanakn pada sistem kita. Package yang kita buat ini yaitu kebutuhan environment yang digunakan pada sistem kita agar lebih mudah penggunaannya.

Pertama, kita buat file env.go dengan fungsi inisialisasi sebagai berikut.

type ServiceEnvironment = string

const (
	DevelopmentEnv  = "development"
	StagingEnv      = "staging"
	ProductionEnv   = "production"
	EnvironmentName = "environment"
	GoVersionName   = "go_version"
)

var (
	envName   = "SERVICE_ENV"
	goVersion string
)

func Init() error {
	err := SetFromEnvFile(".env")
	if err != nil && !os.IsNotExist(err) {
		log.Printf("failed to set env file: %v\n", err)
		return err
	}

	goVersion = runtime.Version()
	return nil
}

func SetFromEnvFile(filepath string) error {
	if _, err := os.Stat(filepath); err != nil {
		return err
	}

	f, err := os.Open(filepath)
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(f)
	if err := scanner.Err(); err != nil {
		return err
	}
	for scanner.Scan() {
		text := scanner.Text()
		text = strings.TrimSpace(text)
		vars := strings.SplitN(text, "=", 2)
		if len(vars) < 2 {
			return err
		}
		if err := os.Setenv(vars[0], vars[1]); err != nil {
			return err
		}
	}
	return nil
}

Fungsi initialize ini digunakan saat berjalannya sistem dengan membaca file .env lalu membaca beberapa tag yang ada pada file tersebut kemudian di set ke dalam enrironment sistem di server.

Ada beberapa fungsi tambahan untuk kebutuhan pada sistem diantaranya yaitu kebutuhan untuk mengambil data sesuai environment yang sedang berjalan misalkan development, staging, dan production.

func ServiceEnv() ServiceEnvironment {
	e := os.Getenv(envName)
	if e == "" {
		e = DevelopmentEnv
	}
	return e
}

func GetVersion() string {
	return goVersion
}

func IsDevelopment() bool {
	return ServiceEnv() == DevelopmentEnv
}

func IsStaging() bool {
	return ServiceEnv() == StagingEnv
}

func IsProduction() bool {
	return ServiceEnv() == ProductionEnv
}

Kemudian selanjutnya kita membuat file baru pada folder baru juga dengan nama pkg/log/log.go dengan isi seperti dibawah ini.

package log

import (
	"errors"
	"os"

	"github.com/santekno/learn-golang-logging/pkg/env"
	"github.com/sirupsen/logrus"
)

var (
	log, _        = NewLogger(&Config{Formatter: &TextFormatter, Level: InfoLevel, LogName: "application.log"})
	JSONFormatter logrus.JSONFormatter
	TextFormatter logrus.TextFormatter
	serviceFields = map[string]interface{}{
		env.EnvironmentName: env.ServiceEnv(),
		env.GoVersionName:   env.GetVersion(),
	}
)

type (
	Level  = logrus.Level
	Logger = *logrus.Logger
)

const (
	// override logrus level
	Paniclevel = logrus.PanicLevel
	FatalLevel = logrus.FatalLevel
	ErrorLevel = logrus.ErrorLevel
	WarnLevel  = logrus.WarnLevel
	InfoLevel  = logrus.InfoLevel
	DebugLevel = logrus.DebugLevel
	TraceLevel = logrus.TraceLevel
)

type Config struct {
	logrus.Formatter
	logrus.Level
	LogName string
}

func NewLogger(cfg *Config) (Logger, error) {
	l := logrus.New()
	if env.IsDevelopment() {
		l.SetFormatter(&logrus.TextFormatter{})
	}
	l.SetFormatter(cfg.Formatter)
	l.SetLevel(cfg.Level)
	return l, nil
}

func SetConfig(cfg *Config) error {
	if cfg.LogName == "" {
		return errors.New("log name is empty")
	}

	if !env.IsDevelopment() {
		// initiation create file for logger
		file, err := os.OpenFile(cfg.LogName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
		if err != nil {
			log.Fatal(err)
		}

		// set output file into logrus
		log.SetOutput(file)
	}

	log.SetFormatter(cfg.Formatter)
	log.SetLevel(cfg.Level)
	return nil
}

Pada file tersebut terdapat fungsi NewLogger disini kita akan inisialisasi Logrus dengan kebutuhan yang sudah ditentukan sesuai dengan parameter yang diinginkan pada config. Setelah itu kita juga membuat SetConfig lalu jika ingin membuat logging pada package yg kita buat ini sudah disematkan fields yang dibutuhkan seperti mengeset environment dan versi golang yang digunakan. Maka kita sediakan fungsi yang melakukan set tersebut seperti ini.

func Debug(args ...interface{}) {
	log.WithFields(serviceFields).Debug(args...)
}

func Info(args ...interface{}) {
	log.WithFields(serviceFields).Info(args...)
}

func Warn(args ...interface{}) {
	log.WithFields(serviceFields).Warn(args...)
}

func Error(args ...interface{}) {
	log.WithFields(serviceFields).Error(args...)
}

func Fatal(args ...interface{}) {
	log.WithFields(serviceFields).Fatal(args...)
}

Terakhir kita buat fungsi main pada file main.go dengan isi seperti dibawah ini.

func main() {
	// initialize environment
	err := env.Init()
	if err != nil {
		log.Fatal(err)
	}

	// initialize config log
	err = log.SetConfig(&log.Config{
		Formatter: &log.TextFormatter,
		Level:     log.TraceLevel,
		LogName:   "application.log",
	})
	if err != nil {
		log.Fatal(err)
	}

	log.Debug("singleton debug")
	log.Info("singleton info")
	log.Warn("singleton warn")
	log.Error("singleton debug")
	log.Fatal("singleton fatal")
}

Pada kode diatas bisa dilihat pertama ada inisialisasi environment untuk memastikan sistem kita berjalan dalam environment yang sesuai. Kemudian dilanjutkan dengan inisialisasi konfigurasi logging pada package yang sudah kita buat dan selanjutnya lakukan Logger seperti biasa.

Maka jika kita jalankan kode main tersebut akan terlihat seperti ini.

DEBU[0000] singleton debug                               environment=development go_version=go1.21.1
INFO[0000] singleton info                                environment=development go_version=go1.21.1
WARN[0000] singleton warn                                environment=development go_version=go1.21.1
ERRO[0000] singleton debug                               environment=development go_version=go1.21.1
FATA[0000] singleton fatal                               environment=development go_version=go1.21.1

Dan jika kita pindah dengan enviromnet misalkan untuk kebutuhan di production maka kita bisa tambahkan file environment menjadi ini

SERVICE_ENV=production
comments powered by Disqus