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