feat: add host system logs (#13285)

This commit is contained in:
ssongliu
2026-07-17 17:54:36 +08:00
committed by GitHub
parent d7723fd42a
commit b02cdbc5f9
26 changed files with 1283 additions and 2 deletions
+39
View File
@@ -2,6 +2,7 @@ package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
@@ -20,3 +21,41 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Read host logs
// @Accept json
// @Param request body dto.SystemLogReq true "request"
// @Produce json
// @Success 200 {object} dto.SystemLogRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/read [post]
func (b *BaseApi) ReadSystemLog(c *gin.Context) {
var req dto.SystemLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := logService.ReadSystemLog(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary List running host services
// @Produce json
// @Success 200 {array} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/services [get]
func (b *BaseApi) ListRunningServices(c *gin.Context) {
data, err := logService.ListRunningServices()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
+28
View File
@@ -1,6 +1,8 @@
package dto
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
@@ -14,3 +16,29 @@ type SearchTaskLogReq struct {
type TaskDTO struct {
model.Task
}
type SystemLogReq struct {
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=500"`
Cursor string `json:"cursor"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Keyword string `json:"keyword"`
Priority string `json:"priority"`
Service string `json:"service"`
}
type SystemLogRes struct {
Source string `json:"source"`
Items []SystemLogItem `json:"items"`
HasMore bool `json:"hasMore"`
NextCursor string `json:"nextCursor"`
}
type SystemLogItem struct {
Timestamp int64 `json:"-"`
Time string `json:"time"`
Priority string `json:"priority"`
Service string `json:"service"`
Message string `json:"message"`
Raw string `json:"raw"`
}
+387
View File
@@ -1,18 +1,405 @@
package service
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/re"
)
type LogService struct{}
const maxSystemLogCursorSkip = 100
const maxFileSystemLogLines = 1000
var (
syslogPriorityKeywords = []struct {
keyword string
priority string
}{
{"emerg", "0"}, {"panic", "0"}, {"alert", "1"}, {"crit", "2"}, {"fatal", "2"},
{"error", "3"}, {"err", "3"}, {"warn", "4"}, {"notice", "5"}, {"debug", "7"}, {"info", "6"},
}
)
type systemLogCursor struct {
EndTime int64 `json:"endTime"`
Skipped int `json:"skipped"`
}
type ILogService interface {
ListSystemLogFile() ([]string, error)
ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, error)
ListRunningServices() ([]string, error)
}
func (u *LogService) ReadSystemLog(req dto.SystemLogReq) (dto.SystemLogRes, error) {
pageSize := req.PageSize
if pageSize == 0 {
pageSize = 100
}
now := time.Now()
startTime := req.StartTime
if startTime.IsZero() {
startTime = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
}
endTime := req.EndTime
if endTime.IsZero() {
endTime = now
}
cursor, err := decodeSystemLogCursor(req.Cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
if cursor != nil {
endTime = time.Unix(0, cursor.EndTime*int64(time.Microsecond))
}
journalctl, err := exec.LookPath("journalctl")
if err != nil {
return u.readFileSystemLog(req, startTime, endTime, pageSize, cursor)
}
args := []string{
"--no-pager", "--reverse", "--output=json",
"--since", formatJournalQueryTime(startTime),
"--until", formatJournalQueryTime(endTime),
}
if service := strings.TrimSpace(req.Service); service != "" {
args = append(args, "-u", service)
}
if priority := strings.TrimSpace(req.Priority); priority != "" {
args = append(args, "--priority", priority)
}
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
args = append(args, "--grep", keyword)
}
queryLines := pageSize + 1
if cursor != nil {
queryLines += cursor.Skipped
}
queryArgs := append(args, "--lines="+strconv.Itoa(queryLines))
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
output, err := exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
cancel()
if err != nil {
return dto.SystemLogRes{}, fmt.Errorf("read host system logs failed: %s", strings.TrimSpace(string(output)))
}
content := strings.TrimSpace(string(output))
if content == "" || strings.HasPrefix(content, "-- No entries --") {
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
}
items := parseJournalLogItems(content)
if cursor != nil {
items, err = skipSystemLogCursorItems(items, *cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
}
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
}
func (u *LogService) readFileSystemLog(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
items := make([]dto.SystemLogItem, 0)
for _, logFile := range []string{"/var/log/syslog", "/var/log/messages", "/var/log/system.log"} {
if _, err := os.Stat(logFile); err != nil {
continue
}
content, err := readLastSystemLogLines(logFile, maxFileSystemLogLines)
if err != nil {
return dto.SystemLogRes{}, err
}
for _, line := range strings.Split(content, "\n") {
item, ok := parseFileSystemLogItem(line)
if !ok || !matchSystemLogItem(item, req, startTime, endTime) {
continue
}
items = append(items, item)
}
}
sort.SliceStable(items, func(i, j int) bool { return items[i].Timestamp > items[j].Timestamp })
if cursor != nil {
var err error
items, err = skipSystemLogCursorItems(items, *cursor)
if err != nil {
return dto.SystemLogRes{}, err
}
}
return buildSystemLogResponse("file", items, pageSize, cursor)
}
func readLastSystemLogLines(logFile string, lines int) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
output, err := exec.CommandContext(ctx, "tail", "-n", strconv.Itoa(lines), logFile).Output()
if err != nil {
return "", err
}
return string(output), nil
}
func buildSystemLogResponse(source string, items []dto.SystemLogItem, pageSize int, cursor *systemLogCursor) (dto.SystemLogRes, error) {
hasMore := len(items) > pageSize
if hasMore {
items = items[:pageSize]
}
res := dto.SystemLogRes{Source: source, Items: items, HasMore: hasMore}
if !hasMore || len(items) == 0 {
return res, nil
}
lastItem := items[len(items)-1]
skipped := countSystemLogTimestamp(items, lastItem.Timestamp)
if cursor != nil && cursor.EndTime == lastItem.Timestamp {
skipped += cursor.Skipped
}
nextCursor, err := encodeSystemLogCursor(systemLogCursor{EndTime: lastItem.Timestamp, Skipped: skipped})
if err != nil {
return dto.SystemLogRes{}, err
}
res.NextCursor = nextCursor
return res, nil
}
func decodeSystemLogCursor(value string) (*systemLogCursor, error) {
value = strings.TrimSpace(value)
if value == "" {
return nil, nil
}
decoded, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return nil, fmt.Errorf("invalid system log cursor")
}
var cursor systemLogCursor
if err := json.Unmarshal(decoded, &cursor); err != nil || cursor.EndTime <= 0 || cursor.Skipped < 0 || cursor.Skipped > maxSystemLogCursorSkip {
return nil, fmt.Errorf("invalid system log cursor")
}
return &cursor, nil
}
func encodeSystemLogCursor(cursor systemLogCursor) (string, error) {
value, err := json.Marshal(cursor)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(value), nil
}
func skipSystemLogCursorItems(items []dto.SystemLogItem, cursor systemLogCursor) ([]dto.SystemLogItem, error) {
skipped := 0
for len(items) > 0 && skipped < cursor.Skipped && items[0].Timestamp == cursor.EndTime {
items = items[1:]
skipped++
}
if skipped != cursor.Skipped {
return nil, fmt.Errorf("system log cursor has expired")
}
return items, nil
}
func countSystemLogTimestamp(items []dto.SystemLogItem, timestamp int64) int {
count := 0
for _, item := range items {
if item.Timestamp == timestamp {
count++
}
}
return count
}
func formatJournalQueryTime(value time.Time) string {
return value.In(time.Local).Format("2006-01-02 15:04:05.000000")
}
func parseFileSystemLogItem(line string) (dto.SystemLogItem, bool) {
raw := strings.TrimSpace(line)
if raw == "" {
return dto.SystemLogItem{}, false
}
priority := "6"
if matches := re.GetRegex(re.SyslogPRIPattern).FindStringSubmatch(raw); len(matches) == 2 {
value, _ := strconv.Atoi(matches[1])
priority = strconv.Itoa(value % 8)
raw = strings.TrimSpace(strings.TrimPrefix(raw, matches[0]))
}
logTime, rest, ok := parseFileSystemLogTime(raw)
if !ok {
return dto.SystemLogItem{}, false
}
service, message := parseFileSystemLogService(rest)
if priority == "6" {
priority = detectFileSystemLogPriority(message)
}
return dto.SystemLogItem{
Timestamp: logTime.UnixMicro(),
Time: logTime.Format("2006-01-02 15:04:05"),
Priority: priority,
Service: service,
Message: message,
Raw: line,
}, true
}
func parseFileSystemLogTime(value string) (time.Time, string, bool) {
if matches := re.GetRegex(re.SyslogRFC3339Pattern).FindStringSubmatch(value); len(matches) == 3 {
for _, layout := range []string{time.RFC3339Nano, "2006-01-02 15:04:05.999999999Z07:00"} {
if parsed, err := time.Parse(layout, matches[1]); err == nil {
return parsed.Local(), matches[2], true
}
}
for _, layout := range []string{"2006-01-02T15:04:05.999999999", "2006-01-02 15:04:05.999999999"} {
if parsed, err := time.ParseInLocation(layout, matches[1], time.Local); err == nil {
return parsed, matches[2], true
}
}
}
if matches := re.GetRegex(re.SyslogRFC3164Pattern).FindStringSubmatch(value); len(matches) == 3 {
parsed, err := time.ParseInLocation("Jan _2 15:04:05", matches[1], time.Local)
if err != nil {
return time.Time{}, "", false
}
now := time.Now()
parsed = time.Date(now.Year(), parsed.Month(), parsed.Day(), parsed.Hour(), parsed.Minute(), parsed.Second(), 0, time.Local)
if parsed.After(now.Add(24 * time.Hour)) {
parsed = parsed.AddDate(-1, 0, 0)
}
return parsed, matches[2], true
}
return time.Time{}, "", false
}
func parseFileSystemLogService(value string) (string, string) {
if matches := re.GetRegex(re.SyslogServicePattern).FindStringSubmatch(value); len(matches) == 3 {
return matches[1], matches[2]
}
// RFC5424 records have the form HOST APP-NAME PROCID MSGID STRUCTURED-DATA MSG.
fields := strings.Fields(value)
if len(fields) >= 6 {
return fields[1], strings.Join(fields[5:], " ")
}
return "", value
}
func detectFileSystemLogPriority(message string) string {
lowerMessage := strings.ToLower(message)
for _, item := range syslogPriorityKeywords {
if strings.Contains(lowerMessage, item.keyword) {
return item.priority
}
}
return "6"
}
func matchSystemLogItem(item dto.SystemLogItem, req dto.SystemLogReq, startTime, endTime time.Time) bool {
itemTime := time.UnixMicro(item.Timestamp)
if itemTime.Before(startTime) || itemTime.After(endTime) {
return false
}
if priority := strings.TrimSpace(req.Priority); priority != "" && item.Priority != priority {
return false
}
if service := strings.TrimSpace(req.Service); service != "" && !matchFileSystemLogService(item.Service, service) {
return false
}
if keyword := strings.TrimSpace(req.Keyword); keyword != "" && !strings.Contains(strings.ToLower(item.Raw), strings.ToLower(keyword)) {
return false
}
return true
}
func matchFileSystemLogService(itemService, requestedService string) bool {
itemService = strings.TrimSpace(itemService)
requestedService = strings.TrimSpace(requestedService)
if itemService == "" || requestedService == "" {
return itemService == requestedService
}
return strings.EqualFold(strings.TrimSuffix(itemService, ".service"), strings.TrimSuffix(requestedService, ".service"))
}
func (u *LogService) ListRunningServices() ([]string, error) {
systemctl, err := exec.LookPath("systemctl")
if err != nil {
return []string{}, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
args := []string{"list-units", "--type=service", "--state=running", "--no-legend", "--no-pager", "--plain"}
output, err := exec.CommandContext(ctx, systemctl, args...).Output()
if err != nil {
return []string{}, nil
}
services := make([]string, 0)
for _, line := range strings.Split(string(output), "\n") {
fields := strings.Fields(line)
if len(fields) == 0 || !strings.HasSuffix(fields[0], ".service") {
continue
}
services = append(services, fields[0])
}
sort.Strings(services)
return services, nil
}
func parseJournalLogItems(content string) []dto.SystemLogItem {
entries := strings.Split(content, "\n")
items := make([]dto.SystemLogItem, 0, len(entries))
for _, entry := range entries {
var fields map[string]interface{}
if err := json.Unmarshal([]byte(entry), &fields); err != nil {
continue
}
items = append(items, dto.SystemLogItem{
Timestamp: journalFieldMicroseconds(fields),
Time: formatJournalTimestamp(journalFieldString(fields, "__REALTIME_TIMESTAMP")),
Priority: journalFieldString(fields, "PRIORITY"),
Service: journalFieldString(fields, "_SYSTEMD_UNIT"),
Message: journalFieldString(fields, "MESSAGE"),
Raw: entry,
})
}
return items
}
func journalFieldMicroseconds(fields map[string]interface{}) int64 {
value, _ := strconv.ParseInt(journalFieldString(fields, "__REALTIME_TIMESTAMP"), 10, 64)
return value
}
func journalFieldString(fields map[string]interface{}, key string) string {
value, ok := fields[key]
if !ok || value == nil {
return ""
}
switch item := value.(type) {
case string:
return item
case float64:
return strconv.FormatInt(int64(item), 10)
case []interface{}:
values := make([]string, 0, len(item))
for _, value := range item {
values = append(values, fmt.Sprint(value))
}
return strings.Join(values, " ")
default:
return fmt.Sprint(item)
}
}
func formatJournalTimestamp(value string) string {
microseconds, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return value
}
return time.Unix(0, microseconds*int64(time.Microsecond)).Local().Format("2006-01-02 15:04:05")
}
func NewILogService() ILogService {
+11 -1
View File
@@ -5174,6 +5174,15 @@
"formatZH": "更新 CDN 配置",
"formatEN": "update CDN config"
},
"/xpack/waf/config/global/spider": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新全局蜘蛛允许列表",
"formatEN": "update global spider allowlist"
},
"/xpack/waf/config/global/state": {
"bodyKeys": [
"scope",
@@ -5187,7 +5196,8 @@
"/xpack/waf/config/website/state": {
"bodyKeys": [
"scope",
"state"
"state",
"mode"
],
"paramKeys": [],
"beforeFunctions": [],
+2
View File
@@ -12,6 +12,8 @@ func (s *LogRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
operationRouter.GET("/system/files", baseApi.GetSystemFiles)
operationRouter.POST("/system/read", baseApi.ReadSystemLog)
operationRouter.GET("/system/services", baseApi.ListRunningServices)
operationRouter.POST("/tasks/search", baseApi.PageTasks)
operationRouter.POST("/tasks/read", baseApi.ReadTaskLogByLine)
operationRouter.GET("/tasks/executing/count", baseApi.CountExecutingTasks)
+8
View File
@@ -44,6 +44,10 @@ const (
SSHDisconnectPattern = `^Received disconnect from ([0-9a-fA-F:.]+) port (\d+)`
SSHMaxAuthPattern = `^error: maximum authentication attempts exceeded for (?:(?:invalid user) )?(.+?) from ([0-9a-fA-F:.]+) port (\d+)`
SSHNotAllowedPattern = `^User (.+?) from ([0-9a-fA-F:.]+) not allowed`
SyslogPRIPattern = `^<(\d{1,3})>(?:\d\s+)?`
SyslogRFC3164Pattern = `^([A-Z][a-z]{2}\s{1,2}\d{1,2}\s\d{2}:\d{2}:\d{2})\s+(.*)$`
SyslogRFC3339Pattern = `^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(.*)$`
SyslogServicePattern = `^(?:\S+\s+)?([[:alnum:]_.@/-]+)(?:\[\d+\])?:\s*(.*)$`
)
var regexMap = make(map[string]*regexp.Regexp)
@@ -88,6 +92,10 @@ func Init() {
SSHDisconnectPattern,
SSHMaxAuthPattern,
SSHNotAllowedPattern,
SyslogPRIPattern,
SyslogRFC3164Pattern,
SyslogRFC3339Pattern,
SyslogServicePattern,
}
for _, pattern := range patterns {
+162
View File
@@ -20119,6 +20119,77 @@ const docTemplate = `{
]
}
},
"/logs/system/read": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SystemLogReq"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.SystemLogRes"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Read host logs",
"tags": [
"Logs"
]
}
},
"/logs/system/services": {
"get": {
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "List running host services",
"tags": [
"Logs"
]
}
},
"/logs/tasks/executing/count": {
"get": {
"responses": {
@@ -38445,6 +38516,74 @@ const docTemplate = `{
],
"type": "object"
},
"dto.SystemLogItem": {
"properties": {
"message": {
"type": "string"
},
"priority": {
"type": "string"
},
"raw": {
"type": "string"
},
"service": {
"type": "string"
},
"time": {
"type": "string"
}
},
"type": "object"
},
"dto.SystemLogReq": {
"properties": {
"cursor": {
"type": "string"
},
"endTime": {
"type": "string"
},
"keyword": {
"type": "string"
},
"pageSize": {
"maximum": 500,
"minimum": 1,
"type": "integer"
},
"priority": {
"type": "string"
},
"service": {
"type": "string"
},
"startTime": {
"type": "string"
}
},
"type": "object"
},
"dto.SystemLogRes": {
"properties": {
"hasMore": {
"type": "boolean"
},
"items": {
"items": {
"$ref": "#/definitions/dto.SystemLogItem"
},
"type": "array"
},
"nextCursor": {
"type": "string"
},
"source": {
"type": "string"
}
},
"type": "object"
},
"dto.Tag": {
"properties": {
"key": {
@@ -38769,6 +38908,9 @@ const docTemplate = `{
"group": {
"type": "string"
},
"isAppendOnly": {
"type": "boolean"
},
"isDetail": {
"type": "boolean"
},
@@ -38778,6 +38920,9 @@ const docTemplate = `{
"isHidden": {
"type": "boolean"
},
"isImmutable": {
"type": "boolean"
},
"isSymlink": {
"type": "boolean"
},
@@ -41066,6 +41211,10 @@ const docTemplate = `{
},
"type": "array"
},
"gatewayArgs": {
"maxLength": 4096,
"type": "string"
},
"gatewayImage": {
"type": "string"
},
@@ -41198,6 +41347,10 @@ const docTemplate = `{
},
"type": "array"
},
"gatewayArgs": {
"maxLength": 4096,
"type": "string"
},
"gatewayImage": {
"type": "string"
},
@@ -44612,6 +44765,9 @@ const docTemplate = `{
"group": {
"type": "string"
},
"isAppendOnly": {
"type": "boolean"
},
"isDetail": {
"type": "boolean"
},
@@ -44621,6 +44777,9 @@ const docTemplate = `{
"isHidden": {
"type": "boolean"
},
"isImmutable": {
"type": "boolean"
},
"isSymlink": {
"type": "boolean"
},
@@ -44881,6 +45040,9 @@ const docTemplate = `{
},
"type": "array"
},
"gatewayArgs": {
"type": "string"
},
"gatewayImage": {
"type": "string"
},
+162
View File
@@ -20115,6 +20115,77 @@
]
}
},
"/logs/system/read": {
"post": {
"consumes": [
"application/json"
],
"parameters": [
{
"description": "request",
"in": "body",
"name": "request",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SystemLogReq"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.SystemLogRes"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "Read host logs",
"tags": [
"Logs"
]
}
},
"/logs/system/services": {
"get": {
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"items": {
"type": "string"
},
"type": "array"
}
}
},
"security": [
{
"ApiKeyAuth": []
},
{
"Timestamp": []
}
],
"summary": "List running host services",
"tags": [
"Logs"
]
}
},
"/logs/tasks/executing/count": {
"get": {
"responses": {
@@ -38441,6 +38512,74 @@
],
"type": "object"
},
"dto.SystemLogItem": {
"properties": {
"message": {
"type": "string"
},
"priority": {
"type": "string"
},
"raw": {
"type": "string"
},
"service": {
"type": "string"
},
"time": {
"type": "string"
}
},
"type": "object"
},
"dto.SystemLogReq": {
"properties": {
"cursor": {
"type": "string"
},
"endTime": {
"type": "string"
},
"keyword": {
"type": "string"
},
"pageSize": {
"maximum": 500,
"minimum": 1,
"type": "integer"
},
"priority": {
"type": "string"
},
"service": {
"type": "string"
},
"startTime": {
"type": "string"
}
},
"type": "object"
},
"dto.SystemLogRes": {
"properties": {
"hasMore": {
"type": "boolean"
},
"items": {
"items": {
"$ref": "#/definitions/dto.SystemLogItem"
},
"type": "array"
},
"nextCursor": {
"type": "string"
},
"source": {
"type": "string"
}
},
"type": "object"
},
"dto.Tag": {
"properties": {
"key": {
@@ -38765,6 +38904,9 @@
"group": {
"type": "string"
},
"isAppendOnly": {
"type": "boolean"
},
"isDetail": {
"type": "boolean"
},
@@ -38774,6 +38916,9 @@
"isHidden": {
"type": "boolean"
},
"isImmutable": {
"type": "boolean"
},
"isSymlink": {
"type": "boolean"
},
@@ -41062,6 +41207,10 @@
},
"type": "array"
},
"gatewayArgs": {
"maxLength": 4096,
"type": "string"
},
"gatewayImage": {
"type": "string"
},
@@ -41194,6 +41343,10 @@
},
"type": "array"
},
"gatewayArgs": {
"maxLength": 4096,
"type": "string"
},
"gatewayImage": {
"type": "string"
},
@@ -44608,6 +44761,9 @@
"group": {
"type": "string"
},
"isAppendOnly": {
"type": "boolean"
},
"isDetail": {
"type": "boolean"
},
@@ -44617,6 +44773,9 @@
"isHidden": {
"type": "boolean"
},
"isImmutable": {
"type": "boolean"
},
"isSymlink": {
"type": "boolean"
},
@@ -44877,6 +45036,9 @@
},
"type": "array"
},
"gatewayArgs": {
"type": "string"
},
"gatewayImage": {
"type": "string"
},
+11 -1
View File
@@ -5174,6 +5174,15 @@
"formatZH": "更新 CDN 配置",
"formatEN": "update CDN config"
},
"/xpack/waf/config/global/spider": {
"bodyKeys": [
"rules"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新全局蜘蛛允许列表",
"formatEN": "update global spider allowlist"
},
"/xpack/waf/config/global/state": {
"bodyKeys": [
"scope",
@@ -5187,7 +5196,8 @@
"/xpack/waf/config/website/state": {
"bodyKeys": [
"scope",
"state"
"state",
"mode"
],
"paramKeys": [],
"beforeFunctions": [],
+1
View File
@@ -124,6 +124,7 @@ var WebUrlMap = map[string]struct{}{
"/logs": {},
"/logs/operation": {},
"/logs/host": {},
"/logs/login": {},
"/logs/website": {},
"/logs/system": {},
+25
View File
@@ -43,6 +43,31 @@ export namespace Log {
logType: string;
}
export interface SystemLog {
source: string;
items: SystemLogItem[];
hasMore: boolean;
nextCursor: string;
}
export interface SystemLogSearch {
pageSize: number;
cursor?: string;
startTime?: Date;
endTime?: Date;
keyword?: string;
priority?: string;
service?: string;
}
export interface SystemLogItem {
time: string;
priority: string;
service: string;
message: string;
raw: string;
}
export interface SearchTaskReq extends ReqPage {
type: string;
status: string;
+10
View File
@@ -21,6 +21,16 @@ export const getSystemFiles = (node?: string) => {
return http.get<Array<string>>(`/logs/system/files${params}`);
};
export const readSystemLogs = (params: Log.SystemLogSearch, node?: string) => {
const query = node ? `?operateNode=${node}` : '';
return http.post<Log.SystemLog>(`/logs/system/read${query}`, params);
};
export const listRunningServices = (node?: string) => {
const query = node ? `?operateNode=${node}` : '';
return http.get<string[]>(`/logs/system/services${query}`);
};
export const cleanLogs = (param: Log.CleanLog) => {
return http.post(`/core/logs/clean`, param);
};
+15
View File
@@ -2122,6 +2122,21 @@ const message = {
loginAgent: 'Login agent',
loginStatus: 'Status',
system: 'System logs',
hostSystem: 'Host logs',
previousPage: 'Previous page',
nextPage: 'Next page',
filter: 'Filter',
service: 'Service',
priority: 'Priority',
priorityEmergency: 'Emergency',
priorityAlert: 'Alert',
priorityCritical: 'Critical',
priorityError: 'Error',
priorityWarning: 'Warning',
priorityNotice: 'Notice',
priorityInfo: 'Info',
priorityDebug: 'Debug',
message: 'Message',
deleteLogs: 'Clean logs',
resource: 'Resource',
detail: {
+15
View File
@@ -2159,6 +2159,21 @@ const message = {
loginAgent: 'Agente de acceso',
loginStatus: 'Estado',
system: 'Logs del sistema',
hostSystem: 'Logs del host',
previousPage: 'Página anterior',
nextPage: 'Página siguiente',
filter: 'Filtrar',
service: 'Servicio',
priority: 'Prioridad',
priorityEmergency: 'Emergencia',
priorityAlert: 'Alerta',
priorityCritical: 'Crítico',
priorityError: 'Error',
priorityWarning: 'Advertencia',
priorityNotice: 'Aviso',
priorityInfo: 'Información',
priorityDebug: 'Depuración',
message: 'Mensaje',
deleteLogs: 'Limpiar logs',
resource: 'Recurso',
detail: {
+15
View File
@@ -2107,6 +2107,21 @@ const message = {
loginAgent: 'مرورگر ورود',
loginStatus: 'وضعیت',
system: 'لاگ سیستم',
hostSystem: 'لاگ‌های میزبان',
previousPage: 'صفحه قبل',
nextPage: 'صفحه بعد',
filter: 'فیلتر',
service: 'سرویس',
priority: 'اولویت',
priorityEmergency: 'اضطراری',
priorityAlert: 'هشدار',
priorityCritical: 'بحرانی',
priorityError: 'خطا',
priorityWarning: 'اخطار',
priorityNotice: 'اعلان',
priorityInfo: 'اطلاعات',
priorityDebug: 'اشکال‌زدایی',
message: 'پیام',
deleteLogs: 'پاک‌سازی لاگ‌ها',
resource: 'منبع',
detail: {
+15
View File
@@ -2127,6 +2127,21 @@ const message = {
loginAgent: 'ログインエージェント',
loginStatus: '状態',
system: 'システムログ',
hostSystem: 'ホストログ',
previousPage: '前のページ',
nextPage: '次のページ',
filter: 'フィルター',
service: 'サービス',
priority: '優先度',
priorityEmergency: '緊急',
priorityAlert: '警報',
priorityCritical: '重大',
priorityError: 'エラー',
priorityWarning: '警告',
priorityNotice: '通知',
priorityInfo: '情報',
priorityDebug: 'デバッグ',
message: 'メッセージ',
deleteLogs: 'クリーンログ',
resource: 'リソース',
detail: {
+15
View File
@@ -2085,6 +2085,21 @@ const message = {
loginAgent: '로그인 에이전트',
loginStatus: '상태',
system: '시스템 로그',
hostSystem: '호스트 로그',
previousPage: '이전 페이지',
nextPage: '다음 페이지',
filter: '필터',
service: '서비스',
priority: '우선순위',
priorityEmergency: '긴급',
priorityAlert: '경보',
priorityCritical: '심각',
priorityError: '오류',
priorityWarning: '경고',
priorityNotice: '알림',
priorityInfo: '정보',
priorityDebug: '디버그',
message: '메시지',
deleteLogs: '로그 정리',
resource: '자원',
detail: {
+15
View File
@@ -2154,6 +2154,21 @@ const message = {
loginAgent: 'Ejen Log Masuk',
loginStatus: 'Status',
system: 'Log Sistem',
hostSystem: 'Log hos',
previousPage: 'Halaman sebelumnya',
nextPage: 'Halaman seterusnya',
filter: 'Tapis',
service: 'Perkhidmatan',
priority: 'Keutamaan',
priorityEmergency: 'Kecemasan',
priorityAlert: 'Amaran',
priorityCritical: 'Kritikal',
priorityError: 'Ralat',
priorityWarning: 'Peringatan',
priorityNotice: 'Notis',
priorityInfo: 'Maklumat',
priorityDebug: 'Nyahpepijat',
message: 'Mesej',
deleteLogs: 'Bersihkan Log',
resource: 'Sumber',
detail: {
+15
View File
@@ -2269,6 +2269,21 @@ const message = {
loginAgent: 'Agente de login',
loginStatus: 'Status',
system: 'Logs do sistema',
hostSystem: 'Logs do host',
previousPage: 'Página anterior',
nextPage: 'Próxima página',
filter: 'Filtrar',
service: 'Serviço',
priority: 'Prioridade',
priorityEmergency: 'Emergência',
priorityAlert: 'Alerta',
priorityCritical: 'Crítico',
priorityError: 'Erro',
priorityWarning: 'Aviso',
priorityNotice: 'Notificação',
priorityInfo: 'Informação',
priorityDebug: 'Depuração',
message: 'Mensagem',
deleteLogs: 'Limpar logs',
resource: 'Recurso',
detail: {
+15
View File
@@ -2141,6 +2141,21 @@ const message = {
loginAgent: 'Агент входа',
loginStatus: 'Статус',
system: 'Системные логи',
hostSystem: 'Журналы хоста',
previousPage: 'Предыдущая страница',
nextPage: 'Следующая страница',
filter: 'Фильтр',
service: 'Сервис',
priority: 'Приоритет',
priorityEmergency: 'Авария',
priorityAlert: 'Тревога',
priorityCritical: 'Критический',
priorityError: 'Ошибка',
priorityWarning: 'Предупреждение',
priorityNotice: 'Уведомление',
priorityInfo: 'Информация',
priorityDebug: 'Отладка',
message: 'Сообщение',
deleteLogs: 'Очистить логи',
resource: 'Ресурс',
detail: {
+15
View File
@@ -2151,6 +2151,21 @@ const message = {
loginAgent: 'Giriş aracısı',
loginStatus: 'Durum',
system: 'Sistem logları',
hostSystem: 'Ana makine günlükleri',
previousPage: 'Önceki sayfa',
nextPage: 'Sonraki sayfa',
filter: 'Filtrele',
service: 'Servis',
priority: 'Öncelik',
priorityEmergency: 'Acil',
priorityAlert: 'Alarm',
priorityCritical: 'Kritik',
priorityError: 'Hata',
priorityWarning: 'Uyarı',
priorityNotice: 'Bildirim',
priorityInfo: 'Bilgi',
priorityDebug: 'Hata ayıklama',
message: 'Mesaj',
deleteLogs: 'Logları temizle',
resource: 'Kaynak',
detail: {
+15
View File
@@ -1986,6 +1986,21 @@ const message = {
loginAgent: '使用者代理',
loginStatus: '登入狀態',
system: '系統日誌',
hostSystem: '主機日誌',
previousPage: '上一頁',
nextPage: '下一頁',
filter: '篩選',
service: '服務',
priority: '優先級',
priorityEmergency: '緊急 Emergency',
priorityAlert: '警報 Alert',
priorityCritical: '嚴重 Critical',
priorityError: '錯誤 Error',
priorityWarning: '警告 Warning',
priorityNotice: '通知 Notice',
priorityInfo: '資訊 Info',
priorityDebug: '除錯 Debug',
message: '訊息',
deleteLogs: '清空日誌',
resource: '資源',
detail: {
+15
View File
@@ -1989,6 +1989,21 @@ const message = {
loginAgent: '用户代理',
loginStatus: '登录状态',
system: '系统日志',
hostSystem: '主机日志',
previousPage: '上一页',
nextPage: '下一页',
filter: '过滤',
service: '服务',
priority: '优先级',
priorityEmergency: '紧急 Emergency',
priorityAlert: '警报 Alert',
priorityCritical: '严重 Critical',
priorityError: '错误 Error',
priorityWarning: '警告 Warning',
priorityNotice: '通知 Notice',
priorityInfo: '信息 Info',
priorityDebug: '调试 Debug',
message: '消息',
deleteLogs: '清空日志',
resource: '资源',
detail: {
+12
View File
@@ -67,6 +67,18 @@ const logsRouter = {
permission: 'log_view',
},
},
{
path: 'host',
name: 'HostSystemLog',
component: () => import('@/views/log/host-system/index.vue'),
hidden: true,
meta: {
parent: 'menu.logs',
title: 'logs.hostSystem',
activeMenu: '/logs',
permission: 'log_view',
},
},
{
path: 'ssh',
name: 'SSHLog2',
@@ -0,0 +1,256 @@
<template>
<LayoutContent v-loading="loading" :title="$t('logs.hostSystem')">
<template #rightToolBar>
<el-input v-model="keyword" class="p-w-200" clearable :placeholder="$t('logs.filter')" />
<el-date-picker
v-model="timeRange"
class="p-w-360"
type="datetimerange"
range-separator="-"
:start-placeholder="$t('commons.search.timeStart')"
:end-placeholder="$t('commons.search.timeEnd')"
:shortcuts="shortcuts"
@change="changeTimeRange"
/>
<el-select v-model="priority" class="p-w-200" clearable>
<template #prefix>{{ $t('logs.priority') }}</template>
<el-option
v-for="item in priorities"
:key="item.value"
:label="priorityLabel(item.value)"
:value="item.value"
/>
</el-select>
<el-select v-model="service" class="p-w-200" clearable filterable allow-create>
<template #prefix>{{ $t('logs.service') }}</template>
<el-option v-for="item in services" :key="item" :label="item" :value="item" />
</el-select>
<el-checkbox border v-model="watching" @change="changeWatch">
{{ $t('commons.button.watch') }}
</el-checkbox>
<TableRefresh @search="resetAndLoadLogs" />
</template>
<template #main>
<ComplexTable :data="logs" :pagination-config="paginationConfig" @row-click="openDetail">
<el-table-column prop="time" :label="$t('commons.table.date')" width="220" show-overflow-tooltip />
<el-table-column :label="$t('logs.priority')" width="120">
<template #default="{ row }">
<el-tag size="small" :type="priorityType(row.priority)">
{{ priorityLabel(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="service" :label="$t('logs.service')" width="220" show-overflow-tooltip />
<el-table-column prop="message" :label="$t('logs.message')" min-width="360" show-overflow-tooltip />
<template #pagination>
<div class="flex items-center gap-2">
<el-select v-model="paginationConfig.pageSize" class="p-w-100" @change="changePageSize">
<el-option v-for="size in pageSizes" :key="size" :label="String(size)" :value="size" />
</el-select>
<el-button :disabled="cursorIndex === 0 || loading" @click="loadPreviousPage">
{{ $t('logs.previousPage') }}
</el-button>
<el-button :disabled="!hasMore || loading" @click="loadNextPage">
{{ $t('logs.nextPage') }}
</el-button>
</div>
</template>
</ComplexTable>
</template>
</LayoutContent>
<el-drawer v-model="detailOpen" :title="$t('logs.hostSystem')" size="50%">
<el-descriptions v-if="selectedLog" :column="1" border>
<el-descriptions-item :label="$t('commons.table.date')">{{ selectedLog.time || '-' }}</el-descriptions-item>
<el-descriptions-item :label="$t('logs.priority')">
{{ priorityLabel(selectedLog.priority) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('logs.service')">{{ selectedLog.service || '-' }}</el-descriptions-item>
<el-descriptions-item :label="$t('logs.message')">{{ selectedLog.message }}</el-descriptions-item>
</el-descriptions>
<pre v-if="selectedLog" class="log-raw">{{ selectedLog.raw }}</pre>
</el-drawer>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { Log } from '@/api/interface/log';
import { listRunningServices, readSystemLogs } from '@/api/modules/log';
import { useGlobalStore } from '@/composables/useGlobalStore';
import i18n from '@/lang';
import { shortcuts } from '@/utils/shortcuts';
const { currentNode } = useGlobalStore();
const logs = ref<Log.SystemLogItem[]>([]);
const loading = ref(false);
const watching = ref(false);
const keyword = ref('');
const priority = ref('');
const service = ref('');
const services = ref<string[]>([]);
const detailOpen = ref(false);
const selectedLog = ref<Log.SystemLogItem>();
const timeRange = ref<[Date, Date]>([new Date(new Date().setHours(0, 0, 0, 0)), new Date()]);
const paginationConfig = reactive({
pageSize: 100,
cacheSizeKey: 'host-system-logs-page-size',
});
const pageSizes = [5, 10, 20, 50, 100, 200, 500];
const cursorHistory = ref<string[]>(['']);
const cursorIndex = ref(0);
const hasMore = ref(false);
let timer: ReturnType<typeof setTimeout> | undefined;
let filterTimer: ReturnType<typeof setTimeout> | undefined;
let requestID = 0;
const priorities = [
{ value: '0', labelKey: 'logs.priorityEmergency' },
{ value: '1', labelKey: 'logs.priorityAlert' },
{ value: '2', labelKey: 'logs.priorityCritical' },
{ value: '3', labelKey: 'logs.priorityError' },
{ value: '4', labelKey: 'logs.priorityWarning' },
{ value: '5', labelKey: 'logs.priorityNotice' },
{ value: '6', labelKey: 'logs.priorityInfo' },
{ value: '7', labelKey: 'logs.priorityDebug' },
];
const loadLogs = async (cursor = cursorHistory.value[cursorIndex.value]) => {
const currentRequestID = ++requestID;
loading.value = true;
try {
const res = await readSystemLogs(
{
pageSize: paginationConfig.pageSize,
cursor,
startTime: timeRange.value?.[0],
endTime: timeRange.value?.[1],
keyword: keyword.value,
priority: priority.value,
service: service.value,
},
currentNode.value,
);
if (currentRequestID !== requestID) return;
logs.value = res.data.items || [];
hasMore.value = res.data.hasMore;
if (res.data.nextCursor) {
cursorHistory.value[cursorIndex.value + 1] = res.data.nextCursor;
}
} finally {
if (currentRequestID === requestID) loading.value = false;
}
};
const resetAndLoadLogs = () => {
requestID++;
cursorHistory.value = [''];
cursorIndex.value = 0;
hasMore.value = false;
return loadLogs('');
};
const loadNextPage = () => {
if (!hasMore.value) return;
cursorIndex.value++;
loadLogs();
};
const loadPreviousPage = () => {
if (cursorIndex.value === 0) return;
cursorIndex.value--;
loadLogs();
};
const changePageSize = () => {
localStorage.setItem(paginationConfig.cacheSizeKey, String(paginationConfig.pageSize));
resetAndLoadLogs();
};
const loadServices = async () => {
try {
const res = await listRunningServices(currentNode.value);
services.value = res.data || [];
} catch {
services.value = [];
}
};
const scheduleWatch = () => {
timer = setTimeout(async () => {
if (!watching.value) return;
try {
if (!loading.value) {
await resetAndLoadLogs();
}
} finally {
if (watching.value) {
scheduleWatch();
}
}
}, 3000);
};
const changeWatch = () => {
if (timer) {
clearTimeout(timer);
timer = undefined;
}
if (watching.value) {
scheduleWatch();
}
};
const changeTimeRange = () => {
resetAndLoadLogs();
};
const openDetail = (row: Log.SystemLogItem) => {
selectedLog.value = row;
detailOpen.value = true;
};
const priorityLabel = (value: string) => {
const priority = priorities.find((item) => item.value === value);
return priority ? i18n.global.t(priority.labelKey) : value || '-';
};
const priorityType = (value: string) => {
if (['0', '1', '2', '3'].includes(value)) return 'danger';
if (value === '4') return 'warning';
return 'info';
};
onMounted(() => {
const cachedPageSize = Number(localStorage.getItem(paginationConfig.cacheSizeKey));
if (pageSizes.includes(cachedPageSize)) {
paginationConfig.pageSize = cachedPageSize;
}
resetAndLoadLogs();
loadServices();
});
onUnmounted(() => {
if (timer) clearTimeout(timer);
if (filterTimer) clearTimeout(filterTimer);
});
watch([keyword, priority, service], () => {
requestID++;
if (filterTimer) clearTimeout(filterTimer);
filterTimer = setTimeout(resetAndLoadLogs, 300);
});
</script>
<style scoped lang="scss">
.log-raw {
overflow: auto;
padding: 12px;
color: #f5f5f5;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
background: var(--panel-logs-bg-color);
border: 1px solid var(--el-border-color-darker);
border-radius: 4px;
}
</style>
+4
View File
@@ -15,6 +15,10 @@ const buttons = [
label: i18n.global.t('logs.panelLog'),
path: '/logs/operation',
},
{
label: i18n.global.t('logs.hostSystem'),
path: '/logs/host',
},
{
label: i18n.global.t('ssh.loginLogs'),
path: '/logs/ssh',