Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 获取应用日志 #1691

Merged
merged 10 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cnb-builder-shim/internal/devsandbox/watchsrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ var DefaultLayersDir = "/layers"
// DefaultAppDir is the default build dir
var DefaultAppDir = utils.EnvOrDefault("CNB_APP_DIR", "/app")

// DefaultAppLogDir is the saas default log dir
var DefaultAppLogDir = utils.EnvOrDefault("CNB_APP_LOG_DIR", "/v3logs")

// AppReloadEvent 事件
type AppReloadEvent struct {
ID string
Expand Down
24 changes: 24 additions & 0 deletions cnb-builder-shim/internal/devsandbox/webserver/serializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* TencentBlueKing is pleased to support the open source community by making
* 蓝鲸智云 - PaaS 平台 (BlueKing - PaaS System) available.
* Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/

package webserver

// LogQueryParams : log query params
type LogQueryParams struct {
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
Lines int `form:"lines" binding:"omitempty,gte=1,lte=200"`
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
}
26 changes: 26 additions & 0 deletions cnb-builder-shim/internal/devsandbox/webserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func New(lg *logr.Logger) (*WebServer, error) {
mgr := service.NewDeployManager()
r.POST("/deploys", DeployHandler(s, mgr))
r.GET("/deploys/:deployID/results", ResultHandler(mgr))
r.GET("/app_logs", AppLogHandler())
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved

return s, nil
}
Expand Down Expand Up @@ -196,4 +197,29 @@ func ResultHandler(svc service.DeployServiceHandler) gin.HandlerFunc {
}
}

// AppLogHandler 获取 app 日志
func AppLogHandler() gin.HandlerFunc {
return func(c *gin.Context) {
var queryParams LogQueryParams
if err := c.ShouldBindQuery(&queryParams); err != nil {
// 验证失败
c.JSON(http.StatusBadRequest, gin.H{
"message": "查询参数无效,lines 必须是 1 到 200 之间的整数",
})
return
}
// 如果没有提供 lines 参数,则设置默认值 100
if queryParams.Lines == 0 {
queryParams.Lines = 100
}
// 读取日志
appLogPath := path.Join(devsandbox.DefaultAppDir, devsandbox.DefaultAppLogDir)
logs, err := service.GetAppLogs(appLogPath, queryParams.Lines)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": fmt.Sprintf("get app log error: %s", err.Error())})
}
c.JSON(http.StatusOK, gin.H{"log": logs})
}
}

var _ devsandbox.DevWatchServer = (*WebServer)(nil)
104 changes: 104 additions & 0 deletions cnb-builder-shim/internal/devsandbox/webserver/service/app_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* TencentBlueKing is pleased to support the open source community by making
* 蓝鲸智云 - PaaS 平台 (BlueKing - PaaS System) available.
* Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*
* We undertake not to change the open source license (MIT license) applicable
* to the current version of the project delivered to anyone in the future.
*/

package service

import (
"bufio"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)

func GetAppLogs(logPath string, lines int) (map[string][]string, error) {
logs := make(map[string][]string)
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
logFiles, err := getLatestLogFiles(logPath)
if err != nil {
return nil, err
}
for logType, files := range logFiles {
for _, file := range files {
lastXLines, err := getLastLines(logPath, file.Name(), lines)
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
continue
}
logs[logType] = append(lastXLines, logs[logType]...)
if len(lastXLines) >= lines {
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
break
}
}
}
return logs, nil
}

// getLatestLogFiles 按日志类型分类,并且获取最新的日志文件列表
// 日志文件名称需要符合格式:{{process_type}}-{{random_str}}-{{log_type}}.log
func getLatestLogFiles(logDir string) (map[string][]os.FileInfo, error) {
logFiles := make(map[string][]os.FileInfo)
err := filepath.Walk(logDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 过滤非日志文件, 并且根据文件名称将日志进行分类
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".log") {
logName := strings.TrimSuffix(strings.ToLower(info.Name()), filepath.Ext(info.Name()))
lastDashIndex := strings.LastIndex(logName, "-")
if lastDashIndex == -1 {
return fmt.Errorf("invalid log file name: %s", info.Name())
}
logType := logName[lastDashIndex+1:]
logFiles[logType] = append(logFiles[logType], info)
}
return nil
})
if err != nil {
return nil, err
}
// 按日志类型进行时间排序
for _, files := range logFiles {
sort.Slice(files, func(i, j int) bool {
return files[i].ModTime().After(files[j].ModTime())
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
})
}
return logFiles, nil
}
Copy link
Collaborator

@narasux narasux Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func getLogType(info os.FileInfo) string {
    if info.IsDir() {
        return ""
    }

    filename := info.Name()
    extName := filepath.Ext(filename)

    // 使用文件名称小写格式作为日志类型
    if extName == ".log" {
        return strings.ToLower(strings.TrimSuffix(filename, extName)) 
    }
    
    return ""
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

稍稍优化下,可读性会更好,目前的写法没有重点,且重复代码太多了,看着累人

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw,在外面依赖 logType == "" 判断其实也不是好的实践,返回 (string,error)/(string,bool)可能会更好?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

好,我下个 pr 一起修改


// getLastLines returns the last lines of the log file
func getLastLines(logPath string, fileName string, lines int) ([]string, error) {
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
filePath := filepath.Join(logPath, fileName)
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var logs []string
scanner := bufio.NewScanner(file)
// 全量读取日志文件,日志文件都有进行分片,对于内存压力应该还好
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
for scanner.Scan() {
logs = append(logs, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
if len(logs) > lines {
logs = logs[len(logs)-lines:]
}
return logs, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ var _ = Describe("Test DeployManager", func() {
var m *DeployManager
var tmpAppDir string

testSrcFilePath := filepath.Join("testdata", "templates", "django-helloworld")
testSrcFilePath := filepath.Join("testdata", "templates", "helloworld")
oldAppDir := devsandbox.DefaultAppDir

BeforeEach(func() {
Expand All @@ -55,12 +55,13 @@ var _ = Describe("Test DeployManager", func() {

Describe("Test deploy", func() {
It("test deploy", func() {
result, _ := m.Deploy(testSrcFilePath)
result, err := m.Deploy(testSrcFilePath)
Expect(err).To(BeNil())

Expect(len(result.DeployID)).To(Equal(32))
Expect(result.Status).To(Equal(devsandbox.ReloadProcessing))

_, err := os.Stat(path.Join(devsandbox.DefaultAppDir, "Procfile"))
_, err = os.Stat(path.Join(devsandbox.DefaultAppDir, "Procfile"))
Expect(err).To(BeNil())

// 验证隐藏目录不会被覆盖(删除)
Expand Down Expand Up @@ -102,3 +103,46 @@ var _ = Describe("Test DeployManager", func() {
})
})
})

var _ = Describe("Test GetAppLogs", func() {
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
var err error
var logPath string
var celeryLogPath string
var mysqlLogPath string
var newCeleryLogPath string
BeforeEach(func() {
logPath, err = os.MkdirTemp("", "log_test")
Expect(err).To(BeNil())
celeryLogPath = filepath.Join(logPath, "celery-test-celery.log")
mysqlLogPath = filepath.Join(logPath, "web-test-mysql.log")
newCeleryLogPath = filepath.Join(logPath, "celery-test-new-celery.log")
logContent1 := "value1\nvalue2\n"
logContent2 := "value3\nvalue4\n"
err := os.WriteFile(celeryLogPath, []byte(logContent1), 0644)
Expect(err).To(BeNil())
err = os.WriteFile(newCeleryLogPath, []byte(logContent2), 0644)
err = os.WriteFile(mysqlLogPath, []byte(logContent2), 0644)
Expect(err).To(BeNil())
})
AfterEach(func() {
Expect(os.RemoveAll(logPath)).To(BeNil())
})
Describe("Test GetAppLogs", func() {
It("test lines < logs", func() {
logs, err := GetAppLogs(logPath, 1)
Expect(err).To(BeNil())
Expect(len(logs["celery"])).To(Equal(1))
Expect(logs["celery"]).To(Equal([]string{"value4"}))
Expect(len(logs["mysql"])).To(Equal(1))
Expect(logs["mysql"]).To(Equal([]string{"value4"}))
})
It("test lines > logs", func() {
logs, err := GetAppLogs(logPath, 5)
Expect(err).To(BeNil())
Expect(len(logs["celery"])).To(Equal(4))
Expect(logs["celery"]).To(Equal([]string{"value1", "value2", "value3", "value4"}))
Expect(len(logs["mysql"])).To(Equal(2))
Expect(logs["mysql"]).To(Equal([]string{"value3", "value4"}))
})
})
})
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
spec_version: 2
SheepSheepChen marked this conversation as resolved.
Show resolved Hide resolved
module:
language: Python
scripts:
pre_release_hook: "python manage.py migrate --no-input"
processes:
web:
command: python manage.py runserver 0.0.0.0:8080