從零開始寫 Docker(十)---實現 mydocker logs 檢視容器日誌

探索云原生發表於2024-04-09

mydocker-logs.png

本文為從零開始寫 Docker 系列第十篇,實現類似 docker logs 的功能,使得我們能夠查檢視容器日誌。


完整程式碼見:https://github.com/lixd/mydocker
歡迎 Star


推薦閱讀以下文章對 docker 基本實現有一個大致認識:

  • 核心原理深入理解 Docker 核心原理:Namespace、Cgroups 和 Rootfs
  • 基於 namespace 的檢視隔離探索 Linux Namespace:Docker 隔離的神奇背後
  • 基於 cgroups 的資源限制
    • 初探 Linux Cgroups:資源控制的奇妙世界
    • 深入剖析 Linux Cgroups 子系統:資源精細管理
    • Docker 與 Linux Cgroups:資源隔離的魔法之旅
  • 基於 overlayfs 的檔案系統Docker 魔法解密:探索 UnionFS 與 OverlayFS
  • 基於 veth pair、bridge、iptables 等等技術的 Docker 網路揭秘 Docker 網路:手動實現 Docker 橋接網路

開發環境如下:

root@mydocker:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 20.04.2 LTS
Release:	20.04
Codename:	focal
root@mydocker:~# uname -r
5.4.0-74-generic

注意:需要使用 root 使用者

1. 概述

上一篇已經實現了mydocker ps 命令,可以檢視到當前執行中的容器了。

本篇主要實現 mydocker logs,讓我們可以隨時檢視容器日誌。

一般來說,對於容器中執行的程序,使日誌列印到標準輸出是一個非常好的實現方案,因此需要將容器中的標準輸出儲存下來,以便需要的時候訪問。

我們就以此作為思路來實現 mydocker logs 命令:

  • 啟動時將容器程序的標準輸出掛載到/var/lib/mydocker/containers/{containerId}/{containerId}-json.log檔案中
  • mydocker logs 則讀取這個檔案以獲取容器日誌

實際上 docker 實現也類似,他會把容器日誌儲存在var/lib/docker/containers/{containerId}/{containerId}-json.log 檔案中。

2. 實現

具體實現包括兩部分:

  • 1)重定向輸出到檔案
  • 2)實現 mydocker logs 命令

輸出重定向

首先,需要修改一下原來的實現,在建立後臺執行容器的時候,把程序的標準輸出重新定向一下到日誌檔案。

前臺容器依舊列印到 Stdout 即可

func NewParentProcess(tty bool, volume, containerId string) (*exec.Cmd, *os.File) {
// 省略其他記憶體
	if tty {
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	} else {
		// 對於後臺執行容器,將 stdout、stderr 重定向到日誌檔案中,便於後續檢視
		dirPath := fmt.Sprintf(InfoLocFormat, containerId)
		if err := os.MkdirAll(dirURL, constant.Perm0622); err != nil {
			log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
			return nil, nil
		}
		stdLogFilePath := dirPath + LogFile
		stdLogFile, err := os.Create(stdLogFilePath)
		if err != nil {
			log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
			return nil, nil
		}
		cmd.Stdout = stdLogFile
		cmd.Stderr = stdLogFile
	}
// ...
}

實現 logs 命令

在 main_command.go 中新增一個 logCommand:

var logCommand = cli.Command{
    Name:  "logs",
    Usage: "print logs of a container",
    Action: func(context *cli.Context) error {
       if len(context.Args()) < 1 {
          return fmt.Errorf("please input your container name")
       }
       containerName := context.Args().Get(0)
       logContainer(containerName)
       return nil
    },
}

並加到 main 函式中。

func main(){
  // 省略其他內容
  app.Commands = []cli.Command{
       initCommand,
       runCommand,
       commitCommand,
       listCommand,
       logCommand,
    }
}

具體實現如下:

func logContainer(containerName string) {
    logFileLocation := fmt.Sprintf(container.InfoLocFormat, containerName) + container.LogFile
    file, err := os.Open(logFileLocation)
    defer file.Close()
    if err != nil {
       log.Errorf("Log container open file %s error %v", logFileLocation, err)
       return
    }
    content, err := ioutil.ReadAll(file)
    if err != nil {
       log.Errorf("Log container read file %s error %v", logFileLocation, err)
       return
    }
    _, err = fmt.Fprint(os.Stdout, string(content))
    if err != nil {
       log.Errorf("Log container Fprint  error %v", err)
       return
    }
}

實現很簡單,根據 containerId 拼接出完整路徑,讀取檔案內容並重定向到標準輸出即可。

3. 測試

啟動一個後臺容器

root@mydocker:~/feat-logs/mydocker# go build .
root@mydocker:~/feat-logs/mydocker# ./mydocker run -d -name mytop top
{"level":"info","msg":"createTty false","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0  }","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"busybox:/root/busybox busybox.tar:/root/busybox.tar","time":"2024-01-26T11:25:53+08:00"}
{"level":"error","msg":"mkdir dir /root/merged error. mkdir /root/merged: file exists","time":"2024-01-26T11:25:53+08:00"}
{"level":"error","msg":"mkdir dir /root/upper error. mkdir /root/upper: file exists","time":"2024-01-26T11:25:53+08:00"}
{"level":"error","msg":"mkdir dir /root/work error. mkdir /root/work: file exists","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged]","time":"2024-01-26T11:25:53+08:00"}
{"level":"info","msg":"command all is top","time":"2024-01-26T11:25:53+08:00"}

檢視容器列表

root@mydocker:~/feat-logs/mydocker# ./mydocker ps
{"level":"error","msg":"read file /var/lib/mydocker/containers/0439540405/config.json error open /var/lib/mydocker/containers/0439540405/config.json: no such file or directory","time":"2024-01-26T11:26:13+08:00"}
{"level":"error","msg":"get container info error open /var/lib/mydocker/containers/0439540405/config.json: no such file or directory","time":"2024-01-26T11:26:13+08:00"}
ID           NAME         PID         STATUS      COMMAND     CREATED
0439540405   mytop        171754      running     top         2024-01-26 11:25:53

容器 Id 為 0439540405,檢視對應目錄是否生成了日誌檔案

root@mydocker:~/feat-logs/mydocker# ls /var/lib/mydocker/containers/0439540405/
0439540405-json.log config.json

其中的0439540405-json.log 就是日誌檔案,config.json 則是上一次新增的容器資訊記錄檔案。

檢視日誌檔案內容

root@mydocker:~/feat-logs/mydocker# cat /var/lib/mydocker/containers/0439540405/0439540405-json.log
Mem: 1793456K used, 241932K free, 1064K shrd, 91276K buff, 1354272K cached
CPU:  0.4% usr  0.0% sys  0.0% nic 99.3% idle  0.0% io  0.0% irq  0.2% sirq
Load average: 0.01 0.01 0.00 1/141 4
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND

說明,日誌儲存是正常的。

接下里執行 mydocker logs 看看是否能查詢到日誌

root@mydocker:~/feat-logs/mydocker# ./mydocker logs 0439540405
Mem: 1793424K used, 241964K free, 1064K shrd, 91316K buff, 1354280K cached
CPU:  0.0% usr  0.0% sys  0.0% nic  100% idle  0.0% io  0.0% irq  0.0% sirq
Load average: 0.00 0.00 0.00 1/141 4
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND

可以看到,mydocker logs 命令成功執行並輸出了容器的日誌。

至此,說明我們的 mydocker logs 命令實現是 ok 的。

4. 小結

本篇主要實現 mydocker logs 命令,和 docker 實現基本類似:

  • 容器啟動把 stdout、stderr 重定向到 /var/lib/mydocker/container/{containerId}/{containerId}-json.log 檔案
  • 執行 mydocker logs 則根據容器 Id 找到對應檔案,讀取檔案內容並列印

【從零開始寫 Docker 系列】持續更新中,搜尋公眾號【探索雲原生】訂閱,閱讀更多文章。



完整程式碼見:https://github.com/lixd/mydocker
歡迎關注~

相關程式碼見 feat-logs 分支,測試指令碼如下:

需要提前在 /root 目錄準備好 busybox.tar 檔案,具體見第四篇第二節。

# 克隆程式碼
git clone -b feat-logs https://github.com/lixd/mydocker.git
cd mydocker
# 拉取依賴並編譯
go mod tidy
go build .
# 測試 
./mydocker run -d -name c1 top
# 檢視容器 Id
./mydocker ps
# 根據 Id 查詢日誌
./mydocker logs ${containerId}

相關文章