github.com/grafana/pyroscope@v1.18.0/cmd/profilecli/bucket.go (about) 1 package main 2 3 import ( 4 "context" 5 "embed" 6 "flag" 7 "fmt" 8 "log" 9 "net/http" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/dustin/go-humanize" 15 "github.com/grafana/dskit/server" 16 "github.com/oklog/ulid/v2" 17 "github.com/olekukonko/tablewriter" 18 "github.com/pkg/errors" 19 "github.com/prometheus/common/model" 20 "github.com/thanos-io/objstore" 21 22 "github.com/grafana/pyroscope/pkg/block" 23 "github.com/grafana/pyroscope/pkg/block/metadata" 24 phlareobj "github.com/grafana/pyroscope/pkg/objstore" 25 objstoreclient "github.com/grafana/pyroscope/pkg/objstore/client" 26 "github.com/grafana/pyroscope/pkg/operations" 27 ) 28 29 type bucketParams struct { 30 objectStoreCfg objstoreclient.Config 31 } 32 33 func (b *bucketParams) initClient(ctx context.Context) (phlareobj.Bucket, error) { 34 return objstoreclient.NewBucket( 35 ctx, 36 b.objectStoreCfg, 37 "storage", 38 ) 39 } 40 41 type bucketWebToolParams struct { 42 *bucketParams 43 httpListenPort int 44 } 45 46 func addBucketParams(cmd commander) *bucketParams { 47 var ( 48 params = &bucketParams{} 49 ) 50 // keep in sync with objstoreclient.RegisterFlags 51 fs := flag.NewFlagSet("", flag.ContinueOnError) 52 params.objectStoreCfg.RegisterFlagsWithPrefix("storage.", fs) 53 fs.VisitAll(func(f *flag.Flag) { 54 switch f.Name { 55 case "storage.backend": 56 cmd.Flag(f.Name, f.Usage).Default("filesystem").StringVar(¶ms.objectStoreCfg.Backend) 57 case "storage.prefix": 58 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.Prefix) 59 case "storage.filesystem.dir": 60 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.Filesystem.Directory) 61 case "storage.gcs.bucket-name": 62 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.GCS.BucketName) 63 case "storage.s3.bucket-name": 64 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.S3.BucketName) 65 case "storage.s3.endpoint": 66 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.S3.Endpoint) 67 case "storage.s3.region": 68 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.S3.Region) 69 case "storage.azure.container-name": 70 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.Azure.ContainerName) 71 case "storage.azure.account-name": 72 cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(¶ms.objectStoreCfg.Azure.StorageAccountName) 73 default: 74 break 75 } 76 }) 77 return params 78 } 79 80 //go:embed static 81 var staticFiles embed.FS 82 83 func addBucketWebToolParams(cmd commander) *bucketWebToolParams { 84 var ( 85 params = &bucketWebToolParams{} 86 ) 87 params.bucketParams = addBucketParams(cmd) 88 cmd.Flag("http-listen-port", "The port to run the HTTP server on.").Default("4201").IntVar(¶ms.httpListenPort) 89 return params 90 } 91 92 type bucketWebTool struct { 93 params *bucketWebToolParams 94 } 95 96 func newBucketWebTool(params *bucketWebToolParams) *bucketWebTool { 97 return &bucketWebTool{ 98 params: params, 99 } 100 } 101 102 func (t *bucketWebTool) run(ctx context.Context) error { 103 s, err := server.New(server.Config{ 104 HTTPListenPort: t.params.httpListenPort, 105 Log: logger, 106 }) 107 if err != nil { 108 return err 109 } 110 111 b, err := t.params.initClient(ctx) 112 if err != nil { 113 log.Fatal(err) 114 return err 115 } 116 117 handlers := operations.Handlers{ 118 Logger: logger, 119 Bucket: b, 120 } 121 122 s.HTTP.PathPrefix("/static").Handler(http.FileServer(http.FS(staticFiles))) 123 s.HTTP.Path("/ops/object-store/tenants").HandlerFunc(handlers.CreateIndexHandler()) 124 s.HTTP.Path("/ops/object-store/tenants/{tenant}/blocks").HandlerFunc(handlers.CreateBlocksHandler()) 125 s.HTTP.Path("/ops/object-store/tenants/{tenant}/blocks/{block}").HandlerFunc(handlers.CreateBlockDetailsHandler()) 126 127 out := output(ctx) 128 129 fmt.Fprintf(out, "The bucket web tool is available at http://localhost:%d/ops/object-store/tenants\n", t.params.httpListenPort) 130 131 if err := s.Run(); err != nil { 132 return err 133 } 134 135 return nil 136 } 137 138 func labelStrings(v []int32, s []string, sb *strings.Builder) { 139 pairs := metadata.LabelPairs(v) 140 for pairs.Next() { 141 p := pairs.At() 142 first := true 143 for len(p) > 0 { 144 if first { 145 first = false 146 _, _ = sb.WriteString("- ") 147 } else { 148 _, _ = sb.WriteString(" ") 149 } 150 _, _ = sb.WriteString(s[p[0]]) 151 _, _ = sb.WriteString(`="`) 152 _, _ = sb.WriteString(s[p[1]]) 153 _, _ = sb.WriteString(`"`) 154 _, _ = sb.WriteString("\n") 155 p = p[2:] 156 } 157 } 158 } 159 160 func bucketInspectV2(ctx context.Context, params *bucketParams, paths []string) error { 161 b, err := params.initClient(ctx) 162 if err != nil { 163 return err 164 } 165 166 lst := bucketList{} 167 168 for _, path := range paths { 169 obj, err := block.NewObjectFromPath(ctx, b, path) 170 if err != nil { 171 return err 172 } 173 174 e, err := bucketListElemFromPath(path) 175 if err != nil { 176 return err 177 } 178 179 meta := obj.Metadata() 180 stringTable := meta.GetStringTable() 181 e.Size = meta.GetSize() 182 e.CompactionLevel = meta.GetCompactionLevel() 183 184 lst.lst = lst.lst[:0] 185 lst.lst = append(lst.lst, *e) 186 lst.renderInspectTable(ctx) 187 188 sb := new(strings.Builder) 189 tbl := tablewriter.NewWriter(output(ctx)) 190 tbl.SetAutoWrapText(false) 191 tbl.SetHeader([]string{"Tenant", "Dataset Name", "From", "Duration", "Size", "Labels"}) 192 193 for _, ds := range meta.Datasets { 194 sb.Reset() 195 labelStrings(ds.Labels, stringTable, sb) 196 tbl.Append([]string{ 197 stringTable[ds.Tenant], 198 stringTable[ds.Name], 199 model.Time(ds.MinTime).Time().Format(time.RFC3339), 200 (time.Duration(ds.MaxTime-ds.MinTime) * time.Millisecond).String(), 201 humanize.Bytes(ds.Size), 202 strings.TrimRight(sb.String(), "\n"), 203 }) 204 } 205 tbl.Render() 206 207 } 208 209 return nil 210 } 211 212 type bucketList struct { 213 lst []bucketListElem 214 tbl *tablewriter.Table 215 } 216 217 func (b bucketList) renderInspectTable(ctx context.Context) { 218 if b.tbl == nil { 219 b.tbl = tablewriter.NewWriter(output(ctx)) 220 } else { 221 b.tbl.ClearRows() 222 } 223 b.tbl.SetHeader([]string{"Block ID", "Time", "Type", "Shard", "Tenant", "Compaction Level", "Size", "Path"}) 224 for _, e := range b.lst { 225 b.tbl.Append([]string{ 226 e.ID.String(), 227 e.ID.Timestamp().Format(time.RFC3339), 228 e.Type, 229 strconv.Itoa(e.Shard), 230 e.Tenant, 231 strconv.Itoa(int(e.CompactionLevel)), 232 humanize.Bytes(e.Size), 233 e.Path, 234 }) 235 236 } 237 b.tbl.Render() 238 } 239 240 func (b bucketList) renderListTable(ctx context.Context) { 241 if b.tbl == nil { 242 b.tbl = tablewriter.NewWriter(output(ctx)) 243 } else { 244 b.tbl.ClearRows() 245 } 246 b.tbl.SetHeader([]string{"Block ID", "Time", "Type", "Shard", "Tenant", "Path"}) 247 for _, e := range b.lst { 248 b.tbl.Append([]string{ 249 e.ID.String(), 250 e.ID.Timestamp().Format(time.RFC3339), 251 e.Type, 252 strconv.Itoa(e.Shard), 253 e.Tenant, 254 e.Path, 255 }) 256 257 } 258 b.tbl.Render() 259 } 260 261 type bucketListElem struct { 262 ID ulid.ULID 263 Type string 264 Shard int 265 Tenant string 266 Path string 267 CompactionLevel uint32 268 Size uint64 269 } 270 271 var errUnexpectedPath = errors.New("unexpected path") 272 273 func bucketListElemFromPath(s string) (*bucketListElem, error) { 274 parts := strings.Split(s, "/") 275 if parts[len(parts)-1] != "block.bin" { 276 return nil, errUnexpectedPath 277 } 278 279 id, err := ulid.Parse(parts[len(parts)-2]) 280 if err != nil { 281 return nil, err 282 } 283 shard, err := strconv.Atoi(parts[1]) 284 if err != nil { 285 return nil, err 286 } 287 288 tenant := parts[2] 289 if parts[0] == "segments" { 290 tenant = "various" 291 } 292 293 return &bucketListElem{ 294 ID: id, 295 Type: parts[0], 296 Shard: shard, 297 Tenant: tenant, 298 Path: s, 299 }, nil 300 } 301 302 func bucketListV2(ctx context.Context, params *bucketParams) error { 303 b, err := params.initClient(ctx) 304 if err != nil { 305 return err 306 } 307 308 lst := bucketList{} 309 limit := 40 // might be depending on the size of the terminal 310 311 addBlock := func(name string) error { 312 e, err := bucketListElemFromPath(name) 313 if err != nil { 314 if err == errUnexpectedPath { 315 return nil 316 } 317 return err 318 } 319 320 lst.lst = append(lst.lst, *e) 321 if len(lst.lst) > limit { 322 lst.renderListTable(ctx) 323 lst.lst = lst.lst[:0] 324 } 325 326 return nil 327 } 328 329 if err := b.Iter(ctx, "segments", addBlock, objstore.WithRecursiveIter()); err != nil { 330 return err 331 } 332 if err := b.Iter(ctx, "blocks", addBlock, objstore.WithRecursiveIter()); err != nil { 333 return err 334 } 335 lst.renderListTable(ctx) 336 return nil 337 }