Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 15 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ permissions:
packages: write

env:
REGISTRY: ghcr.io
IMAGE: ${{ github.repository }}
REGISTRY: docker.io
IMAGE: juroapp
APP_NAME: github-actions-exporter

jobs:
Expand All @@ -38,7 +38,7 @@ jobs:

- name: Build app
run: CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-X 'main.version=${{ steps.version.outputs.full }}'" -o bin/${{ env.APP_NAME }} .

- name: Generate MD5
run: md5sum bin/${{ env.APP_NAME }} > bin/${{ env.APP_NAME }}.md5

Expand All @@ -52,11 +52,18 @@ jobs:
bin/${{ env.APP_NAME }}.md5
prerelease: ${{ steps.version.outputs.prerelease }}

- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}/${{ env.APP_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
Expand All @@ -65,13 +72,12 @@ jobs:
type=semver,pattern={{major}}
type=sha

- name: Log in to GitHub Container Registry
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
Expand All @@ -80,4 +86,4 @@ jobs:
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Authentication can either via a Github Token or the Github App Authentication 3
| Github Api URL | github_api_url, url | GITHUB_API_URL | api.github.com | Github API URL (primarily for Github Enterprise usage) |
| Github Enterprise Name | enterprise_name | ENTERPRISE_NAME | "" | Enterprise name. Needed for enterprise endpoints (/enterprises/{ENTERPRISE_NAME}/*). Currently used to get Enterprise level tunners status |
| Fields to export | export_fields | EXPORT_FIELDS | repo,id,node_id,head_branch,head_sha,run_number,workflow_id,workflow,event,status | A comma separated list of fields for workflow metrics that should be exported |
| Export job fields | export_job_fields | EXPORT_JOB_FIELDS | repo,workflow,job_name,conclusion,event | A comma-separated list of fields for job-level metrics that should be exported |

## Exported stats

Expand Down Expand Up @@ -94,6 +95,48 @@ Gauge type
| workflow | Workflow Name |
| status | Workflow status (completed/in_progress) |

### github_workflow_job_status
Gauge type

**Result possibility**

| ID | Description |
|---|---|
| 0 | Failure |
| 1 | Success |
| 2 | Skipped |
| 3 | In Progress |
| 4 | Queued |

**Fields**

| Name | Description |
|---|---|
| repo | Repository like \<org>/\<repo> |
| workflow | Workflow Name |
| job_name | Name of the job |
| conclusion | Job conclusion (success/failure/skipped/cancelled) |
| event | Event type like push/pull_request/... |

### github_workflow_job_duration_seconds
Gauge type

**Result possibility**

| Gauge | Description |
|---|---|
| seconds | Number of seconds that a specific workflow job took time to complete. |

**Fields**

| Name | Description |
|---|---|
| repo | Repository like \<org>/\<repo> |
| workflow | Workflow Name |
| job_name | Name of the job |
| conclusion | Job conclusion (success/failure/skipped/cancelled) |
| event | Event type like push/pull_request/... |

### github_job
> :warning: **This is a duplicate of the `github_workflow_run_status` metric that will soon be deprecated, do not use anymore.**

Expand Down
1 change: 1 addition & 0 deletions charts/github-actions-exporter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ env:
GITHUB_API_URL: api.github.com
# ENTERPRISE_NAME: "" # Needed for enterprise endpoints (/enterprises/{ENTERPRISE_NAME}/*)
# EXPORT_FIELDS: "repo,id,node_id,head_branch,head_sha,run_number,workflow_id,workflow,event,status" # A comma separated list of fields for workflow metrics that should be exported
# EXPORT_JOB_FIELDS: "repo,workflow,job_name,conclusion,event" # A comma separated list of fields for job metrics that should be exported
# For the github authentications need to create a secret by default called actions-exporter
# for authentication via github personal token
# key: github_token
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (
Debug bool
EnterpriseName string
WorkflowFields string
WorkflowJobFields string
)

// InitConfiguration - set configuration from env vars or command parameters
Expand Down Expand Up @@ -113,6 +114,13 @@ func InitConfiguration() []cli.Flag {
Value: "repo,id,node_id,head_branch,head_sha,run_number,workflow_id,workflow,event,status",
Destination: &WorkflowFields,
},
&cli.StringFlag{
Name: "export_job_fields",
EnvVars: []string{"EXPORT_JOB_FIELDS"},
Usage: "A comma separated list of fields for job metrics that should be exported",
Value: "repo,workflow,job_name,conclusion,event,run_id,job_id",
Destination: &WorkflowJobFields,
},
&cli.BoolFlag{
Name: "fetch_workflow_run_usage",
EnvVars: []string{"FETCH_WORKFLOW_RUN_USAGE"},
Expand Down
117 changes: 117 additions & 0 deletions pkg/metrics/get_workflow_jobs_from_github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package metrics

import (
"context"
"log"
"strings"
"strconv"
"time"

"github.com/google/go-github/v45/github"
"github.com/spendesk/github-actions-exporter/pkg/config"
)

// getJobFieldValue - returns a value for a given field from a GitHub workflow job
func getJobFieldValue(repo string, workflow string, event string, job *github.WorkflowJob, field string) string {
switch field {
case "repo":
return repo
case "workflow":
return workflow
case "job_name":
return job.GetName()
case "conclusion":
return job.GetConclusion()
case "event":
return event
case "run_id":
return strconv.FormatInt(job.GetRunID(), 10)
case "job_id":
return strconv.FormatInt(job.GetID(), 10)
}
log.Printf("Tried to fetch invalid job field '%s'", field)
return ""
}

func getRelevantJobFields(repo string, workflow string, event string, job *github.WorkflowJob) []string {
relevantFields := strings.Split(config.WorkflowJobFields, ",")
result := make([]string, len(relevantFields))
for i, field := range relevantFields {
result[i] = getJobFieldValue(repo, workflow, event,job, field)
}
return result
}

func getJobsForRun(owner string, repo string, runID int64) []*github.WorkflowJob {
opt := &github.ListWorkflowJobsOptions{
ListOptions: github.ListOptions{PerPage: 100},
}
var jobs []*github.WorkflowJob
for {
resp, rr, err := client.Actions.ListWorkflowJobs(context.Background(), owner, repo, runID, opt)
if rl_err, ok := err.(*github.RateLimitError); ok {
log.Printf("ListWorkflowJobs ratelimited. Pausing until %s", rl_err.Rate.Reset.Time.String())
time.Sleep(time.Until(rl_err.Rate.Reset.Time))
continue
} else if err != nil {
log.Printf("ListWorkflowJobs error for run %d in repo %s/%s: %s", runID, owner, repo, err.Error())
return jobs
}
jobs = append(jobs, resp.Jobs...)
if rr.NextPage == 0 {
break
}
opt.Page = rr.NextPage
}
return jobs
}

// getWorkflowJobsFromGithub - fetch jobs for each workflow run and emit metrics
func getWorkflowJobsFromGithub() {
for {
for _, repo := range repositories {
r := strings.Split(repo, "/")
runs := getRecentWorkflowRuns(r[0], r[1])

// Create a map of run IDs to their events
runEventMap := make(map[int64]string)
for _, run := range runs {
runEventMap[run.GetID()] = run.GetEvent()
}

for _, run := range runs {
event := runEventMap[run.GetID()]
workflowName := getFieldValue(repo, *run, "workflow")
jobs := getJobsForRun(r[0], r[1], run.GetID())

for _, job := range jobs {
fields := getRelevantJobFields(repo, workflowName, event, job)

var status float64 = 0
switch job.GetConclusion() {
case "success":
status = 1
case "skipped":
status = 2
case "in_progress":
status = 3
case "queued":
status = 4
}
workflowJobStatusGauge.WithLabelValues(fields...).Set(status)

start := job.GetStartedAt()
end := job.GetCompletedAt()

if !start.IsZero() && !end.IsZero() {
duration := end.Time.Sub(start.Time).Seconds()
workflowJobDurationGauge.WithLabelValues(fields...).Set(duration)
} else {
log.Printf("Skipping duration metric for job %s in run %d (start or end time missing)", job.GetName(), job.GetRunID())
}
}
}
}
time.Sleep(time.Duration(config.Github.Refresh) * time.Second)
}
}
37 changes: 29 additions & 8 deletions pkg/metrics/get_workflow_runs_from_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func getFieldValue(repo string, run github.WorkflowRun, field string) string {
return *run.Event
case "status":
return *run.Status
case "conclusion":
return run.GetConclusion()
}
log.Printf("Tried to fetch invalid field '%s'", field)
return ""
Expand Down Expand Up @@ -109,21 +111,41 @@ func getWorkflowRunsFromGithub() {
for _, repo := range repositories {
r := strings.Split(repo, "/")
runs := getRecentWorkflowRuns(r[0], r[1])

for _, run := range runs {
var s float64 = 0
if run.GetConclusion() == "success" {
switch run.GetConclusion() {
case "completed":
s = 1
} else if run.GetConclusion() == "skipped" {
case "action_required":
s = 2
} else if run.GetConclusion() == "in_progress" {
case "cancelled":
s = 3
} else if run.GetConclusion() == "queued" {
case "failure":
s = 4
case "neutral":
s = 5
case "skipped":
s = 6
case "stale":
s = 7
case "success":
s = 8
case "timed_out":
s = 9
case "in_progress":
s = 10
case "queued":
s = 11
case "requested":
s = 12
case "waiting":
s = 13
case "pending":
s = 14
default:
s = 0 // unknown status
}

fields := getRelevantFields(repo, run)

workflowRunStatusGauge.WithLabelValues(fields...).Set(s)

var run_usage *github.WorkflowRunUsage = nil
Expand All @@ -140,7 +162,6 @@ func getWorkflowRunsFromGithub() {
}
}
}

time.Sleep(time.Duration(config.Github.Refresh) * time.Second)
}
}
19 changes: 19 additions & 0 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ var (
err error
workflowRunStatusGauge *prometheus.GaugeVec
workflowRunDurationGauge *prometheus.GaugeVec
workflowJobStatusGauge *prometheus.GaugeVec
workflowJobDurationGauge *prometheus.GaugeVec
)

// InitMetrics - register metrics in prometheus lib and start func for monitor
Expand All @@ -41,10 +43,26 @@ func InitMetrics() {
},
strings.Split(config.WorkflowFields, ","),
)
workflowJobStatusGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "github_workflow_job_status",
Help: "Status of each job inside GitHub workflow run",
},
strings.Split(config.WorkflowJobFields, ","),
)
workflowJobDurationGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "github_workflow_job_duration_seconds",
Help: "Duration of each GitHub workflow job in seconds",
},
strings.Split(config.WorkflowJobFields, ","),
)
prometheus.MustRegister(runnersGauge)
prometheus.MustRegister(runnersOrganizationGauge)
prometheus.MustRegister(workflowRunStatusGauge)
prometheus.MustRegister(workflowRunDurationGauge)
prometheus.MustRegister(workflowJobStatusGauge)
prometheus.MustRegister(workflowJobDurationGauge)
prometheus.MustRegister(workflowBillGauge)
prometheus.MustRegister(runnersEnterpriseGauge)

Expand All @@ -65,6 +83,7 @@ func InitMetrics() {
go getRunnersFromGithub()
go getRunnersOrganizationFromGithub()
go getWorkflowRunsFromGithub()
go getWorkflowJobsFromGithub()
go getRunnersEnterpriseFromGithub()
}

Expand Down