mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 16:13:59 +08:00
570 lines
17 KiB
Go
570 lines
17 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"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/i18n"
|
|
"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"`
|
|
JournalCursor string `json:"journalCursor,omitempty"`
|
|
}
|
|
|
|
type ILogService interface {
|
|
ListSystemLogFile() ([]string, error)
|
|
GetSystemLogStatus() (dto.SystemLogStatus, 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 && cursor.JournalCursor == "" {
|
|
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)
|
|
}
|
|
queryArgs := buildJournalQueryArgs(req, startTime, endTime, pageSize, cursor)
|
|
output, err := executeJournalQuery(journalctl, queryArgs)
|
|
if err != nil {
|
|
return handleJournalQueryError(req, output, err, func() ([]byte, error) {
|
|
probeReq := req
|
|
probeReq.Keyword = ""
|
|
probeReq.Priority = ""
|
|
probeReq.Service = ""
|
|
probeArgs := buildJournalQueryArgs(probeReq, startTime, endTime, 1, cursor)
|
|
return executeJournalQuery(journalctl, probeArgs)
|
|
})
|
|
}
|
|
content := strings.TrimSpace(string(output))
|
|
if content == "" || strings.HasPrefix(content, "-- No entries --") {
|
|
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
|
}
|
|
items := trimJournalLogItemsToStartTime(parseJournalLogItems(content), startTime)
|
|
if cursor != nil && cursor.JournalCursor == "" {
|
|
items, err = skipSystemLogCursorItems(items, *cursor)
|
|
if err != nil {
|
|
return dto.SystemLogRes{}, err
|
|
}
|
|
}
|
|
return buildSystemLogResponse("journalctl", items, pageSize, cursor)
|
|
}
|
|
|
|
func executeJournalQuery(journalctl string, queryArgs []string) ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
return exec.CommandContext(ctx, journalctl, queryArgs...).CombinedOutput()
|
|
}
|
|
|
|
func (u *LogService) GetSystemLogStatus() (dto.SystemLogStatus, error) {
|
|
journalctl, err := exec.LookPath("journalctl")
|
|
if err != nil {
|
|
return dto.SystemLogStatus{
|
|
Source: "file",
|
|
KeywordFilterSupported: true,
|
|
}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
versionOutput, err := exec.CommandContext(ctx, journalctl, "--version").CombinedOutput()
|
|
cancel()
|
|
if err != nil {
|
|
return dto.SystemLogStatus{}, fmt.Errorf("read journalctl version failed: %s", strings.TrimSpace(string(versionOutput)))
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
|
|
helpOutput, err := exec.CommandContext(ctx, journalctl, "--help").CombinedOutput()
|
|
cancel()
|
|
if err != nil {
|
|
return dto.SystemLogStatus{}, fmt.Errorf("read journalctl capabilities failed: %s", strings.TrimSpace(string(helpOutput)))
|
|
}
|
|
supported := journalctlHelpSupportsGrep(string(helpOutput))
|
|
status := dto.SystemLogStatus{
|
|
Source: "journalctl",
|
|
Version: firstOutputLine(string(versionOutput)),
|
|
KeywordFilterSupported: supported,
|
|
}
|
|
if !supported {
|
|
status.Message = i18n.Get("ErrSystemLogKeywordFilterUnsupported")
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func journalctlHelpSupportsGrep(help string) bool {
|
|
return strings.Contains(help, "--grep=")
|
|
}
|
|
|
|
func handleJournalQueryError(
|
|
req dto.SystemLogReq,
|
|
output []byte,
|
|
queryErr error,
|
|
probe func() ([]byte, error),
|
|
) (dto.SystemLogRes, error) {
|
|
if hasSystemLogFilter(req) && probe != nil {
|
|
probeOutput, probeErr := probe()
|
|
if probeErr == nil {
|
|
return dto.SystemLogRes{Source: "journalctl", Items: []dto.SystemLogItem{}}, nil
|
|
}
|
|
return dto.SystemLogRes{}, newJournalQueryError(probeOutput, probeErr)
|
|
}
|
|
return dto.SystemLogRes{}, newJournalQueryError(output, queryErr)
|
|
}
|
|
|
|
func newJournalQueryError(output []byte, queryErr error) error {
|
|
message := strings.TrimSpace(string(output))
|
|
if message == "" && queryErr != nil {
|
|
message = queryErr.Error()
|
|
}
|
|
return fmt.Errorf("read host system logs failed: %s", message)
|
|
}
|
|
|
|
func trimJournalLogItemsToStartTime(items []dto.SystemLogItem, startTime time.Time) []dto.SystemLogItem {
|
|
startTimestamp := startTime.UnixMicro()
|
|
for i, item := range items {
|
|
if item.Timestamp < startTimestamp {
|
|
return items[:i]
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func hasSystemLogFilter(req dto.SystemLogReq) bool {
|
|
return strings.TrimSpace(req.Keyword) != "" ||
|
|
strings.TrimSpace(req.Priority) != "" ||
|
|
strings.TrimSpace(req.Service) != ""
|
|
}
|
|
|
|
func firstOutputLine(output string) string {
|
|
for _, line := range strings.Split(output, "\n") {
|
|
if line = strings.TrimSpace(line); line != "" {
|
|
return line
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func buildJournalQueryArgs(req dto.SystemLogReq, startTime, endTime time.Time, pageSize int, cursor *systemLogCursor) []string {
|
|
args := []string{
|
|
"--no-pager", "--reverse", "--output=json",
|
|
}
|
|
if cursor != nil && cursor.JournalCursor != "" {
|
|
args = append(args, "--after-cursor="+cursor.JournalCursor)
|
|
} else {
|
|
args = append(args,
|
|
"--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+".."+priority)
|
|
}
|
|
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
|
|
args = append(args, "--grep", regexp.QuoteMeta(keyword), "--case-sensitive=no")
|
|
}
|
|
queryLines := pageSize + 1
|
|
if cursor != nil && cursor.JournalCursor == "" {
|
|
queryLines += cursor.Skipped
|
|
}
|
|
return append(args, "--lines="+strconv.Itoa(queryLines))
|
|
}
|
|
|
|
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]
|
|
next := systemLogCursor{EndTime: lastItem.Timestamp}
|
|
if lastItem.Cursor != "" {
|
|
next.JournalCursor = lastItem.Cursor
|
|
} else {
|
|
next.Skipped = countSystemLogTimestamp(items, lastItem.Timestamp)
|
|
if cursor != nil && cursor.EndTime == lastItem.Timestamp {
|
|
next.Skipped += cursor.Skipped
|
|
}
|
|
}
|
|
nextCursor, err := encodeSystemLogCursor(next)
|
|
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 {
|
|
// Fractional seconds are rejected by some journalctl versions.
|
|
return value.In(time.Local).Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
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),
|
|
Cursor: journalFieldString(fields, "__CURSOR"),
|
|
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 {
|
|
return &LogService{}
|
|
}
|
|
|
|
func (u *LogService) ListSystemLogFile() ([]string, error) {
|
|
var listFile []string
|
|
files, err := os.ReadDir(global.Dir.LogDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
listMap := make(map[string]struct{})
|
|
for _, item := range files {
|
|
if item.IsDir() || !strings.HasPrefix(item.Name(), "1Panel") {
|
|
continue
|
|
}
|
|
if item.Name() == "1Panel.log" || item.Name() == "1Panel-Core.log" {
|
|
itemName := time.Now().Format("2006-01-02")
|
|
if _, ok := listMap[itemName]; ok {
|
|
continue
|
|
}
|
|
listMap[itemName] = struct{}{}
|
|
listFile = append(listFile, itemName)
|
|
continue
|
|
}
|
|
itemFileName := strings.TrimPrefix(item.Name(), "1Panel-Core-")
|
|
itemFileName = strings.TrimPrefix(itemFileName, "1Panel-")
|
|
itemFileName = strings.TrimSuffix(itemFileName, ".gz")
|
|
itemFileName = strings.TrimSuffix(itemFileName, ".log")
|
|
if len(itemFileName) == 0 {
|
|
continue
|
|
}
|
|
if _, ok := listMap[itemFileName]; ok {
|
|
continue
|
|
}
|
|
listMap[itemFileName] = struct{}{}
|
|
listFile = append(listFile, itemFileName)
|
|
}
|
|
if len(listFile) < 2 {
|
|
return listFile, nil
|
|
}
|
|
sort.Slice(listFile, func(i, j int) bool {
|
|
return listFile[i] > listFile[j]
|
|
})
|
|
|
|
return listFile, nil
|
|
}
|