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(&params.objectStoreCfg.Backend)
    57  		case "storage.prefix":
    58  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.Prefix)
    59  		case "storage.filesystem.dir":
    60  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.Filesystem.Directory)
    61  		case "storage.gcs.bucket-name":
    62  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.GCS.BucketName)
    63  		case "storage.s3.bucket-name":
    64  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.S3.BucketName)
    65  		case "storage.s3.endpoint":
    66  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.S3.Endpoint)
    67  		case "storage.s3.region":
    68  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.S3.Region)
    69  		case "storage.azure.container-name":
    70  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.objectStoreCfg.Azure.ContainerName)
    71  		case "storage.azure.account-name":
    72  			cmd.Flag(f.Name, f.Usage).Default(f.DefValue).StringVar(&params.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(&params.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  }