Skip to content

Commit

Permalink
feat: Embed a grafana and explore-profiles instance into pyroscope (#…
Browse files Browse the repository at this point in the history
…3428)

* feat: Embed a grafana instance into pyroscope

This provides an optional target `embedded-grafana` to
download/configure/run a dedicated Grafana instance with Explore
Profiles deployed.

This is meant not to be used as production, but useful to quickly
discover profiles.

After this PR merges a user can run this:

```
$ docker run -p 4040:4040 -p 4041:4041 grafana/pyroscope --target all,embedded-grafana
```

And on localhost:4040 there will the classic pyroscope and on :4041,
there will be Grafana with Explore Profiles.

* Update explore profiles to v0.1.5

* Update contribution guide to mention embedded Grafana
  • Loading branch information
simonswine committed Aug 21, 2024
1 parent c712e86 commit 336d198
Show file tree
Hide file tree
Showing 8 changed files with 645 additions and 3 deletions.
6 changes: 6 additions & 0 deletions cmd/pyroscope/help-all.txt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions cmd/pyroscope/help.txt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion docs/internal/contributing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,19 @@ analytics:
# Prints the application banner at startup.
# CLI flag: -config.show_banner
[show_banner: <boolean> | default = true]

embedded_grafana:
# The directory where the Grafana data will be stored.
# CLI flag: -embedded-grafana.data-path
[data_path: <string> | default = "./data/__embedded_grafana/"]

# The port on which the Grafana will listen.
# CLI flag: -embedded-grafana.listen-port
[listen_port: <int> | default = 4041]

# The URL of the Pyroscope instance to use for the Grafana datasources.
# CLI flag: -embedded-grafana.pyroscope-url
[pyroscope_url: <string> | default = "http://localhost:4040"]
```
### server
Expand Down
256 changes: 256 additions & 0 deletions pkg/embedded/grafana/assets.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 336d198

Please sign in to comment.