|
1 | 1 | package vlstorage |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "encoding/json" |
5 | 6 | "flag" |
6 | 7 | "fmt" |
7 | 8 | "io" |
8 | 9 | "math" |
9 | 10 | "net/http" |
| 11 | + "strconv" |
10 | 12 | "time" |
11 | 13 |
|
12 | 14 | "github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup" |
|
57 | 59 | partitionManageAuthKey = flagutil.NewPassword("partitionManageAuthKey", "authKey, which must be passed in query string to /internal/partition/* . It overrides -httpAuth.* . "+ |
58 | 60 | "See https://docs.victoriametrics.com/victorialogs/#partitions-lifecycle") |
59 | 61 |
|
| 62 | + deleteAuthKey = flagutil.NewPassword("deleteAuthKey", "authKey, which must be passed in query string to /delete and /internal/delete . It overrides -httpAuth.* . "+ |
| 63 | + "See https://docs.victoriametrics.com/victorialogs/#delete-log-rows") |
| 64 | + |
60 | 65 | storageNodeAddrs = flagutil.NewArrayString("storageNode", "Comma-separated list of TCP addresses for storage nodes to route the ingested logs to and to send select queries to. "+ |
61 | 66 | "If the list is empty, then the ingested logs are stored and queried locally from -storageDataPath") |
62 | 67 | insertConcurrency = flag.Int("insert.concurrency", 2, "The average number of concurrent data ingestion requests, which can be sent to every -storageNode") |
@@ -94,6 +99,11 @@ var netstorageInsert *netinsert.Storage |
94 | 99 |
|
95 | 100 | var netstorageSelect *netselect.Storage |
96 | 101 |
|
| 102 | +// CheckDeleteAuth validates auth for delete endpoints. |
| 103 | +func CheckDeleteAuth(w http.ResponseWriter, r *http.Request) bool { |
| 104 | + return httpserver.CheckAuthFlag(w, r, deleteAuthKey) |
| 105 | +} |
| 106 | + |
97 | 107 | // Init initializes vlstorage. |
98 | 108 | // |
99 | 109 | // Stop must be called when vlstorage is no longer needed |
@@ -166,7 +176,7 @@ func initNetworkStorage() { |
166 | 176 | netstorageInsert = netinsert.NewStorage(*storageNodeAddrs, authCfgs, isTLSs, *insertConcurrency, *insertDisableCompression) |
167 | 177 |
|
168 | 178 | logger.Infof("initializing select service for nodes %s", *storageNodeAddrs) |
169 | | - netstorageSelect = netselect.NewStorage(*storageNodeAddrs, authCfgs, isTLSs, *selectDisableCompression) |
| 179 | + netstorageSelect = netselect.NewStorage(*storageNodeAddrs, authCfgs, isTLSs, *selectDisableCompression, deleteAuthKey.Get()) |
170 | 180 |
|
171 | 181 | logger.Infof("initialized all the network services") |
172 | 182 | } |
@@ -246,6 +256,8 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool { |
246 | 256 | return processPartitionSnapshotCreate(w, r) |
247 | 257 | case "/internal/partition/snapshot/list": |
248 | 258 | return processPartitionSnapshotList(w, r) |
| 259 | + case "/delete": |
| 260 | + return processDelete(r.Context(), w, r) |
249 | 261 | } |
250 | 262 | return false |
251 | 263 | } |
@@ -397,6 +409,83 @@ func writeJSONResponse(w http.ResponseWriter, response any) { |
397 | 409 | w.Write(responseBody) |
398 | 410 | } |
399 | 411 |
|
| 412 | +// processDelete handles the /delete endpoint for both local and remote storage nodes. |
| 413 | +// It supports POST for scheduling deletes and GET for retrieving task status. |
| 414 | +func processDelete(ctx context.Context, w http.ResponseWriter, r *http.Request) bool { |
| 415 | + if localStorage != nil { |
| 416 | + if !CheckDeleteAuth(w, r) { |
| 417 | + return true |
| 418 | + } |
| 419 | + } |
| 420 | + |
| 421 | + switch r.Method { |
| 422 | + case http.MethodPost: |
| 423 | + if tenantIDsStr := r.FormValue("tenant_ids"); tenantIDsStr != "" { |
| 424 | + tenantIDs, err := logstorage.UnmarshalTenantIDs([]byte(tenantIDsStr)) |
| 425 | + if err != nil { |
| 426 | + httpserver.Errorf(w, r, "cannot unmarshal tenant_ids=%q: %s", tenantIDsStr, err) |
| 427 | + return true |
| 428 | + } |
| 429 | + |
| 430 | + timestampStr := r.FormValue("timestamp") |
| 431 | + timestamp, err := strconv.ParseInt(timestampStr, 10, 64) |
| 432 | + if err != nil { |
| 433 | + httpserver.Errorf(w, r, "cannot parse timestamp=%q: %s", timestampStr, err) |
| 434 | + return true |
| 435 | + } |
| 436 | + |
| 437 | + qStr := r.FormValue("query") |
| 438 | + q, err := logstorage.ParseQueryAtTimestamp(qStr, timestamp) |
| 439 | + if err != nil { |
| 440 | + httpserver.Errorf(w, r, "cannot parse query=%q: %s", qStr, err) |
| 441 | + return true |
| 442 | + } |
| 443 | + |
| 444 | + if err := DeleteRows(ctx, tenantIDs, q); err != nil { |
| 445 | + httpserver.Errorf(w, r, "cannot delete rows: %s", err) |
| 446 | + return true |
| 447 | + } |
| 448 | + } else { |
| 449 | + tenantID, err := logstorage.GetTenantIDFromRequest(r) |
| 450 | + if err != nil { |
| 451 | + httpserver.Errorf(w, r, "cannot obtain tenantID: %s", err) |
| 452 | + return true |
| 453 | + } |
| 454 | + |
| 455 | + qStr := r.FormValue("query") |
| 456 | + q, err := logstorage.ParseQuery(qStr) |
| 457 | + if err != nil { |
| 458 | + httpserver.Errorf(w, r, "cannot parse query [%s]: %s", qStr, err) |
| 459 | + return true |
| 460 | + } |
| 461 | + |
| 462 | + if err := DeleteRows(ctx, []logstorage.TenantID{tenantID}, q); err != nil { |
| 463 | + httpserver.Errorf(w, r, "%s", err) |
| 464 | + return true |
| 465 | + } |
| 466 | + } |
| 467 | + |
| 468 | + w.Header().Set("Content-Type", "text/plain") |
| 469 | + w.WriteHeader(http.StatusOK) |
| 470 | + |
| 471 | + case http.MethodGet: |
| 472 | + tasks, err := ListDeleteTasks(ctx) |
| 473 | + if err != nil { |
| 474 | + httpserver.Errorf(w, r, "cannot list delete tasks: %s", err) |
| 475 | + return true |
| 476 | + } |
| 477 | + |
| 478 | + w.Header().Set("Content-Type", "application/json") |
| 479 | + if err := json.NewEncoder(w).Encode(tasks); err != nil { |
| 480 | + httpserver.Errorf(w, r, "internal error: %s", err) |
| 481 | + } |
| 482 | + |
| 483 | + default: |
| 484 | + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
| 485 | + } |
| 486 | + return true |
| 487 | +} |
| 488 | + |
400 | 489 | // Storage implements insertutil.LogRowsStorage interface |
401 | 490 | type Storage struct{} |
402 | 491 |
|
@@ -500,6 +589,41 @@ func GetStreamIDs(qctx *logstorage.QueryContext, limit uint64) ([]logstorage.Val |
500 | 589 | return netstorageSelect.GetStreamIDs(qctx, limit) |
501 | 590 | } |
502 | 591 |
|
| 592 | +// DeleteRows marks rows matching q with the Deleted marker (full or partial) and flushes markers to disk immediately. |
| 593 | +func DeleteRows(ctx context.Context, tenantIDs []logstorage.TenantID, q *logstorage.Query) error { |
| 594 | + if err := logstorage.ValidateDeleteQuery(q); err != nil { |
| 595 | + return fmt.Errorf("validate query: %w", err) |
| 596 | + } |
| 597 | + |
| 598 | + if localStorage != nil { |
| 599 | + return localStorage.DeleteRows(ctx, tenantIDs, q) |
| 600 | + } |
| 601 | + |
| 602 | + return netstorageSelect.DeleteRows(ctx, tenantIDs, q) |
| 603 | +} |
| 604 | + |
| 605 | +// IsLocalStorage confirms whether the running instance is a storage node. |
| 606 | +func IsLocalStorage() bool { |
| 607 | + return localStorage != nil |
| 608 | +} |
| 609 | + |
| 610 | +// ListDeleteTasks collects delete task information either from the local storage or from all configured storage nodes. |
| 611 | +// It returns a slice with the tasks and an extra Storage field indicating the source node address (or "local" for the embedded storage). |
| 612 | +func ListDeleteTasks(ctx context.Context) ([]logstorage.DeleteTaskInfoWithSource, error) { |
| 613 | + if localStorage != nil { |
| 614 | + tasks := localStorage.ListDeleteTasks() |
| 615 | + out := make([]logstorage.DeleteTaskInfoWithSource, len(tasks)) |
| 616 | + for i, t := range tasks { |
| 617 | + out[i] = logstorage.DeleteTaskInfoWithSource{ |
| 618 | + DeleteTaskInfo: t, |
| 619 | + } |
| 620 | + } |
| 621 | + return out, nil |
| 622 | + } |
| 623 | + |
| 624 | + return netstorageSelect.ListDeleteTasks(ctx) |
| 625 | +} |
| 626 | + |
503 | 627 | func writeStorageMetrics(w io.Writer, strg *logstorage.Storage) { |
504 | 628 | var ss logstorage.StorageStats |
505 | 629 | strg.UpdateStats(&ss) |
|
0 commit comments