Skip to content

Commit

Permalink
Merge pull request #3 from Rucknar/updates
Browse files Browse the repository at this point in the history
Updated version, supports pagination
  • Loading branch information
jwholdsworth authored Jan 8, 2019
2 parents 2043ce4 + d58196b commit bbddd60
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 74 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
jira-cloud-exporter
TODO
133 changes: 87 additions & 46 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"

Expand All @@ -13,23 +14,23 @@ import (
log "github.com/sirupsen/logrus"
)

// JiraCollector initiates the collection of metrics from a JIRA instance
func JiraCollector() *JiraMetrics {
return &JiraMetrics{
jiraIssues: prometheus.NewDesc(prometheus.BuildFQName("jira", "cloud", "exporter"),
// JiraCollector initiates the collection of Metrics from a JIRA instance
func JiraCollector() *Metrics {
return &Metrics{
issue: prometheus.NewDesc(prometheus.BuildFQName("jira", "cloud", "issue"),
"Shows the number of issues matching the JQL",
[]string{"status", "project", "key", "assignee"}, nil,
),
}
}

// Describe writes all descriptors to the prometheus desc channel.
func (collector *JiraMetrics) Describe(ch chan<- *prometheus.Desc) {
ch <- collector.jiraIssues
func (collector *Metrics) Describe(ch chan<- *prometheus.Desc) {
ch <- collector.issue
}

//Collect implements required collect function for all prometheus collectors
func (collector *JiraMetrics) Collect(ch chan<- prometheus.Metric) {
func (collector *Metrics) Collect(ch chan<- prometheus.Metric) {

collectedIssues, err := fetchJiraIssues()
if err != nil {
Expand All @@ -39,7 +40,7 @@ func (collector *JiraMetrics) Collect(ch chan<- prometheus.Metric) {

for _, issue := range collectedIssues.Issues {
createdTimestamp := convertToUnixTime(issue.Fields.Created)
ch <- prometheus.MustNewConstMetric(collector.jiraIssues, prometheus.CounterValue, createdTimestamp, issue.Fields.Status.Name, issue.Fields.Project.Name, issue.Key, issue.Fields.Assignee.Name)
ch <- prometheus.MustNewConstMetric(collector.issue, prometheus.CounterValue, createdTimestamp, issue.Fields.Status.Name, issue.Fields.Project.Name, issue.Key, issue.Fields.Assignee.Name)
}
}

Expand All @@ -54,64 +55,104 @@ func convertToUnixTime(timestamp string) float64 {
return float64(dateTime.Unix())
}

func fetchJiraIssues() (JiraIssues, error) {
func fetchJiraIssues() (jiraIssue, error) {

cfgs, err := config.Init()
if err != nil {
log.Error(err)
}
var AllIssues JiraIssues
var AllIssues jiraIssue

for _, cfg := range cfgs {
var jiraIssues JiraIssues

// Confirm the Jira URL begins with the http:// or https:// scheme specification
// Also emit a warning if HTTPS isn't being used
if !strings.HasPrefix(cfg.JiraURL, "http") {
err := fmt.Errorf("The Jira URL: %s does not begin with 'http'", cfg.JiraURL)
return jiraIssues, err
} else if !strings.HasPrefix(cfg.JiraURL, "https://") {
log.Warn("The Jira URL: ", cfg.JiraURL, " is insecure, your API token is being sent in clear text")
}
if len(cfg.JiraUsername) < 6 {
log.Warn("The Jira username has fewer than 6 characters, are you sure it is valid?")
}
if len(cfg.JiraToken) < 10 {
log.Warn("The Jira token has fewer than 10 characters, are you sure it is valid?")
}
var ji jiraIssue

client := http.Client{}
url := fmt.Sprintf("%s/rest/api/2/search?jql=%s", cfg.JiraURL, cfg.JiraJql)
req, err := http.NewRequest(http.MethodGet, url, nil)
err = validateJiraCfg(cfg)
if err != nil {
return jiraIssues, err
return ji, err
}
req.Header.Set("User-Agent", "jira-cloud-exporter")
req.SetBasicAuth(cfg.JiraUsername, cfg.JiraToken)
log.Info(fmt.Sprintf("Sending request to %s", url))
res, err := client.Do(req)

url := fmt.Sprintf("%s/rest/api/2/search?jql=%s", cfg.JiraURL, cfg.JiraJql)
resp, err := fetchAPIResults(url, cfg.JiraUsername, cfg.JiraToken)

err = json.Unmarshal(resp, &ji)
if err != nil {
return jiraIssues, err
return ji, err
}

body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
return jiraIssues, err
}
AllIssues.Issues = append(AllIssues.Issues, ji.Issues...)

jsonError := json.Unmarshal(body, &jiraIssues)
if jsonError != nil {
return jiraIssues, err
}
// Pagination support
if ji.Total > len(AllIssues.Issues) {
var startsAt int

for {

AllIssues.Issues = append(AllIssues.Issues, jiraIssues.Issues...)
// we use startsAt to track our process through the pagination
// here we set it to the lenghth of the intial capture, + 1
startsAt = len(AllIssues.Issues) + 1

url := fmt.Sprintf("%s/rest/api/2/search?jql=%s&startAt=%d", cfg.JiraURL, cfg.JiraJql, startsAt)
resp, err := fetchAPIResults(url, cfg.JiraUsername, cfg.JiraToken)

err = json.Unmarshal(resp, &ji)
if err != nil {
return ji, err
}

AllIssues.Issues = append(AllIssues.Issues, ji.Issues...)

// The API has a funny way of counting, to ensure we get all issues
// We break when no more issues are returned
if len(ji.Issues) == 0 {
log.Debug("No futher issues returned from API")
break
}

}
}
}

return AllIssues, nil
}

type error interface {
Error() string
func fetchAPIResults(url, user, token string) ([]byte, error) {

client := http.Client{}

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

req.Header.Set("User-Agent", "jira-cloud-exporter")
req.SetBasicAuth(user, token)
log.Infof(fmt.Sprintf("Sending request to %s", url))

resp, err := client.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
return body, err
}

func validateJiraCfg(cfg config.Config) error {

_, err := url.ParseRequestURI(cfg.JiraURL)
if err != nil {
return fmt.Errorf("Error validating URL, please ensure the URL is valid: %v", err)
}

if !strings.HasPrefix(cfg.JiraURL, "https://") {
return fmt.Errorf("The Jira URL: %s is insecure, your API token is being sent in clear text", cfg.JiraURL)
}

if cfg.JiraUsername == "" || cfg.JiraToken == "" {
return fmt.Errorf("Check credentials supplied are set and valid")
}

return nil
}
50 changes: 22 additions & 28 deletions collector/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,28 @@ package collector

import "github.com/prometheus/client_golang/prometheus"

type JiraMetrics struct {
jiraIssues *prometheus.Desc
// Metrics tracks all the contextual metrics for this exporter
type Metrics struct {
issue *prometheus.Desc
}

type JiraIssue struct {
Fields Fields `json:"fields"`
Key string `json:"key"`
}

type Fields struct {
Assignee Assignee `json:"assignee"`
Project Project `json:"project"`
Status Status `json:"status"`
Created string `json:"created"`
}

type Assignee struct {
Name string `json:"name"`
}

type Status struct {
Name string `json:"name"`
}

type Project struct {
Name string `json:"name"`
}

type JiraIssues struct {
Issues []JiraIssue `json:"issues"`
type jiraIssue struct {
Issues []struct {
Fields struct {
Assignee struct {
Name string `json:"name"`
} `json:"assignee"`
Created string `json:"created"`
Project struct {
Name string `json:"name"`
} `json:"project"`
Status struct {
Name string `json:"name"`
} `json:"status"`
} `json:"fields"`
Key string `json:"key"`
} `json:"issues"`
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
}

0 comments on commit bbddd60

Please sign in to comment.