diff --git a/cmd/pyroscope/help-all.txt.tmpl b/cmd/pyroscope/help-all.txt.tmpl index 122b9354d8..e99f55a05f 100644 --- a/cmd/pyroscope/help-all.txt.tmpl +++ b/cmd/pyroscope/help-all.txt.tmpl @@ -253,6 +253,12 @@ Usage of ./pyroscope: Backend storage to use for the ring. Supported values are: consul, etcd, inmemory, memberlist, multi. (default "memberlist") -distributor.zone-awareness-enabled True to enable the zone-awareness and replicate ingested samples across different availability zones. + -embedded-grafana.data-path string + The directory where the Grafana data will be stored. (default "./data/__embedded_grafana/") + -embedded-grafana.listen-port int + The port on which the Grafana will listen. (default 4041) + -embedded-grafana.pyroscope-url string + The URL of the Pyroscope instance to use for the Grafana datasources. (default "http://localhost:4040") -etcd.dial-timeout duration The dial timeout for the etcd connection. (default 10s) -etcd.endpoints string diff --git a/cmd/pyroscope/help.txt.tmpl b/cmd/pyroscope/help.txt.tmpl index ad077e714e..c133f3e8d3 100644 --- a/cmd/pyroscope/help.txt.tmpl +++ b/cmd/pyroscope/help.txt.tmpl @@ -77,6 +77,12 @@ Usage of ./pyroscope: Backend storage to use for the ring. Supported values are: consul, etcd, inmemory, memberlist, multi. (default "memberlist") -distributor.zone-awareness-enabled True to enable the zone-awareness and replicate ingested samples across different availability zones. + -embedded-grafana.data-path string + The directory where the Grafana data will be stored. (default "./data/__embedded_grafana/") + -embedded-grafana.listen-port int + The port on which the Grafana will listen. (default 4041) + -embedded-grafana.pyroscope-url string + The URL of the Pyroscope instance to use for the Grafana datasources. (default "http://localhost:4040") -etcd.endpoints string The etcd endpoints to connect to. -etcd.password string diff --git a/docs/internal/contributing/README.md b/docs/internal/contributing/README.md index 44192cf4fe..00104f7f37 100644 --- a/docs/internal/contributing/README.md +++ b/docs/internal/contributing/README.md @@ -19,7 +19,7 @@ a piece of work is finished it should: To be able to run make targets you'll need to install: -- [Go](https://go.dev/doc/install) (> 1.19) +- [Go](https://go.dev/doc/install) (>= 1.21) - [Docker](https://docs.docker.com/engine/install/) All other required tools will be automatically downloaded `$(pwd)/.tmp/bin`. @@ -86,6 +86,16 @@ replace `image: grafana/pyroscope` with the local tag name you got from docker-i - '4040:4040' ``` +#### Run with Pyroscope with embedded Grafana + Explore Profiles + +In order to quickly test the whole stack it is possible to run an embedded Grafana by using target parameter: + +``` +go run ./cmd/pyroscope --target all,embedded-grafana +``` + +This will start additional to Pyroscope on `:4040`, the embedded Grafana on port `:4041`. + #### Front end development **Versions for development tools**: diff --git a/docs/sources/configure-server/reference-configuration-parameters/index.md b/docs/sources/configure-server/reference-configuration-parameters/index.md index f24d239132..08a2d64375 100644 --- a/docs/sources/configure-server/reference-configuration-parameters/index.md +++ b/docs/sources/configure-server/reference-configuration-parameters/index.md @@ -268,6 +268,19 @@ analytics: # Prints the application banner at startup. # CLI flag: -config.show_banner [show_banner: | default = true] + +embedded_grafana: + # The directory where the Grafana data will be stored. + # CLI flag: -embedded-grafana.data-path + [data_path: | default = "./data/__embedded_grafana/"] + + # The port on which the Grafana will listen. + # CLI flag: -embedded-grafana.listen-port + [listen_port: | default = 4041] + + # The URL of the Pyroscope instance to use for the Grafana datasources. + # CLI flag: -embedded-grafana.pyroscope-url + [pyroscope_url: | default = "http://localhost:4040"] ``` ### server diff --git a/pkg/embedded/grafana/assets.go b/pkg/embedded/grafana/assets.go new file mode 100644 index 0000000000..2bffd6c44c --- /dev/null +++ b/pkg/embedded/grafana/assets.go @@ -0,0 +1,256 @@ +package grafana + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" +) + +type CompressType int + +const ( + CompressTypeNone CompressType = iota + CompressTypeGzip + CompressTypeZip +) + +const ( + modeDir = 0755 + modeFile = 0644 +) + +type releaseArtifacts []releaseArtifact + +func (releases releaseArtifacts) selectBy(os, arch string) *releaseArtifact { + var nonArch *releaseArtifact + for idx, r := range releases { + if r.OS == "" && r.Arch == "" && nonArch == nil { + nonArch = &releases[idx] + continue + } + if r.OS == os && r.Arch == arch { + return &r + } + } + return nonArch +} + +type releaseArtifact struct { + URL string + Sha256Sum []byte + OS string + Arch string + CompressType CompressType + StripComponents int +} + +func (release *releaseArtifact) download(ctx context.Context, logger log.Logger, destPath string) (string, error) { + targetPath := filepath.Join(destPath, "assets", hex.EncodeToString(release.Sha256Sum)) + + // check if already exists + if len(release.Sha256Sum) > 0 { + stat, err := os.Stat(targetPath) + if err != nil { + if !os.IsNotExist(err) { + return "", err + } + } + if err == nil && stat.IsDir() { + level.Info(logger).Log("msg", "release exists already", "url", release.URL, "hash", hex.EncodeToString(release.Sha256Sum)) + return targetPath, nil + } + } + + level.Info(logger).Log("msg", "download new release", "url", release.URL) + req, err := http.NewRequestWithContext(ctx, "GET", release.URL, nil) + req.Header.Set("User-Agent", "pyroscope/embedded-grafana") + if err != nil { + return "", err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + file, err := os.CreateTemp("", "pyroscope-download") + if err != nil { + return "", err + } + defer os.Remove(file.Name()) + + hash := sha256.New() + r := io.TeeReader(resp.Body, hash) + + _, err = io.Copy(file, r) + if err != nil { + return "", err + } + + err = file.Close() + if err != nil { + return "", err + } + + actHashSum := hex.EncodeToString(hash.Sum(nil)) + if expHashSum := hex.EncodeToString(release.Sha256Sum); actHashSum != expHashSum { + return "", fmt.Errorf("hash mismatch: expected %s, got %s", expHashSum, actHashSum) + } + + switch release.CompressType { + case CompressTypeNone: + return targetPath, os.Rename(file.Name(), targetPath) + case CompressTypeGzip: + file, err = os.Open(file.Name()) + if err != nil { + return "", err + } + defer file.Close() + + err = extractTarGz(file, targetPath, release.StripComponents) + if err != nil { + return "", err + } + case CompressTypeZip: + file, err = os.Open(file.Name()) + if err != nil { + return "", err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return "", err + } + + err = extractZip(file, stat.Size(), targetPath, release.StripComponents) + if err != nil { + return "", err + } + } + + return targetPath, nil +} + +func clearPath(name string, destPath string, stripComponents int) string { + isSeparator := func(r rune) bool { + return r == os.PathSeparator + } + list := strings.FieldsFunc(name, isSeparator) + if len(list) > stripComponents { + list = list[stripComponents:] + } + return filepath.Join(append([]string{destPath}, list...)...) +} + +func extractZip(zipStream io.ReaderAt, size int64, destPath string, stripComponents int) error { + zipReader, err := zip.NewReader(zipStream, size) + if err != nil { + return fmt.Errorf("ExtractZip: NewReader failed: %s", err.Error()) + } + + for _, f := range zipReader.File { + p := clearPath(f.Name, destPath, stripComponents) + if f.FileInfo().IsDir() { + err := os.MkdirAll(p, modeDir) + if err != nil { + return fmt.Errorf("ExtractZip: MkdirAll() failed: %s", err.Error()) + } + continue + } + + dir, _ := filepath.Split(p) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, modeDir); err != nil { + return fmt.Errorf("ExtractZip: MkdirAll() failed: %s", err.Error()) + } + } + + fileInArchive, err := f.Open() + if err != nil { + return fmt.Errorf("ExtractZip: Open() failed: %s", err.Error()) + } + + outFile, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, f.FileInfo().Mode()) + if err != nil { + return fmt.Errorf("ExtractZip: OpenFile() failed: %s", err.Error()) + } + if _, err := io.Copy(outFile, fileInArchive); err != nil { + return fmt.Errorf("ExtractZip: Copy() failed: %s", err.Error()) + } + } + + return nil + +} + +func extractTarGz(gzipStream io.Reader, destPath string, stripComponents int) error { + uncompressedStream, err := gzip.NewReader(gzipStream) + if err != nil { + return errors.New("ExtractTarGz: NewReader failed") + } + + tarReader := tar.NewReader(uncompressedStream) + + for { + header, err := tarReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return fmt.Errorf("ExtractTarGz: Next() failed: %s", err.Error()) + } + + p := clearPath(header.Name, destPath, stripComponents) + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(p, modeDir); err != nil { + return fmt.Errorf("ExtractTarGz: Mkdir() failed: %s", err.Error()) + } + case tar.TypeReg: + dir, _ := filepath.Split(p) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, modeDir); err != nil { + return fmt.Errorf("ExtractTarGz: MkdirAll() failed: %s", err.Error()) + } + } + outFile, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fs.FileMode(header.Mode)) + if err != nil { + return fmt.Errorf("ExtractTarGz: OpenFile() failed: %s", err.Error()) + } + if _, err := io.Copy(outFile, tarReader); err != nil { + return fmt.Errorf("ExtractTarGz: Copy() failed: %s", err.Error()) + } + outFile.Close() + + default: + return fmt.Errorf( + "ExtractTarGz: unknown type: %v in %s", + header.Typeflag, + header.Name) + } + } + + return nil +} diff --git a/pkg/embedded/grafana/grafana.go b/pkg/embedded/grafana/grafana.go new file mode 100644 index 0000000000..41abde4273 --- /dev/null +++ b/pkg/embedded/grafana/grafana.go @@ -0,0 +1,337 @@ +package grafana + +import ( + "bufio" + "context" + "encoding/hex" + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "syscall" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/grafana/dskit/services" + "golang.org/x/sync/errgroup" + "gopkg.in/yaml.v3" +) + +func mustHexDecode(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +var exploreProfileReleases = releaseArtifacts{ + { + URL: "https://github.com/grafana/explore-profiles/releases/download/v0.1.5/grafana-pyroscope-app-405.zip", + Sha256Sum: mustHexDecode("6e133f49b0528633146ccf77bb07f09ea1e4aaac3ce9563df42cfd54f6f7e753"), + CompressType: CompressTypeZip, + }, +} + +var grafanaReleases = releaseArtifacts{ + { + URL: "https://dl.grafana.com/oss/release/grafana-11.1.0.linux-amd64.tar.gz", + Sha256Sum: mustHexDecode("33822a0b275ea4f216c9a3bdda53d1dba668e3e9873dc52104bc565bcbd8d856"), + OS: "linux", + Arch: "amd64", + CompressType: CompressTypeGzip, + StripComponents: 1, + }, + { + URL: "https://dl.grafana.com/oss/release/grafana-11.1.0.linux-arm64.tar.gz", + Sha256Sum: mustHexDecode("80b36751c29593b8fdb72906bd05f8833631dd826b8447bcdc9ba9bb0f6122aa"), + OS: "linux", + Arch: "arm64", + CompressType: CompressTypeGzip, + StripComponents: 1, + }, + { + URL: "https://dl.grafana.com/oss/release/grafana-11.1.0.darwin-amd64.tar.gz", + Sha256Sum: mustHexDecode("96984def29a8d2d2f93471b2f012e9750deb54ab54b41272dc0cd9fc481e0c7d"), + OS: "darwin", + Arch: "amd64", + CompressType: CompressTypeGzip, + StripComponents: 1, + }, + { + URL: "https://dl.grafana.com/oss/release/grafana-11.1.0.darwin-arm64.tar.gz", + Sha256Sum: mustHexDecode("a7498744d8951c46f742bdc56d429473912fed6daa81fdba9711f2cfc51b8143"), + OS: "darwin", + Arch: "arm64", + CompressType: CompressTypeGzip, + StripComponents: 1, + }, +} + +type app struct { + cfg Config + logger log.Logger + + grafanaRelease *releaseArtifact + exploreProfileRelease *releaseArtifact + + dataPath string + pluginsPath string + provisioningPath string + + g *errgroup.Group +} + +type Config struct { + DataPath string `yaml:"data_path" json:"data_path"` + ListenPort int `yaml:"listen_port" json:"listen_port"` + PyroscopeURL string `yaml:"pyroscope_url" json:"pyroscope_url"` +} + +// RegisterFlags registers distributor-related flags. +func (cfg *Config) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar(&cfg.DataPath, "embedded-grafana.data-path", "./data/__embedded_grafana/", "The directory where the Grafana data will be stored.") + fs.IntVar(&cfg.ListenPort, "embedded-grafana.listen-port", 4041, "The port on which the Grafana will listen.") + fs.StringVar(&cfg.PyroscopeURL, "embedded-grafana.pyroscope-url", "http://localhost:4040", "The URL of the Pyroscope instance to use for the Grafana datasources.") +} + +func New(cfg Config, logger log.Logger) (services.Service, error) { + var err error + cfg.DataPath, err = filepath.Abs(cfg.DataPath) + if err != nil { + return nil, err + } + + grafanaRelease := grafanaReleases.selectBy(runtime.GOOS, runtime.GOARCH) + if grafanaRelease == nil { + return nil, fmt.Errorf("no Grafana release found for %s/%s", runtime.GOOS, runtime.GOARCH) + } + + exploreProfileRelease := exploreProfileReleases.selectBy(runtime.GOOS, runtime.GOARCH) + if exploreProfileRelease == nil { + level.Warn(logger).Log("msg", fmt.Sprintf("no Explore Profile plugin release found for %s/%s", runtime.GOOS, runtime.GOARCH)) + } + + a := &app{ + cfg: cfg, + logger: logger, + grafanaRelease: grafanaRelease, + exploreProfileRelease: exploreProfileRelease, + + dataPath: filepath.Join(cfg.DataPath, "data"), + pluginsPath: filepath.Join(cfg.DataPath, "plugins"), + provisioningPath: filepath.Join(cfg.DataPath, "provisioning"), + } + return services.NewBasicService(a.starting, a.running, a.stopping), nil +} + +func (a *app) downloadExploreProfiles(ctx context.Context) error { + // download the explore-profiles plugin + pluginPath, err := a.exploreProfileRelease.download(ctx, a.logger, a.cfg.DataPath) + if err != nil { + return err + } + + // symlink the explore-profiles plugin to the plugins directory + err = os.MkdirAll(a.pluginsPath, modeDir) + if err != nil { + return err + } + + linkDest := filepath.Join(a.pluginsPath, "grafana-pyroscope-app") + linkSource, err := filepath.Rel(a.pluginsPath, filepath.Join(pluginPath, "grafana-pyroscope-app")) + if err != nil { + return err + } + + stat, err := os.Lstat(linkDest) + if err == nil { + if stat.Mode()&os.ModeSymlink == os.ModeSymlink { + // already existing and symlink + target, err := os.Readlink(filepath.Join(a.pluginsPath, "grafana-pyroscope-app")) + if err != nil { + return err + } + + if target == linkSource { + return nil + } + + // recreate the symlink if it points to a different path + err = os.Remove(linkDest) + if err != nil { + return err + } + } else { + return fmt.Errorf("file exists and is not a symlink: %+#v", stat) + } + } else if !os.IsNotExist(err) { + return err + } + + return os.Symlink(linkSource, linkDest) +} + +func writeYAML(logger log.Logger, path string, data interface{}) error { + err := os.MkdirAll(filepath.Dir(path), modeDir) + if err != nil { + return err + } + + f, err := os.Create(path) + defer func() { + err := f.Close() + if err != nil { + level.Error(logger).Log("msg", "failed to close file", "path", path, "err", err) + } + }() + if err != nil { + return err + } + + _, err = f.Write([]byte("# Note: Do not edit this file directly. It is managed by pyroscope.\n")) + if err != nil { + return err + } + + yamlData, err := yaml.Marshal(data) + if err != nil { + return err + } + + _, err = f.Write(yamlData) + return err + +} + +func (a *app) provisioningDatasource(_ context.Context) error { + return writeYAML( + a.logger, + filepath.Join(a.provisioningPath, "datasources", "embedded-grafana.yaml"), + map[string]interface{}{ + "apiVersion": 1, + "datasources": []interface{}{ + map[string]interface{}{ + "uid": "pyroscope", + "type": "grafana-pyroscope-datasource", + "name": "Pyroscope", + "url": a.cfg.PyroscopeURL, + "jsonData": map[string]interface{}{ + "keepCookies": []string{"GitSession"}, + "overridesDefault": true, + }, + }, + }, + }, + ) +} + +func (a *app) provisioningPlugins(_ context.Context) error { + return writeYAML( + a.logger, + filepath.Join(a.provisioningPath, "plugins", "embedded-grafana.yaml"), + map[string]interface{}{ + "apiVersion": 1, + "apps": []interface{}{ + map[string]interface{}{ + "type": "grafana-pyroscope-app", + }, + }, + }, + ) +} + +func (a *app) starting(ctx context.Context) error { + if a.exploreProfileRelease != nil { + err := a.downloadExploreProfiles(ctx) + if err != nil { + return err + } + } + + err := a.provisioningDatasource(ctx) + if err != nil { + return err + } + + err = a.provisioningPlugins(ctx) + if err != nil { + return err + } + + grafanaPath, err := a.grafanaRelease.download(ctx, a.logger, a.cfg.DataPath) + if err != nil { + return err + } + + cmd := exec.Command( + filepath.Join(grafanaPath, "bin/grafana"), + "server", + "--homepath", + grafanaPath, + ) + cmd.Dir = a.cfg.DataPath + cmd.Env = os.Environ() + setIfNotExists := func(key, value string) { + if os.Getenv(key) == "" { + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value)) + } + } + setIfNotExists("GF_PATHS_DATA", a.dataPath) + setIfNotExists("GF_PATHS_PLUGINS", a.pluginsPath) + setIfNotExists("GF_PATHS_PROVISIONING", a.provisioningPath) + setIfNotExists("GF_AUTH_ANONYMOUS_ENABLED", "true") + setIfNotExists("GF_AUTH_ANONYMOUS_ORG_ROLE", "Admin") + setIfNotExists("GF_AUTH_DISABLE_LOGIN_FORM", "true") + setIfNotExists("GF_SERVER_HTTP_PORT", strconv.Itoa(a.cfg.ListenPort)) + + a.g, _ = errgroup.WithContext(ctx) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + a.g.Go(func() error { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + level.Info(a.logger).Log("stream", "stdout", "msg", scanner.Text()) + } + return scanner.Err() + }) + + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + a.g.Go(func() error { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + level.Info(a.logger).Log("stream", "stderr", "msg", scanner.Text()) + } + return scanner.Err() + }) + + if err = cmd.Start(); err != nil { + return err + } + + a.g.Go(func() error { + <-ctx.Done() + return cmd.Process.Signal(syscall.SIGINT) + }) + + a.g.Go(cmd.Wait) + + return nil +} + +func (a *app) stopping(failureCase error) error { + return nil +} + +func (a *app) running(ctx context.Context) error { + return a.g.Wait() +} diff --git a/pkg/phlare/modules.go b/pkg/phlare/modules.go index 0fe7b78ded..faba84dcf4 100644 --- a/pkg/phlare/modules.go +++ b/pkg/phlare/modules.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "os" + "slices" "time" "connectrpc.com/connect" @@ -37,6 +38,7 @@ import ( apiversion "github.com/grafana/pyroscope/pkg/api/version" "github.com/grafana/pyroscope/pkg/compactor" "github.com/grafana/pyroscope/pkg/distributor" + "github.com/grafana/pyroscope/pkg/embedded/grafana" "github.com/grafana/pyroscope/pkg/frontend" "github.com/grafana/pyroscope/pkg/ingester" objstoreclient "github.com/grafana/pyroscope/pkg/objstore/client" @@ -79,6 +81,7 @@ const ( Admin string = "admin" TenantSettings string = "tenant-settings" AdHocProfiles string = "ad-hoc-profiles" + EmbeddedGrafana string = "embedded-grafana" // Experimental modules @@ -394,7 +397,7 @@ func (f *Phlare) initStorage() (_ services.Service, err error) { f.storageBucket = b } - if f.Cfg.Target.String() != All && f.storageBucket == nil { + if !slices.Contains(f.Cfg.Target, All) && f.storageBucket == nil { return nil, errors.New("storage bucket configuration is required when running in microservices mode") } @@ -561,6 +564,10 @@ func (f *Phlare) initAdmin() (services.Service, error) { return a, nil } +func (f *Phlare) initEmbeddedGrafana() (services.Service, error) { + return grafana.New(f.Cfg.EmbeddedGrafana, f.logger) +} + type statusService struct { statusv1.UnimplementedStatusServiceServer defaultConfig *Config diff --git a/pkg/phlare/phlare.go b/pkg/phlare/phlare.go index e92ac5ebb2..c09e0b1be9 100644 --- a/pkg/phlare/phlare.go +++ b/pkg/phlare/phlare.go @@ -11,6 +11,7 @@ import ( "os" "runtime" "runtime/debug" + "slices" "sort" "strings" @@ -42,6 +43,7 @@ import ( "github.com/grafana/pyroscope/pkg/cfg" "github.com/grafana/pyroscope/pkg/compactor" "github.com/grafana/pyroscope/pkg/distributor" + "github.com/grafana/pyroscope/pkg/embedded/grafana" compactionworker "github.com/grafana/pyroscope/pkg/experiment/compactor" segmentwriter "github.com/grafana/pyroscope/pkg/experiment/ingester" "github.com/grafana/pyroscope/pkg/experiment/metastore" @@ -96,6 +98,8 @@ type Config struct { Analytics usagestats.Config `yaml:"analytics"` ShowBanner bool `yaml:"show_banner,omitempty"` + EmbeddedGrafana grafana.Config `yaml:"embedded_grafana,omitempty"` + ConfigFile string `yaml:"-"` ConfigExpandEnv bool `yaml:"-"` @@ -166,6 +170,7 @@ func (c *Config) RegisterFlagsWithContext(ctx context.Context, f *flag.FlagSet) c.LimitsConfig.RegisterFlags(f) c.Compactor.RegisterFlags(f, log.NewLogfmtLogger(os.Stderr)) c.API.RegisterFlags(f) + c.EmbeddedGrafana.RegisterFlags(f) c.v2Experiment = os.Getenv("PYROSCOPE_V2_EXPERIMENT") != "" if c.v2Experiment { @@ -346,6 +351,7 @@ func (f *Phlare) setupModuleManager() error { mm.RegisterModule(All, nil) mm.RegisterModule(TenantSettings, f.initTenantSettings) mm.RegisterModule(AdHocProfiles, f.initAdHocProfiles) + mm.RegisterModule(EmbeddedGrafana, f.initEmbeddedGrafana) // Add dependencies deps := map[string][]string{ @@ -370,6 +376,7 @@ func (f *Phlare) setupModuleManager() error { Version: {API, MemberlistKV}, TenantSettings: {API, Storage}, AdHocProfiles: {API, Overrides, Storage}, + EmbeddedGrafana: {API}, } // Experimental modules. @@ -454,7 +461,7 @@ func (f *Phlare) Run() error { } // Start profiling when Pyroscope is ready - if !f.Cfg.SelfProfiling.DisablePush && f.Cfg.Target.String() == All { + if !f.Cfg.SelfProfiling.DisablePush && slices.Contains(f.Cfg.Target, All) { _, err := pyroscope.Start(pyroscope.Config{ ApplicationName: "pyroscope", ServerAddress: fmt.Sprintf("http://%s:%d", "localhost", f.Cfg.Server.HTTPListenPort),