package utils import ( "encoding/json" "io" "os" "runtime/debug" "sync" "time" ) // Define a Level type to represent the severity level for a log entry. type Level int8 // Initialize constants which represent a specific severity level. We use the iota // keyword as a shortcut to const ( LevelInfo Level = iota LevelError LevelFatal LevelOff ) // assign successive integer values to the constants. // Has the value 0. // Has the value 1. // Has the value 2. // Has the value 3. // Return a human-friendly string for the severity level. func (l Level) String() string { switch l { case LevelInfo: return "INFO" case LevelError: return "ERROR" case LevelFatal: return "FATAL" default: return "" } } // Define a custom Logger type. This holds the output destination that the log entries // will be written to, the minimum severity level that log entries will be written for, // plus a mutex for coordinating the writes. type Logger struct { out io.Writer minLevel Level mu sync.Mutex } // Return a new Logger instance which writes log entries at or above a minimum severity // level to a specific output destination. func New(out io.Writer, minLevel Level) *Logger { return &Logger{out: out, minLevel: minLevel} } // Declare some helper methods for writing log entries at the different levels. Notice // that these all accept a map as the second parameter which can contain any arbitrary // 'properties' that you want to appear in the log entry. func (l *Logger) PrintInfo(message string, properties map[string]string) { l.print(LevelInfo, message, properties) } func (l *Logger) PrintError(err error, properties map[string]string) { l.print(LevelError, err.Error(), properties) } func (l *Logger) PrintFatal(err error, properties map[string]string) { l.print(LevelFatal, err.Error(), properties) os.Exit(1) // For entries at the FATAL level, we also terminate the application. } // Print is an internal method for writing the log entry. func (l *Logger) print(level Level, message string, properties map[string]string) (int, error) { // If the severity level of the log entry is below the minimum severity for the // logger, then return with no further action. if level < l.minLevel { return 0, nil } // Declare an anonymous struct holding the data for the log entry. aux := struct { Level string `json:"level"` Time string `json:"time"` Message string `json:"message"` Properties map[string]string `json:"properties,omitempty"` Trace string `json:"trace,omitempty"` }{ Level: level.String(), Time: time.Now().UTC().Format(time.RFC3339), Message: message, Properties: properties, } // Include a stack trace for entries at the ERROR and FATAL levels. if level >= LevelError { aux.Trace = string(debug.Stack()) } // Declare a line variable for holding the actual log entry text. var line []byte // Marshal the anonymous struct to JSON and store it in the line variable. If there // was a problem creating the JSON, set the contents of the log entry to be that // plain-text error message instead. line, err := json.Marshal(aux) if err != nil { line = []byte(LevelError.String() + ": unable to marshal log message: " + err.Error()) } // Lock the mutex so that no two writes to the output destination cannot happen // concurrently. If we don't do this, it's possible that the text for two or more // log entries will be intermingled in the output. l.mu.Lock() defer l.mu.Unlock() // Write the log entry followed by a newline. return l.out.Write(append(line, '\n')) } // We also implement a Write() method on our Logger type so that it satisfies the // io.Writer interface. This writes a log entry at the ERROR level with no additional // properties. func (l *Logger) Write(message []byte) (n int, err error) { return l.print(LevelError, string(message), nil) }