Skip to content

Commit 75009ef

Browse files
committed
feature: show metrics about GH Actions billing
1 parent e6ab870 commit 75009ef

File tree

173 files changed

+14423
-1093
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

173 files changed

+14423
-1093
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ You need just to select the `Check Run` event and the set your secret (that you
1818

1919
![gh_webook](./assets/gh_webhook.png)
2020

21+
Also it collects the Action Billing metrics, for that you will need to setup a GitHub API Access Token
22+
23+
When configuring for an organization Access tokens must have the `repo` or `admin:org` scope.
24+
When configuring for an user Access tokens must have the `user` scope.
25+
26+
2127
### Prerequisites
2228

2329
To run this project, you will need a [working Go environment](https://golang.org/doc/install).

collector.go

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,73 @@
11
package main
22

33
import (
4-
"github.com/google/go-github/v32/github"
4+
"context"
5+
6+
"github.com/google/go-github/v33/github"
57
"github.com/prometheus/client_golang/prometheus"
68
)
79

8-
var histogramVec = prometheus.NewHistogramVec(prometheus.HistogramOpts{
9-
Name: "workflow_execution_time_seconds",
10-
Help: "Time that a workflow took to run.",
11-
Buckets: prometheus.ExponentialBuckets(1, 1.4, 30),
12-
},
13-
[]string{"org", "repo", "workflow_name"},
10+
var (
11+
histogramVec = prometheus.NewHistogramVec(prometheus.HistogramOpts{
12+
Name: "workflow_execution_time_seconds",
13+
Help: "Time that a workflow took to run.",
14+
Buckets: prometheus.ExponentialBuckets(1, 1.4, 30),
15+
},
16+
[]string{"org", "repo", "workflow_name"},
17+
)
18+
19+
totalMinutesUsedActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
20+
Name: "actions_total_minutes_used_minutes",
21+
Help: "Total minutes used for the GitHub Actions.",
22+
},
23+
[]string{"org", "user"},
24+
)
25+
26+
includedMinutesUsedActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
27+
Name: "actions_included_minutes",
28+
Help: "Included Minutes for the GitHub Actions.",
29+
},
30+
[]string{"org", "user"},
31+
)
32+
33+
totalPaidMinutesActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
34+
Name: "actions_total_paid_minutes",
35+
Help: "Paid Minutes for the GitHub Actions.",
36+
},
37+
[]string{"org", "user"},
38+
)
39+
40+
totalMinutesUsedUbuntuActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
41+
Name: "actions_total_minutes_used_ubuntu_minutes",
42+
Help: "Total minutes used for Ubuntu type for the GitHub Actions.",
43+
},
44+
[]string{"org", "user"},
45+
)
46+
47+
totalMinutesUsedMacOSActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
48+
Name: "actions_total_minutes_used_macos_minutes",
49+
Help: "Total minutes used for MacOS type for the GitHub Actions.",
50+
},
51+
[]string{"org", "user"},
52+
)
53+
54+
totalMinutesUsedWindowsActions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
55+
Name: "actions_total_minutes_used_windows_minutes",
56+
Help: "Total minutes used for Windows type for the GitHub Actions.",
57+
},
58+
[]string{"org", "user"},
59+
)
1460
)
1561

1662
func init() {
1763
//Register metrics with prometheus
1864
prometheus.MustRegister(histogramVec)
65+
prometheus.MustRegister(totalMinutesUsedActions)
66+
prometheus.MustRegister(includedMinutesUsedActions)
67+
prometheus.MustRegister(totalPaidMinutesActions)
68+
prometheus.MustRegister(totalMinutesUsedUbuntuActions)
69+
prometheus.MustRegister(totalMinutesUsedMacOSActions)
70+
prometheus.MustRegister(totalMinutesUsedWindowsActions)
1971
}
2072

2173
// CollectWorkflowRun collect the workflow execution run metric
@@ -33,3 +85,36 @@ func CollectWorkflowRun(checkRunEvent *github.CheckRunEvent) {
3385

3486
histogramVec.WithLabelValues(org, repo, workflowName).Observe(executionTime)
3587
}
88+
89+
// CollectActionBilling collect the action billing.
90+
func (c *GHActionExporter) CollectActionBilling() {
91+
if *gitHubOrg != "" {
92+
actionsBilling, _, err := c.GHClient.Billing.GetActionsBillingOrg(context.TODO(), *gitHubOrg)
93+
if err != nil {
94+
c.Logger.Log("msg", "failed to retrive the actions billing for an org", "org", *gitHubOrg, "err", err)
95+
return
96+
}
97+
98+
totalMinutesUsedActions.WithLabelValues(*gitHubOrg, "").Set(float64(actionsBilling.TotalMinutesUsed))
99+
includedMinutesUsedActions.WithLabelValues(*gitHubOrg, "").Set(float64(actionsBilling.IncludedMinutes))
100+
totalPaidMinutesActions.WithLabelValues(*gitHubOrg, "").Set(float64(actionsBilling.TotalPaidMinutesUsed))
101+
totalMinutesUsedUbuntuActions.WithLabelValues(*gitHubOrg, "").Set(float64(actionsBilling.MinutesUsedBreakdown.Ubuntu))
102+
totalMinutesUsedMacOSActions.WithLabelValues(*gitHubOrg, "").Set(float64(actionsBilling.MinutesUsedBreakdown.MacOS))
103+
totalMinutesUsedWindowsActions.WithLabelValues(*gitHubOrg, "").Set(float64(actionsBilling.MinutesUsedBreakdown.Windows))
104+
}
105+
106+
if *gitHubUser != "" {
107+
actionsBilling, _, err := c.GHClient.Billing.GetActionsBillingUser(context.TODO(), *gitHubUser)
108+
if err != nil {
109+
c.Logger.Log("msg", "failed to retrive the actions billing for an user", "user", *gitHubUser, "err", err)
110+
return
111+
}
112+
113+
totalMinutesUsedActions.WithLabelValues("", *gitHubUser).Set(float64(actionsBilling.TotalMinutesUsed))
114+
includedMinutesUsedActions.WithLabelValues("", *gitHubUser).Set(float64(actionsBilling.IncludedMinutes))
115+
totalPaidMinutesActions.WithLabelValues("", *gitHubUser).Set(float64(actionsBilling.TotalPaidMinutesUsed))
116+
totalMinutesUsedUbuntuActions.WithLabelValues("", *gitHubUser).Set(float64(actionsBilling.MinutesUsedBreakdown.Ubuntu))
117+
totalMinutesUsedMacOSActions.WithLabelValues("", *gitHubUser).Set(float64(actionsBilling.MinutesUsedBreakdown.MacOS))
118+
totalMinutesUsedWindowsActions.WithLabelValues("", *gitHubUser).Set(float64(actionsBilling.MinutesUsedBreakdown.Windows))
119+
}
120+
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ go 1.15
55
require (
66
github.com/go-kit/kit v0.10.0
77
github.com/google/go-cmp v0.5.1 // indirect
8-
github.com/google/go-github/v32 v32.1.0
8+
github.com/google/go-github/v33 v33.0.1-0.20210219143306-5c87615fe927
99
github.com/prometheus/client_golang v1.8.0
1010
github.com/prometheus/common v0.15.0
11+
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
1112
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
1213
google.golang.org/protobuf v1.25.0 // indirect
1314
gopkg.in/alecthomas/kingpin.v2 v2.2.6

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
100100
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
101101
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
102102
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
103-
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
104-
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
103+
github.com/google/go-github/v33 v33.0.1-0.20210219143306-5c87615fe927 h1:GJWYiEEQLQ22a3BnB3nRFZt47xnVXKFUM9IdNfpKqi8=
104+
github.com/google/go-github/v33 v33.0.1-0.20210219143306-5c87615fe927/go.mod h1:GMdDnVZY/2TsWgp/lkYnpSAh6TrzhANBBwm6k6TTEXg=
105105
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
106106
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
107107
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

main.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"net"
@@ -12,24 +13,30 @@ import (
1213

1314
"github.com/go-kit/kit/log"
1415
"github.com/go-kit/kit/log/level"
16+
"github.com/google/go-github/v33/github"
1517
"github.com/prometheus/client_golang/prometheus"
1618
"github.com/prometheus/client_golang/prometheus/promhttp"
1719
"github.com/prometheus/common/promlog"
1820
"github.com/prometheus/common/promlog/flag"
1921
"github.com/prometheus/common/version"
22+
"golang.org/x/oauth2"
2023
"gopkg.in/alecthomas/kingpin.v2"
2124
)
2225

2326
var (
24-
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9101").String()
25-
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
26-
ghWebHookPath = kingpin.Flag("web.gh-webhook-path", "Path under which to expose metrics.").Default("/gh_event").String()
27-
gitHubToken = kingpin.Flag("gh.github-webhook-token", "GitHub Webhook Token.").Default("").String()
27+
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9101").String()
28+
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String()
29+
ghWebHookPath = kingpin.Flag("web.gh-webhook-path", "Path under which to expose metrics.").Default("/gh_event").String()
30+
gitHubToken = kingpin.Flag("gh.github-webhook-token", "GitHub Webhook Token.").Default("").String()
31+
gitHubAPIToken = kingpin.Flag("gh.github-api-token", "GitHub API Token.").Default("").String()
32+
gitHubOrg = kingpin.Flag("gh.github-org", "GitHub Organization.").Default("").String()
33+
gitHubUser = kingpin.Flag("gh.github-user", "GitHub User.").Default("").String()
2834
)
2935

3036
// GHActionExporter struct to hold some information
3137
type GHActionExporter struct {
32-
Logger log.Logger
38+
GHClient *github.Client
39+
Logger log.Logger
3340
}
3441

3542
func init() {
@@ -47,6 +54,11 @@ func main() {
4754
level.Info(logger).Log("msg", "Starting ghactions_exporter", "version", version.Info())
4855
level.Info(logger).Log("build_context", version.BuildContext())
4956

57+
if err := validateFlags(*gitHubAPIToken, *gitHubToken, *gitHubOrg, *gitHubUser); err != nil {
58+
level.Error(logger).Log("msg", "Missing configure flags", "err", err)
59+
os.Exit(1)
60+
}
61+
5062
gh := NewGHActionExporter(logger)
5163

5264
srv := http.Server{}
@@ -128,24 +140,33 @@ func parseUnixSocketAddress(address string) (string, string, error) {
128140
return unixSocketPath, requestPath, nil
129141
}
130142

131-
func validateFlags(token, org, repo string) error {
143+
func validateFlags(apiToken, token, org, user string) error {
132144
if token == "" {
133-
return errors.New("Please configure the GitHub Token")
145+
return errors.New("Please configure the GitHub Webhook Token")
134146
}
135147

136-
if org == "" {
137-
return errors.New("Please configure the GitHub Organization")
148+
if apiToken == "" {
149+
return errors.New("Please configure the GitHub API Token")
138150
}
139151

140-
if repo == "" {
141-
return errors.New("Please configure the GitHub Repository")
152+
if org == "" && user == "" {
153+
fmt.Print(org, user)
154+
return errors.New("Please configure the GitHub Organization or GitHub User ")
142155
}
143156

144157
return nil
145158
}
146159

147160
func NewGHActionExporter(logger log.Logger) *GHActionExporter {
161+
ctx := context.Background()
162+
ts := oauth2.StaticTokenSource(
163+
&oauth2.Token{AccessToken: *gitHubAPIToken},
164+
)
165+
tc := oauth2.NewClient(ctx, ts)
166+
client := github.NewClient(tc)
167+
148168
return &GHActionExporter{
149-
Logger: logger,
169+
GHClient: client,
170+
Logger: logger,
150171
}
151172
}

model/check_run_event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"encoding/json"
55
"io"
66

7-
"github.com/google/go-github/v32/github"
7+
"github.com/google/go-github/v33/github"
88
)
99

1010
// CheckRunEventFromJSON decodes the incomming message to a github.CheckRunEvent

model/ping_event.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"encoding/json"
55
"io"
66

7-
"github.com/google/go-github/v32/github"
7+
"github.com/google/go-github/v33/github"
88
)
99

1010
// PingEventFromJSON decodes the incomming message to a github.PingEvent

0 commit comments

Comments
 (0)