Skip to content

Commit a45829a

Browse files
committed
feat: Track git provider API usage metrics
Every time we access a Git provider's API we may use some of that provider's API limits. Medium-sized providers may have many repositories using PaC but may not have the computer overhead to support permissive API rate limits. In some cases we have seen pipelines delayed due to API rate limiting. By tracking the Git providers' API load by PaC we can gain insight into potential redundancies, hot spots, and optimizations to the Git provider API use. Since the Git provider API clients do not expose these metrics and are not designed in a way that is easy to decorate, the closest proxy to tracking actual API calls is tracking accesses to the API client itself. Since one client-method call equates to one API call, this metric should track the API usage well. However if a developer saves a reference to the API client then they will be able to make API calls without the metric incrementing; we will have to watch out for this if we want the metric to be accurate.
1 parent dbbc100 commit a45829a

File tree

11 files changed

+292
-19
lines changed

11 files changed

+292
-19
lines changed

docs/content/docs/install/metrics.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ You can configure these exporters by referring to the [observability configurati
1212

1313
| Name | Type | Description |
1414
|------------------------------------------------------|---------|--------------------------------------------------------------------|
15+
| `pipelines_as_code_git_provider_api_request_count` | Counter | Number of API requests submitted to git providers |
1516
| `pipelines_as_code_pipelinerun_count` | Counter | Number of pipelineruns created by pipelines-as-code |
1617
| `pipelines_as_code_pipelinerun_duration_seconds_sum` | Counter | Number of seconds all pipelineruns have taken in pipelines-as-code |
1718
| `pipelines_as_code_running_pipelineruns_count` | Gauge | Number of running pipelineruns in pipelines-as-code |

pkg/metrics/metrics.go

+54-10
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ var runningPRCount = stats.Float64("pipelines_as_code_running_pipelineruns_count
3030
"number of running pipelineruns by pipelines as code",
3131
stats.UnitDimensionless)
3232

33+
var gitProviderAPIRequestCount = stats.Int64(
34+
"pipelines_as_code_git_provider_api_request_count",
35+
"number of API requests from pipelines as code to git providers",
36+
stats.UnitDimensionless,
37+
)
38+
3339
// Recorder holds keys for metrics.
3440
type Recorder struct {
3541
initialized bool
@@ -61,36 +67,42 @@ func NewRecorder() (*Recorder, error) {
6167

6268
provider, errRegistering := tag.NewKey("provider")
6369
if errRegistering != nil {
70+
ErrRegistering = errRegistering
6471
return
6572
}
6673
R.provider = provider
6774

6875
eventType, errRegistering := tag.NewKey("event-type")
6976
if errRegistering != nil {
77+
ErrRegistering = errRegistering
7078
return
7179
}
7280
R.eventType = eventType
7381

7482
namespace, errRegistering := tag.NewKey("namespace")
7583
if errRegistering != nil {
84+
ErrRegistering = errRegistering
7685
return
7786
}
7887
R.namespace = namespace
7988

8089
repository, errRegistering := tag.NewKey("repository")
8190
if errRegistering != nil {
91+
ErrRegistering = errRegistering
8292
return
8393
}
8494
R.repository = repository
8595

8696
status, errRegistering := tag.NewKey("status")
8797
if errRegistering != nil {
98+
ErrRegistering = errRegistering
8899
return
89100
}
90101
R.status = status
91102

92103
reason, errRegistering := tag.NewKey("reason")
93104
if errRegistering != nil {
105+
ErrRegistering = errRegistering
94106
return
95107
}
96108
R.reason = reason
@@ -116,11 +128,18 @@ func NewRecorder() (*Recorder, error) {
116128
Aggregation: view.LastValue(),
117129
TagKeys: []tag.Key{R.namespace, R.repository},
118130
}
131+
gitProviderAPIRequestView = &view.View{
132+
Description: gitProviderAPIRequestCount.Description(),
133+
Measure: gitProviderAPIRequestCount,
134+
Aggregation: view.Count(),
135+
TagKeys: []tag.Key{R.provider, R.eventType, R.namespace, R.repository},
136+
}
119137
)
120138

121-
view.Unregister(prCountView, prDurationView, runningPRView)
122-
errRegistering = view.Register(prCountView, prDurationView, runningPRView)
139+
view.Unregister(prCountView, prDurationView, runningPRView, gitProviderAPIRequestView)
140+
errRegistering = view.Register(prCountView, prDurationView, runningPRView, gitProviderAPIRequestView)
123141
if errRegistering != nil {
142+
ErrRegistering = errRegistering
124143
R.initialized = false
125144
return
126145
}
@@ -129,12 +148,19 @@ func NewRecorder() (*Recorder, error) {
129148
return R, ErrRegistering
130149
}
131150

132-
// Count logs number of times a pipelinerun is ran for a provider.
133-
func (r *Recorder) Count(provider, event, namespace, repository string) error {
151+
func (r Recorder) assertInitialized() error {
134152
if !r.initialized {
135153
return fmt.Errorf(
136154
"ignoring the metrics recording for pipelineruns, failed to initialize the metrics recorder")
137155
}
156+
return nil
157+
}
158+
159+
// Count logs number of times a pipelinerun is ran for a provider.
160+
func (r *Recorder) Count(provider, event, namespace, repository string) error {
161+
if err := r.assertInitialized(); err != nil {
162+
return err
163+
}
138164

139165
ctx, err := tag.New(
140166
context.Background(),
@@ -153,9 +179,8 @@ func (r *Recorder) Count(provider, event, namespace, repository string) error {
153179

154180
// CountPRDuration collects duration taken by a pipelinerun in seconds accumulate them in prDurationCount.
155181
func (r *Recorder) CountPRDuration(namespace, repository, status, reason string, duration time.Duration) error {
156-
if !r.initialized {
157-
return fmt.Errorf(
158-
"ignoring the metrics recording for pipelineruns, failed to initialize the metrics recorder")
182+
if err := r.assertInitialized(); err != nil {
183+
return err
159184
}
160185

161186
ctx, err := tag.New(
@@ -175,9 +200,8 @@ func (r *Recorder) CountPRDuration(namespace, repository, status, reason string,
175200

176201
// RunningPipelineRuns emits the number of running PipelineRuns for a repository and namespace.
177202
func (r *Recorder) RunningPipelineRuns(namespace, repository string, runningPRs float64) error {
178-
if !r.initialized {
179-
return fmt.Errorf(
180-
"ignoring the metrics recording for pipelineruns, failed to initialize the metrics recorder")
203+
if err := r.assertInitialized(); err != nil {
204+
return err
181205
}
182206

183207
ctx, err := tag.New(
@@ -266,6 +290,26 @@ func (r *Recorder) ReportRunningPipelineRuns(ctx context.Context, lister listers
266290
}
267291
}
268292

293+
func (r *Recorder) ReportGitProviderAPIUsage(provider, event, namespace, repository string) error {
294+
if err := r.assertInitialized(); err != nil {
295+
return err
296+
}
297+
298+
ctx, err := tag.New(
299+
context.Background(),
300+
tag.Insert(r.provider, provider),
301+
tag.Insert(r.eventType, event),
302+
tag.Insert(r.namespace, namespace),
303+
tag.Insert(r.repository, repository),
304+
)
305+
if err != nil {
306+
return err
307+
}
308+
309+
metrics.Record(ctx, gitProviderAPIRequestCount.M(1))
310+
return nil
311+
}
312+
269313
func ResetRecorder() {
270314
Once = sync.Once{}
271315
R = nil

pkg/provider/bitbucketcloud/bitbucket.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1212
"github.com/openshift-pipelines/pipelines-as-code/pkg/changedfiles"
1313
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
14+
"github.com/openshift-pipelines/pipelines-as-code/pkg/metrics"
1415
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1516
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
1617
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
@@ -24,17 +25,43 @@ var _ provider.Interface = (*Provider)(nil)
2425
type Provider struct {
2526
bbClient *bitbucket.Client
2627
Logger *zap.SugaredLogger
28+
metrics *metrics.Recorder
2729
run *params.Run
2830
pacInfo *info.PacOpts
2931
Token, APIURL *string
3032
Username *string
3133
provenance string
34+
repo *v1alpha1.Repository
35+
triggerEvent string
3236
}
3337

3438
func (v Provider) Client() *bitbucket.Client {
39+
v.recordAPIUsageMetrics()
3540
return v.bbClient
3641
}
3742

43+
func (v *Provider) recordAPIUsageMetrics() {
44+
if v.metrics == nil {
45+
m, err := metrics.NewRecorder()
46+
if err != nil {
47+
v.Logger.Errorf("Error initializing bitbucketcloud metrics recorder: %v", err)
48+
return
49+
}
50+
v.metrics = m
51+
}
52+
53+
name := ""
54+
namespace := ""
55+
if v.repo != nil {
56+
name = v.repo.Name
57+
namespace = v.repo.Namespace
58+
}
59+
60+
if err := v.metrics.ReportGitProviderAPIUsage("bitbucketcloud", v.triggerEvent, namespace, name); err != nil {
61+
v.Logger.Errorf("Error reporting git API usage metrics: %v", err)
62+
}
63+
}
64+
3865
// CheckPolicyAllowing TODO: Implement ME.
3966
func (v *Provider) CheckPolicyAllowing(_ context.Context, _ *info.Event, _ []string) (bool, string) {
4067
return false, ""
@@ -180,7 +207,7 @@ func (v *Provider) GetFileInsideRepo(_ context.Context, event *info.Event, path,
180207
return v.getBlob(event, revision, path)
181208
}
182209

183-
func (v *Provider) SetClient(_ context.Context, run *params.Run, event *info.Event, _ *v1alpha1.Repository, _ *events.EventEmitter) error {
210+
func (v *Provider) SetClient(_ context.Context, run *params.Run, event *info.Event, repo *v1alpha1.Repository, _ *events.EventEmitter) error {
184211
if event.Provider.Token == "" {
185212
return fmt.Errorf("no git_provider.secret has been set in the repo crd")
186213
}
@@ -191,6 +218,8 @@ func (v *Provider) SetClient(_ context.Context, run *params.Run, event *info.Eve
191218
v.Token = &event.Provider.Token
192219
v.Username = &event.Provider.User
193220
v.run = run
221+
v.repo = repo
222+
v.triggerEvent = event.EventType
194223
return nil
195224
}
196225

pkg/provider/bitbucketdatacenter/bitbucketdatacenter.go

+31-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1717
"github.com/openshift-pipelines/pipelines-as-code/pkg/changedfiles"
1818
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
19+
"github.com/openshift-pipelines/pipelines-as-code/pkg/metrics"
1920
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
2021
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
2122
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
@@ -34,6 +35,7 @@ type Provider struct {
3435
bbClient *bbv1.APIClient // temporarily keeping it after the refactor finishes, will be removed
3536
scmClient *scm.Client
3637
Logger *zap.SugaredLogger
38+
metrics *metrics.Recorder
3739
run *params.Run
3840
pacInfo *info.PacOpts
3941
baseURL string
@@ -42,9 +44,12 @@ type Provider struct {
4244
apiURL string
4345
provenance string
4446
projectKey string
47+
repo *v1alpha1.Repository
48+
triggerEvent string
4549
}
4650

4751
func (v Provider) Client() *bbv1.APIClient {
52+
v.recordAPIUsageMetrics()
4853
return v.bbClient
4954
}
5055

@@ -53,13 +58,36 @@ func (v *Provider) SetBitBucketClient(client *bbv1.APIClient) {
5358
}
5459

5560
func (v Provider) ScmClient() *scm.Client {
61+
v.recordAPIUsageMetrics()
5662
return v.scmClient
5763
}
5864

5965
func (v *Provider) SetScmClient(client *scm.Client) {
6066
v.scmClient = client
6167
}
6268

69+
func (v *Provider) recordAPIUsageMetrics() {
70+
if v.metrics == nil {
71+
m, err := metrics.NewRecorder()
72+
if err != nil {
73+
v.Logger.Errorf("Error initializing bitbucketserver metrics recorder: %v", err)
74+
return
75+
}
76+
v.metrics = m
77+
}
78+
79+
name := ""
80+
namespace := ""
81+
if v.repo != nil {
82+
name = v.repo.Name
83+
namespace = v.repo.Namespace
84+
}
85+
86+
if err := v.metrics.ReportGitProviderAPIUsage("bitbucketserver", v.triggerEvent, namespace, name); err != nil {
87+
v.Logger.Errorf("Error reporting git API usage metrics: %v", err)
88+
}
89+
}
90+
6391
func (v *Provider) SetPacInfo(pacInfo *info.PacOpts) {
6492
v.pacInfo = pacInfo
6593
}
@@ -273,7 +301,7 @@ func removeLastSegment(urlStr string) string {
273301
return u.String()
274302
}
275303

276-
func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.Event, _ *v1alpha1.Repository, _ *events.EventEmitter) error {
304+
func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.Event, repo *v1alpha1.Repository, _ *events.EventEmitter) error {
277305
if event.Provider.User == "" {
278306
return fmt.Errorf("no spec.git_provider.user has been set in the repo crd")
279307
}
@@ -318,6 +346,8 @@ func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.E
318346
v.scmClient = client
319347
}
320348
v.run = run
349+
v.repo = repo
350+
v.triggerEvent = event.EventType
321351
_, resp, err := v.ScmClient().Users.FindLogin(ctx, event.Provider.User)
322352
if resp != nil && resp.Status == http.StatusUnauthorized {
323353
return fmt.Errorf("cannot get user %s with token: %w", event.Provider.User, err)

pkg/provider/gitea/gitea.go

+27
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1515
"github.com/openshift-pipelines/pipelines-as-code/pkg/changedfiles"
1616
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
17+
"github.com/openshift-pipelines/pipelines-as-code/pkg/metrics"
1718
"github.com/openshift-pipelines/pipelines-as-code/pkg/opscomments"
1819
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1920
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
@@ -45,6 +46,7 @@ var _ provider.Interface = (*Provider)(nil)
4546
type Provider struct {
4647
giteaClient *gitea.Client
4748
Logger *zap.SugaredLogger
49+
metrics *metrics.Recorder
4850
pacInfo *info.PacOpts
4951
Token *string
5052
giteaInstanceURL string
@@ -53,16 +55,40 @@ type Provider struct {
5355
repo *v1alpha1.Repository
5456
eventEmitter *events.EventEmitter
5557
run *params.Run
58+
triggerEvent string
5659
}
5760

5861
func (v Provider) Client() *gitea.Client {
62+
v.recordAPIUsageMetrics()
5963
return v.giteaClient
6064
}
6165

6266
func (v *Provider) SetGiteaClient(client *gitea.Client) {
6367
v.giteaClient = client
6468
}
6569

70+
func (v *Provider) recordAPIUsageMetrics() {
71+
if v.metrics == nil {
72+
m, err := metrics.NewRecorder()
73+
if err != nil {
74+
v.Logger.Errorf("Error initializing gitea metrics recorder: %v", err)
75+
return
76+
}
77+
v.metrics = m
78+
}
79+
80+
name := ""
81+
namespace := ""
82+
if v.repo != nil {
83+
name = v.repo.Name
84+
namespace = v.repo.Namespace
85+
}
86+
87+
if err := v.metrics.ReportGitProviderAPIUsage(v.giteaInstanceURL, v.triggerEvent, namespace, name); err != nil {
88+
v.Logger.Errorf("Error reporting git API usage metrics: %v", err)
89+
}
90+
}
91+
6692
func (v *Provider) SetPacInfo(pacInfo *info.PacOpts) {
6793
v.pacInfo = pacInfo
6894
}
@@ -118,6 +144,7 @@ func (v *Provider) SetClient(_ context.Context, run *params.Run, runevent *info.
118144
v.eventEmitter = emitter
119145
v.repo = repo
120146
v.run = run
147+
v.triggerEvent = runevent.EventType
121148
return nil
122149
}
123150

0 commit comments

Comments
 (0)