Skip to content

Commit 7bed80a

Browse files
authored
feat(v2): add admin page for inspecting TSDB indices (#4555)
1 parent 3a744bb commit 7bed80a

File tree

9 files changed

+580
-140
lines changed

9 files changed

+580
-140
lines changed

pkg/api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ type AdminService interface {
307307
DatasetProfilesHandler(w http.ResponseWriter, r *http.Request)
308308
ProfileDownloadHandler(w http.ResponseWriter, r *http.Request)
309309
ProfileCallTreeHandler(w http.ResponseWriter, r *http.Request)
310+
DatasetTSDBIndexHandler(w http.ResponseWriter, r *http.Request)
310311
}
311312

312313
func (a *API) RegisterAdmin(ad AdminService) {
@@ -317,6 +318,7 @@ func (a *API) RegisterAdmin(ad AdminService) {
317318
a.RegisterRoute("/ops/object-store/tenants/{tenant}/blocks/{block}/datasets/profiles", http.HandlerFunc(ad.DatasetProfilesHandler), a.registerOptionsPublicAccess()...)
318319
a.RegisterRoute("/ops/object-store/tenants/{tenant}/blocks/{block}/datasets/profiles/download", http.HandlerFunc(ad.ProfileDownloadHandler), a.registerOptionsPublicAccess()...)
319320
a.RegisterRoute("/ops/object-store/tenants/{tenant}/blocks/{block}/datasets/profiles/call-tree", http.HandlerFunc(ad.ProfileCallTreeHandler), a.registerOptionsPublicAccess()...)
321+
a.RegisterRoute("/ops/object-store/tenants/{tenant}/blocks/{block}/datasets/index", http.HandlerFunc(ad.DatasetTSDBIndexHandler), a.registerOptionsPublicAccess()...)
320322

321323
a.indexPage.AddLinks(defaultWeight, "Admin", []IndexPageLink{
322324
{Desc: "Object Storage Tenants & Blocks", Path: "/ops/object-store/tenants"},

pkg/operations/admin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ func (a *Admin) ProfileDownloadHandler(w http.ResponseWriter, r *http.Request) {
6161
func (a *Admin) ProfileCallTreeHandler(w http.ResponseWriter, r *http.Request) {
6262
http.Error(w, "Profile call tree not available in v1 storage", http.StatusNotFound)
6363
}
64+
65+
func (a *Admin) DatasetTSDBIndexHandler(w http.ResponseWriter, r *http.Request) {
66+
http.Error(w, "Dataset TSDB index not available in v1 storage", http.StatusNotFound)
67+
}

pkg/operations/v2/admin.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ func (a *Admin) ProfileDownloadHandler(w http.ResponseWriter, r *http.Request) {
6161
func (a *Admin) ProfileCallTreeHandler(w http.ResponseWriter, r *http.Request) {
6262
a.handlers.CreateDatasetProfileCallTreeHandler()(w, r)
6363
}
64+
65+
func (a *Admin) DatasetTSDBIndexHandler(w http.ResponseWriter, r *http.Request) {
66+
a.handlers.CreateDatasetTSDBIndexHandler()(w, r)
67+
}
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
package v2
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
"github.com/dustin/go-humanize"
10+
"github.com/gorilla/mux"
11+
"github.com/pkg/errors"
12+
13+
metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1"
14+
"github.com/grafana/pyroscope/pkg/block"
15+
"github.com/grafana/pyroscope/pkg/block/metadata"
16+
phlaremodel "github.com/grafana/pyroscope/pkg/model"
17+
"github.com/grafana/pyroscope/pkg/phlaredb"
18+
"github.com/grafana/pyroscope/pkg/phlaredb/tsdb/index"
19+
httputil "github.com/grafana/pyroscope/pkg/util/http"
20+
)
21+
22+
func (h *Handlers) CreateDatasetDetailsHandler() func(http.ResponseWriter, *http.Request) {
23+
return func(w http.ResponseWriter, r *http.Request) {
24+
vars := mux.Vars(r)
25+
tenantId := vars["tenant"]
26+
if tenantId == "" {
27+
httputil.Error(w, errors.New("No tenant id provided"))
28+
return
29+
}
30+
blockId := vars["block"]
31+
if blockId == "" {
32+
httputil.Error(w, errors.New("No block id provided"))
33+
return
34+
}
35+
datasetName := r.URL.Query().Get("dataset")
36+
if datasetName == "" {
37+
httputil.Error(w, errors.New("No dataset name provided"))
38+
return
39+
}
40+
// Handle special case for empty dataset name
41+
if datasetName == "_empty" {
42+
datasetName = ""
43+
}
44+
shardStr := r.URL.Query().Get("shard")
45+
if shardStr == "" {
46+
httputil.Error(w, errors.New("No shard provided"))
47+
return
48+
}
49+
var shard uint32
50+
if _, err := fmt.Sscanf(shardStr, "%d", &shard); err != nil {
51+
httputil.Error(w, errors.Wrap(err, "invalid shard parameter"))
52+
return
53+
}
54+
55+
blockTenant := r.URL.Query().Get("block_tenant")
56+
57+
metadataResp, err := h.MetastoreClient.GetBlockMetadata(r.Context(), &metastorev1.GetBlockMetadataRequest{
58+
Blocks: &metastorev1.BlockList{
59+
Tenant: blockTenant,
60+
Shard: shard,
61+
Blocks: []string{blockId},
62+
},
63+
})
64+
if err != nil {
65+
httputil.Error(w, errors.Wrap(err, "failed to get block metadata"))
66+
return
67+
}
68+
69+
if len(metadataResp.Blocks) == 0 {
70+
httputil.Error(w, errors.New("Block not found"))
71+
return
72+
}
73+
74+
blockMeta := metadataResp.Blocks[0]
75+
76+
var foundDataset *metastorev1.Dataset
77+
for _, ds := range blockMeta.Datasets {
78+
dsName := blockMeta.StringTable[ds.Name]
79+
if dsName == datasetName {
80+
foundDataset = ds
81+
break
82+
}
83+
}
84+
85+
if foundDataset == nil {
86+
httputil.Error(w, errors.New("Dataset not found"))
87+
return
88+
}
89+
90+
dataset := h.convertDataset(foundDataset, blockMeta.StringTable)
91+
92+
err = pageTemplates.datasetDetailsTemplate.Execute(w, datasetDetailsPageContent{
93+
User: tenantId,
94+
BlockID: blockId,
95+
Shard: shard,
96+
BlockTenant: blockTenant,
97+
Dataset: &dataset,
98+
Now: time.Now().UTC().Format(time.RFC3339),
99+
})
100+
if err != nil {
101+
httputil.Error(w, err)
102+
return
103+
}
104+
}
105+
}
106+
107+
func (h *Handlers) convertDataset(ds *metastorev1.Dataset, stringTable []string) datasetDetails {
108+
tenant := stringTable[ds.Tenant]
109+
datasetName := stringTable[ds.Name]
110+
111+
var labelSets []labelSet
112+
pairs := metadata.LabelPairs(ds.Labels)
113+
for pairs.Next() {
114+
p := pairs.At()
115+
var currentSet labelSet
116+
for len(p) > 0 {
117+
if len(p) >= 2 {
118+
key := stringTable[p[0]]
119+
val := stringTable[p[1]]
120+
currentSet.Pairs = append(currentSet.Pairs, labelPair{Key: key, Value: val})
121+
p = p[2:]
122+
} else {
123+
break
124+
}
125+
}
126+
if len(currentSet.Pairs) > 0 {
127+
labelSets = append(labelSets, currentSet)
128+
}
129+
}
130+
131+
var profilesSize, indexSize, symbolsSize uint64
132+
if len(ds.TableOfContents) >= 3 {
133+
profilesSize = ds.TableOfContents[1] - ds.TableOfContents[0]
134+
indexSize = ds.TableOfContents[2] - ds.TableOfContents[1]
135+
symbolsSize = (ds.TableOfContents[0] + ds.Size) - ds.TableOfContents[2]
136+
}
137+
138+
var profilesPercentage, indexPercentage, symbolsPercentage float64
139+
if ds.Size > 0 {
140+
profilesPercentage = (float64(profilesSize) / float64(ds.Size)) * 100
141+
indexPercentage = (float64(indexSize) / float64(ds.Size)) * 100
142+
symbolsPercentage = (float64(symbolsSize) / float64(ds.Size)) * 100
143+
}
144+
145+
return datasetDetails{
146+
Tenant: tenant,
147+
Name: datasetName,
148+
MinTime: msToTime(ds.MinTime).UTC().Format(time.RFC3339),
149+
MaxTime: msToTime(ds.MaxTime).UTC().Format(time.RFC3339),
150+
Size: humanize.Bytes(ds.Size),
151+
ProfilesSize: humanize.Bytes(profilesSize),
152+
IndexSize: humanize.Bytes(indexSize),
153+
SymbolsSize: humanize.Bytes(symbolsSize),
154+
ProfilesPercentage: profilesPercentage,
155+
IndexPercentage: indexPercentage,
156+
SymbolsPercentage: symbolsPercentage,
157+
LabelSets: labelSets,
158+
}
159+
}
160+
161+
func (h *Handlers) CreateDatasetTSDBIndexHandler() func(http.ResponseWriter, *http.Request) {
162+
return func(w http.ResponseWriter, r *http.Request) {
163+
req, err := parseDatasetRequest(r)
164+
if err != nil {
165+
httputil.Error(w, err)
166+
return
167+
}
168+
169+
blockMeta, foundDataset, err := h.getDatasetMetadata(r.Context(), req)
170+
if err != nil {
171+
httputil.Error(w, err)
172+
return
173+
}
174+
175+
dataset := h.convertDataset(foundDataset, blockMeta.StringTable)
176+
177+
TSDBIndex, err := h.readTSDBIndex(r.Context(), blockMeta, foundDataset)
178+
if err != nil {
179+
httputil.Error(w, errors.Wrap(err, "failed to read TSDB index"))
180+
return
181+
}
182+
183+
err = pageTemplates.datasetIndexTemplate.Execute(w, datasetIndexPageContent{
184+
User: req.TenantID,
185+
BlockID: req.BlockID,
186+
Shard: req.Shard,
187+
BlockTenant: req.BlockTenant,
188+
Dataset: &dataset,
189+
TSDBIndex: TSDBIndex,
190+
Now: time.Now().UTC().Format(time.RFC3339),
191+
})
192+
if err != nil {
193+
httputil.Error(w, err)
194+
return
195+
}
196+
}
197+
}
198+
199+
func (h *Handlers) readTSDBIndex(ctx context.Context, blockMeta *metastorev1.BlockMeta, dataset *metastorev1.Dataset) (*tsdbIndexInfo, error) {
200+
obj := block.NewObject(h.Bucket, blockMeta)
201+
if err := obj.Open(ctx); err != nil {
202+
return nil, fmt.Errorf("failed to open block object: %w", err)
203+
}
204+
defer obj.Close()
205+
206+
ds := block.NewDataset(dataset, obj)
207+
if err := ds.Open(ctx, block.SectionTSDB); err != nil {
208+
return nil, fmt.Errorf("failed to open dataset: %w", err)
209+
}
210+
defer ds.Close()
211+
212+
idx := ds.Index()
213+
214+
from, through := idx.Bounds()
215+
fromTime := time.Unix(0, from).UTC().Format(time.RFC3339)
216+
throughTime := time.Unix(0, through).UTC().Format(time.RFC3339)
217+
218+
labels, err := h.getIndexLabels(idx)
219+
if err != nil {
220+
return nil, fmt.Errorf("failed to get labels: %w", err)
221+
}
222+
223+
symbolIter := idx.Symbols()
224+
var symbols []string
225+
for symbolIter.Next() {
226+
symbols = append(symbols, symbolIter.At())
227+
}
228+
if err := symbolIter.Err(); err != nil {
229+
return nil, fmt.Errorf("failed to iterate symbols: %w", err)
230+
}
231+
232+
series, err := h.getIndexSeries(idx)
233+
if err != nil {
234+
return nil, fmt.Errorf("failed to get series: %w", err)
235+
}
236+
237+
return &tsdbIndexInfo{
238+
From: fromTime,
239+
Through: throughTime,
240+
Checksum: idx.Checksum(),
241+
Series: series,
242+
Symbols: symbols,
243+
LabelValueSets: labels,
244+
}, nil
245+
}
246+
247+
func (h *Handlers) getIndexLabels(idx phlaredb.IndexReader) ([]labelValueSet, error) {
248+
labelNames, err := idx.LabelNames()
249+
if err != nil {
250+
return nil, fmt.Errorf("failed to get label names: %w", err)
251+
}
252+
var labelValueSets []labelValueSet
253+
for _, labelName := range labelNames {
254+
values, err := idx.LabelValues(labelName)
255+
if err != nil {
256+
return nil, fmt.Errorf("failed to get label values for %s: %w", labelName, err)
257+
}
258+
259+
labelValueSets = append(labelValueSets, labelValueSet{
260+
LabelName: labelName,
261+
LabelValues: values,
262+
})
263+
}
264+
return labelValueSets, nil
265+
}
266+
267+
func (h *Handlers) getIndexSeries(idx phlaredb.IndexReader) ([]seriesInfo, error) {
268+
k2, v2 := index.AllPostingsKey()
269+
seriesPostings, err := idx.Postings(k2, nil, v2)
270+
if err != nil {
271+
return nil, fmt.Errorf("failed to get series postings: %w", err)
272+
}
273+
274+
var seriesList []seriesInfo
275+
var lbls phlaremodel.Labels
276+
chunks := make([]index.ChunkMeta, 1)
277+
278+
seriesIdx := uint32(0)
279+
for seriesPostings.Next() {
280+
seriesRef := seriesPostings.At()
281+
_, err := idx.Series(seriesRef, &lbls, &chunks)
282+
if err != nil {
283+
return nil, fmt.Errorf("failed to get series %d: %w", seriesRef, err)
284+
}
285+
286+
var labelPairs []labelPair
287+
for _, lbl := range lbls {
288+
labelPairs = append(labelPairs, labelPair{
289+
Key: lbl.Name,
290+
Value: lbl.Value,
291+
})
292+
}
293+
294+
seriesList = append(seriesList, seriesInfo{
295+
SeriesIndex: seriesIdx,
296+
SeriesRef: uint64(seriesRef),
297+
Labels: labelPairs,
298+
})
299+
seriesIdx++
300+
}
301+
if err := seriesPostings.Err(); err != nil {
302+
return nil, fmt.Errorf("failed to iterate series postings: %w", err)
303+
}
304+
305+
return seriesList, nil
306+
}

0 commit comments

Comments
 (0)