diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index ed7aa52c8a..ac23416592 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -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 { diff --git a/docs/querying/api.md b/docs/querying/api.md index a17bd1e3a2..2f3c79cef7 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -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 diff --git a/tsdb/db.go b/tsdb/db.go index 0f2d8c311e..4d21d4dc12 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -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) diff --git a/web/api/v1/api.go b/web/api/v1/api.go index c924c9092c..07fb4e9bb8 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -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 != "" { diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 254ce074ce..2c9d2d978b 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -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{} diff --git a/web/web_test.go b/web/web_test.go index ea7e099041..7783909266 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -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},