Skip to content

Commit 7c39d5a

Browse files
committed
update
1 parent 14c4618 commit 7c39d5a

31 files changed

+3034
-54
lines changed

app/victoria-logs/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
8484
fmt.Fprintf(w, "Useful endpoints:</br>")
8585
httpserver.WriteAPIHelp(w, [][2]string{
8686
{"select/vmui", "Web UI for VictoriaLogs"},
87+
{"select/async_tasks", "async tasks"},
8788
{"metrics", "available service metrics"},
8889
{"flags", "command-line flags"},
8990
})

app/vlselect/async_tasks.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package vlselect
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"html/template"
7+
"net/http"
8+
"sort"
9+
"time"
10+
11+
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httpserver"
12+
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
13+
14+
"github.com/VictoriaMetrics/VictoriaLogs/app/vlstorage"
15+
)
16+
17+
var asyncTasksTmpl = template.Must(template.New("asyncTasks").Parse(`
18+
<!DOCTYPE html>
19+
<html lang="en">
20+
<head>
21+
<meta http-equiv="refresh" content="5">
22+
<meta charset="utf-8">
23+
<title>VictoriaLogs — Async tasks (aggregated)</title>
24+
<style>
25+
:root {
26+
--bg: #f9f9f9;
27+
--border: #d0d0d0;
28+
--header-bg: #fafafa;
29+
--row-alt-bg: #ffffff;
30+
--row-hover: #eef2ff;
31+
--font: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
32+
--font-size: 14px;
33+
}
34+
html,body { margin: 0; padding: 0; font-family: var(--font); background: var(--bg); font-size: var(--font-size); }
35+
main { padding: 16px; max-width: 1400px; }
36+
h2 { margin: 0 0 12px; font-weight: 600; }
37+
.table-container { overflow-x: auto; background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border-radius: 8px; }
38+
table { width: 100%; border-collapse: collapse; }
39+
th,td { padding: 8px 12px; border: 1px solid var(--border); text-align: left; vertical-align: top; word-wrap: break-word; }
40+
thead th { background: var(--header-bg); position: sticky; top: 0; z-index: 1; font-weight: 600; }
41+
tbody tr:nth-child(odd) { background: var(--row-alt-bg); }
42+
tbody tr:hover { background: var(--row-hover); }
43+
.col-storage { width: 200px; max-width: 200px; }
44+
.col-created { width: 180px; max-width: 180px; }
45+
.col-type { width: 120px; max-width: 120px; }
46+
.col-status { width: 100px; max-width: 100px; }
47+
.col-tenant { width: 180px; max-width: 180px; }
48+
.col-done { width: 180px; max-width: 180px; }
49+
.col-result { width: 150px; max-width: 150px; }
50+
.col-payload { width: 300px; max-width: 300px; }
51+
.status { font-weight: 600; padding: 3px 8px; border-radius: 4px; display:inline-block; text-transform: capitalize; font-size: 12px; }
52+
.status.pending { background:#fff4cc; color:#856404; }
53+
.status.success { background:#d3f9d8; color:#14532d; }
54+
.status.error { background:#f8d7da; color:#842029; }
55+
.payload-cell { max-height: 100px; overflow-y: auto; }
56+
.payload-cell pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-size: 12px; }
57+
.truncated { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58+
</style>
59+
</head>
60+
<body>
61+
<main>
62+
<h2>Async tasks</h2>
63+
64+
{{- if .Tasks }}
65+
<div class="table-container">
66+
<table>
67+
<thead>
68+
<tr>
69+
{{- if .ShowStorage }}
70+
<th class="col-storage">Storage</th>
71+
{{- end }}
72+
<th class="col-created">Created</th>
73+
<th class="col-type">Type</th>
74+
<th class="col-status">Status</th>
75+
{{- if .ShowTenant }}
76+
<th class="col-tenant">Tenant</th>
77+
{{- end }}
78+
<th class="col-done">Done</th>
79+
<th class="col-result">Result</th>
80+
<th class="col-payload">Payload</th>
81+
</tr>
82+
</thead>
83+
<tbody>
84+
{{- range .Tasks }}
85+
<tr>
86+
{{- if $.ShowStorage }}
87+
<td class="col-storage truncated" title="{{ .Storage }}">{{ .Storage }}</td>
88+
{{- end }}
89+
<td>{{ .Created }}</td>
90+
<td>{{ .Type }}</td>
91+
<td><span class="status {{ .Status }}">{{ .Status }}</span></td>
92+
{{- if $.ShowTenant }}
93+
<td class="col-tenant truncated" title="{{ .Tenant }}">{{ html .Tenant }}</td>
94+
{{- end }}
95+
<td>{{ .Done }}</td>
96+
<td class="col-result truncated" title="{{ .Result }}">{{ html .Result }}</td>
97+
<td class="col-payload payload-cell"><pre>{{ html .PayloadJSON }}</pre></td>
98+
</tr>
99+
{{- end }}
100+
</tbody>
101+
</table>
102+
</div>
103+
{{- else }}
104+
<p>No async tasks found.</p>
105+
{{- end }}
106+
</main>
107+
</body>
108+
</html>
109+
`))
110+
111+
func processAsyncTasksRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
112+
tasks, err := vlstorage.ListAsyncTasks(ctx)
113+
if err != nil {
114+
httpserver.Errorf(w, r, "%s", err)
115+
return
116+
}
117+
118+
// JSON output support
119+
if format := r.FormValue("format"); format == "json" || r.Header.Get("Accept") == "application/json" {
120+
w.Header().Set("Content-Type", "application/json")
121+
if err := json.NewEncoder(w).Encode(tasks); err != nil {
122+
httpserver.Errorf(w, r, "cannot encode response: %s", err)
123+
}
124+
return
125+
}
126+
127+
// Sort tasks by Created time descending for better UX in HTML view
128+
sort.Slice(tasks, func(i, j int) bool {
129+
// Sort by CreatedTime desc, then by Storage desc
130+
if tasks[i].CreatedTime != tasks[j].CreatedTime {
131+
return tasks[i].CreatedTime > tasks[j].CreatedTime
132+
}
133+
return tasks[i].Storage > tasks[j].Storage
134+
})
135+
136+
// Build view model for template.
137+
type row struct {
138+
Storage string
139+
Type string
140+
Status string
141+
Tenant string
142+
PayloadJSON string
143+
Created string
144+
Done string
145+
Result string
146+
}
147+
148+
// Check if we should show tenant column (if any tenant is not the default)
149+
showTenant := false
150+
for _, t := range tasks {
151+
if t.Tenant != "{accountID=0,projectID=0}" && t.Tenant != "*" {
152+
showTenant = true
153+
break
154+
}
155+
}
156+
157+
vm := struct {
158+
Tasks []row
159+
ShowTenant bool
160+
ShowStorage bool
161+
}{
162+
ShowTenant: showTenant,
163+
ShowStorage: !vlstorage.IsLocalStorage(),
164+
}
165+
166+
for _, t := range tasks {
167+
payloadJSON, _ := json.Marshal(t.Payload)
168+
169+
createdStr := time.Unix(0, t.CreatedTime).Format(time.RFC3339)
170+
done := "-"
171+
if t.DoneTime > 0 {
172+
done = time.Unix(0, t.DoneTime).Format(time.RFC3339)
173+
}
174+
175+
result := t.Error
176+
if t.Status == "success" {
177+
result = "OK"
178+
}
179+
180+
vm.Tasks = append(vm.Tasks, row{
181+
Storage: t.Storage,
182+
Type: string(t.Type),
183+
Status: string(t.Status),
184+
Tenant: t.Tenant,
185+
PayloadJSON: string(payloadJSON),
186+
Created: createdStr,
187+
Done: done,
188+
Result: result,
189+
})
190+
}
191+
192+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
193+
if err := asyncTasksTmpl.Execute(w, vm); err != nil {
194+
logger.Errorf("cannot execute async tasks template: %s", err)
195+
httpserver.Errorf(w, r, "internal error: %s", err)
196+
}
197+
}

app/vlselect/main.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717

1818
"github.com/VictoriaMetrics/VictoriaLogs/app/vlselect/internalselect"
1919
"github.com/VictoriaMetrics/VictoriaLogs/app/vlselect/logsql"
20+
"github.com/VictoriaMetrics/VictoriaLogs/app/vlstorage"
21+
"github.com/VictoriaMetrics/VictoriaLogs/lib/logstorage"
2022
)
2123

2224
var (
@@ -101,6 +103,11 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
101103
func selectHandler(w http.ResponseWriter, r *http.Request, path string) bool {
102104
ctx := r.Context()
103105

106+
if path == "/select/async_tasks" {
107+
processAsyncTasksRequest(r.Context(), w, r)
108+
return true
109+
}
110+
104111
if path == "/select/vmui" {
105112
// VMUI access via incomplete url without `/` in the end. Redirect to complete url.
106113
// Use relative redirect, since the hostname and path prefix may be incorrect if VictoriaMetrics
@@ -122,6 +129,11 @@ func selectHandler(w http.ResponseWriter, r *http.Request, path string) bool {
122129
return true
123130
}
124131

132+
if path == "/select/delete" {
133+
processDeleteSelectRequest(ctx, w, r)
134+
return true
135+
}
136+
125137
if path == "/select/logsql/tail" {
126138
logsqlTailRequests.Inc()
127139
// Process live tailing request without timeout, since it is OK to run live tailing requests for very long time.
@@ -209,6 +221,30 @@ func decRequestConcurrency() {
209221
<-concurrencyLimitCh
210222
}
211223

224+
// processDeleteSelectRequest handles "/select/delete" endpoint by proxying to internal delete.
225+
func processDeleteSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) {
226+
tenantID, err := logstorage.GetTenantIDFromRequest(r)
227+
if err != nil {
228+
httpserver.Errorf(w, r, "cannot obtain tenantID: %s", err)
229+
return
230+
}
231+
232+
qStr := r.FormValue("query")
233+
q, err := logstorage.ParseQuery(qStr)
234+
if err != nil {
235+
httpserver.Errorf(w, r, "cannot parse query [%s]: %s", qStr, err)
236+
return
237+
}
238+
239+
if err := vlstorage.DeleteRows(ctx, []logstorage.TenantID{tenantID}, q); err != nil {
240+
httpserver.Errorf(w, r, "%s", err)
241+
return
242+
}
243+
244+
w.Header().Set("Content-Type", "text/plain")
245+
_, _ = w.Write([]byte("ok"))
246+
}
247+
212248
func processSelectRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, path string) bool {
213249
httpserver.EnableCORS(w, r)
214250
startTime := time.Now()

0 commit comments

Comments
 (0)