added log level selection via query parameter and paths as well as setting a limit for lines of the query
This commit is contained in:
		
							parent
							
								
									1ce6d2e676
								
							
						
					
					
						commit
						97c77506c8
					
				
							
								
								
									
										52
									
								
								main.go
								
								
								
								
							
							
						
						
									
										52
									
								
								main.go
								
								
								
								
							| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"html/template"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -126,6 +127,17 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.Contains(r.Header.Get("Accept"), "application") {
 | 
			
		||||
		w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
		jsonData, err := json.Marshal(agents)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			http.Error(w, "Failed to encode agents to JSON", http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		w.Write(jsonData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(w, "Failed to fetch agents", http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -162,12 +174,31 @@ func getAgentIds(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	var logs[] logger.LogEntry
 | 
			
		||||
	logs, err := logger.FetchLogs(10)
 | 
			
		||||
	// Warning this bit me in the nose: var count is []string, but
 | 
			
		||||
	// variable = count[0] is casted to int automatically
 | 
			
		||||
	// when the string is a number. Jesus Christ, this is odd behavior!
 | 
			
		||||
	levels := r.URL.Query()["level"]
 | 
			
		||||
	countStr := r.URL.Query()["limit"]
 | 
			
		||||
	var limit int = 10
 | 
			
		||||
	if len(countStr) > 0 {
 | 
			
		||||
		parsedCount, err := strconv.Atoi(countStr[0])
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			limit = parsedCount
 | 
			
		||||
		} else {
 | 
			
		||||
			http.Error(w, "Invalid count value", http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// for i, logLine := range logs {
 | 
			
		||||
	// 	logs[i].Message = strings.ReplaceAll(logLine.Message, `"`, `\"`)
 | 
			
		||||
	// }
 | 
			
		||||
	// This enables not only `level` GET parameters but also selecting by paths
 | 
			
		||||
	// For example /logs/error is now identical to /logs?level=error
 | 
			
		||||
	parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/logs/"), "/")
 | 
			
		||||
	if (len(levels) == 0) && len(parts) > 0 && parts[0] != "" {
 | 
			
		||||
	 	levels = []string{parts[0]}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Call the police... I mean logger
 | 
			
		||||
	logs, err := logger.FetchLogs(limit, levels)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(w, "Error fetching logs", http.StatusInternalServerError)
 | 
			
		||||
| 
						 | 
				
			
			@ -176,16 +207,6 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
	renderTemplate(w, "templates/partials/logs_partial.html", logs)
 | 
			
		||||
 | 
			
		||||
// 	fmt.Fprintf(w, "<div>")
 | 
			
		||||
// 	// for _, logEntry := range logsToSend {
 | 
			
		||||
// 	for _, logEntry := range logs {
 | 
			
		||||
// 		fmt.Fprintf(w, "<p><strong>[%s] [%s]</strong> %s</p>", logEntry.Timestamp, logEntry.Level, logEntry.Message)
 | 
			
		||||
// 	}
 | 
			
		||||
// 	fmt.Fprintf(w, "</div>")
 | 
			
		||||
 | 
			
		||||
	// w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	// json.NewEncoder(w).Encode(logs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -211,6 +232,7 @@ func main() {
 | 
			
		|||
	webMux.HandleFunc("/agentIds", getAgentIds)
 | 
			
		||||
	webMux.HandleFunc("/agents/{agentId}", agentsHandler)
 | 
			
		||||
	webMux.HandleFunc("/logs", logsHandler)
 | 
			
		||||
	webMux.HandleFunc("/logs/{level}", logsHandler)
 | 
			
		||||
 | 
			
		||||
	db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,21 +95,40 @@ func InsertLog(level LogLevel, message string) error {
 | 
			
		|||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FetchLogs(limit int) ([]LogEntry, error) {
 | 
			
		||||
func FetchLogs(limit int, levels []string) ([]LogEntry, error) {
 | 
			
		||||
	lite_dbMutex.Lock()
 | 
			
		||||
	defer lite_dbMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	query := `SELECT timestamp, level, message FROM logs ORDER BY timestamp DESC LIMIT ?`
 | 
			
		||||
	rows, err := Lite_db.Query(query, limit)
 | 
			
		||||
	if len(levels) == 0 {
 | 
			
		||||
		levels = []string{"%"}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var args []interface{}
 | 
			
		||||
	placeholders := make([]string, len(levels))
 | 
			
		||||
 | 
			
		||||
	for i, level := range levels {
 | 
			
		||||
		placeholders[i] = "level LIKE ?"
 | 
			
		||||
		args = append(args, level)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	query := fmt.Sprintf(`
 | 
			
		||||
		SELECT timestamp, level, message
 | 
			
		||||
		FROM logs
 | 
			
		||||
		WHERE %s
 | 
			
		||||
		ORDER BY timestamp DESC
 | 
			
		||||
		LIMIT ?`, strings.Join(placeholders, " OR "))
 | 
			
		||||
 | 
			
		||||
	args = append(args, limit)
 | 
			
		||||
 | 
			
		||||
	// rows, err := Lite_db.Query(query, level, limit)
 | 
			
		||||
	rows, err := Lite_db.Query(query, args...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return  nil, fmt.Errorf("Error fetching logs: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer rows.Close()
 | 
			
		||||
 | 
			
		||||
	// var logs []string
 | 
			
		||||
	var logs []LogEntry
 | 
			
		||||
	for rows.Next() {
 | 
			
		||||
		// var message string
 | 
			
		||||
		var logEntry LogEntry
 | 
			
		||||
		if err := rows.Scan( &logEntry.Timestamp, &logEntry.Level, &logEntry.Message); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("Error scanning row: %w", err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,20 +90,36 @@
 | 
			
		|||
 | 
			
		||||
    </script>
 | 
			
		||||
    <style>
 | 
			
		||||
        :root{
 | 
			
		||||
          --grey-color: #1B2B34;
 | 
			
		||||
          --error-color: #EC5f67;
 | 
			
		||||
          --warning-color: #F99157;
 | 
			
		||||
          --yellow-color: #FAC863;
 | 
			
		||||
          --info-color: #99C794;
 | 
			
		||||
          --teal-color: #5FB3B3;
 | 
			
		||||
          --blue-color: #6699CC;
 | 
			
		||||
          --debug-color: #C594C5;
 | 
			
		||||
          --fatal-color: #AB7967;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .log-info, .log-warning, .log-error, .log-fatal, .log-debug{
 | 
			
		||||
              font-family: "Lucida Console", Monaco, monospace;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .log-info {
 | 
			
		||||
            color: green;
 | 
			
		||||
            color: var(--info-color);
 | 
			
		||||
        }
 | 
			
		||||
        .log-warning {
 | 
			
		||||
            color: orange;
 | 
			
		||||
            color: var(--warning-color);
 | 
			
		||||
        }
 | 
			
		||||
        .log-error {
 | 
			
		||||
            color: red;
 | 
			
		||||
            color: var(--error-color);
 | 
			
		||||
        }
 | 
			
		||||
        .log-fatal {
 | 
			
		||||
            color: blue;
 | 
			
		||||
            color: var(--fatal-color);
 | 
			
		||||
        }
 | 
			
		||||
        .log-debug {
 | 
			
		||||
            color: violet;
 | 
			
		||||
            color: var(--debug-color);
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -140,23 +156,32 @@
 | 
			
		|||
                    <pre id="commandOutput"></pre>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                            <!-- Logs Section -->
 | 
			
		||||
                <h3>Logs</h3>
 | 
			
		||||
                <div id="logs-container" hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML" hx-trigger="every 3s">
 | 
			
		||||
                    <!-- Logs will be injected here -->
 | 
			
		||||
                <!-- </div> -->
 | 
			
		||||
                <!-- <button hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML"> -->
 | 
			
		||||
                <!--     Load Logs -->
 | 
			
		||||
                <!-- </button> -->
 | 
			
		||||
                <!-- <button hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML" hx-trigger="every 8s"> -->
 | 
			
		||||
                <!--     Auto-Refresh Logs -->
 | 
			
		||||
                <!-- </button> -->
 | 
			
		||||
            <!-- </div> -->
 | 
			
		||||
        <!-- Agent Details -->
 | 
			
		||||
        <div class="col" id="agentDetails">
 | 
			
		||||
            <h3>Details</h3>
 | 
			
		||||
            <p>Select an agent to view details.</p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
            <!-- Agent Details -->
 | 
			
		||||
            <div class="col" id="agentDetails">
 | 
			
		||||
                <h3>Details</h3>
 | 
			
		||||
                <p>Select an agent to view details.</p>
 | 
			
		||||
        <!-- Logs Section -->
 | 
			
		||||
        <h3>Logs</h3>
 | 
			
		||||
 | 
			
		||||
            <form id="log-filter-form"
 | 
			
		||||
              hx-get="/logs"
 | 
			
		||||
              hx-target="#logs-container"
 | 
			
		||||
              hx-swap="innerHTML"
 | 
			
		||||
              hx-trigger="change from:.log-filter, every 3s"
 | 
			
		||||
              hx-include="#log-filter-form">
 | 
			
		||||
 | 
			
		||||
              <label><input type="checkbox" class="log-filter" name="level" value="info" checked> Info</label>
 | 
			
		||||
              <label><input type="checkbox" class="log-filter" name="level" value="warning"> Warning</label>
 | 
			
		||||
              <label><input type="checkbox" class="log-filter" name="level" value="error"> Error</label>
 | 
			
		||||
              <label><input type="checkbox" class="log-filter" name="level" value="debug"> Debug</label>
 | 
			
		||||
              <label><input type="checkbox" class="log-filter" name="level" value="fatal"> Fatal</label>
 | 
			
		||||
 | 
			
		||||
            </form>
 | 
			
		||||
 | 
			
		||||
            <div id="logs-container">
 | 
			
		||||
                <!-- Logs will load here -->
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
{{range .}}
 | 
			
		||||
    <p class="log-{{.Level}}">
 | 
			
		||||
        <strong>ts={{.Timestamp}} level={{.Level}}</strong> msg="{{.Message}}"
 | 
			
		||||
    </p>
 | 
			
		||||
    <div class="log-{{.Level}}">
 | 
			
		||||
            ts={{.Timestamp}} level={{.Level}}msg="{{.Message}}"
 | 
			
		||||
    </div>
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue