Skip to content

Commit 984a30e

Browse files
committed
feat(host): rotate container logs
1 parent b064a76 commit 984a30e

4 files changed

Lines changed: 230 additions & 4 deletions

File tree

pkg/hostman/guestman/guestman.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ func (m *SGuestManager) startContainerSyncLoop() {
205205
m.reconcileContainerLoop(m.podCache)
206206
}()
207207
}
208+
StartContainerLogRotateLoop(m)
208209
}
209210
}
210211

pkg/hostman/guestman/pod.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ type PodInstance interface {
144144
IsInternalRemoved(ctrCriId string) bool
145145

146146
GetPodContainerCriIds() []string
147+
148+
// For container log rotation: log dir, relative log path per container, and ctrId->criId map
149+
GetPodLogDir() string
150+
GetContainerLogPath(ctrId string) string
151+
ListContainerCriIds() map[string]string
147152
}
148153

149154
type sContainer struct {
@@ -634,6 +639,26 @@ func (s *sPodGuestInstance) getPodLogDir() string {
634639
return filepath.Join(s.HomeDir(), "logs")
635640
}
636641

642+
func (s *sPodGuestInstance) GetPodLogDir() string {
643+
return s.getPodLogDir()
644+
}
645+
646+
func (s *sPodGuestInstance) getContainerLogPath(ctrId string) string {
647+
return filepath.Join(fmt.Sprintf("%s.log", ctrId))
648+
}
649+
650+
func (s *sPodGuestInstance) GetContainerLogPath(ctrId string) string {
651+
return s.getContainerLogPath(ctrId)
652+
}
653+
654+
func (s *sPodGuestInstance) ListContainerCriIds() map[string]string {
655+
out := make(map[string]string, len(s.containers))
656+
for ctrId, c := range s.containers {
657+
out[ctrId] = c.CRIId
658+
}
659+
return out
660+
}
661+
637662
func (s *sPodGuestInstance) getShmDir() string {
638663
return filepath.Join(s.HomeDir(), "shm")
639664
}
@@ -1681,10 +1706,6 @@ func (s *sPodGuestInstance) CreateContainer(ctx context.Context, userCred mcclie
16811706
return nil, nil
16821707
}
16831708

1684-
func (s *sPodGuestInstance) getContainerLogPath(ctrId string) string {
1685-
return filepath.Join(fmt.Sprintf("%s.log", ctrId))
1686-
}
1687-
16881709
func (s *sPodGuestInstance) getLxcfsMounts() []*runtimeapi.Mount {
16891710
// lxcfsPath := "/var/lib/lxc/lxcfs"
16901711
lxcfsPath := options.HostOptions.LxcfsPath
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2019 Yunion
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package guestman
16+
17+
import (
18+
"context"
19+
"os"
20+
"path/filepath"
21+
"strconv"
22+
"sync"
23+
"time"
24+
25+
"github.com/docker/go-units"
26+
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
27+
28+
"yunion.io/x/log"
29+
"yunion.io/x/pkg/errors"
30+
31+
"yunion.io/x/onecloud/pkg/hostman/options"
32+
)
33+
34+
const (
35+
containerLogRotateInterval = 10 * time.Minute
36+
)
37+
38+
var (
39+
containerLogRotateMu sync.Mutex
40+
)
41+
42+
// RunContainerLogRotate runs log rotation for all running pod containers once.
43+
// It is safe to call concurrently; only one run executes at a time.
44+
func RunContainerLogRotate(ctx context.Context, manager *SGuestManager, maxSizeBytes int64, maxFiles int) {
45+
if maxSizeBytes <= 0 || maxFiles <= 0 {
46+
return
47+
}
48+
if !containerLogRotateMu.TryLock() {
49+
return
50+
}
51+
defer containerLogRotateMu.Unlock()
52+
53+
cri := manager.host.GetCRI()
54+
if cri == nil {
55+
return
56+
}
57+
runtimeClient := cri.GetRuntimeClient()
58+
if runtimeClient == nil {
59+
return
60+
}
61+
62+
manager.Servers.Range(func(_id, value interface{}) bool {
63+
select {
64+
case <-ctx.Done():
65+
return false
66+
default:
67+
}
68+
pod, ok := value.(PodInstance)
69+
if !ok {
70+
return true
71+
}
72+
if !pod.IsRunning() {
73+
return true
74+
}
75+
logDir := pod.GetPodLogDir()
76+
for ctrId, criId := range pod.ListContainerCriIds() {
77+
if criId == "" {
78+
continue
79+
}
80+
logPath := filepath.Join(logDir, pod.GetContainerLogPath(ctrId))
81+
if err := rotateContainerLog(ctx, logPath, criId, maxSizeBytes, maxFiles, runtimeClient); err != nil {
82+
log.Warningf("rotate container log %s (cri %s): %v", logPath, criId, err)
83+
}
84+
}
85+
return true
86+
})
87+
}
88+
89+
// rotateContainerLog rotates the container log file at logPath if it exceeds maxSizeBytes,
90+
// keeps up to maxFiles (current + rotated), then calls ReopenContainerLog for the container.
91+
func rotateContainerLog(ctx context.Context, logPath, criId string, maxSizeBytes int64, maxFiles int, runtimeClient runtimeapi.RuntimeServiceClient) error {
92+
dir := filepath.Dir(logPath)
93+
base := filepath.Base(logPath)
94+
// Always try to cleanup stale rotated logs, even if we don't rotate this time.
95+
cleanupRotatedLogs(dir, base, maxFiles)
96+
97+
info, err := os.Stat(logPath)
98+
if err != nil {
99+
if os.IsNotExist(err) {
100+
return nil
101+
}
102+
return err
103+
}
104+
if !info.Mode().IsRegular() {
105+
return nil
106+
}
107+
if info.Size() < maxSizeBytes {
108+
return nil
109+
}
110+
111+
// Rename from high to low so we don't overwrite: .(n-1)->.n, ..., .1->.2, then main->.1
112+
for i := maxFiles - 1; i >= 2; i-- {
113+
src := filepath.Join(dir, base+"."+strconv.Itoa(i-1))
114+
dst := filepath.Join(dir, base+"."+strconv.Itoa(i))
115+
if _, err := os.Stat(src); err != nil {
116+
if os.IsNotExist(err) {
117+
continue
118+
}
119+
return err
120+
}
121+
if err := os.Rename(src, dst); err != nil {
122+
log.Warningf("rename %s -> %s: %v", src, dst, err)
123+
}
124+
}
125+
// Then rotate current log to .1
126+
dst1 := filepath.Join(dir, base+".1")
127+
if err := os.Rename(logPath, dst1); err != nil {
128+
return errors.Wrapf(err, "rename %s -> %s", logPath, dst1)
129+
}
130+
// Cleanup again after shift.
131+
cleanupRotatedLogs(dir, base, maxFiles)
132+
133+
_, err = runtimeClient.ReopenContainerLog(ctx, &runtimeapi.ReopenContainerLogRequest{
134+
ContainerId: criId,
135+
})
136+
if err != nil {
137+
// If runtime failed to reopen the log, try best to rename back so containerd keeps writing to logPath.
138+
if _, statErr := os.Stat(logPath); os.IsNotExist(statErr) {
139+
if rbErr := os.Rename(dst1, logPath); rbErr != nil && !os.IsNotExist(rbErr) {
140+
log.Warningf("reopen log failed, rename back %s -> %s: %v", dst1, logPath, rbErr)
141+
}
142+
}
143+
return errors.Wrap(err, "ReopenContainerLog")
144+
}
145+
return nil
146+
}
147+
148+
func cleanupRotatedLogs(dir, base string, maxFiles int) {
149+
// Keep only .1 .. .(maxFiles-1). Remove .maxFiles and above.
150+
if maxFiles <= 0 {
151+
return
152+
}
153+
// Stop after some consecutive not-exist to avoid infinite loop.
154+
miss := 0
155+
for i := maxFiles; i < maxFiles+100; i++ {
156+
p := filepath.Join(dir, base+"."+strconv.Itoa(i))
157+
if err := os.Remove(p); err != nil {
158+
if os.IsNotExist(err) {
159+
miss++
160+
if miss >= 20 {
161+
return
162+
}
163+
continue
164+
}
165+
log.Errorf("remove old container log %s: %v", p, err)
166+
continue
167+
}
168+
log.Infof("remove old container log %s", p)
169+
miss = 0
170+
}
171+
}
172+
173+
// StartContainerLogRotateLoop starts a goroutine that periodically runs container log rotation
174+
// when options are enabled. Call from guestman after manager and host are ready.
175+
func StartContainerLogRotateLoop(manager *SGuestManager) {
176+
maxSizeStr := options.HostOptions.ContainerLogMaxSize
177+
maxFiles := options.HostOptions.ContainerLogMaxFiles
178+
if maxSizeStr == "" || maxFiles <= 0 {
179+
return
180+
}
181+
maxSizeBytes, err := units.FromHumanSize(maxSizeStr)
182+
if err != nil {
183+
log.Warningf("parse ContainerLogMaxSize %q: %v, disable container log rotate", maxSizeStr, err)
184+
return
185+
}
186+
if maxSizeBytes <= 0 {
187+
return
188+
}
189+
190+
go func() {
191+
ticker := time.NewTicker(containerLogRotateInterval)
192+
defer ticker.Stop()
193+
for range ticker.C {
194+
ctx, cancel := context.WithTimeout(context.Background(), 2*containerLogRotateInterval)
195+
RunContainerLogRotate(ctx, manager, maxSizeBytes, maxFiles)
196+
cancel()
197+
}
198+
}()
199+
log.Infof("container log rotate started: maxSize=%s, maxFiles=%d", maxSizeStr, maxFiles)
200+
}

pkg/hostman/options/options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ type SHostOptions struct {
251251
EnableDirtyRecoverySeconds int `help:"Seconds to delay enable dirty guests recovery feature, default 15 minutes" default:"900"`
252252
EnableContainerCniPortmap bool `help:"Use container cni portmap plugin" default:"false"`
253253
DisableReconcileContainer bool `help:"disable reconcile container" default:"false"`
254+
255+
// Container log rotation (Docker-style max-size and max-file)
256+
ContainerLogMaxSize string `help:"Max size of container log file before rotation (e.g. 10m, 100k). Disabled if empty or <= 0" default:"256m"`
257+
ContainerLogMaxFiles int `help:"Max number of container log files to keep (current + rotated). Disabled if <= 0" default:"1"`
254258
}
255259

256260
func (o SHostOptions) HostLocalNetconfPath(br string) string {

0 commit comments

Comments
 (0)