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"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"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 {
|
if err != nil {
|
||||||
http.Error(w, "Failed to fetch agents", http.StatusInternalServerError)
|
http.Error(w, "Failed to fetch agents", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -162,12 +174,31 @@ func getAgentIds(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func logsHandler(w http.ResponseWriter, r *http.Request) {
|
func logsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var logs[] logger.LogEntry
|
// Warning this bit me in the nose: var count is []string, but
|
||||||
logs, err := logger.FetchLogs(10)
|
// 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 {
|
// This enables not only `level` GET parameters but also selecting by paths
|
||||||
// logs[i].Message = strings.ReplaceAll(logLine.Message, `"`, `\"`)
|
// 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 {
|
if err != nil {
|
||||||
http.Error(w, "Error fetching logs", http.StatusInternalServerError)
|
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)
|
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("/agentIds", getAgentIds)
|
||||||
webMux.HandleFunc("/agents/{agentId}", agentsHandler)
|
webMux.HandleFunc("/agents/{agentId}", agentsHandler)
|
||||||
webMux.HandleFunc("/logs", logsHandler)
|
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)
|
db = database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
|
@ -95,21 +95,40 @@ func InsertLog(level LogLevel, message string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchLogs(limit int) ([]LogEntry, error) {
|
func FetchLogs(limit int, levels []string) ([]LogEntry, error) {
|
||||||
lite_dbMutex.Lock()
|
lite_dbMutex.Lock()
|
||||||
defer lite_dbMutex.Unlock()
|
defer lite_dbMutex.Unlock()
|
||||||
|
|
||||||
query := `SELECT timestamp, level, message FROM logs ORDER BY timestamp DESC LIMIT ?`
|
if len(levels) == 0 {
|
||||||
rows, err := Lite_db.Query(query, limit)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error fetching logs: %w", err)
|
return nil, fmt.Errorf("Error fetching logs: %w", err)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
// var logs []string
|
|
||||||
var logs []LogEntry
|
var logs []LogEntry
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
// var message string
|
|
||||||
var logEntry LogEntry
|
var logEntry LogEntry
|
||||||
if err := rows.Scan( &logEntry.Timestamp, &logEntry.Level, &logEntry.Message); err != nil {
|
if err := rows.Scan( &logEntry.Timestamp, &logEntry.Level, &logEntry.Message); err != nil {
|
||||||
return nil, fmt.Errorf("Error scanning row: %w", err)
|
return nil, fmt.Errorf("Error scanning row: %w", err)
|
||||||
|
|
|
@ -90,20 +90,36 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<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 {
|
.log-info {
|
||||||
color: green;
|
color: var(--info-color);
|
||||||
}
|
}
|
||||||
.log-warning {
|
.log-warning {
|
||||||
color: orange;
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
.log-error {
|
.log-error {
|
||||||
color: red;
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
.log-fatal {
|
.log-fatal {
|
||||||
color: blue;
|
color: var(--fatal-color);
|
||||||
}
|
}
|
||||||
.log-debug {
|
.log-debug {
|
||||||
color: violet;
|
color: var(--debug-color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -140,23 +156,32 @@
|
||||||
<pre id="commandOutput"></pre>
|
<pre id="commandOutput"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logs Section -->
|
<!-- Agent Details -->
|
||||||
<h3>Logs</h3>
|
<div class="col" id="agentDetails">
|
||||||
<div id="logs-container" hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML" hx-trigger="every 3s">
|
<h3>Details</h3>
|
||||||
<!-- Logs will be injected here -->
|
<p>Select an agent to view details.</p>
|
||||||
<!-- </div> -->
|
</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 -->
|
<!-- Logs Section -->
|
||||||
<div class="col" id="agentDetails">
|
<h3>Logs</h3>
|
||||||
<h3>Details</h3>
|
|
||||||
<p>Select an agent to view details.</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{range .}}
|
{{range .}}
|
||||||
<p class="log-{{.Level}}">
|
<div class="log-{{.Level}}">
|
||||||
<strong>ts={{.Timestamp}} level={{.Level}}</strong> msg="{{.Message}}"
|
ts={{.Timestamp}} level={{.Level}}msg="{{.Message}}"
|
||||||
</p>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in New Issue