api: Create /status/tsdb/blocks endpoint.

this endpoint serves blocks data to the client.

Signed-off-by: sujal shah <sujalshah28092004@gmail.com>
This commit is contained in:
sujal shah 2025-06-05 23:22:08 +05:30 committed by Sujal Shah
parent 74aca682b7
commit 4408a6bcaf
6 changed files with 151 additions and 2 deletions

View File

@ -1755,6 +1755,21 @@ func (s *readyStorage) CleanTombstones() error {
return tsdb.ErrNotReady
}
// BlockMetas implements the api_v1.TSDBAdminStats and api_v2.TSDBAdmin interfaces.
func (s *readyStorage) BlockMetas() ([]tsdb.BlockMeta, error) {
if x := s.get(); x != nil {
switch db := x.(type) {
case *tsdb.DB:
return db.BlockMetas(), nil
case *agent.DB:
return nil, agent.ErrUnsupported
default:
panic(fmt.Sprintf("unknown storage type %T", db))
}
}
return nil, tsdb.ErrNotReady
}
// Delete implements the api_v1.TSDBAdminStats and api_v2.TSDBAdmin interfaces.
func (s *readyStorage) Delete(ctx context.Context, mint, maxt int64, ms ...*labels.Matcher) error {
if x := s.get(); x != nil {

View File

@ -1355,6 +1355,64 @@ curl http://localhost:9090/api/v1/status/tsdb
}
```
*New in v3.6.0*
### TSDB Blocks
**NOTE**: This endpoint is **experimental** and might change in the future. The endpoint name and the exact format of the returned data may change between Prometheus versions. The **exact metadata returned** by this endpoint is an implementation detail and may change in future Prometheus versions.
The following endpoint returns the list of currently loaded TSDB blocks and their metadata.
```
GET /api/v1/status/tsdb/blocks
```
This endpoint returns the following information for each block:
- `ulid`: Unique ID of the block.
- `minTime`: Minimum timestamp (in milliseconds) of the block.
- `maxTime`: Maximum timestamp (in milliseconds) of the block.
- `stats`:
- `numSeries`: Number of series in the block.
- `numSamples`: Number of samples in the block.
- `numChunks`: Number of chunks in the block.
- `compaction`:
- `level`: The compaction level of the block.
- `sources`: List of ULIDs of source blocks used to compact this block.
- `version`: The block version.
```bash
curl http://localhost:9090/api/v1/status/tsdb/blocks
```
```json
{
"status": "success",
"data": {
"blocks": [
{
"ulid": "01JZ8JKZY6XSK3PTDP9ZKRWT60",
"minTime": 1750860620060,
"maxTime": 1750867200000,
"stats": {
"numSamples": 13701,
"numSeries": 716,
"numChunks": 716
},
"compaction": {
"level": 1,
"sources": [
"01JZ8JKZY6XSK3PTDP9ZKRWT60"
]
},
"version": 1
}
]
}
}
```
*New in v2.15*
### WAL Replay Stats

View File

@ -1074,6 +1074,16 @@ func (db *DB) Dir() string {
return db.dir
}
// BlockMetas returns the list of metadata for all blocks.
func (db *DB) BlockMetas() []BlockMeta {
blocks := db.Blocks()
metas := make([]BlockMeta, 0, len(blocks))
for _, b := range blocks {
metas = append(metas, b.Meta())
}
return metas
}
func (db *DB) run(ctx context.Context) {
defer close(db.donec)

View File

@ -186,6 +186,7 @@ type TSDBAdminStats interface {
Snapshot(dir string, withHead bool) error
Stats(statsByLabelName string, limit int) (*tsdb.Stats, error)
WALReplayStatus() (tsdb.WALReplayStatus, error)
BlockMetas() ([]tsdb.BlockMeta, error)
}
type QueryOpts interface {
@ -404,6 +405,7 @@ func (api *API) Register(r *route.Router) {
r.Get("/status/buildinfo", wrap(api.serveBuildInfo))
r.Get("/status/flags", wrap(api.serveFlags))
r.Get("/status/tsdb", wrapAgent(api.serveTSDBStatus))
r.Get("/status/tsdb/blocks", wrapAgent(api.serveTSDBBlocks))
r.Get("/status/walreplay", api.serveWALReplayStatus)
r.Get("/notifications", api.notifications)
r.Get("/notifications/live", api.notificationsSSE)
@ -1740,6 +1742,19 @@ func TSDBStatsFromIndexStats(stats []index.Stat) []TSDBStat {
return result
}
func (api *API) serveTSDBBlocks(_ *http.Request) apiFuncResult {
blockMetas, err := api.db.BlockMetas()
if err != nil {
return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("error getting block metadata: %w", err)}, nil, nil}
}
return apiFuncResult{
data: map[string][]tsdb.BlockMeta{
"blocks": blockMetas,
},
}
}
func (api *API) serveTSDBStatus(r *http.Request) apiFuncResult {
limit := 10
if s := r.FormValue("limit"); s != "" {

View File

@ -15,6 +15,7 @@ package v1
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@ -31,6 +32,7 @@ import (
"time"
jsoniter "github.com/json-iterator/go"
"github.com/oklog/ulid/v2"
"github.com/prometheus/client_golang/prometheus"
config_util "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
@ -3793,10 +3795,15 @@ func assertAPIResponseMetadataLen(t *testing.T, got interface{}, expLen int) {
}
type fakeDB struct {
err error
err error
blockMetas []tsdb.BlockMeta
}
func (f *fakeDB) CleanTombstones() error { return f.err }
func (f *fakeDB) CleanTombstones() error { return f.err }
func (f *fakeDB) BlockMetas() ([]tsdb.BlockMeta, error) {
return f.blockMetas, nil
}
func (f *fakeDB) Delete(context.Context, int64, int64, ...*labels.Matcher) error { return f.err }
func (f *fakeDB) Snapshot(string, bool) error { return f.err }
func (f *fakeDB) Stats(statsByLabelName string, limit int) (_ *tsdb.Stats, retErr error) {
@ -4122,6 +4129,45 @@ func TestRespondSuccess_DefaultCodecCannotEncodeResponse(t *testing.T) {
require.JSONEq(t, `{"status":"error","errorType":"not_acceptable","error":"cannot encode response as application/default-format"}`, string(body))
}
func TestServeTSDBBlocks(t *testing.T) {
blockMeta := tsdb.BlockMeta{
ULID: ulid.MustNew(ulid.Now(), nil),
MinTime: 0,
MaxTime: 1000,
Stats: tsdb.BlockStats{
NumSeries: 10,
},
}
db := &fakeDB{
blockMetas: []tsdb.BlockMeta{blockMeta},
}
api := &API{
db: db,
}
req := httptest.NewRequest(http.MethodGet, "/api/v1/status/tsdb/blocks", nil)
w := httptest.NewRecorder()
result := api.serveTSDBBlocks(req)
json.NewEncoder(w).Encode(result.data)
resp := w.Result()
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
var resultData struct {
Blocks []tsdb.BlockMeta `json:"blocks"`
}
err := json.NewDecoder(resp.Body).Decode(&resultData)
require.NoError(t, err)
require.Len(t, resultData.Blocks, 1)
require.Equal(t, blockMeta, resultData.Blocks[0])
}
func TestRespondError(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
api := API{}

View File

@ -52,6 +52,10 @@ type dbAdapter struct {
*tsdb.DB
}
func (a *dbAdapter) BlockMetas() ([]tsdb.BlockMeta, error) {
return a.DB.BlockMetas(), nil
}
func (a *dbAdapter) Stats(statsByLabelName string, limit int) (*tsdb.Stats, error) {
return a.Head().Stats(statsByLabelName, limit), nil
}
@ -569,6 +573,7 @@ func TestAgentAPIEndPoints(t *testing.T) {
"/query_range": {http.MethodGet, http.MethodPost},
"/query_exemplars": {http.MethodGet, http.MethodPost},
"/status/tsdb": {http.MethodGet},
"/status/tsdb/blocks": {http.MethodGet},
"/alerts": {http.MethodGet},
"/rules": {http.MethodGet},
"/admin/tsdb/delete_series": {http.MethodPost, http.MethodPut},