Skip to content

Commit 7f45054

Browse files
authored
feat: improve host log management (#13345)
1 parent 0c046ed commit 7f45054

11 files changed

Lines changed: 219 additions & 33 deletions

File tree

agent/app/api/v2/ssh.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,21 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
224224
helper.SuccessWithData(c, tmpFile)
225225
}
226226

227+
// @Tags SSH
228+
// @Summary Clean host SSH logs
229+
// @Success 200
230+
// @Security ApiKeyAuth
231+
// @Security Timestamp
232+
// @Router /hosts/ssh/log/clean [post]
233+
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 SSH 登录日志","formatEN":"clean SSH login logs"}
234+
func (b *BaseApi) CleanSSHLogs(c *gin.Context) {
235+
if err := sshService.CleanLog(); err != nil {
236+
helper.InternalServer(c, err)
237+
return
238+
}
239+
helper.Success(c)
240+
}
241+
227242
// @Tags SSH
228243
// @Summary Load host SSH conf
229244
// @Accept json

agent/app/dto/logs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type SystemLogRes struct {
3636

3737
type SystemLogItem struct {
3838
Timestamp int64 `json:"-"`
39+
Cursor string `json:"-"`
3940
Time string `json:"time"`
4041
Priority string `json:"priority"`
4142
Service string `json:"service"`

agent/app/service/logs.go

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ var (
3333
)
3434

3535
type systemLogCursor struct {
36-
EndTime int64 `json:"endTime"`
37-
Skipped int `json:"skipped"`
36+
EndTime int64 `json:"endTime"`
37+
Skipped int `json:"skipped"`
38+
JournalCursor string `json:"journalCursor,omitempty"`
3839
}
3940

4041
type ILogService interface {
@@ -61,32 +62,14 @@ func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, erro
6162
if err != nil {
6263
return dto.SystemLogRes{}, err
6364
}
64-
if cursor != nil {
65+
if cursor != nil && cursor.JournalCursor == "" {
6566
endTime = time.Unix(0, cursor.EndTime*int64(time.Microsecond))
6667
}
6768
journalctl, err := exec.LookPath("journalctl")
6869
if err != nil {
6970
return u.readFileSystemLog(req, startTime, endTime, pageSize, cursor)
7071
}
71-
args := []string{
72-
"--no-pager", "--reverse", "--output=json",
73-
"--since", formatJournalQueryTime(startTime),
74-
"--until", formatJournalQueryTime(endTime),
75-
}
76-
if service := strings.TrimSpace(req.Service); service != "" {
77-
args = append(args, "-u", service)
78-
}
79-
if priority := strings.TrimSpace(req.Priority); priority != "" {
80-
args = append(args, "--priority", priority)
81-
}
82-
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
83-
args = append(args, "--grep", keyword)
84-
}
85-
queryLines := pageSize + 1
86-
if cursor != nil {
87-
queryLines += cursor.Skipped
88-
}
89-
queryArgs := append(args, "--lines="+strconv.Itoa(queryLines))
72+
queryArgs := buildJournalQueryArgs(req, startTime, endTime, pageSize, cursor)
9073
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
9174
output, err := exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
9275
cancel()
@@ -98,7 +81,7 @@ func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, erro
9881
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
9982
}
10083
items := parseJournalLogItems(content)
101-
if cursor != nil {
84+
if cursor != nil && cursor.JournalCursor == "" {
10285
items, err = skipSystemLogCursorItems(items, *cursor)
10386
if err != nil {
10487
return dto.SystemLogRes{}, err
@@ -107,6 +90,32 @@ func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, erro
10790
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
10891
}
10992

93+
func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) []string {
94+
args := []string{
95+
"--no-pager", "--reverse", "--output=json",
96+
"--since", formatJournalQueryTime(startTime),
97+
}
98+
if cursor != nil && cursor.JournalCursor != "" {
99+
args = append(args, "--after-cursor="+cursor.JournalCursor)
100+
} else {
101+
args = append(args, "--until", formatJournalQueryTime(endTime))
102+
}
103+
if service := strings.TrimSpace(req.Service); service != "" {
104+
args = append(args, "-u", service)
105+
}
106+
if priority := strings.TrimSpace(req.Priority); priority != "" {
107+
args = append(args, "--priority", priority)
108+
}
109+
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
110+
args = append(args, "--grep", keyword)
111+
}
112+
queryLines := pageSize + 1
113+
if cursor != nil && cursor.JournalCursor == "" {
114+
queryLines += cursor.Skipped
115+
}
116+
return append(args, "--lines="+strconv.Itoa(queryLines))
117+
}
118+
110119
func (u *LogService) readFileSystemLog(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
111120
items := make([]dto.SystemLogItem, 0)
112121
for _, logFile := range []string{"/var/log/syslog", "/var/log/messages", "/var/log/system.log"} {
@@ -156,11 +165,16 @@ func buildSystemLogResponse(source string, items []dto.SystemLogItem, pageSize i
156165
return res, nil
157166
}
158167
lastItem := items[len(items)-1]
159-
skipped := countSystemLogTimestamp(items, lastItem.Timestamp)
160-
if cursor != nil && cursor.EndTime == lastItem.Timestamp {
161-
skipped += cursor.Skipped
168+
next := systemLogCursor{EndTime: lastItem.Timestamp}
169+
if lastItem.Cursor != "" {
170+
next.JournalCursor = lastItem.Cursor
171+
} else {
172+
next.Skipped = countSystemLogTimestamp(items, lastItem.Timestamp)
173+
if cursor != nil && cursor.EndTime == lastItem.Timestamp {
174+
next.Skipped += cursor.Skipped
175+
}
162176
}
163-
nextCursor, err := encodeSystemLogCursor(systemLogCursor{EndTime: lastItem.Timestamp, Skipped: skipped})
177+
nextCursor, err := encodeSystemLogCursor(next)
164178
if err != nil {
165179
return dto.SystemLogRes{}, err
166180
}
@@ -215,7 +229,8 @@ func countSystemLogTimestamp(items []dto.SystemLogItem, timestamp int64) int {
215229
}
216230

217231
func formatJournalQueryTime(value time.Time) string {
218-
return value.In(time.Local).Format("2006-01-02 15:04:05.000000")
232+
// Fractional seconds are rejected by some journalctl versions.
233+
return value.In(time.Local).Format("2006-01-02 15:04:05")
219234
}
220235

221236
func parseFileSystemLogItem(line string) (dto.SystemLogItem, bool) {
@@ -358,6 +373,7 @@ func parseJournalLogItems(content string) []dto.SystemLogItem {
358373
}
359374
items = append(items, dto.SystemLogItem{
360375
Timestamp: journalFieldMicroseconds(fields),
376+
Cursor: journalFieldString(fields, "__CURSOR"),
361377
Time: formatJournalTimestamp(journalFieldString(fields, "__REALTIME_TIMESTAMP")),
362378
Priority: journalFieldString(fields, "PRIORITY"),
363379
Service: journalFieldString(fields, "_SYSTEMD_UNIT"),

agent/app/service/ssh.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type ISSHService interface {
5252

5353
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
5454
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
55+
CleanLog() error
5556

5657
SyncRootCert() error
5758
CreateRootCert(req dto.RootCertOperate) error
@@ -658,6 +659,15 @@ type sshFileItem struct {
658659
Year int
659660
}
660661

662+
func isSSHLogFileName(name string) bool {
663+
for _, baseName := range []string{"auth.log", "secure"} {
664+
if name == baseName || strings.HasPrefix(name, baseName+".") || strings.HasPrefix(name, baseName+"-") {
665+
return true
666+
}
667+
}
668+
return false
669+
}
670+
661671
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
662672
var fileList []sshFileItem
663673
var data []dto.SSHHistory
@@ -667,10 +677,16 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
667677
return 0, data, err
668678
}
669679
for _, item := range fileItems {
670-
if item.IsDir() || (!strings.HasPrefix(item.Name(), "secure") && !strings.HasPrefix(item.Name(), "auth.log")) {
680+
if item.IsDir() || !isSSHLogFileName(item.Name()) {
681+
continue
682+
}
683+
info, err := item.Info()
684+
if err != nil {
685+
return 0, data, err
686+
}
687+
if !info.Mode().IsRegular() {
671688
continue
672689
}
673-
info, _ := item.Info()
674690
itemPath := path.Join(baseDir, info.Name())
675691
if strings.HasSuffix(itemPath, ".gz") {
676692
if _, err := os.Stat(strings.TrimSuffix(itemPath, ".gz")); err == nil {
@@ -725,6 +741,41 @@ func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []d
725741
return int64(total), data, nil
726742
}
727743

744+
func (u *SSHService) CleanLog() error {
745+
return cleanSSHLogFiles("/var/log")
746+
}
747+
748+
func cleanSSHLogFiles(baseDir string) error {
749+
fileItems, err := os.ReadDir(baseDir)
750+
if err != nil {
751+
return err
752+
}
753+
for _, item := range fileItems {
754+
if item.IsDir() || !isSSHLogFileName(item.Name()) {
755+
continue
756+
}
757+
info, err := item.Info()
758+
if err != nil {
759+
return err
760+
}
761+
if !info.Mode().IsRegular() {
762+
continue
763+
}
764+
765+
itemPath := path.Join(baseDir, item.Name())
766+
if item.Name() == "auth.log" || item.Name() == "secure" {
767+
if err := os.Truncate(itemPath, 0); err != nil {
768+
return err
769+
}
770+
continue
771+
}
772+
if err := os.Remove(itemPath); err != nil {
773+
return err
774+
}
775+
}
776+
return nil
777+
}
778+
728779
func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) {
729780
_, logs, err := u.LoadLog(ctx, req)
730781
if err != nil {

agent/cmd/server/docs/x-log.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3609,6 +3609,13 @@
36093609
"formatZH": "修改 SSH 配置文件 [key]",
36103610
"formatEN": "update SSH conf [key]"
36113611
},
3612+
"/hosts/ssh/log/clean": {
3613+
"bodyKeys": [],
3614+
"paramKeys": [],
3615+
"beforeFunctions": [],
3616+
"formatZH": "清空 SSH 登录日志",
3617+
"formatEN": "clean SSH login logs"
3618+
},
36123619
"/hosts/ssh/operate": {
36133620
"bodyKeys": [
36143621
"operation"

agent/router/ro_host.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
4949
hostRouter.POST("/ssh/update", baseApi.UpdateSSH)
5050
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
5151
hostRouter.POST("/ssh/log/export", baseApi.ExportSSHLogs)
52+
hostRouter.POST("/ssh/log/clean", baseApi.CleanSSHLogs)
5253
hostRouter.POST("/ssh/operate", baseApi.OperateSSH)
5354
hostRouter.POST("/ssh/file", baseApi.LoadSSHFile)
5455
hostRouter.POST("/ssh/file/update", baseApi.UpdateSSHByFile)

core/cmd/server/docs/docs.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19445,6 +19445,34 @@ const docTemplate = `{
1944519445
]
1944619446
}
1944719447
},
19448+
"/hosts/ssh/log/clean": {
19449+
"post": {
19450+
"responses": {
19451+
"200": {
19452+
"description": "OK"
19453+
}
19454+
},
19455+
"security": [
19456+
{
19457+
"ApiKeyAuth": []
19458+
},
19459+
{
19460+
"Timestamp": []
19461+
}
19462+
],
19463+
"summary": "Clean host SSH logs",
19464+
"tags": [
19465+
"SSH"
19466+
],
19467+
"x-panel-log": {
19468+
"BeforeFunctions": [],
19469+
"bodyKeys": [],
19470+
"formatEN": "clean SSH login logs",
19471+
"formatZH": "清空 SSH 登录日志",
19472+
"paramKeys": []
19473+
}
19474+
}
19475+
},
1944819476
"/hosts/ssh/log/export": {
1944919477
"post": {
1945019478
"consumes": [
@@ -46537,4 +46565,4 @@ var SwaggerInfo = &swag.Spec{
4653746565

4653846566
func init() {
4653946567
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
46540-
}
46568+
}

core/cmd/server/docs/swagger.json

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19441,6 +19441,34 @@
1944119441
]
1944219442
}
1944319443
},
19444+
"/hosts/ssh/log/clean": {
19445+
"post": {
19446+
"responses": {
19447+
"200": {
19448+
"description": "OK"
19449+
}
19450+
},
19451+
"security": [
19452+
{
19453+
"ApiKeyAuth": []
19454+
},
19455+
{
19456+
"Timestamp": []
19457+
}
19458+
],
19459+
"summary": "Clean host SSH logs",
19460+
"tags": [
19461+
"SSH"
19462+
],
19463+
"x-panel-log": {
19464+
"BeforeFunctions": [],
19465+
"bodyKeys": [],
19466+
"formatEN": "clean SSH login logs",
19467+
"formatZH": "清空 SSH 登录日志",
19468+
"paramKeys": []
19469+
}
19470+
}
19471+
},
1944419472
"/hosts/ssh/log/export": {
1944519473
"post": {
1944619474
"consumes": [
@@ -46516,4 +46544,4 @@
4651646544
"type": "object"
4651746545
}
4651846546
}
46519-
}
46547+
}

core/cmd/server/docs/x-log.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3609,6 +3609,13 @@
36093609
"formatZH": "修改 SSH 配置文件 [key]",
36103610
"formatEN": "update SSH conf [key]"
36113611
},
3612+
"/hosts/ssh/log/clean": {
3613+
"bodyKeys": [],
3614+
"paramKeys": [],
3615+
"beforeFunctions": [],
3616+
"formatZH": "清空 SSH 登录日志",
3617+
"formatEN": "clean SSH login logs"
3618+
},
36123619
"/hosts/ssh/operate": {
36133620
"bodyKeys": [
36143621
"operation"

frontend/src/api/modules/host.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ export const loadSSHLogs = (params: Host.searchSSHLog, currentNode?: string) =>
200200
export const exportSSHLogs = (params: Host.searchSSHLog) => {
201201
return http.post<string>(`/hosts/ssh/log/export`, params, TimeoutEnum.T_40S);
202202
};
203+
export const cleanSSHLogs = () => {
204+
return http.post(`/hosts/ssh/log/clean`, {});
205+
};
203206

204207
export const listDisks = () => {
205208
return http.get<Host.CompleteDiskInfo>(`/hosts/disks`);

0 commit comments

Comments
 (0)