Skip to content

Commit 2063655

Browse files
authored
feat: Track metrics around usage of the GitHub API (#3273)
1 parent 75bb639 commit 2063655

File tree

12 files changed

+222
-70
lines changed

12 files changed

+222
-70
lines changed

pkg/clientpool/ingester_client_pool.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ func (f *ingesterPoolFactory) FromInstance(inst ring.InstanceDesc) (ring_client.
5959
if err != nil {
6060
return nil, err
6161
}
62+
63+
httpClient := util.InstrumentedDefaultHTTPClient(util.WithTracingTransport())
6264
return &ingesterPoolClient{
63-
IngesterServiceClient: ingesterv1connect.NewIngesterServiceClient(util.InstrumentedHTTPClient(), "http://"+inst.Addr, f.options...),
65+
IngesterServiceClient: ingesterv1connect.NewIngesterServiceClient(httpClient, "http://"+inst.Addr, f.options...),
6466
HealthClient: grpc_health_v1.NewHealthClient(conn),
6567
Closer: conn,
6668
}, nil

pkg/clientpool/store_gateway_client_pool.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ func (f *storeGatewayPoolFactory) FromInstance(inst ring.InstanceDesc) (ring_cli
4444
if err != nil {
4545
return nil, err
4646
}
47+
48+
httpClient := util.InstrumentedDefaultHTTPClient(util.WithTracingTransport())
4749
return &storeGatewayPoolClient{
48-
StoreGatewayServiceClient: storegatewayv1connect.NewStoreGatewayServiceClient(util.InstrumentedHTTPClient(), "http://"+inst.Addr, f.options...),
50+
StoreGatewayServiceClient: storegatewayv1connect.NewStoreGatewayServiceClient(httpClient, "http://"+inst.Addr, f.options...),
4951
HealthClient: grpc_health_v1.NewHealthClient(conn),
5052
Closer: conn,
5153
}, nil

pkg/querier/querier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func New(params *NewQuerierParams) (*Querier, error) {
131131
params.IngestersRing,
132132
),
133133
storeGatewayQuerier: storeGatewayQuerier,
134-
VCSServiceHandler: vcs.New(params.Logger),
134+
VCSServiceHandler: vcs.New(params.Logger, params.Reg),
135135
storageBucket: params.StorageBucket,
136136
tenantConfigProvider: params.CfgProvider,
137137
limits: params.Overrides,

pkg/querier/vcs/client/github.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import (
1515
)
1616

1717
// GithubClient returns a github client.
18-
func GithubClient(ctx context.Context, token *oauth2.Token) (*githubClient, error) {
18+
func GithubClient(ctx context.Context, token *oauth2.Token, client *http.Client) (*githubClient, error) {
1919
return &githubClient{
20-
client: github.NewClient(nil).WithAuthToken(token.AccessToken),
20+
client: github.NewClient(client).WithAuthToken(token.AccessToken),
2121
}, nil
2222
}
2323

@@ -30,6 +30,7 @@ func (gh *githubClient) GetCommit(ctx context.Context, owner, repo, ref string)
3030
if err != nil {
3131
return nil, err
3232
}
33+
3334
return &vcsv1.GetCommitResponse{
3435
Sha: toString(commit.SHA),
3536
Message: toString(commit.Commit.Message),
@@ -45,25 +46,30 @@ func (gh *githubClient) GetFile(ctx context.Context, req FileRequest) (File, err
4546
// We could abstract away git provider using git protocol
4647
// git clone https://x-access-token:<token>@github.com/owner/repo.git
4748
// For now we use the github client.
49+
4850
file, _, _, err := gh.client.Repositories.GetContents(ctx, req.Owner, req.Repo, req.Path, &github.RepositoryContentGetOptions{Ref: req.Ref})
4951
if err != nil {
5052
var githubErr *github.ErrorResponse
51-
if ok := errors.As(err, &githubErr); ok && githubErr.Response.StatusCode == http.StatusNotFound {
53+
if errors.As(err, &githubErr) && githubErr.Response.StatusCode == http.StatusNotFound {
5254
return File{}, fmt.Errorf("%w: %s", ErrNotFound, err)
5355
}
5456
return File{}, err
5557
}
58+
5659
if file == nil {
5760
return File{}, ErrNotFound
5861
}
62+
5963
// We only support files retrieval.
6064
if file.Type != nil && *file.Type != "file" {
6165
return File{}, connect.NewError(connect.CodeInvalidArgument, errors.New("path is not a file"))
6266
}
67+
6368
content, err := file.GetContent()
6469
if err != nil {
6570
return File{}, err
6671
}
72+
6773
return File{
6874
Content: content,
6975
URL: toString(file.HTMLURL),

pkg/querier/vcs/client/metrics.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"regexp"
7+
"time"
8+
9+
"github.com/go-kit/log"
10+
"github.com/go-kit/log/level"
11+
"github.com/prometheus/client_golang/prometheus"
12+
"github.com/prometheus/client_golang/prometheus/promauto"
13+
14+
"github.com/grafana/pyroscope/pkg/util"
15+
)
16+
17+
var (
18+
githubRouteMatchers = map[string]*regexp.Regexp{
19+
// Get repository contents.
20+
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content
21+
"/repos/{owner}/{repo}/contents/{path}": regexp.MustCompile(`^\/repos\/\S+\/\S+\/contents\/\S+$`),
22+
23+
// Get a commit.
24+
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit
25+
"/repos/{owner}/{repo}/commits/{ref}": regexp.MustCompile(`^\/repos\/\S+\/\S+\/commits\/\S+$`),
26+
27+
// Refresh auth token.
28+
// https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token
29+
"/login/oauth/access_token": regexp.MustCompile(`^\/login\/oauth\/access_token$`),
30+
}
31+
)
32+
33+
func InstrumentedHTTPClient(logger log.Logger, reg prometheus.Registerer) *http.Client {
34+
apiDuration := promauto.With(reg).NewHistogramVec(
35+
prometheus.HistogramOpts{
36+
Namespace: "pyroscope",
37+
Name: "vcs_github_request_duration",
38+
Help: "Duration of GitHub API requests in seconds",
39+
Buckets: prometheus.ExponentialBucketsRange(0.1, 10, 8),
40+
},
41+
[]string{"method", "route", "status_code"},
42+
)
43+
44+
defaultClient := &http.Client{
45+
Timeout: 10 * time.Second,
46+
Transport: http.DefaultTransport,
47+
}
48+
client := util.InstrumentedHTTPClient(defaultClient, withGitHubMetricsTransport(logger, apiDuration))
49+
return client
50+
}
51+
52+
// withGitHubMetricsTransport wraps a transport with a client to track GitHub
53+
// API usage.
54+
func withGitHubMetricsTransport(logger log.Logger, hv *prometheus.HistogramVec) util.RoundTripperInstrumentFunc {
55+
return func(next http.RoundTripper) http.RoundTripper {
56+
return util.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
57+
route := matchGitHubAPIRoute(req.URL.Path)
58+
statusCode := ""
59+
start := time.Now()
60+
61+
res, err := next.RoundTrip(req)
62+
if err == nil {
63+
statusCode = fmt.Sprintf("%d", res.StatusCode)
64+
}
65+
66+
if route == "unknown_route" {
67+
level.Warn(logger).Log("path", req.URL.Path, "msg", "unknown GitHub API route")
68+
}
69+
hv.WithLabelValues(req.Method, route, statusCode).Observe(time.Since(start).Seconds())
70+
71+
return res, err
72+
})
73+
}
74+
}
75+
76+
func matchGitHubAPIRoute(path string) string {
77+
for route, regex := range githubRouteMatchers {
78+
if regex.MatchString(path) {
79+
return route
80+
}
81+
}
82+
83+
return "unknown_route"
84+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package client
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func Test_matchGitHubAPIRoute(t *testing.T) {
10+
tests := []struct {
11+
Name string
12+
Path string
13+
Want string
14+
}{
15+
{
16+
Name: "GetContents",
17+
Path: "/repos/grafana/pyroscope/contents/pkg/querier/querier.go",
18+
Want: "/repos/{owner}/{repo}/contents/{path}",
19+
},
20+
{
21+
Name: "GetContents with dash",
22+
Path: "/repos/connectrpc/connect-go/contents/protocol.go",
23+
Want: "/repos/{owner}/{repo}/contents/{path}",
24+
},
25+
{
26+
Name: "GetContents without path",
27+
Path: "/repos/grafana/pyroscope/contents/",
28+
Want: "unknown_route",
29+
},
30+
{
31+
Name: "GetContents with whitespace in path",
32+
Path: "/repos/grafana/pyroscope/contents/path with spaces",
33+
Want: "unknown_route",
34+
},
35+
{
36+
Name: "GetCommit",
37+
Path: "/repos/grafana/pyroscope/commits/abcdef1234567890",
38+
Want: "/repos/{owner}/{repo}/commits/{ref}",
39+
},
40+
{
41+
Name: "GetCommit with lowercase and uppercase ref",
42+
Path: "/repos/grafana/pyroscope/commits/abcdefABCDEF1234567890",
43+
Want: "/repos/{owner}/{repo}/commits/{ref}",
44+
},
45+
{
46+
Name: "GetCommit with non-hexadecimal ref",
47+
Path: "/repos/grafana/pyroscope/commits/HEAD",
48+
Want: "/repos/{owner}/{repo}/commits/{ref}",
49+
},
50+
{
51+
Name: "GetCommit without commit",
52+
Path: "/repos/grafana/pyroscope/commits/",
53+
Want: "unknown_route",
54+
},
55+
{
56+
Name: "Refresh",
57+
Path: "/login/oauth/access_token",
58+
Want: "/login/oauth/access_token",
59+
},
60+
{
61+
Name: "empty path",
62+
Path: "",
63+
Want: "unknown_route",
64+
},
65+
{
66+
Name: "unmapped path",
67+
Path: "/some/random/path",
68+
Want: "unknown_route",
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.Name, func(t *testing.T) {
74+
got := matchGitHubAPIRoute(tt.Path)
75+
require.Equal(t, tt.Want, got)
76+
})
77+
}
78+
}

pkg/querier/vcs/github.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ func githubOAuthConfig() (*oauth2.Config, error) {
6565

6666
// refreshGithubToken sends a request configured for the GitHub API and marshals
6767
// the response into a githubAuthToken.
68-
func refreshGithubToken(req *http.Request) (*githubAuthToken, error) {
69-
client := http.Client{
70-
Timeout: 10 * time.Second,
71-
}
68+
func refreshGithubToken(req *http.Request, client *http.Client) (*githubAuthToken, error) {
7269
res, err := client.Do(req)
7370
if err != nil {
7471
return nil, fmt.Errorf("failed to make request: %w", err)

pkg/querier/vcs/github.md

Lines changed: 0 additions & 41 deletions
This file was deleted.

pkg/querier/vcs/github_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func Test_refreshGithubToken(t *testing.T) {
6666
req, err := http.NewRequest("POST", fakeGithubAPI.URL, nil)
6767
require.NoError(t, err)
6868

69-
got, err := refreshGithubToken(req)
69+
got, err := refreshGithubToken(req, http.DefaultClient)
7070
require.NoError(t, err)
7171
require.Equal(t, want, *got)
7272
}

pkg/querier/vcs/service.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/go-kit/log"
1212
giturl "github.com/kubescape/go-git-url"
1313
"github.com/kubescape/go-git-url/apis"
14+
"github.com/prometheus/client_golang/prometheus"
1415
"golang.org/x/oauth2"
1516

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

2425
type Service struct {
25-
logger log.Logger
26+
logger log.Logger
27+
httpClient *http.Client
2628
}
2729

28-
func New(logger log.Logger) *Service {
30+
func New(logger log.Logger, reg prometheus.Registerer) *Service {
31+
httpClient := client.InstrumentedHTTPClient(logger, reg)
32+
2933
return &Service{
30-
logger: logger,
34+
logger: logger,
35+
httpClient: httpClient,
3136
}
3237
}
3338

@@ -81,7 +86,7 @@ func (q *Service) GithubRefresh(ctx context.Context, req *connect.Request[vcsv1.
8186
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to refresh token"))
8287
}
8388

84-
githubToken, err := refreshGithubToken(githubRequest)
89+
githubToken, err := refreshGithubToken(githubRequest, q.httpClient)
8590
if err != nil {
8691
q.logger.Log("err", err, "msg", "failed to refresh token with GitHub")
8792
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to refresh token"))
@@ -130,7 +135,7 @@ func (q *Service) GetFile(ctx context.Context, req *connect.Request[vcsv1.GetFil
130135
}
131136

132137
// todo: we can support multiple provider: bitbucket, gitlab, etc.
133-
ghClient, err := client.GithubClient(ctx, token)
138+
ghClient, err := client.GithubClient(ctx, token, q.httpClient)
134139
if err != nil {
135140
return nil, err
136141
}
@@ -173,7 +178,7 @@ func (q *Service) GetCommit(ctx context.Context, req *connect.Request[vcsv1.GetC
173178
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("only GitHub repositories are supported"))
174179
}
175180

176-
ghClient, err := client.GithubClient(ctx, token)
181+
ghClient, err := client.GithubClient(ctx, token, q.httpClient)
177182
if err != nil {
178183
return nil, err
179184
}

0 commit comments

Comments
 (0)