Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Track metrics around usage of the GitHub API #3273

Merged
merged 8 commits into from
May 20, 2024
2 changes: 1 addition & 1 deletion pkg/querier/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func New(params *NewQuerierParams) (*Querier, error) {
params.IngestersRing,
),
storeGatewayQuerier: storeGatewayQuerier,
VCSServiceHandler: vcs.New(params.Logger),
VCSServiceHandler: vcs.New(params.Logger, params.Reg),
storageBucket: params.StorageBucket,
tenantConfigProvider: params.CfgProvider,
}
Expand Down
24 changes: 18 additions & 6 deletions pkg/querier/vcs/client/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,26 @@ import (
)

// GithubClient returns a github client.
func GithubClient(ctx context.Context, token *oauth2.Token) (*githubClient, error) {
func GithubClient(ctx context.Context, token *oauth2.Token, metrics Metrics) (*githubClient, error) {
return &githubClient{
client: github.NewClient(nil).WithAuthToken(token.AccessToken),
client: github.NewClient(nil).WithAuthToken(token.AccessToken),
simonswine marked this conversation as resolved.
Show resolved Hide resolved
metrics: metrics,
}, nil
}

type githubClient struct {
client *github.Client
client *github.Client
metrics Metrics
}

func (gh *githubClient) GetCommit(ctx context.Context, owner, repo, ref string) (*vcsv1.GetCommitResponse, error) {
commit, _, err := gh.client.Repositories.GetCommit(ctx, owner, repo, ref, nil)
start := time.Now()
commit, res, err := gh.client.Repositories.GetCommit(ctx, owner, repo, ref, nil)
gh.metrics.GetCommitObserve(time.Since(start), res, err)
if err != nil {
return nil, err
}

return &vcsv1.GetCommitResponse{
Sha: toString(commit.SHA),
Message: toString(commit.Commit.Message),
Expand All @@ -45,25 +50,32 @@ func (gh *githubClient) GetFile(ctx context.Context, req FileRequest) (File, err
// We could abstract away git provider using git protocol
// git clone https://x-access-token:<token>@github.com/owner/repo.git
// For now we use the github client.
file, _, _, err := gh.client.Repositories.GetContents(ctx, req.Owner, req.Repo, req.Path, &github.RepositoryContentGetOptions{Ref: req.Ref})

start := time.Now()
file, _, res, err := gh.client.Repositories.GetContents(ctx, req.Owner, req.Repo, req.Path, &github.RepositoryContentGetOptions{Ref: req.Ref})
gh.metrics.GetFileObserve(time.Since(start), res, err)
if err != nil {
var githubErr *github.ErrorResponse
if ok := errors.As(err, &githubErr); ok && githubErr.Response.StatusCode == http.StatusNotFound {
if errors.As(err, &githubErr) && githubErr.Response.StatusCode == http.StatusNotFound {
return File{}, fmt.Errorf("%w: %s", ErrNotFound, err)
}
return File{}, err
}

if file == nil {
return File{}, ErrNotFound
}

// We only support files retrieval.
if file.Type != nil && *file.Type != "file" {
return File{}, connect.NewError(connect.CodeInvalidArgument, errors.New("path is not a file"))
}

content, err := file.GetContent()
if err != nil {
return File{}, err
}

return File{
Content: content,
URL: toString(file.HTMLURL),
Expand Down
103 changes: 103 additions & 0 deletions pkg/querier/vcs/client/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package client

import (
"fmt"
"time"

"github.com/google/go-github/v58/github"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

// Metrics can record events surrounding various client actions, like logging in
// or fetching a file.
type Metrics interface {
LoginObserve(elapsed time.Duration, err error)
RefreshObserve(elapsed time.Duration, err error)
GetCommitObserve(elapsed time.Duration, res *github.Response, err error)
GetFileObserve(elapsed time.Duration, res *github.Response, err error)
}

func NewMetrics(reg prometheus.Registerer) Metrics {
return &githubMetrics{
APIDuration: promauto.With(reg).NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "pyroscope",
Name: "vcs_github_request_duration",
Help: "Duration of GitHub API requests in seconds",
Buckets: prometheus.ExponentialBucketsRange(0.1, 10, 8),
},
[]string{"path", "status"},
),
APIRateLimit: promauto.With(reg).NewGauge(prometheus.GaugeOpts{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that this metric might not be too helpful/accurate:

My understanding is that we are subject to rate limits of the github user that authenticated via oauth2. So if we have two github users using the service at the same time we would constantly have a jumping value, between those two values of remaining requests.

I do think this is something important to watch. Maybe this could be a histogram with fitting buckets, that would show us how close to being rate limited we got.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right here. The rate limit remaining here is per-user (doc) since we're using a user token. If we had an installation token, the rate limit would be per app.

How I'm using the metric here is not helpful at all. Like you said, it's going to jump to whatever remaining rate limit the last token had. I'm going to rethink how to track this, as a gauge is clearly not the right approach.

Namespace: "pyroscope",
Name: "vcs_github_rate_limit",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Name: "vcs_github_rate_limit",
Name: "vcs_github_remaining_request_quota",

Help: "Remaining GitHub API requests before rate limiting occurs",
}),
}
}

type githubMetrics struct {
APIDuration *prometheus.HistogramVec
APIRateLimit prometheus.Gauge
}

func (m *githubMetrics) LoginObserve(elapsed time.Duration, err error) {
// We technically don't know the true status codes of the OAuth login flow,
// but we'll assume "no error" means 200 and "an error" means 400. A 400 is
// chosen because that's what we report to the user.
status := "200"
if err != nil {
status = "400"
}

m.APIDuration.
WithLabelValues("/login/oauth/authorize", status).
Observe(elapsed.Seconds())
}

func (m *githubMetrics) RefreshObserve(elapsed time.Duration, err error) {
// We technically don't know the true status codes of the OAuth login flow,
// but we'll assume "no error" means 200 and "an error" means 400. A 400 is
// chosen because that's what we report to the user.
status := "200"
if err != nil {
status = "400"
}

m.APIDuration.
WithLabelValues("/login/oauth/access_token", status).
Observe(elapsed.Seconds())
}

func (m *githubMetrics) GetCommitObserve(elapsed time.Duration, res *github.Response, err error) {
var status string
if res != nil {
status = fmt.Sprintf("%d", res.StatusCode)
m.APIRateLimit.Set(float64(res.Rate.Remaining))
}

if err != nil {
status = "500"
}

m.APIDuration.
WithLabelValues("/repos/{owner}/{repo}/commits/{ref}", status).
Observe(elapsed.Seconds())
}

func (m *githubMetrics) GetFileObserve(elapsed time.Duration, res *github.Response, err error) {
var status string
if res != nil {
status = fmt.Sprintf("%d", res.StatusCode)
m.APIRateLimit.Set(float64(res.Rate.Remaining))
}

if err != nil {
status = "500"
}

m.APIDuration.
WithLabelValues("/repos/{owner}/{repo}/contents/{path}", status).
Observe(elapsed.Seconds())
}
41 changes: 0 additions & 41 deletions pkg/querier/vcs/github.md

This file was deleted.

17 changes: 12 additions & 5 deletions pkg/querier/vcs/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/go-kit/log"
giturl "github.com/kubescape/go-git-url"
"github.com/kubescape/go-git-url/apis"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/oauth2"

vcsv1 "github.com/grafana/pyroscope/api/gen/proto/go/vcs/v1"
Expand All @@ -22,12 +23,14 @@ import (
var _ vcsv1connect.VCSServiceHandler = (*Service)(nil)

type Service struct {
logger log.Logger
logger log.Logger
metrics client.Metrics
}

func New(logger log.Logger) *Service {
func New(logger log.Logger, reg prometheus.Registerer) *Service {
return &Service{
logger: logger,
logger: logger,
metrics: client.NewMetrics(reg),
}
}

Expand All @@ -44,7 +47,9 @@ func (q *Service) GithubLogin(ctx context.Context, req *connect.Request[vcsv1.Gi
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to authorize with GitHub"))
}

start := time.Now()
token, err := cfg.Exchange(ctx, req.Msg.AuthorizationCode)
q.metrics.LoginObserve(time.Since(start), err)
if err != nil {
q.logger.Log("err", err, "msg", "failed to exchange authorization code with GitHub")
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("failed to authorize with GitHub"))
Expand Down Expand Up @@ -75,7 +80,9 @@ func (q *Service) GithubRefresh(ctx context.Context, req *connect.Request[vcsv1.
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to refresh token"))
}

start := time.Now()
githubToken, err := refreshGithubToken(githubRequest)
q.metrics.RefreshObserve(time.Since(start), err)
if err != nil {
q.logger.Log("err", err, "msg", "failed to refresh token with GitHub")
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to refresh token"))
Expand Down Expand Up @@ -118,7 +125,7 @@ func (q *Service) GetFile(ctx context.Context, req *connect.Request[vcsv1.GetFil
}

// todo: we can support multiple provider: bitbucket, gitlab, etc.
ghClient, err := client.GithubClient(ctx, token)
ghClient, err := client.GithubClient(ctx, token, q.metrics)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -161,7 +168,7 @@ func (q *Service) GetCommit(ctx context.Context, req *connect.Request[vcsv1.GetC
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("only GitHub repositories are supported"))
}

ghClient, err := client.GithubClient(ctx, token)
ghClient, err := client.GithubClient(ctx, token, q.metrics)
if err != nil {
return nil, err
}
Expand Down
Loading