Skip to content

Commit afe6d08

Browse files
authored
Merge pull request #6 from Team-TicketMate/20251205_#5_컨테이너_로그_실시간_로깅_기능_추가
컨테이너_로그_실시간_로깅_기능_추가 : feat : 실시간 로깅 구현 https://github.com/Team-Ticke…
2 parents 4f3d811 + 35c5a90 commit afe6d08

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

internal/dockercli/docker_cli.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package dockercli
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
7+
"io"
68
"os/exec"
79
"strings"
810
)
@@ -101,3 +103,40 @@ func FetchContainerLogs(containerID string, tailLines *int) (string, error) {
101103

102104
return output, nil
103105
}
106+
107+
func StreamContainerLogs(ctx context.Context, containerID string, tailLines *int, output io.Writer) error {
108+
trimmedID := strings.TrimSpace(containerID)
109+
if trimmedID == "" {
110+
return fmt.Errorf("컨테이너 ID가 비어있습니다")
111+
}
112+
113+
args := []string{"logs"}
114+
115+
if tailLines != nil {
116+
if *tailLines <= 0 {
117+
return fmt.Errorf("tailLines 는 1 이상의 숫자여야합니다")
118+
}
119+
args = append(args, "--tail", fmt.Sprintf("%d", *tailLines))
120+
}
121+
122+
// docker logs --tail N --follow <id>
123+
args = append(args, "--follow", trimmedID)
124+
125+
// CommandContext를 사용해서 request 컨텍스트가 취소되면 docker 프로세스도 종료
126+
cmd := exec.CommandContext(ctx, "docker", args...)
127+
128+
// stdout, stderr 모두 HTTP 응답으로 보냄
129+
cmd.Stdout = output
130+
cmd.Stderr = output
131+
132+
if err := cmd.Start(); err != nil {
133+
return fmt.Errorf("'docker logs --follow' 시작에 실패했습니다: %w", err)
134+
}
135+
136+
// Wait은 컨텍스트 취소 또는 프로세스 종료까지 블로킹
137+
if err := cmd.Wait(); err != nil {
138+
return fmt.Errorf("'docker logs --follow' 실행 중 오류가 발생했습니다: %w", err)
139+
}
140+
141+
return nil
142+
}

internal/httpapi/containers_handler.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,36 @@ import (
1010
"ticketmate-logviewer/internal/dockercli"
1111
)
1212

13+
type flushWriter struct {
14+
writer http.ResponseWriter
15+
flusher http.Flusher
16+
}
17+
18+
// ResponseWriter 가 http.Flusher를 지원하는지 확인하고 감싸주는 함수
19+
func newFlushWriter(writer http.ResponseWriter) (*flushWriter, error) {
20+
flusher, ok := writer.(http.Flusher)
21+
if !ok {
22+
return nil, fmt.Errorf("http.ResponseWriter가 스트리밍을 지원하지 않습니다")
23+
}
24+
25+
return &flushWriter{
26+
writer: writer,
27+
flusher: flusher,
28+
}, nil
29+
}
30+
31+
// io.Writer 인터페이스 구현
32+
func (flushWriterInstance *flushWriter) Write(p []byte) (int, error) {
33+
written, err := flushWriterInstance.writer.Write(p)
34+
if err != nil {
35+
return written, err
36+
}
37+
38+
// 매번 쓰고 나서 즉시 클라이언트로 flush
39+
flushWriterInstance.flusher.Flush()
40+
return written, nil
41+
}
42+
1343
func containersHandler(writer http.ResponseWriter, request *http.Request) {
1444
if request.Method != http.MethodGet {
1545
http.Error(writer, "허용되지 않은 HTTP 메서드 압니다", http.StatusMethodNotAllowed)
@@ -62,6 +92,34 @@ func containerLogsHandler(writer http.ResponseWriter, request *http.Request) {
6292
return
6393
}
6494

95+
// follow 파라미터 처리: follow = true 또는 follow = 1이면 실시간 스트리밍
96+
followParam := request.URL.Query().Get("follow")
97+
follow := strings.EqualFold(followParam, "true") || followParam == "1"
98+
99+
// 로그는 text/plain 스트리밍 또는 전체 응답
100+
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
101+
102+
if follow {
103+
// 실시간 스트리밍 모드
104+
streamWriter, newWriterErr := newFlushWriter(writer)
105+
if newWriterErr != nil {
106+
log.Printf("HTTP 스트리밍을 지원하지 않는 환경입니다: %v", newWriterErr)
107+
http.Error(writer, "스트리밍을 지원하지 않는 환경입니다", http.StatusInternalServerError)
108+
return
109+
}
110+
111+
log.Printf("컨테이너 로그 스트리밍 시작 (follow 모드). 컨테이너 ID: %s", containerID)
112+
113+
// request.Context() 를 넘겨서 클라이언트가 연결을 끊으면 doker 프로세스도 종료
114+
streamErr := dockercli.StreamContainerLogs(request.Context(), containerID, tailLines, streamWriter)
115+
if streamErr != nil {
116+
// 스트리밍 중 에러는 서버 로그로만 출력. 응답 바디에는 보내지 않음
117+
log.Printf("컨테이너 로그 스트리밍 실패. 컨테이너 ID: %s: %v", containerID, streamErr)
118+
}
119+
log.Printf("컨테이너 로그 스트리밍 종료. 컨테이너 ID: %s", containerID)
120+
return
121+
}
122+
65123
logText, err := dockercli.FetchContainerLogs(containerID, tailLines)
66124
if err != nil {
67125
log.Printf("컨테이너 로그 조회에 실패했습니다. 컨테이너 ID: %s: %v", containerID, err)

0 commit comments

Comments
 (0)