2025-05-15 16:23:59 -04:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
2026-06-04 12:45:39 -04:00
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
"hub.krkn.tech/KrakenTech/crowsnest/internal/easyTime"
|
|
|
|
|
"hub.krkn.tech/KrakenTech/crowsnest/internal/pretty"
|
2025-05-15 16:23:59 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
// Add Subcommand to db command
|
|
|
|
|
rootCmd.AddCommand(logsCmd)
|
|
|
|
|
|
|
|
|
|
logsCmd.Flags().IntVarP(&logLastXLogs, "last", "l", 0, "Number of logs to show")
|
|
|
|
|
logsCmd.Flags().StringVarP(&logStartDate, "start", "s", "", "Start date for logs")
|
|
|
|
|
logsCmd.Flags().StringVarP(&logEndDate, "end", "e", "", "End date for logs")
|
|
|
|
|
logsCmd.Flags().StringVarP(&logSeverity, "severity", "v", "", "Comma delimited Log severity to show (info, error, fatal)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LogEntry represents a parsed log entry
|
|
|
|
|
type LogEntry struct {
|
|
|
|
|
Level string `json:"level"`
|
|
|
|
|
Timestamp string `json:"ts"` // Store as string initially
|
|
|
|
|
Message string `json:"msg"`
|
|
|
|
|
Details map[string]interface{} `json:"-"`
|
|
|
|
|
ParsedTime time.Time // Parsed time for sorting and filtering
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
logLastXLogs int
|
|
|
|
|
logStartDate string
|
|
|
|
|
logEndDate string
|
|
|
|
|
logSeverity string
|
|
|
|
|
|
|
|
|
|
logsCmd = &cobra.Command{
|
|
|
|
|
Use: "logs",
|
|
|
|
|
Short: "View logs",
|
|
|
|
|
Long: `View logs for the Dehasher CLI.`,
|
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
logsPath := filepath.Join(os.Getenv("HOME"), ".local", "share", "Dehasher", "logs")
|
|
|
|
|
|
|
|
|
|
if logSeverity == "" {
|
|
|
|
|
logSeverity = "info,error,fatal"
|
|
|
|
|
} else {
|
|
|
|
|
logSeverity = strings.ToLower(logSeverity)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
logInfo bool
|
|
|
|
|
logError bool
|
|
|
|
|
logFatal bool
|
|
|
|
|
)
|
|
|
|
|
if strings.Contains(logSeverity, "info") {
|
|
|
|
|
logInfo = true
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(logSeverity, "error") {
|
|
|
|
|
logError = true
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(logSeverity, "fatal") {
|
|
|
|
|
logFatal = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var allLogs []string
|
|
|
|
|
if logSeverity == "info" {
|
|
|
|
|
allLogs = append(allLogs, filepath.Join(logsPath, "info.log"))
|
|
|
|
|
} else if logSeverity == "error" || logSeverity == "fatal" {
|
|
|
|
|
allLogs = append(allLogs, filepath.Join(logsPath, "error.log"))
|
|
|
|
|
} else {
|
|
|
|
|
allLogs = append(allLogs, filepath.Join(logsPath, "info.log"), filepath.Join(logsPath, "error.log"))
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 20:21:36 -04:00
|
|
|
var timeChunk easyTime.TimeChunk
|
|
|
|
|
if logStartDate != "" {
|
|
|
|
|
timeChunk = easyTime.NewTimeChunk(logStartDate, logEndDate, debugGlobal)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-15 16:23:59 -04:00
|
|
|
var parsedLogs []LogEntry
|
|
|
|
|
for _, logFile := range allLogs {
|
|
|
|
|
// Read the log file
|
|
|
|
|
logData, err := os.ReadFile(logFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Error reading log file %s: %v\n", logFile, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Split the file into lines
|
|
|
|
|
logLines := strings.Split(strings.TrimSpace(string(logData)), "\n")
|
|
|
|
|
for _, line := range logLines {
|
|
|
|
|
if line == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the JSON log entry
|
|
|
|
|
var entry LogEntry
|
|
|
|
|
var rawEntry map[string]interface{}
|
|
|
|
|
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
|
|
|
|
fmt.Printf("Error parsing log entry: %v\n", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 20:21:36 -04:00
|
|
|
// Unmarshal to get additional fields
|
2025-05-15 16:23:59 -04:00
|
|
|
if err := json.Unmarshal([]byte(line), &rawEntry); err != nil {
|
|
|
|
|
fmt.Printf("Error parsing raw log entry: %v\n", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the timestamp
|
|
|
|
|
parsedTime, err := time.Parse("2006-01-02T15:04:05.999-0700", entry.Timestamp)
|
|
|
|
|
if err != nil {
|
2025-05-16 20:21:36 -04:00
|
|
|
// Try RFC3339
|
2025-05-15 16:23:59 -04:00
|
|
|
parsedTime, err = time.Parse(time.RFC3339, entry.Timestamp)
|
|
|
|
|
if err != nil {
|
2025-05-16 20:21:36 -04:00
|
|
|
// Try RFC3339Nano
|
2025-05-15 16:23:59 -04:00
|
|
|
parsedTime, err = time.Parse(time.RFC3339Nano, entry.Timestamp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("Error parsing timestamp '%s': %v\n", entry.Timestamp, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
entry.ParsedTime = parsedTime
|
|
|
|
|
|
|
|
|
|
// Store additional fields in Details
|
|
|
|
|
entry.Details = make(map[string]interface{})
|
|
|
|
|
for k, v := range rawEntry {
|
|
|
|
|
if k != "level" && k != "ts" && k != "msg" {
|
|
|
|
|
entry.Details[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter by severity
|
|
|
|
|
if (logInfo && strings.EqualFold(entry.Level, "INFO")) ||
|
|
|
|
|
(logError && strings.EqualFold(entry.Level, "ERROR")) ||
|
|
|
|
|
(logFatal && strings.EqualFold(entry.Level, "FATAL")) {
|
|
|
|
|
|
|
|
|
|
// Filter by date range if specified
|
2025-05-16 20:21:36 -04:00
|
|
|
if timeChunk.IsSet() {
|
|
|
|
|
if entry.ParsedTime.Before(timeChunk.StartTime) || entry.ParsedTime.After(timeChunk.EndTime) {
|
2025-05-15 16:23:59 -04:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parsedLogs = append(parsedLogs, entry)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Limit the number of logs if specified
|
|
|
|
|
if logLastXLogs > 0 && len(parsedLogs) > logLastXLogs {
|
|
|
|
|
// Sort logs by timestamp (newest first)
|
|
|
|
|
// This is a simple bubble sort for demonstration
|
|
|
|
|
for i := 0; i < len(parsedLogs)-1; i++ {
|
|
|
|
|
for j := 0; j < len(parsedLogs)-i-1; j++ {
|
|
|
|
|
if parsedLogs[j].ParsedTime.Before(parsedLogs[j+1].ParsedTime) {
|
|
|
|
|
parsedLogs[j], parsedLogs[j+1] = parsedLogs[j+1], parsedLogs[j]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
parsedLogs = parsedLogs[:logLastXLogs]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Display logs in a table
|
|
|
|
|
if len(parsedLogs) > 0 {
|
|
|
|
|
// Prepare table headers and rows
|
|
|
|
|
headers := []string{"Date", "Severity", "Message", "Details"}
|
|
|
|
|
rows := make([][]string, len(parsedLogs))
|
|
|
|
|
|
|
|
|
|
for i, log := range parsedLogs {
|
|
|
|
|
// Format timestamp
|
|
|
|
|
timeStr := log.ParsedTime.Format("2006-01-02 15:04:05")
|
|
|
|
|
|
|
|
|
|
// Format details
|
|
|
|
|
detailsStr := ""
|
|
|
|
|
for k, v := range log.Details {
|
|
|
|
|
detailsStr += fmt.Sprintf("%s: %v, ", k, v)
|
|
|
|
|
}
|
|
|
|
|
if len(detailsStr) > 2 {
|
|
|
|
|
detailsStr = detailsStr[:len(detailsStr)-2] // Remove trailing comma and space
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows[i] = []string{timeStr, log.Level, log.Message, detailsStr}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Display the table
|
|
|
|
|
pretty.Table(headers, rows)
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println("No logs found matching the specified criteria.")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Severity int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
INFO Severity = iota
|
|
|
|
|
ERROR
|
|
|
|
|
FATAL
|
|
|
|
|
UNKNOWN Severity = -1
|
|
|
|
|
)
|