Skip to content

Commit e0eaa9d

Browse files
bwnutskalgurn
andauthored
fix: prevent exporter panic, harden auth config, improve error handling (#156)
* fix: prevent exporter panic, harden auth config, improve error handling * feat: set request timeout * fixed test issue --------- Co-authored-by: Kostiantyn Kulbachnyi <[email protected]>
1 parent e0f1674 commit e0eaa9d

File tree

5 files changed

+65
-33
lines changed

5 files changed

+65
-33
lines changed

internal/github_client/github_client.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,29 @@ import (
1515
"golang.org/x/oauth2"
1616
)
1717

18-
func GetRemainingLimits(c *github.Client) RateLimits {
19-
ctx := context.Background()
20-
21-
limits, _, err := c.RateLimit.Get(ctx)
22-
if err != nil {
23-
utils.RespError(err)
24-
}
25-
26-
return RateLimits{
27-
Limit: limits.Core.Limit,
28-
Remaining: limits.Core.Remaining,
29-
Used: limits.Core.Limit - limits.Core.Remaining,
30-
SecondsLeft: time.Until(limits.Core.Reset.Time).Seconds(),
31-
}
18+
func GetRemainingLimits(c *github.Client) (RateLimits, error) {
19+
if c == nil {
20+
return RateLimits{}, utils.RespError(fmt.Errorf("github client is nil"))
21+
}
22+
23+
// Set a timeout of 5 seconds for the request
24+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
25+
defer cancel()
26+
27+
limits, _, err := c.RateLimit.Get(ctx)
28+
if err != nil {
29+
return RateLimits{}, utils.RespError(err)
30+
}
31+
if limits == nil || limits.Core == nil {
32+
return RateLimits{}, utils.RespError(fmt.Errorf("rate limit response is nil"))
33+
}
34+
35+
return RateLimits{
36+
Limit: limits.Core.Limit,
37+
Remaining: limits.Core.Remaining,
38+
Used: limits.Core.Limit - limits.Core.Remaining,
39+
SecondsLeft: time.Until(limits.Core.Reset.Time).Seconds(),
40+
}, nil
3241
}
3342

3443
func (c *TokenConfig) InitClient() *github.Client {
@@ -57,11 +66,18 @@ func InitConfig() GithubClient {
5766
installationID, _ = strconv.ParseInt(envInstallationID, 10, 64)
5867
}
5968

69+
// Only read organization/repo names if InstallationID is not provided
70+
var orgName, repoName string
71+
if envInstallationID == "" {
72+
orgName = utils.GetOSVar("GITHUB_ORG_NAME")
73+
repoName = utils.GetOSVar("GITHUB_REPO_NAME")
74+
}
75+
6076
auth = &AppConfig{
6177
AppID: appID,
6278
InstallationID: installationID,
63-
OrgName: utils.GetOSVar("GITHUB_ORG_NAME"),
64-
RepoName: utils.GetOSVar("GITHUB_REPO_NAME"),
79+
OrgName: orgName,
80+
RepoName: repoName,
6581
PrivateKeyPath: utils.GetOSVar("GITHUB_PRIVATE_KEY_PATH"),
6682
}
6783
} else {

internal/github_client/github_client_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ func TestGetRemainingLimits(t *testing.T) {
7272
),
7373
)
7474
c := github.NewClient(mockedHTTPClient)
75-
limits := GetRemainingLimits(c)
75+
limits, err := GetRemainingLimits(c)
76+
assert.NoError(t, err)
7677

7778
assert.Equal(t, limit, limits.Limit, "The limits should be equal")
7879
assert.Equal(t, remaining, limits.Remaining, "The remaining limits should be equal")

internal/prometheus_exporter/server.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,30 +49,40 @@ func (collector *LimitsCollector) Describe(ch chan<- *prometheus.Desc) {
4949
ch <- collector.SecondsLeft
5050
}
5151

52-
func (collector *LimitsCollector) Collect(ch chan<- prometheus.Metric) {
53-
54-
auth := github_client.InitConfig()
55-
limits := github_client.GetRemainingLimits(auth.InitClient())
56-
if logMetricCollection {
57-
log.Printf("Collected metrics for %s", githubAccount)
58-
log.Printf("Limit: %d | Used: %d | Remaining: %d", limits.Limit, limits.Used, limits.Remaining)
59-
}
60-
//Write latest value for each metric in the prometheus metric channel.
61-
//Note that you can pass CounterValue, GaugeValue, or UntypedValue types here.
52+
func (collector *LimitsCollector) emitMetrics(ch chan<- prometheus.Metric, limits github_client.RateLimits) {
6253
m1 := prometheus.MustNewConstMetric(collector.LimitTotal, prometheus.GaugeValue, float64(limits.Limit))
6354
m2 := prometheus.MustNewConstMetric(collector.LimitRemaining, prometheus.GaugeValue, float64(limits.Remaining))
6455
m3 := prometheus.MustNewConstMetric(collector.LimitUsed, prometheus.GaugeValue, float64(limits.Used))
6556
m4 := prometheus.MustNewConstMetric(collector.SecondsLeft, prometheus.GaugeValue, limits.SecondsLeft)
66-
m1 = prometheus.NewMetricWithTimestamp(time.Now(), m1)
67-
m2 = prometheus.NewMetricWithTimestamp(time.Now(), m2)
68-
m3 = prometheus.NewMetricWithTimestamp(time.Now(), m3)
69-
m4 = prometheus.NewMetricWithTimestamp(time.Now(), m4)
57+
now := time.Now()
58+
m1 = prometheus.NewMetricWithTimestamp(now, m1)
59+
m2 = prometheus.NewMetricWithTimestamp(now, m2)
60+
m3 = prometheus.NewMetricWithTimestamp(now, m3)
61+
m4 = prometheus.NewMetricWithTimestamp(now, m4)
7062
ch <- m1
7163
ch <- m2
7264
ch <- m3
7365
ch <- m4
7466
}
7567

68+
func (collector *LimitsCollector) Collect(ch chan<- prometheus.Metric) {
69+
auth := github_client.InitConfig()
70+
limits, err := github_client.GetRemainingLimits(auth.InitClient())
71+
if err != nil {
72+
// On error expose zeros and return to avoid panics during scraping
73+
if logMetricCollection {
74+
log.Printf("error collecting metrics for %s: %v", githubAccount, err)
75+
}
76+
collector.emitMetrics(ch, github_client.RateLimits{})
77+
return
78+
}
79+
if logMetricCollection {
80+
log.Printf("Collected metrics for %s", githubAccount)
81+
log.Printf("Limit: %d | Used: %d | Remaining: %d", limits.Limit, limits.Used, limits.Remaining)
82+
}
83+
collector.emitMetrics(ch, limits)
84+
}
85+
7686
func Run() {
7787
// Default to logging metric collection
7888
if logMetricCollectionParseErr != nil {

internal/utils/utils.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package utils
33
import (
44
"errors"
55
"fmt"
6+
"log"
67
"os"
78
)
89

10+
// RespError logs and returns a wrapped error for callers to handle.
11+
// This function does not terminate the process.
912
func RespError(err error) error {
1013
if err != nil {
11-
errMsg := fmt.Sprintf("there was an error during the call execution: %s\n", err)
14+
errMsg := fmt.Sprintf("there was an error during the call execution: %s", err)
15+
log.Printf("%s", errMsg)
1216
return errors.New(errMsg)
1317
}
1418
return nil
@@ -18,6 +22,7 @@ func GetOSVar(envVar string) string {
1822
value, present := os.LookupEnv(envVar)
1923
if !present {
2024
err := fmt.Sprintf("environment variable %s not set", envVar)
25+
// Log the error but do not terminate; caller should validate the value.
2126
RespError(errors.New(err))
2227
return ""
2328
}

internal/utils/utils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ func TestGetOSVar(t *testing.T) {
2424

2525
func TestRespError(t *testing.T) {
2626
err := errors.New("test")
27-
assert.Equal(t, errors.New("there was an error during the call execution: test\n"), RespError(err))
27+
assert.Equal(t, errors.New("there was an error during the call execution: test"), RespError(err))
2828
}

0 commit comments

Comments
 (0)