使用Golang Docker API无法获取单元测试日志的问题与存储方案
Golang调用Docker API无法获取Python unittest日志问题解决
问题描述
用Golang调用Docker API时,能正常获取容器内fmt.Print这类标准输出日志,但抓不到Python unittest生成的日志,同时需要把unittest日志存到变量里。
问题原因
- Python的unittest模块默认把日志输出到标准错误流(stderr),而原代码调用
ContainerLogs时只开启了ShowStdout: true,没捕获stderr,所以看不到unittest的输出。 - Docker容器环境属于非交互式终端,部分输出可能有缓冲,但核心问题还是stderr未被采集。
解决步骤
1. 同时捕获stdout和stderr
修改ContainerLogs的参数,同时开启ShowStdout和ShowStderr,这样就能拿到unittest的日志。
2. 将日志存储到变量
用bytes.Buffer替代os.Stdout/os.Stderr来接收日志内容,之后可以把缓冲里的内容转为字符串,存到变量中复用。
3. 可选:调整unittest输出行为(非必需)
如果想让unittest输出到stdout,可以在测试代码里修改unittest.main()的参数,比如:
if __name__ == '__main__': import sys unittest.main(stream=sys.stdout)
但更推荐直接捕获stderr,这是unittest的标准行为,不需要改动Python代码。
修改后的完整代码
package main import ( "bytes" "context" "fmt" "os" "path/filepath" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" ) func main() { userCode := ` def two_sum(nums, target): num_dict = {} for i, num in enumerate(nums): complement = target - num if complement in num_dict: return [num_dict[complement], i] num_dict[num] = i return [] ` // Predefined test cases testCode := ` import unittest from main import two_sum class TestTwoSum(unittest.TestCase): def test_case_1(self): self.assertEqual(two_sum([2, 7, 11, 15], 9), [0, 1]) def test_case_2(self): self.assertEqual(two_sum([3, 2, 4], 6), [1, 2]) if __name__ == '__main__': unittest.main() ` cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } defer cli.Close() // 1. Create 2 temp files hostWorkingDir, err := os.MkdirTemp("", "") check(err) fmt.Println("Temp dir name:", hostWorkingDir) defer os.RemoveAll(hostWorkingDir) codeFilePath := filepath.Join(hostWorkingDir, "main.py") codeFile, err := os.Create(codeFilePath) if err != nil { fmt.Println("failed to create source file", err) return } fmt.Println("Code file name:", codeFile.Name()) defer codeFile.Close() testFilePath := filepath.Join(hostWorkingDir, "test.py") testFile, err := os.Create(testFilePath) if err != nil { fmt.Println("failed to create test file", err) return } fmt.Println("Test file name:", testFile.Name()) defer testFile.Close() // 2. Write code into 2 temp files: _, err = codeFile.WriteString(userCode) if err != nil { fmt.Print("failed to write source file") return } _, err = testFile.WriteString(testCode) if err != nil { fmt.Print("failed to write test file") return } // 3. Create Container ctx := context.Background() workingDir := "/work" programFileDirectory, codePath := filepath.Split(codeFile.Name()) _, testPath := filepath.Split(testFile.Name()) codePath = filepath.Join(workingDir, codePath) fmt.Println(codePath) testPath = filepath.Join(workingDir, testPath) fmt.Println(testPath) resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: "docker.io/library/python:3.13-rc-slim", Cmd: []string{"timeout", "--foreground", "10s", "python", "test.py"}, WorkingDir: workingDir, AttachStdin: true, AttachStdout: true, AttachStderr: true, OpenStdin: true, StdinOnce: true, }, &container.HostConfig{ Binds: []string{fmt.Sprintf("%s:%s", programFileDirectory, workingDir)}, Resources: container.Resources{ CPUQuota: 1000000, Memory: int64(256 * 1024 * 1024), // 256 MB }, NetworkMode: "none"}, nil, nil, "") if err != nil { panic(err) } //4. Start Container if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { panic(err) } //5. Wait for container to response statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { panic(err) } case <-statusCh: } // 关键修改1:同时开启ShowStdout和ShowStderr out, err := cli.ContainerLogs(ctx, resp.ID, container.LogsOptions{ ShowStdout: true, ShowStderr: true, }) if err != nil { panic(err) } // 关键修改2:用Buffer接收日志,存储到变量 var stdoutBuf, stderrBuf bytes.Buffer _, err = stdcopy.StdCopy(&stdoutBuf, &stderrBuf, out) if err != nil { panic(err) } // 输出查看结果 fmt.Println("=== 标准输出内容 ===") fmt.Println(stdoutBuf.String()) fmt.Println("=== 标准错误内容(unittest日志) ===") fmt.Println(stderrBuf.String()) // 把日志存储到变量,后续可按需使用 allLogs := stdoutBuf.String() + "\n" + stderrBuf.String() fmt.Println("=== 全部日志内容 ===") fmt.Println(allLogs) } func check(e error) { if e != nil { panic(e) } }
关键修改说明
- 日志捕获参数:在
container.LogsOptions中新增ShowStderr: true,确保采集到unittest输出的stderr内容。 - 日志存储:使用
bytes.Buffer替代控制台输出,将stdout和stderr分别存入缓冲,之后可以转为字符串变量保存或处理。
内容的提问来源于stack exchange,提问作者Triet Dao
相关产品推荐
相关产品推荐

