diff --git a/internal/server/billing_metrics_exporter.go b/internal/server/billing_metrics_exporter.go index 90f6afc..06d4520 100644 --- a/internal/server/billing_metrics_exporter.go +++ b/internal/server/billing_metrics_exporter.go @@ -8,7 +8,6 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/google/go-github/v50/github" - "golang.org/x/oauth2" ) type BillingMetricsExporter struct { @@ -18,12 +17,7 @@ type BillingMetricsExporter struct { } func NewBillingMetricsExporter(logger log.Logger, opts Opts) *BillingMetricsExporter { - ctx := context.Background() - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: opts.GitHubAPIToken}, - ) - tc := oauth2.NewClient(ctx, ts) - client := github.NewClient(tc) + client := getGithubClient(opts.GitHubAPIToken) return &BillingMetricsExporter{ Logger: logger, diff --git a/internal/server/github_client.go b/internal/server/github_client.go new file mode 100644 index 0000000..3540ed7 --- /dev/null +++ b/internal/server/github_client.go @@ -0,0 +1,25 @@ +package server + +import ( + "context" + + "github.com/google/go-github/v50/github" + "golang.org/x/oauth2" +) + +var client *github.Client + +func getGithubClient(githubToken string) *github.Client { + if client != nil { + return client + } + + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: githubToken}, + ) + tc := oauth2.NewClient(ctx, ts) + client = github.NewClient(tc) + + return client +} diff --git a/internal/server/metrics.go b/internal/server/metrics.go index b5a694f..93841cf 100644 --- a/internal/server/metrics.go +++ b/internal/server/metrics.go @@ -63,21 +63,21 @@ var ( totalMinutesUsedUbuntuActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "actions_total_minutes_used_ubuntu_minutes", - Help: "Total minutes used for Ubuntu type for the GitHub Actions. To be deprecate, use actions_total_minutes_used_by_host_minutes", + Help: "Total minutes used for Ubuntu type for the GitHub Actions. To be deprecated, use actions_total_minutes_used_by_host_minutes", }, []string{"org", "user"}, ) totalMinutesUsedMacOSActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "actions_total_minutes_used_macos_minutes", - Help: "Total minutes used for MacOS type for the GitHub Actions. To be deprecate, use actions_total_minutes_used_by_host_minutes", + Help: "Total minutes used for MacOS type for the GitHub Actions. To be deprecated, use actions_total_minutes_used_by_host_minutes", }, []string{"org", "user"}, ) totalMinutesUsedWindowsActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "actions_total_minutes_used_windows_minutes", - Help: "Total minutes used for Windows type for the GitHub Actions. To be deprecate, use actions_total_minutes_used_by_host_minutes", + Help: "Total minutes used for Windows type for the GitHub Actions. To be deprecated, use actions_total_minutes_used_by_host_minutes", }, []string{"org", "user"}, ) @@ -88,6 +88,13 @@ var ( }, []string{"org", "user", "host_type"}, ) + + numberOfSelfHostedRunners = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "github_actions_num_self_hosted_org_runners", + Help: "Number of self-hosted org runners for the GitHub Actions.", + }, + []string{"org"}, + ) ) func init() { @@ -104,6 +111,7 @@ func init() { prometheus.MustRegister(totalMinutesUsedMacOSActions) prometheus.MustRegister(totalMinutesUsedWindowsActions) prometheus.MustRegister(totalMinutesUsedByHostTypeActions) + prometheus.MustRegister(numberOfSelfHostedRunners) } type WorkflowObserver interface { diff --git a/internal/server/runner_metrics.go b/internal/server/runner_metrics.go new file mode 100644 index 0000000..43bf1a7 --- /dev/null +++ b/internal/server/runner_metrics.go @@ -0,0 +1,61 @@ +package server + +import ( + "context" + "time" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/google/go-github/v50/github" +) + +type RunnerMetricsExporter struct { + GHClient *github.Client + Logger log.Logger + Opts Opts +} + +func NewRunnerMetricsExporter(logger log.Logger, opts Opts) *RunnerMetricsExporter { + client := getGithubClient(opts.GitHubAPIToken) + + return &RunnerMetricsExporter{ + GHClient: client, + Logger: logger, + Opts: opts, + } +} + +func (e *RunnerMetricsExporter) StartOrgRunnerMetricsCollection(ctx context.Context) { + if e.Opts.GitHubOrg == "" { + _ = level.Info(e.Logger).Log("msg", "Github org is not set, no org runner metrics will be collected.") + return + } + if e.Opts.GitHubAPIToken == "" { + _ = level.Info(e.Logger).Log("msg", "Github token is not set, no org runner metrics will be collected.") + return + } + + ticker := time.NewTicker(time.Duration(e.Opts.RunnersAPIPollSeconds) * time.Second) + go func() { + for { + select { + case <-ticker.C: + e.collectOrgRunnerMetrics(ctx) + case <-ctx.Done(): + ticker.Stop() + _ = level.Info(e.Logger).Log("msg", "Stopped polling for org runner metrics.") + return + } + } + }() +} + +func (e *RunnerMetricsExporter) collectOrgRunnerMetrics(ctx context.Context) { + runners, _, err := e.GHClient.Actions.ListOrganizationRunners(ctx, e.Opts.GitHubOrg, nil) + + if err != nil { + _ = e.Logger.Log("msg", "Failed to retrieve org runners for org ", e.Opts.GitHubOrg, " ", err) + } + + numberOfSelfHostedRunners.WithLabelValues(e.Opts.GitHubOrg).Set(float64(runners.TotalCount)) +} diff --git a/internal/server/server.go b/internal/server/server.go index 6ad5811..65b553f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -27,6 +27,9 @@ type Opts struct { GitHubOrg string GitHubUser string BillingAPIPollSeconds int + + GitHubRunnersRepos []string + RunnersAPIPollSeconds int } type Server struct { @@ -55,6 +58,9 @@ func NewServer(logger log.Logger, opts Opts) *Server { _ = level.Info(logger).Log("msg", fmt.Sprintf("not exporting user billing: %v", err)) } + runnerMetricsExporter := NewRunnerMetricsExporter(logger, opts) + runnerMetricsExporter.StartOrgRunnerMetricsCollection(context.TODO()) + muxIngress := http.NewServeMux() httpServerIngress := &http.Server{ Handler: muxIngress, diff --git a/main.go b/main.go index 486d10b..16cf08b 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,8 @@ var ( gitHubOrg = kingpin.Flag("gh.github-org", "GitHub Organization.").Envar("GITHUB_ORG").Default("").String() gitHubUser = kingpin.Flag("gh.github-user", "GitHub User.").Default("").String() gitHubBillingPollingSeconds = kingpin.Flag("gh.billing-poll-seconds", "Frequency at which to poll billing API.").Default("5").Int() + gitHubRunnersRepos = kingpin.Flag("gh.runners-repos", "The name of the repositories where runners are configures.").Default("").Strings() + gitHubRunnersPollingSeconds = kingpin.Flag("gh.runners-poll-seconds", "Frequency at which to poll runners API.").Default("5").Int() ) func init() { @@ -62,6 +64,8 @@ func main() { GitHubUser: *gitHubUser, GitHubOrg: *gitHubOrg, BillingAPIPollSeconds: *gitHubBillingPollingSeconds, + GitHubRunnersRepos: *gitHubRunnersRepos, + RunnersAPIPollSeconds: *gitHubRunnersPollingSeconds, }) go func() { err := srv.Serve(context.Background())