SoFunction
Updated on 2025-03-05

Go language programming implementation supports six levels of log library

Preface

The log output methods provided by the Golang standard log library include Print, Fatal, Panic, etc., and there are no common log levels such as Debug, Info, Error, etc., and it is not easy to use. In this article, let’s use a log library to record logs of different levels.

In fact, to pursue simplicity, the three output methods of Golang standard log library are enough, and it is easy to understand:

  • Print is used to record an ordinary program log, and developers can remember anything they want.
  • Fatal is used to record a log that causes the program to crash and exit the program.
  • Panic is used to log an exception log and trigger panic.

However, for those who are used to Debug, Info, and Error, they are still a little uncomfortable; for the need to distinguish log levels more carefully, the standard log library also provides a general Output method, and it is also OK for developers to add levels to the string to be output, but it is always a bit awkward and not direct enough.

There are many excellent three-party log libraries on the market, such as uber open source zap, and common ones include zerolog, logrus, etc. However, I still want to use one by myself, because most open source products will not fully meet my needs and have many functions that I cannot use, which will increase the complexity of the system. It is difficult to say whether there are hidden pits. Of course, the possibility of getting into the pit is also very high. Moreover, after reading the implementation of the official log library, I feel that I can simply encapsulate and implement the functions I want and can hold it.

Initial Requirements

My initial requirements here are:

  • Write logs to disk files, one folder per month and one file per hour.
  • Common log levels are supported: Trace, Debug, Info, Warn, Error, Fatal, and the program can set the log level.

I named this log library ylog, and the expected usage method is as follows:

(LevelInfo)
("I am a debug log.")
("I am a Info log.")

Technology implementation

Type definition

It is necessary to define a structure to store log level, files to be written, and other information.

type FileLogger struct {
	lastHour int64
	file     *
	Level    LogLevel
	mu       
	iLogger  *
	Path     string
}

Let’s take a look at these parameters:

lastHour is used to record the number of hours when creating a log file. If the hours change, a new log file must be created.

file The log file currently used.

Level The log level currently used.

mu Because logs may be written in different go routines, a mutex is required to ensure that the log files are not created repeatedly.

iLogger standard log library instance, because the standard log library is encapsulated here.

The uppermost directory of the Path log output, such as the logs directory under the root directory of the program, save a string: logs.

Log Level

First define the log level. Here, the log level is actually an int type, from 0 to 5, the level continues to rise.

If set to ToInfo, both the Info level and the logs higher than the Info level can be output.

type LogLevel int
const (
	LevelTrace LogLevel = iota
	LevelDebug
	LevelInfo
	LevelWarn
	LevelError
	LevelFatal
)

As mentioned above, you can add log level to the parameters of the Output method. Here, you can implement different levels of logging methods by encapsulating the Output method. One of the methods posted here is the same encapsulation method, so not all of them are posted:

func (l *FileLogger) CanInfo() bool {
	return  <= LevelInfo 
}
func (l *FileLogger) Info(v ...any) {
	if () {
		()
		v = append([]any{"Info "}, v...)
		(2, (v...))
	}
}

Three things were done before outputting the log:

  • Determine the log level. If the set log level is less than or equal to the current output level, it can output it.
  • Make sure that the log file has been created, and we will talk about how to ensure it later.
  • Insert the log level into the log string.

Then call the Output function of the standard library to output the log. The first parameter here is to obtain the program file name that is currently writing the log. The depth value passed in the program call stack is searched. 2 is just right here.

Write to file

The log of the standard library supports output to multiple targets, as long as the interface is implemented:

type Writer interface {
	Write(p []byte) (n int, err error)
}

Because the file object also implements this interface, you can create an instance here and set it to the embedded standard log library instance, that is, to the iLogger in the FileLogger created earlier. In the ensureFile method, take a look at the implementation of this file:

func (l *FileLogger) ensureFile() (err error) {
	currentTime := ()
	if  == nil {
		()
		defer ()
		if  == nil {
			, err = createFile(&, &currentTime)
			()
			( |  |  | )
			 = getTimeHour(&currentTime)
		}
		return
	}
	currentHour := getTimeHour(&currentTime)
	if  != currentHour {
		()
		defer ()
		if  != currentHour {
			_ = ()
			, err = createFile(&, &currentTime)
			()
			( |  | )
			 = getTimeHour(&currentTime)
		}
	}
	return
}

This is a bit complicated. The basic logic is: if the file instance does not exist, create it; if a new file needs to be created, close the old file first and then create a new file.

Locking is required when changing file instances, otherwise it may operate multiple times and unexpected situations occur.

After setting the output to the file, the Output method of the standard log library will output the log to this file.

Default implementation

After the above series of operations, this FileLogger can be used:

var logger = NewFileLogger(LevelInfo, "logs")
("This is a info.")

However, it is a bit different from the initially conceived usage: ("xxxx")

This requires defining another public function named Info in the ylog package. You can call a default created FileLogger instance in this public function. The code is as follows:

var stdPath = "logs"
var std = NewFileLogger(LevelInfo, stdPath)
func Trace(v ...any) {
	if () {
		()
		v = append([]any{"Trace"}, v...)
		(2, (v...))
	}
}

Note that std's Trace method is not called here, because the first parameter in Output is called. If it is called in a nested manner, there is an extra layer, and this parameter must be set to 3. However, when creating an instance and calling Trace by yourself, this parameter needs to be 2, which creates a conflict.

Through the above operations, the expected log operations can be achieved:

(LevelInfo)
("I am a debug log.")
("I am a Info log.")

Complete program code:/bosima/ylog/tree/v1.0.1

The next article will continue to transform this log library, support the output of Json format logs, and output logs to Kafka. For more information about the Golang log library, please pay attention to my other related articles!