Implement cross-platform log streaming for frpc instances with support for Windows, systemd, and init.d systems. Includes WebSocket API endpoint for real-time log streaming, token validation, and instance ownership checks. Update README and API documentation to reflect new functionality. The implementation handles: - Platform-specific log collection (Windows Event Log, journalctl, log files) - WebSocket-based real-time streaming - Token validation and instance access control - Log level parsing and formatting - Historical log retrieval since service start
410 lines
9.8 KiB
Go
410 lines
9.8 KiB
Go
package frpLogger
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (s *InstanceLogStreamer) streamWindowsLogs() {
|
|
s.sendInfo(fmt.Sprintf("Starting to stream logs for Windows service: %s", s.serviceName))
|
|
|
|
instance, err := DBQueryFrpcInstanceByID(s.instanceID)
|
|
if err != nil {
|
|
s.sendError(fmt.Sprintf("Failed to get instance info: %v", err))
|
|
return
|
|
}
|
|
|
|
startTime := getWindowsServiceStartTime(s.serviceName)
|
|
|
|
logFilePath := s.getLogFilePathFromConfig(instance.ConfigPath)
|
|
|
|
if logFilePath != "" {
|
|
if !startTime.IsZero() {
|
|
s.streamLogFileSince(logFilePath, startTime)
|
|
} else {
|
|
s.streamLogFile(logFilePath)
|
|
}
|
|
return
|
|
}
|
|
|
|
s.streamWindowsEventLogs()
|
|
}
|
|
|
|
func getWindowsServiceStartTime(serviceName string) time.Time {
|
|
cmd := exec.Command("wmic", "service", "where", fmt.Sprintf("name='%s'", serviceName), "get", "ProcessId", "/value")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
|
|
outputStr := string(output)
|
|
var pid int
|
|
lines := strings.Split(outputStr, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "ProcessId=") {
|
|
pidStr := strings.TrimPrefix(line, "ProcessId=")
|
|
pid, _ = fmt.Sscanf(pidStr, "%d", &pid)
|
|
break
|
|
}
|
|
}
|
|
|
|
if pid <= 0 {
|
|
return time.Time{}
|
|
}
|
|
|
|
cmd = exec.Command("wmic", "process", "where", fmt.Sprintf("ProcessId=%d", pid), "get", "CreationDate", "/value")
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
|
|
outputStr = string(output)
|
|
lines = strings.Split(outputStr, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "CreationDate=") {
|
|
dateStr := strings.TrimPrefix(line, "CreationDate=")
|
|
t, err := time.Parse("20060102150405.999999-0700", dateStr)
|
|
if err == nil {
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
|
|
return time.Time{}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) getLogFilePathFromConfig(configPath string) string {
|
|
file, err := os.Open(configPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if strings.HasPrefix(line, "log_file") || strings.HasPrefix(line, "logFile") || strings.HasPrefix(line, "log.to") {
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) == 2 {
|
|
logPath := strings.TrimSpace(parts[1])
|
|
logPath = strings.Trim(logPath, "\"'")
|
|
if logPath != "" && logPath != "console" {
|
|
return logPath
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamLogFile(logFilePath string) {
|
|
s.streamLogFileSince(logFilePath, time.Time{})
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamLogFileSince(logFilePath string, sinceTime time.Time) {
|
|
s.sendInfo(fmt.Sprintf("Streaming log file: %s", logFilePath))
|
|
|
|
if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
|
|
s.sendError(fmt.Sprintf("Log file not found: %s", logFilePath))
|
|
return
|
|
}
|
|
|
|
file, err := os.Open(logFilePath)
|
|
if err != nil {
|
|
s.sendError(fmt.Sprintf("Failed to open log file: %v", err))
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
if !sinceTime.IsZero() {
|
|
s.sendInfo(fmt.Sprintf("Filtering logs since: %s", sinceTime.Format("2006-01-02 15:04:05")))
|
|
s.streamLogFileFromBeginning(file, sinceTime)
|
|
} else {
|
|
file.Seek(0, io.SeekEnd)
|
|
s.streamLogFileFromEnd(file)
|
|
}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamLogFileFromBeginning(file *os.File, sinceTime time.Time) {
|
|
reader := bufio.NewReader(file)
|
|
|
|
for {
|
|
select {
|
|
case <-s.stopChan:
|
|
s.sendInfo("Log streaming stopped")
|
|
return
|
|
default:
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
s.sendError(fmt.Sprintf("Error reading log file: %v", err))
|
|
return
|
|
}
|
|
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
lineTime := s.parseLogLineTime(line)
|
|
if lineTime.IsZero() || lineTime.After(sinceTime) || lineTime.Equal(sinceTime) {
|
|
s.sendLog(s.parseLogLevel(line), line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamLogFileFromEnd(file *os.File) {
|
|
reader := bufio.NewReader(file)
|
|
|
|
for {
|
|
select {
|
|
case <-s.stopChan:
|
|
s.sendInfo("Log streaming stopped")
|
|
return
|
|
default:
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
s.sendError(fmt.Sprintf("Error reading log file: %v", err))
|
|
return
|
|
}
|
|
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
s.sendLog(s.parseLogLevel(line), line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) parseLogLineTime(line string) time.Time {
|
|
layouts := []string{
|
|
"2006/01/02 15:04:05",
|
|
"2006-01-02 15:04:05",
|
|
"2006-01-02T15:04:05",
|
|
time.RFC3339,
|
|
time.RFC3339Nano,
|
|
}
|
|
|
|
for _, layout := range layouts {
|
|
minLen := len(layout)
|
|
if len(line) >= minLen {
|
|
t, err := time.Parse(layout, strings.TrimSpace(line[:minLen]))
|
|
if err == nil {
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
|
|
return time.Time{}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) parseLogLevel(line string) string {
|
|
lowerLine := strings.ToLower(line)
|
|
if strings.Contains(lowerLine, "error") || strings.Contains(lowerLine, "err") {
|
|
return "ERROR"
|
|
}
|
|
if strings.Contains(lowerLine, "warn") || strings.Contains(lowerLine, "warning") {
|
|
return "WARN"
|
|
}
|
|
if strings.Contains(lowerLine, "debug") {
|
|
return "DEBUG"
|
|
}
|
|
if strings.Contains(lowerLine, "info") {
|
|
return "INFO"
|
|
}
|
|
return "INFO"
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamWindowsEventLogs() {
|
|
s.sendInfo(fmt.Sprintf("Streaming Windows Event Logs for service: %s", s.serviceName))
|
|
|
|
s.sendInfo("Attempting to read from Windows Event Log...")
|
|
|
|
cmd := exec.Command("wevtutil", "qe", "Application", "/q:*[System[Provider[@Name='"+s.serviceName+"']]]", "/c:50", "/rd:true", "/f:text")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
s.sendInfo("No events found in Application log, trying System log...")
|
|
}
|
|
|
|
if len(output) > 0 {
|
|
s.parseAndSendEventLogOutput(string(output))
|
|
}
|
|
|
|
cmd = exec.Command("wevtutil", "qe", "System", "/q:*[System[Provider[@Name='"+s.serviceName+"']]]", "/c:50", "/rd:true", "/f:text")
|
|
output, err = cmd.CombinedOutput()
|
|
if err != nil {
|
|
s.sendInfo("No events found in System log either")
|
|
}
|
|
|
|
if len(output) > 0 {
|
|
s.parseAndSendEventLogOutput(string(output))
|
|
}
|
|
|
|
s.sendInfo("Streaming real-time service status...")
|
|
|
|
s.streamServiceStatus()
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) parseAndSendEventLogOutput(output string) {
|
|
events := strings.Split(output, "Event[")
|
|
for _, event := range events {
|
|
if strings.TrimSpace(event) == "" {
|
|
continue
|
|
}
|
|
|
|
var level, msg string
|
|
lines := strings.Split(event, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "Level:") {
|
|
level = strings.TrimSpace(strings.TrimPrefix(line, "Level:"))
|
|
}
|
|
if strings.HasPrefix(line, "Message:") {
|
|
msg = strings.TrimSpace(strings.TrimPrefix(line, "Message:"))
|
|
}
|
|
}
|
|
|
|
if msg != "" {
|
|
if level == "" {
|
|
level = "INFO"
|
|
}
|
|
s.sendLog(level, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamServiceStatus() {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
lastStatus := ""
|
|
|
|
for {
|
|
select {
|
|
case <-s.stopChan:
|
|
s.sendInfo("Service status monitoring stopped")
|
|
return
|
|
case <-ticker.C:
|
|
status := s.getWindowsServiceStatus()
|
|
if status != lastStatus {
|
|
s.sendInfo(fmt.Sprintf("Service status: %s", status))
|
|
lastStatus = status
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) getWindowsServiceStatus() string {
|
|
cmd := exec.Command("sc", "query", s.serviceName)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Sprintf("Error querying service: %v", err)
|
|
}
|
|
|
|
outputStr := string(output)
|
|
if strings.Contains(outputStr, "RUNNING") {
|
|
return "RUNNING"
|
|
} else if strings.Contains(outputStr, "STOPPED") {
|
|
return "STOPPED"
|
|
} else if strings.Contains(outputStr, "PAUSED") {
|
|
return "PAUSED"
|
|
} else if strings.Contains(outputStr, "START_PENDING") {
|
|
return "START_PENDING"
|
|
} else if strings.Contains(outputStr, "STOP_PENDING") {
|
|
return "STOP_PENDING"
|
|
}
|
|
|
|
return "UNKNOWN"
|
|
}
|
|
|
|
func (s *InstanceLogStreamer) streamWindowsLogsAlternative() {
|
|
s.sendInfo(fmt.Sprintf("Starting alternative log streaming for service: %s", s.serviceName))
|
|
|
|
instance, err := DBQueryFrpcInstanceByID(s.instanceID)
|
|
if err != nil {
|
|
s.sendError(fmt.Sprintf("Failed to get instance info: %v", err))
|
|
return
|
|
}
|
|
|
|
startTime := getWindowsServiceStartTime(s.serviceName)
|
|
|
|
configDir := filepath.Dir(instance.ConfigPath)
|
|
possibleLogPaths := []string{
|
|
filepath.Join(configDir, s.serviceName+".log"),
|
|
filepath.Join(configDir, "frpc.log"),
|
|
filepath.Join("C:\\ProgramData\\superfrpc", s.serviceName+".log"),
|
|
filepath.Join(os.TempDir(), s.serviceName+".log"),
|
|
}
|
|
|
|
for _, logPath := range possibleLogPaths {
|
|
if _, err := os.Stat(logPath); err == nil {
|
|
if !startTime.IsZero() {
|
|
s.streamLogFileSince(logPath, startTime)
|
|
} else {
|
|
s.streamLogFile(logPath)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
s.sendInfo("No log file found, streaming service status instead...")
|
|
s.streamServiceStatus()
|
|
}
|
|
|
|
func getWindowsServiceLogs(serviceName string, lines int) ([]string, error) {
|
|
cmd := exec.Command("wevtutil", "qe", "Application",
|
|
fmt.Sprintf("/q:*[System[Provider[@Name='%s']]]", serviceName),
|
|
fmt.Sprintf("/c:%d", lines),
|
|
"/rd:true",
|
|
"/f:text")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query event log: %v, stderr: %s", err, stderr.String())
|
|
}
|
|
|
|
var logs []string
|
|
output := stdout.String()
|
|
events := strings.Split(output, "Event[")
|
|
|
|
for _, event := range events {
|
|
if strings.TrimSpace(event) == "" {
|
|
continue
|
|
}
|
|
|
|
var msg string
|
|
lines := strings.Split(event, "\n")
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "Message:") {
|
|
msg = strings.TrimSpace(strings.TrimPrefix(line, "Message:"))
|
|
}
|
|
}
|
|
|
|
if msg != "" {
|
|
logs = append(logs, msg)
|
|
}
|
|
}
|
|
|
|
return logs, nil
|
|
}
|