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 git provider API usage metrics #2005

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/content/docs/install/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ You can configure these exporters by referring to the [observability configurati

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

**Note:** The metric `pipelines_as_code_git_provider_api_request_count`
is emitted by both the Controller and the Watcher, since both services
use Git providers' APIs. When analyzing this metric, you may need to
combine both services' metrics. For example, using PromQL:

- `sum (pac_controller_pipelines_as_code_git_provider_api_request_count or pac_watcher_pipelines_as_code_git_provider_api_request_count)`
- `sum (rate(pac_controller_pipelines_as_code_git_provider_api_request_count[1m]) or rate(pac_watcher_pipelines_as_code_git_provider_api_request_count[1m]))`
4 changes: 2 additions & 2 deletions pkg/matcher/annotation_matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1350,9 +1350,9 @@ func TestMatchPipelinerunAnnotationAndRepositories(t *testing.T) {
fakeclient, mux, ghTestServerURL, teardown := ghtesthelper.SetupGH()
defer teardown()
vcx := &ghprovider.Provider{
Client: fakeclient,
Token: github.Ptr("None"),
Token: github.Ptr("None"),
}
vcx.SetGithubClient(fakeclient)
if tt.args.runevent.Request == nil {
tt.args.runevent.Request = &info.Request{Header: http.Header{}, Payload: nil}
}
Expand Down
64 changes: 54 additions & 10 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ var runningPRCount = stats.Float64("pipelines_as_code_running_pipelineruns_count
"number of running pipelineruns by pipelines as code",
stats.UnitDimensionless)

var gitProviderAPIRequestCount = stats.Int64(
"pipelines_as_code_git_provider_api_request_count",
"number of API requests from pipelines as code to git providers",
stats.UnitDimensionless,
)

// Recorder holds keys for metrics.
type Recorder struct {
initialized bool
Expand Down Expand Up @@ -61,36 +67,42 @@ func NewRecorder() (*Recorder, error) {

provider, errRegistering := tag.NewKey("provider")
if errRegistering != nil {
ErrRegistering = errRegistering
return
}
R.provider = provider

eventType, errRegistering := tag.NewKey("event-type")
if errRegistering != nil {
ErrRegistering = errRegistering
return
}
R.eventType = eventType

namespace, errRegistering := tag.NewKey("namespace")
if errRegistering != nil {
ErrRegistering = errRegistering
return
}
R.namespace = namespace

repository, errRegistering := tag.NewKey("repository")
if errRegistering != nil {
ErrRegistering = errRegistering
return
}
R.repository = repository

status, errRegistering := tag.NewKey("status")
if errRegistering != nil {
ErrRegistering = errRegistering
return
}
R.status = status

reason, errRegistering := tag.NewKey("reason")
if errRegistering != nil {
ErrRegistering = errRegistering
return
}
R.reason = reason
Expand All @@ -116,11 +128,18 @@ func NewRecorder() (*Recorder, error) {
Aggregation: view.LastValue(),
TagKeys: []tag.Key{R.namespace, R.repository},
}
gitProviderAPIRequestView = &view.View{
Description: gitProviderAPIRequestCount.Description(),
Measure: gitProviderAPIRequestCount,
Aggregation: view.Count(),
TagKeys: []tag.Key{R.provider, R.eventType, R.namespace, R.repository},
Copy link
Member

Choose a reason for hiding this comment

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

would that give the ability to track each provider separately ? i mean is the admin can get insight how many calls has been done for gitlab and for github app when both are used on the same cluster

Copy link
Contributor

Choose a reason for hiding this comment

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

@chmouel Yes, this metric can be filtered using provider tag.

Copy link
Contributor Author

@aThorp96 aThorp96 Mar 27, 2025

Choose a reason for hiding this comment

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

Exactly. Additionally the provider tag for Gitlab and Gitea will use the API URL so you can see insight per provider-instance. Similarly Github will use either github or github-enterprise respectively

}
)

view.Unregister(prCountView, prDurationView, runningPRView)
errRegistering = view.Register(prCountView, prDurationView, runningPRView)
view.Unregister(prCountView, prDurationView, runningPRView, gitProviderAPIRequestView)
errRegistering = view.Register(prCountView, prDurationView, runningPRView, gitProviderAPIRequestView)
if errRegistering != nil {
ErrRegistering = errRegistering
R.initialized = false
return
}
Expand All @@ -129,12 +148,19 @@ func NewRecorder() (*Recorder, error) {
return R, ErrRegistering
}

// Count logs number of times a pipelinerun is ran for a provider.
func (r *Recorder) Count(provider, event, namespace, repository string) error {
func (r Recorder) assertInitialized() error {
if !r.initialized {
return fmt.Errorf(
"ignoring the metrics recording for pipelineruns, failed to initialize the metrics recorder")
}
return nil
}

// Count logs number of times a pipelinerun is ran for a provider.
func (r *Recorder) Count(provider, event, namespace, repository string) error {
if err := r.assertInitialized(); err != nil {
return err
}

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

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

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

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

ctx, err := tag.New(
Expand Down Expand Up @@ -266,6 +290,26 @@ func (r *Recorder) ReportRunningPipelineRuns(ctx context.Context, lister listers
}
}

func (r *Recorder) ReportGitProviderAPIUsage(provider, event, namespace, repository string) error {
if err := r.assertInitialized(); err != nil {
return err
}

ctx, err := tag.New(
Copy link
Contributor

Choose a reason for hiding this comment

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

@aThorp96 at the moment metrics have event tag but user may want to filter out metrics based on event SHA, so it would be better to add it.
Screenshot from 2025-03-20 12-43-33

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Event sha could be useful for debugging. I worry about the cardinality of tags though if we introduce the SHA as a tag. WDYT?

Copy link
Contributor

@zakisk zakisk Mar 26, 2025

Choose a reason for hiding this comment

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

yeah, cardinality could be issue but there is no other way to differentiate that how many API calls were there on an event. for example it has eventType tag but there could be many push events so API request count in this metric will be cumulative in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's fair. I think for that use-case may be better suited for tracing, replaying an event, using a unit or e2e test.

The way I see this being used is to first identify particularly heavy event types and repositories. I think that is useful for an SRE alone, but if an SRE wanted to know how many API calls were made for a given event, I think there are more appropriate tools/techniques than metrics

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair. I think for that use-case may be better suited for tracing, replaying an event, using a unit or e2e test.

I don't think that user may want to run e2e tests in production 🙂

context.Background(),
tag.Insert(r.provider, provider),
tag.Insert(r.eventType, event),
tag.Insert(r.namespace, namespace),
tag.Insert(r.repository, repository),
)
if err != nil {
return err
}

metrics.Record(ctx, gitProviderAPIRequestCount.M(1))
return nil
}

func ResetRecorder() {
Once = sync.Once{}
R = nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/pipelineascode/match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ func TestGetPipelineRunsFromRepo(t *testing.T) {
ConsoleURL: "https://console.url",
}
vcx := &ghprovider.Provider{
Client: fakeclient,
Token: github.Ptr("None"),
Logger: logger,
}
vcx.SetGithubClient(fakeclient)
pacInfo := &info.PacOpts{
Settings: settings.Settings{
ApplicationName: "Pipelines as Code CI",
Expand Down
2 changes: 1 addition & 1 deletion pkg/pipelineascode/pipelineascode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,11 +606,11 @@ func TestRun(t *testing.T) {
},
}
vcx := &ghprovider.Provider{
Client: fakeclient,
Run: cs,
Token: github.Ptr("None"),
Logger: logger,
}
vcx.SetGithubClient(fakeclient)
vcx.SetPacInfo(pacInfo)
p := NewPacs(&tt.runevent, vcx, cs, pacInfo, k8int, logger, nil)
err := p.Run(ctx)
Expand Down
4 changes: 2 additions & 2 deletions pkg/provider/bitbucketcloud/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (v *Provider) IsAllowed(ctx context.Context, event *info.Event) (bool, erro
}

func (v *Provider) isWorkspaceMember(event *info.Event) (bool, error) {
members, err := v.Client.Workspaces.Members(event.Organization)
members, err := v.Client().Workspaces.Members(event.Organization)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -83,7 +83,7 @@ func (v *Provider) checkMember(ctx context.Context, event *info.Event) (bool, er
}

func (v *Provider) checkOkToTestCommentFromApprovedMember(ctx context.Context, event *info.Event) (bool, error) {
commentsIntf, err := v.Client.Repositories.PullRequests.GetComments(&bitbucket.PullRequestsOptions{
commentsIntf, err := v.Client().Repositories.PullRequests.GetComments(&bitbucket.PullRequestsOptions{
Owner: event.Organization,
RepoSlug: event.Repository,
ID: strconv.Itoa(event.PullRequestNumber),
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/bitbucketcloud/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func TestIsAllowed(t *testing.T) {
bbcloudtest.MuxComments(t, mux, tt.event, tt.fields.comments)
bbcloudtest.MuxFiles(t, mux, tt.event, tt.fields.filescontents, "")

v := &Provider{Client: bbclient}
v := &Provider{bbClient: bbclient}
got, err := v.IsAllowed(ctx, tt.event)
if (err != nil) != tt.wantErr {
t.Errorf("Provider.IsAllowed() error = %v, wantErr %v", err, tt.wantErr)
Expand Down
53 changes: 43 additions & 10 deletions pkg/provider/bitbucketcloud/bitbucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
"github.com/openshift-pipelines/pipelines-as-code/pkg/changedfiles"
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
"github.com/openshift-pipelines/pipelines-as-code/pkg/metrics"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
Expand All @@ -22,13 +23,43 @@ import (
var _ provider.Interface = (*Provider)(nil)

type Provider struct {
Client *bitbucket.Client
bbClient *bitbucket.Client
Logger *zap.SugaredLogger
metrics *metrics.Recorder
run *params.Run
pacInfo *info.PacOpts
Token, APIURL *string
Username *string
provenance string
repo *v1alpha1.Repository
triggerEvent string
}

func (v Provider) Client() *bitbucket.Client {
v.recordAPIUsageMetrics()
return v.bbClient
}

func (v *Provider) recordAPIUsageMetrics() {
Copy link
Contributor

Choose a reason for hiding this comment

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

@aThorp96 this is implemented for every provider and seems repeated code, can you find a way for a common logic, wdyt?

if v.metrics == nil {
m, err := metrics.NewRecorder()
if err != nil {
v.Logger.Errorf("Error initializing bitbucketcloud metrics recorder: %v", err)
return
}
v.metrics = m
}
Comment on lines +44 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

@aThorp96 like doing this in a new method on provider interface e.g. GetMetrics and the using that method for emitting metrics from a single common function.


name := ""
namespace := ""
if v.repo != nil {
name = v.repo.Name
namespace = v.repo.Namespace
}

if err := v.metrics.ReportGitProviderAPIUsage("bitbucketcloud", v.triggerEvent, namespace, name); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

@aThorp96 for provider name, you've GetConfig function implemented for every provider and it will persist uniformity of naming convention as well like see here bitbucket-cloud has hyphen. same for above error message as well (line no 47 in this file)

v.Logger.Errorf("Error reporting git API usage metrics: %v", err)
}
}

// CheckPolicyAllowing TODO: Implement ME.
Expand Down Expand Up @@ -104,11 +135,11 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusopts
Revision: event.SHA,
}

if v.Client == nil {
if v.bbClient == nil {
return fmt.Errorf("no token has been set, cannot set status")
}

_, err := v.Client.Repositories.Commits.CreateCommitStatus(cmo, cso)
_, err := v.Client().Repositories.Commits.CreateCommitStatus(cmo, cso)
if err != nil {
return err
}
Expand All @@ -121,7 +152,7 @@ func (v *Provider) CreateStatus(_ context.Context, event *info.Event, statusopts
if statusopts.OriginalPipelineRunName != "" {
onPr = "/" + statusopts.OriginalPipelineRunName
}
_, err = v.Client.Repositories.PullRequests.AddComment(
_, err = v.Client().Repositories.PullRequests.AddComment(
&bitbucket.PullRequestCommentOptions{
Owner: event.Organization,
RepoSlug: event.Repository,
Expand Down Expand Up @@ -161,7 +192,7 @@ func (v *Provider) getDir(event *info.Event, path string) ([]bitbucket.Repositor
Path: path,
}

repositoryFiles, err := v.Client.Repositories.Repository.ListFiles(repoFileOpts)
repositoryFiles, err := v.Client().Repositories.Repository.ListFiles(repoFileOpts)
if err != nil {
return nil, err
}
Expand All @@ -176,17 +207,19 @@ func (v *Provider) GetFileInsideRepo(_ context.Context, event *info.Event, path,
return v.getBlob(event, revision, path)
}

func (v *Provider) SetClient(_ context.Context, run *params.Run, event *info.Event, _ *v1alpha1.Repository, _ *events.EventEmitter) error {
func (v *Provider) SetClient(_ context.Context, run *params.Run, event *info.Event, repo *v1alpha1.Repository, _ *events.EventEmitter) error {
if event.Provider.Token == "" {
return fmt.Errorf("no git_provider.secret has been set in the repo crd")
}
if event.Provider.User == "" {
return fmt.Errorf("no git_provider.user has been in repo crd")
}
v.Client = bitbucket.NewBasicAuth(event.Provider.User, event.Provider.Token)
v.bbClient = bitbucket.NewBasicAuth(event.Provider.User, event.Provider.Token)
v.Token = &event.Provider.Token
v.Username = &event.Provider.User
v.run = run
v.repo = repo
v.triggerEvent = event.EventType
return nil
}

Expand All @@ -195,7 +228,7 @@ func (v *Provider) GetCommitInfo(_ context.Context, event *info.Event) error {
if branchortag == "" {
branchortag = event.HeadBranch
}
response, err := v.Client.Repositories.Commits.GetCommits(&bitbucket.CommitsOptions{
response, err := v.Client().Repositories.Commits.GetCommits(&bitbucket.CommitsOptions{
Owner: event.Organization,
RepoSlug: event.Repository,
Branchortag: branchortag,
Expand Down Expand Up @@ -226,7 +259,7 @@ func (v *Provider) GetCommitInfo(_ context.Context, event *info.Event) error {
event.SHA = commitinfo.Hash

// now to get the default branch from repository.Get
repo, err := v.Client.Repositories.Repository.Get(&bitbucket.RepositoryOptions{
repo, err := v.Client().Repositories.Repository.Get(&bitbucket.RepositoryOptions{
Owner: event.Organization,
RepoSlug: event.Repository,
})
Expand Down Expand Up @@ -278,7 +311,7 @@ func (v *Provider) concatAllYamlFiles(objects []bitbucket.RepositoryFile, event
}

func (v *Provider) getBlob(runevent *info.Event, ref, path string) (string, error) {
blob, err := v.Client.Repositories.Repository.GetFileBlob(&bitbucket.RepositoryBlobOptions{
blob, err := v.Client().Repositories.Repository.GetFileBlob(&bitbucket.RepositoryBlobOptions{
Owner: runevent.Organization,
RepoSlug: runevent.Repository,
Ref: ref,
Expand Down
8 changes: 4 additions & 4 deletions pkg/provider/bitbucketcloud/bitbucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestGetTektonDir(t *testing.T) {
ctx, _ := rtesting.SetupFakeContext(t)
bbclient, mux, tearDown := bbcloudtest.SetupBBCloudClient(t)
defer tearDown()
v := &Provider{Logger: fakelogger, Client: bbclient}
v := &Provider{Logger: fakelogger, bbClient: bbclient}
bbcloudtest.MuxDirContent(t, mux, tt.event, tt.testDirPath, tt.provenance)
content, err := v.GetTektonDir(ctx, tt.event, ".tekton", tt.provenance)
if tt.wantErr != "" {
Expand Down Expand Up @@ -202,7 +202,7 @@ func TestGetCommitInfo(t *testing.T) {
ctx, _ := rtesting.SetupFakeContext(t)
bbclient, mux, tearDown := bbcloudtest.SetupBBCloudClient(t)
defer tearDown()
v := &Provider{Client: bbclient}
v := &Provider{bbClient: bbclient}
bbcloudtest.MuxCommits(t, mux, tt.event, []types.Commit{
tt.commitinfo,
})
Expand Down Expand Up @@ -295,8 +295,8 @@ func TestCreateStatus(t *testing.T) {
bbclient, mux, tearDown := bbcloudtest.SetupBBCloudClient(t)
defer tearDown()
v := &Provider{
Client: bbclient,
run: params.New(),
bbClient: bbclient,
run: params.New(),
pacInfo: &info.PacOpts{
Settings: settings.Settings{
ApplicationName: settings.PACApplicationNameDefaultValue,
Expand Down
Loading
Loading