Resolve host IPs through DNS, add purge, French date format by default
- Reverse DNS (cached PTR lookups): IP hosts are stored with their name in 'host' and the IP in 'host_ip'; older IP-only logs are resolved on display and the host filter shows 'name (IP)'. RDNS / DNS_SERVER env. - Settings > Danger zone: delete all logs (type PURGE to confirm) through VictoriaLogs /delete/run_task; -delete.enable added to docker-compose. ALLOW_PURGE env to disable it. - Remove the custom YYYY-MM-DD - HH:MM:SS:mmm format; default is now the usual French display DD/MM/YYYY HH:MM:SS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
85701a9119
commit
7a706eeb82
11 files changed
+455
-21
No files matched your search
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -11,9 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type API struct {
|
||||
store *Store
|
||||
hub *Hub
|
||||
tags *TagStore
|
||||
store *Store
|
||||
hub *Hub
|
||||
tags *TagStore
|
||||
rdns *ReverseDNS
|
||||
allowPurge bool
|
||||
}
|
||||
|
||||
func (a *API) Routes(mux *http.ServeMux) {
|
||||
@@ -28,6 +32,8 @@ func (a *API) Routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /api/tags/reset", a.resetTags)
|
||||
mux.HandleFunc("PUT /api/tags/{id}", a.updateTag)
|
||||
mux.HandleFunc("DELETE /api/tags/{id}", a.deleteTag)
|
||||
mux.HandleFunc("GET /api/purge", a.purgeStatus)
|
||||
mux.HandleFunc("POST /api/purge", a.purge)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
@@ -70,6 +76,7 @@ func (a *API) logs(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusBadGateway, err)
|
||||
return
|
||||
}
|
||||
a.annotateHosts(rows)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"query": q,
|
||||
"rows": rows,
|
||||
@@ -122,9 +129,39 @@ func toInt(v any) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// annotateHosts adds "host_name" to rows whose host is still an IP address
|
||||
// (logs stored before DNS resolution, or resolved too slowly at reception).
|
||||
func (a *API) annotateHosts(rows []map[string]any) {
|
||||
seen := map[string]bool{}
|
||||
var ips []string
|
||||
for _, row := range rows {
|
||||
if h, _ := row["host"].(string); h != "" && !seen[h] && net.ParseIP(h) != nil {
|
||||
seen[h] = true
|
||||
ips = append(ips, h)
|
||||
}
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return
|
||||
}
|
||||
if len(ips) > 100 {
|
||||
ips = ips[:100]
|
||||
}
|
||||
names := a.rdns.LookupMany(ips, time.Second)
|
||||
for _, row := range rows {
|
||||
if h, _ := row["host"].(string); names[h] != "" {
|
||||
row["host_name"] = names[h]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type facet struct {
|
||||
Value string `json:"value"` // stored value, used for filtering
|
||||
Label string `json:"label"` // displayed text ("name (ip)" for resolved IPs)
|
||||
}
|
||||
|
||||
// GET /api/facets: hosts and apps seen over 7 days, for the filter dropdowns.
|
||||
func (a *API) facets(w http.ResponseWriter, r *http.Request) {
|
||||
res := map[string][]string{}
|
||||
res := map[string][]facet{}
|
||||
for _, field := range []string{"host", "app"} {
|
||||
q := "_time:7d | stats by (" + field + ") count() hits | sort by (hits desc) | limit 300"
|
||||
rows, err := a.store.Query(r.Context(), q)
|
||||
@@ -133,13 +170,26 @@ func (a *API) facets(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
vals := make([]string, 0, len(rows))
|
||||
var ips []string
|
||||
for _, row := range rows {
|
||||
if v, _ := row[field].(string); v != "" {
|
||||
vals = append(vals, v)
|
||||
if field == "host" && net.ParseIP(v) != nil {
|
||||
ips = append(ips, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(vals)
|
||||
res[field] = vals
|
||||
names := a.rdns.LookupMany(ips, time.Second)
|
||||
list := make([]facet, 0, len(vals))
|
||||
for _, v := range vals {
|
||||
label := v
|
||||
if n := names[v]; n != "" {
|
||||
label = n + " (" + v + ")"
|
||||
}
|
||||
list = append(list, facet{Value: v, Label: label})
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool { return list[i].Label < list[j].Label })
|
||||
res[field] = list
|
||||
}
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
@@ -266,6 +316,46 @@ func (a *API) deleteTag(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/purge: whether purging is allowed and how many deletions are running.
|
||||
func (a *API) purgeStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res := map[string]any{"allowed": a.allowPurge, "running": 0}
|
||||
if a.allowPurge {
|
||||
n, err := a.store.PurgeRunning(r.Context())
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
var ce *codedError
|
||||
if errors.As(err, &ce) {
|
||||
res["code"] = ce.code
|
||||
}
|
||||
}
|
||||
res["running"] = n
|
||||
}
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
// POST /api/purge {"confirm":"PURGE"}: deletes every stored log.
|
||||
func (a *API) purge(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.allowPurge {
|
||||
writeErr(w, http.StatusForbidden, &codedError{code: "purge_forbidden", msg: "purging is disabled (ALLOW_PURGE=false)"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Confirm string `json:"confirm"`
|
||||
}
|
||||
_ = json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&body)
|
||||
if body.Confirm != "PURGE" {
|
||||
writeErr(w, http.StatusBadRequest, &codedError{code: "purge_confirm", msg: `type "PURGE" to confirm`})
|
||||
return
|
||||
}
|
||||
id, err := a.store.Purge(r.Context())
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadGateway, err)
|
||||
return
|
||||
}
|
||||
log.Printf("purge of all logs requested from %s (task %s)", r.RemoteAddr, id)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"task_id": id})
|
||||
}
|
||||
|
||||
func (a *API) resetTags(w http.ResponseWriter, r *http.Request) {
|
||||
tags, err := a.tags.Reset()
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user