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