The basic skeleton of the Logrus and log principle (i) in Docker that was analyzed to the container's printout and log function is done in the Entry.log () function, and this Entry.log () also contains two parts: 1) via log Driver the hook function to record the log; 2) output the information to the terminal.
This chapter mainly analyzes how Logrus in Entry.log () prints the information to the terminal output. We still look back to the Entry.log () function and subtract the code that is not used here:
This function isn't declared with a pointer value because otherwise
//race conditions would occur when using multi ple goroutines
func (Entry Entry) log (level level, MSG string) {
//...
If err: = entry. Logger.Hooks.Fire (level, &entry); Err! = Nil {
entry. Logger.mu.Lock ()
FMT. fprintf (OS. Stderr, "Failed to fire Hook:%v\n", err)
entry. Logger.mu.Unlock ()
}
Reader, err: = entry. Reader ()
//...
_, err = Io. Copy (entry. Logger.out, Reader)
//...
}
Here the information output to the terminal is composed of two steps:
1) by entry. Reader () to generate a reader-type value reader, which is the provider of the terminal information;
2) copy (write) The information in reader to the receiver entry. Logger.out. This is by IO. Copy (entry. Logger.out, reader).
Let's take a closer look at the details of these processes. First, prepare the information provider Reader
function IO. Copy (write, reader) has two parameters, the first is the Write interface type, is the "destination" of the information received, and the second is the Reader type, which is the "source" of the information emitted. Let's look at how reader is created.
Reader is an interface provided by the Golang standard IO package, Golang/src/pkg/io/io.go:
Type Reader Interface {
Read (p []byte) (n int, err error)
}
All types that implement the read (P []byte) (n int, err error) method are reader. Here through the entry. Reader () to create a reader object, implemented in the Vendor/src/github.com/sirupsen/logrus/entry.go file:
Returns a reader for the entry, which is a proxy to the formatter.
Func (Entry *entry) Reader () (*bytes. Buffer, error) {
serialized, err: = entry. Logger.Formatter.Format (Entry) //serialized for []bytes structure
return bytes. Newbuffer (serialized), Err //returns a butes. Buffer structure Pointer
}
Entry.reader () do two things: 1) through Etnry. Logger.Formatter.Format (entry) formats the printed information and converts it to []butes type; 2) creates a bytes based on the formatted information. The buffer variable, bytes. Buffer implements the reader interface.
which bytes. Newbuffer () This function is implemented by the Golang standard package bytes: golang/src/pkg/bytes/buffer.go:
bytes. Newbuffer (serialized) returns a buffer structure object
That is, eventually this information provider reader is ultimately a buffer object reference created by Bytes.newbuffer ().
Second, entry. Logger.out
The above has been learned, Io. The "source" of the Copy () function has passed bytes. Newbuffer created well, while IO. The "target" entry of Copy (). What is Logger.out? Eventually we will see this entry. Logger.out is actually Os.stderr, which represents a variant of the UNIX-like system standard error output device. So IO. Copy () "source", "target" all have, then IO. Copy (entry. The logger.out, reader) function completes the writing of the information from "reader" to "writer".
2.1 Entry. The ins and outs of Logger.out
We've already talked about entry. Logger.out is the standard error output device, but it seems to be a mystery, and then we'll untie it.
To untie entry. Logger.out we need to talk a second. The entire logrus.infof () Output printing information function to the call chain of the Entry.log ():
1) Logrus.infof ()--Std.infof ()
2) Std.infof ()/logger.infof () to NewEntry (Logger). Infof (format, args ...)
This STD variable is passed Logrus before the main () function call. New () created,
Func New () *logger {
return &logger{out
: os. Stderr,
formatter:new (textformatter),
Hooks: Make (Levelhooks), level
: infolevel,
}
}
That is, STD is actually a logger () type of reference, Std. Out is set to Os.stderr.
3) NewEntry (logger). Infof (format, args ...)-Entry.log ()
Here Newentry (logger) parameter logger is the preceding STD variable and uses it to create a entry object and initialize Entry.logger
Func NewEntry (Logger *logger) *entry {
return &entry{
Logger:logger,
//Default is three fields, give A little extra-
data:make (Fields, 5),
}
}
The entry logger here is the STD in front, and NewEntry (logger). Infof (format, args ...) The final implementation of this is:
Func (Entry *entry) Info (args ... interface{}) {
if entry. Logger.level >= infolevel {
entry.log (infolevel, FMT. Sprint (args ...)
}
}
That is, the Entry.log () function We originally talked about was called.
So all the way down, in the Entry.log () function of the etnry. Logger.out, the actual is std.out. In the call Logrus. The New () function initializes it to the OS when it creates the Std. Stderr.
that this OS. STDERR is the standard output, which is created by the standard package provided by Golang.
2.2 OS. The ins and outs of stderr
Os. STDERR is a global value defined in the Golang package, specifically defined in the/usr/local/go/pkg/os/file.go file:
Stderr = NewFile (UIntPtr (syscall. Stderr), "/dev/stderr")
The above function, func NewFile (fd uintptr, name string) *file{}, creates and returns a value of type *file based on the argument file descriptor fd and the file path, used for Golang file read-write library functions.
Here NewFile (fd unitptr, name string) Two entry parameters are Syscall respectively. StdErr and "/dev/stderr".
The first parameter is syscall. STDERR is defined in/usr/local/go/pkg/syscall/syscall_unix.go, with a value of stderr=2, which is the standard error device descriptor in Unix-like systems, and the second parameter file is named "/dev/stderr". Which is the standard error device. That is, in the Golang and C language, file Access will also have file descriptors and file names.
Eventually in the Golang we are commonplace in the OS. STDERR is a variable of type *file and represents a standard error device. When file access is performed in Golang, it is usually done through the document structure.
three outputs the information to the OS. Stderr
With this understanding, let's look at how Logrus in Docker writes print information to the OS. STDERR, here is the IO. Copy (entry. Logger.out, reader) for the breakthrough analysis.
function IO. Copy () is defined as follows:
Func Copy (DST Writer, SRC Reader) (written int64, err error) {
//If The Reader has a WriteTo method Copy.
Avoids an allocation and a copy.
If WT, OK: = src. (Writerto); OK {
return wt. WriteTo (DST)
}
//Similarly, if the writer has a Readfrom method, use it to do the copy.
If RT, OK: = DST. (Readerfrom); OK {
return RT. Readfrom (src)
}
buf: = Make ([]byte, 32*1024) for
{
nr, er: = src. Read (BUF)
if nr > 0 {
NW, EW: = DST. Write (Buf[0:nr])
If NW > 0 {
written + = Int64 (NW)
}
if ew! = Nil {
err = ew break
}< c21/>if nr! = NW {
err = Errshortwrite break
}
}
if er = = EOF {break
}
if er! = N Il {
err = er
break
}
}
return written, err
}
Already mentioned before, Io. Copy (DST Writer, SRC Reader) writes the SRC data that implements the Reader:read () interface method to the DST variable that implements the Writer:write () interface method. As already analyzed in the previous section, the DST here in Docker is the OS. Stderr, and Reader is bytes. A variable of type buffer.
The first step in this function is to determine whether the parameter src implements the Writerto method:
If WT, OK: = src. (Writerto); OK { //reader Here is the BYTES.BUFFER structure variable, it implements the Writerto method
return WT. WriteTo (DST)
}
If SRC implements the Writerto method, it passes directly through SRC. Writerto (DST) to complete the writing of information from SRC to DST. Here src as bytes. The buffer type, whose Writerto () is specifically implemented as follows:
bytes. The buffer structure implements the WriteTo method
//WriteTo writes data to W until the buffer was drained or an error occurs.
The return value n is the number of bytes written; It always fits into
A//int, but it's int64 to match the IO. Writerto interface. Any error
//encountered during the write is also returned.
Func (b *buffer) writeto (w io). Writer) (n Int64, err error) {
B.lastread = opinvalid
if B.off < Len (b.buf) {
nbytes: = B.len ()
m, E: = W.write (B.buf[b.off:])
if M > nbytes {
panic ("bytes. Buffer.WriteTo:invalid Write count ")
}
B.off + = m
n = Int64 (m)
if E! = nil {
return n, E
}< c17/>//All bytes should has been written, by definition of
//Write method in IO. Writer
if M! = nbytes {
return n, io. Errshortwrite
}
}
//Buffer is now empty; reset.
B.truncate (0)
return
}
This function removes the code from the inspection section, and the implementation core of the information writing is:
M, E: = W.write (B.buf[b.off:])
actually bytes. The Buffer.writerto () implementation of SRC-to-DST write functionality is ultimately referred to DST. Write (B.buf[b.off:]), which is the OS. Stderr.write (B.buf[b.off:]), which has already been described in the previous OS. STDERR is a variable of type *file, and thus the final call is:
W.write actually calls the OS. Stderr.write (), i.e. file.write (b []byte)
//##### #usr/local/go/pkg/os/file.go
//Write writes Len (b) bytes to The File.
It returns the number of bytes written and an error, if any.
Write returns a non-nil error when n! = Len (b).
Func (f *file) Write (b []byte) (n int, err error) {
if f = = nil {
return 0, Errinvalid
}
N, E: = F.write (b
if n < 0 {
n = 0
}
if n! = Len (b) {
err = io. Errshortwrite
}
Epipecheck (f, E)
if E! = Nil {
err = &patherror{"Write", F.name, E}
}
return N, Err
}
Further observation of its core implementation file.write (b)-->file.write (b):
//##### #usr/local/go/pkg/os/file_unix.go//write writes Len (b) bytes to the file.//It Returns the Nu
Mber of bytes written and an error, if any.
Func (f *file) write (b []byte) (n int, err error) {for {bcap: = b if needsmaxrw && len (bcap) > MAXRW { Bcap = Bcap[:maxrw]} m, err: = Syscall. Write (F.FD, bcap) n + = m//If the syscall wrote some data but not all (short write)//or it returned EINTR and then
Assume it stopped early for//reasons that is uninteresting to the caller, and try again. If 0 < m && m < len (bcap) | | Err = = Syscall.
eintr {b = b[m:] Continue} if NEEDSMAXRW && len (bcap)! = Len (b) && err = nil {b = b[m:] Continue} return N, err}}
You can see that this is already the implementation function of the Golang bottom package, which calls the Syscall package directly syscall. Write () function to implement. It f.fd to write the Len (bcap) length information bcap through a for{} loop. Exit until you have completely written len (bcap) size information or an error occurred halfway through. The F.FD here is the file descriptor for the standard error device in the Unxi system mentioned earlier 2;bcap is the message we want to print, the type is []bytes.
Looks like we're getting closer to the truth, let's see how Golang uses the system call function Syscall. Write () file:
##### #usr/local/go/pkg/syscall/syscall_unix.go
func Write (fd int, p []byte) (n int, err error) {
if raceenabled {
Racereleasemerge (unsafe. Pointer (&iosync))
}
N, err = write (FD, p)
if raceenabled && n > 0 {
racereadrange (unsafe. Pointer (&p[0]), n)
}
return
}
Syscall. Write () calls to the Write () function to implement file FD, the Write () function is associated with the schema:
This FILE is GENERATED by the COMMAND at the TOP; Do not EDIT
//##### #usr/local/go/pkg/syscall/zsyscal_linux_amd64.go
func write (fd int, p []byte) (n int, err Error) {
var _p0 unsafe. Pointer
If Len (p) > 0 {
_p0 = unsafe. Pointer (&p[0])
} else {
_p0 = unsafe. Pointer (&_zero)
}
R0, _, E1: = Syscall (Sys_write, UIntPtr (FD), UIntPtr (_p0), UIntPtr (Len (P)))
n = Int ( R0)
if e1! = 0 {
err = e1
}
return
}
Oh, see here, the C language Department's small partners finally a smile ^_^: Ultimately, it is through the syscall+ system call number + parameters to achieve the final write system call. To compare the underlying library implementations of the write system call in Golang and the write system call in the C language:
GLIBC Library: Inline_syscall_call (Write, FD, buf, nbytes);
Golang:syscall (Sys_write, UIntPtr (FD), UIntPtr (_p0), UIntPtr (Len (p)))
At last, we learned the whole process.
Summarize
Let's summarize the principle of Logrus for terminal information printing in Docker.
1 Logrus offers a similar fmt. Print () similar to the printing function, such as Logrus. DEBUGF (), Logrus.infof () .... And so on, it prints the printed information to the standard error device/dev/stderr.
2 Logrus The printing information msg is encapsulated into the entry type, one for the information format, the other for logging, and the third is the creation of bytes. The buffer structure is used for file read and write in Golang.
3 Logrus Information printing function, finally using the underlying system call package, through the Sys_write system call number to achieve.