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

Input Plugin: GitHub #5587

Merged
merged 29 commits into from
Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f32cc74
Initial Commit
rawkode Mar 7, 2019
2146450
Add github_repository and github_rate_limit measurements
rawkode Mar 15, 2019
a2f65ed
Add License to Tag List in README
rawkode Mar 15, 2019
2c379e8
Fix Variable Naming and Connect TOML Config
rawkode Mar 15, 2019
b6fa54d
GitHub Uses the Vocabulary 'Access Token'; Adopting Over APIKey
rawkode Mar 15, 2019
0835264
Refactored Code to Smaller Testable Functions
rawkode Mar 15, 2019
efd6b3e
Removed HAProxy Copy Pasta from README
rawkode Mar 15, 2019
a00aca8
Add Tests for splitRepositoryName
rawkode Mar 15, 2019
b916a7b
Add tests for GetLicense
rawkode Mar 15, 2019
383d180
Add Tests for GetTags and GetFields
rawkode Mar 15, 2019
ab124eb
Update Example Config to Match Style Guide
rawkode Mar 25, 2019
7723bfe
Alternating List Starting Char
rawkode Mar 25, 2019
aa56ed3
Use SelfStat for Rate Limits
rawkode Mar 25, 2019
d276d47
Remove Redundant NewGitHub Func
rawkode Mar 25, 2019
51f5584
Use Single Letter Receiver
rawkode Mar 25, 2019
3058cac
Using Provided Error Functions
rawkode Mar 28, 2019
c518d98
Use SplitsN
rawkode Mar 28, 2019
d785526
Use SelfStat to Record Blocks by RateLimits
rawkode Mar 28, 2019
9ac2c56
Use Testify for Assertions
rawkode Mar 28, 2019
145adcf
Make Sample Output Match Description
rawkode Mar 28, 2019
1d7299f
Use TableTests for Split String Function
rawkode Mar 28, 2019
4cb4fb6
Create Our Own HttpClient
rawkode Mar 28, 2019
f4ec36c
Pass Context to Client Creation
rawkode Mar 28, 2019
2032b41
Show Access Token as Tag
rawkode Mar 28, 2019
5b4a443
Update Sample Config to Match README
rawkode Mar 29, 2019
b89f3ae
Document Internal Metrics
rawkode Mar 29, 2019
fba1b3d
Remove Full Name
rawkode Mar 29, 2019
9502493
Only Register SelfStats Once
rawkode Mar 29, 2019
58a8d22
Clean Up Tests
rawkode Mar 29, 2019
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
17 changes: 17 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,7 @@
[[override]]
name = "golang.org/x/text"
source = "https://github.com/golang/text.git"

[[constraint]]
name = "github.com/google/go-github"
version = "24.0.1"
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/filecount"
_ "github.com/influxdata/telegraf/plugins/inputs/filestat"
_ "github.com/influxdata/telegraf/plugins/inputs/fluentd"
_ "github.com/influxdata/telegraf/plugins/inputs/github"
_ "github.com/influxdata/telegraf/plugins/inputs/graylog"
_ "github.com/influxdata/telegraf/plugins/inputs/haproxy"
_ "github.com/influxdata/telegraf/plugins/inputs/hddtemp"
Expand Down
47 changes: 47 additions & 0 deletions plugins/inputs/github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# GitHub Input Plugin

The [GitHub](https://www.github.com) input plugin gathers statistics from GitHub repositories.

### Configuration:

```toml
[[inputs.github]]
## List of repositories to monitor
## ex: repositories = ["influxdata/telegraf"]
# repositories = []

## Optional: Unauthenticated requests are limited to 60 per hour.
# access_token = ""

## Optional: Default 5s.
# http_timeout = "5s"
```

### Metrics:

- github_repository
- tags:
- `name` - The repository name
- `owner` - The owner of the repository
- `language` - The primary language of the repository
- `license` - The license set for the repository
- fields:
- `stars` (int)
- `forks` (int)
- `open_issues` (int)
- `size` (int)

* github_rate_limit
- tags:
- `access_token` - An obfusticated reference to the configured access token or "Unauthenticated"
- fields:
- `limit` - How many requests you are limited to (per hour)
- `remaining` - How many requests you have remaining (per hour)
- `blocks` - How many requests have been blocked due to rate limit

### Example Output:

```
github,full_name=influxdata/telegraf,name=telegraf,owner=influxdata,language=Go,license=MIT\ License stars=6401i,forks=2421i,open_issues=722i,size=22611i 1552651811000000000
internal_github,access_token=Unauthenticated rate_limit_remaining=59i,rate_limit_limit=60i,rate_limit_blocks=0i 1552653551000000000
```
184 changes: 184 additions & 0 deletions plugins/inputs/github/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package github

import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"

"github.com/google/go-github/github"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/selfstat"
"golang.org/x/oauth2"
)

// GitHub - plugin main structure
type GitHub struct {
Repositories []string `toml:"repositories"`
AccessToken string `toml:"access_token"`
HTTPTimeout internal.Duration `toml:"http_timeout"`
githubClient *github.Client

obfusticatedToken string

RateLimit selfstat.Stat
RateLimitErrors selfstat.Stat
RateRemaining selfstat.Stat
}

const sampleConfig = `
## List of repositories to monitor
## ex: repositories = ["influxdata/telegraf"]
# repositories = []

## Optional: Unauthenticated requests are limited to 60 per hour.
# access_token = ""

## Optional: Default 5s.
# http_timeout = "5s"
`

// SampleConfig returns sample configuration for this plugin.
func (g *GitHub) SampleConfig() string {
return sampleConfig
}

// Description returns the plugin description.
func (g *GitHub) Description() string {
return "Read repository information from GitHub, including forks, stars, and more."
}

// Create GitHub Client
func (g *GitHub) createGitHubClient(ctx context.Context) (*github.Client, error) {
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
Timeout: g.HTTPTimeout.Duration,
}

g.obfusticatedToken = "Unauthenticated"

if g.AccessToken != "" {
tokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: g.AccessToken},
)
oauthClient := oauth2.NewClient(ctx, tokenSource)
ctx = context.WithValue(ctx, oauth2.HTTPClient, oauthClient)

g.obfusticatedToken = g.AccessToken[0:4] + "..." + g.AccessToken[len(g.AccessToken)-3:]

return github.NewClient(oauthClient), nil
}

return github.NewClient(httpClient), nil
}

// Gather GitHub Metrics
func (g *GitHub) Gather(acc telegraf.Accumulator) error {
ctx := context.Background()

if g.githubClient == nil {
githubClient, err := g.createGitHubClient(ctx)

if err != nil {
return err
}

g.githubClient = githubClient

tokenTags := map[string]string{
"access_token": g.obfusticatedToken,
}

g.RateLimitErrors = selfstat.Register("github", "rate_limit_blocks", tokenTags)
g.RateLimit = selfstat.Register("github", "rate_limit_limit", tokenTags)
g.RateRemaining = selfstat.Register("github", "rate_limit_remaining", tokenTags)
}

var wg sync.WaitGroup
wg.Add(len(g.Repositories))

for _, repository := range g.Repositories {
go func(repositoryName string, acc telegraf.Accumulator) {
defer wg.Done()

owner, repository, err := splitRepositoryName(repositoryName)
if err != nil {
acc.AddError(err)
return
}

repositoryInfo, response, err := g.githubClient.Repositories.Get(ctx, owner, repository)
rawkode marked this conversation as resolved.
Show resolved Hide resolved

if _, ok := err.(*github.RateLimitError); ok {
g.RateLimitErrors.Incr(1)
}

if err != nil {
acc.AddError(err)
return
}

g.RateLimit.Set(int64(response.Rate.Limit))
g.RateRemaining.Set(int64(response.Rate.Remaining))

now := time.Now()
tags := getTags(repositoryInfo)
fields := getFields(repositoryInfo)

acc.AddFields("github_repository", fields, tags, now)
}(repository, acc)
}

wg.Wait()
return nil
}

func splitRepositoryName(repositoryName string) (string, string, error) {
splits := strings.SplitN(repositoryName, "/", 2)

if len(splits) != 2 {
return "", "", fmt.Errorf("%v is not of format 'owner/repository'", repositoryName)
}

return splits[0], splits[1], nil
}

func getLicense(repositoryInfo *github.Repository) string {
if repositoryInfo.GetLicense() != nil {
return *repositoryInfo.License.Name
}

return "None"
}

func getTags(repositoryInfo *github.Repository) map[string]string {
return map[string]string{
"owner": *repositoryInfo.Owner.Login,
"name": *repositoryInfo.Name,
"language": *repositoryInfo.Language,
"license": getLicense(repositoryInfo),
}
}

func getFields(repositoryInfo *github.Repository) map[string]interface{} {
return map[string]interface{}{
"stars": *repositoryInfo.StargazersCount,
"forks": *repositoryInfo.ForksCount,
"open_issues": *repositoryInfo.OpenIssuesCount,
"size": *repositoryInfo.Size,
}
}

func init() {
inputs.Add("github", func() telegraf.Input {
return &GitHub{
HTTPTimeout: internal.Duration{Duration: time.Second * 5},
}
})
}
119 changes: 119 additions & 0 deletions plugins/inputs/github/github_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package github

import (
"reflect"
"testing"

gh "github.com/google/go-github/github"
"github.com/stretchr/testify/require"
)

func TestSplitRepositoryNameWithWorkingExample(t *testing.T) {
rawkode marked this conversation as resolved.
Show resolved Hide resolved
var validRepositoryNames = []struct {
fullName string
owner string
repository string
}{
{"influxdata/telegraf", "influxdata", "telegraf"},
{"influxdata/influxdb", "influxdata", "influxdb"},
{"rawkode/saltstack-dotfiles", "rawkode", "saltstack-dotfiles"},
}

for _, tt := range validRepositoryNames {
t.Run(tt.fullName, func(t *testing.T) {
owner, repository, _ := splitRepositoryName(tt.fullName)

require.Equal(t, tt.owner, owner)
require.Equal(t, tt.repository, repository)
})
}
}

func TestSplitRepositoryNameWithNoSlash(t *testing.T) {
var invalidRepositoryNames = []string{
"influxdata-influxdb",
}

for _, tt := range invalidRepositoryNames {
t.Run(tt, func(t *testing.T) {
_, _, err := splitRepositoryName(tt)

require.NotNil(t, err)
})
}
}

func TestGetLicenseWhenExists(t *testing.T) {
licenseName := "MIT"
rawkode marked this conversation as resolved.
Show resolved Hide resolved
license := gh.License{Name: &licenseName}
repository := gh.Repository{License: &license}

getLicenseReturn := getLicense(&repository)

require.Equal(t, "MIT", getLicenseReturn)
}

func TestGetLicenseWhenMissing(t *testing.T) {
repository := gh.Repository{}

getLicenseReturn := getLicense(&repository)

require.Equal(t, "None", getLicenseReturn)
}

func TestGetTags(t *testing.T) {
licenseName := "MIT"
license := gh.License{Name: &licenseName}

ownerName := "influxdata"
owner := gh.User{Login: &ownerName}

fullName := "influxdata/influxdb"
repositoryName := "influxdb"

language := "Go"

repository := gh.Repository{
FullName: &fullName,
Name: &repositoryName,
License: &license,
Owner: &owner,
Language: &language,
}

getTagsReturn := getTags(&repository)

correctTagsReturn := map[string]string{
"owner": ownerName,
"name": repositoryName,
"language": language,
"license": licenseName,
}

require.Equal(t, true, reflect.DeepEqual(getTagsReturn, correctTagsReturn))
}

func TestGetFields(t *testing.T) {
stars := 1
forks := 2
openIssues := 3
size := 4

repository := gh.Repository{
StargazersCount: &stars,
ForksCount: &forks,
OpenIssuesCount: &openIssues,
Size: &size,
}

getFieldsReturn := getFields(&repository)

correctFieldReturn := make(map[string]interface{})

correctFieldReturn["stars"] = 1
correctFieldReturn["forks"] = 2
correctFieldReturn["open_issues"] = 3
correctFieldReturn["size"] = 4

require.Equal(t, true, reflect.DeepEqual(getFieldsReturn, correctFieldReturn))
}