105 lines
2.8 KiB
Go
105 lines
2.8 KiB
Go
|
|
package logger
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"io"
|
||
|
|
"log"
|
||
|
|
"os"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Fields adalah data tambahan yang disisipkan ke satu baris log, contoh:
|
||
|
|
//
|
||
|
|
// logger.Info("request selesai", logger.Fields{"path": "/users", "status": 200})
|
||
|
|
type Fields map[string]interface{}
|
||
|
|
|
||
|
|
var std = log.New(os.Stdout, "", 0)
|
||
|
|
|
||
|
|
// SetOutput mengganti tujuan tulis log. Berguna untuk unit test (redirect ke buffer)
|
||
|
|
// atau kalau nanti mau kirim log ke file/agent lain.
|
||
|
|
func SetOutput(w io.Writer) {
|
||
|
|
std = log.New(w, "", 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
var sensitiveKeys = map[string]bool{
|
||
|
|
"password": true,
|
||
|
|
"password_confirmation": true,
|
||
|
|
"old_password": true,
|
||
|
|
"new_password": true,
|
||
|
|
"confirm_password": true,
|
||
|
|
"token": true,
|
||
|
|
"access_token": true,
|
||
|
|
"refresh_token": true,
|
||
|
|
"authorization": true,
|
||
|
|
"secret": true,
|
||
|
|
"jwt_secret": true,
|
||
|
|
"credit_card": true,
|
||
|
|
"card_number": true,
|
||
|
|
"cvv": true,
|
||
|
|
"otp": true,
|
||
|
|
}
|
||
|
|
|
||
|
|
const redactedValue = "***REDACTED***"
|
||
|
|
|
||
|
|
// sanitize menyensor field sensitif secara rekursif (termasuk di dalam nested object/body JSON)
|
||
|
|
// supaya data seperti password tidak pernah ikut tercetak di log, walau caller lupa menyaringnya.
|
||
|
|
func sanitize(fields Fields) Fields {
|
||
|
|
if fields == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
clean := make(Fields, len(fields))
|
||
|
|
for key, value := range fields {
|
||
|
|
if sensitiveKeys[strings.ToLower(key)] {
|
||
|
|
clean[key] = redactedValue
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
switch v := value.(type) {
|
||
|
|
case map[string]interface{}:
|
||
|
|
clean[key] = sanitize(Fields(v))
|
||
|
|
case Fields:
|
||
|
|
clean[key] = sanitize(v)
|
||
|
|
default:
|
||
|
|
clean[key] = value
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return clean
|
||
|
|
}
|
||
|
|
|
||
|
|
type entry struct {
|
||
|
|
Time string `json:"time"`
|
||
|
|
Level string `json:"level"`
|
||
|
|
Message string `json:"message"`
|
||
|
|
Fields Fields `json:"fields,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func write(level, msg string, fields Fields) {
|
||
|
|
e := entry{
|
||
|
|
Time: time.Now().Format(time.RFC3339),
|
||
|
|
Level: level,
|
||
|
|
Message: msg,
|
||
|
|
Fields: sanitize(fields),
|
||
|
|
}
|
||
|
|
|
||
|
|
b, err := json.Marshal(e)
|
||
|
|
if err != nil {
|
||
|
|
// fallback kalau ternyata ada value yang gagal di-marshal, tetap harus ada jejak log-nya
|
||
|
|
std.Printf("[%s] %s (gagal marshal fields: %v)", level, msg, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
std.Println(string(b))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Info dipakai untuk mencatat aktivitas normal, contoh: request masuk dan sukses diproses.
|
||
|
|
func Info(msg string, fields Fields) { write("INFO", msg, fields) }
|
||
|
|
|
||
|
|
// Warn dipakai untuk kondisi yang bukan bug tapi perlu diperhatikan, contoh: request 4xx
|
||
|
|
// (input salah, tidak diotorisasi, dst).
|
||
|
|
func Warn(msg string, fields Fields) { write("WARN", msg, fields) }
|
||
|
|
|
||
|
|
// Error dipakai untuk kegagalan sisi server (5xx) atau error tak terduga lainnya yang
|
||
|
|
// butuh investigasi lebih lanjut oleh developer.
|
||
|
|
func Error(msg string, fields Fields) { write("ERROR", msg, fields) }
|