Skip to content
Merged
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
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,26 @@ The `promgithub` service is a lightweight service designed to receive and proces

The `promgithub` service exports the following Prometheus metrics:

| Name | Type | Labels | Description |
|------------------------------------|-----------|------------------------------------------------------------------------------------------------|-------------------------------------------|
| `promgithub_workflow_status` | Counter | `repository`, `branch`, `workflow_name`, `workflow_status`, `conclusion` | Total number of workflow runs with status |
| `promgithub_workflow_duration` | Histogram | `repository`, `branch`, `workflow_name`, `workflow_status`, `conclusion` | Duration of workflow runs |
| `promgithub_workflow_queued` | Gauge | `repository`, `branch`, `workflow_name` | Number of workflow runs queued |
| `promgithub_workflow_in_progress` | Gauge | `repository`, `branch`, `workflow_name` | Number of workflow runs in progress |
| `promgithub_workflow_completed` | Gauge | `repository`, `branch`, `workflow_conclusion`,`workflow_name` | Number of workflow runs completed |
| `promgithub_job_status` | Counter | `runner`, `repository`, `branch`, `workflow_name`, `job_name`, `job_status`, `job_conclusion`, | Total number of jobs with status |
| `promgithub_job_duration` | Histogram | `runner`, `repository`, `branch`, `workflow_name`, `job_name`, `job_status`, `job_conclusion` | Duration of jobs runs in seconds |
| `promgithub_job_queued` | Gauge | `runner`, `repository`, `branch`, `workflow_name`, `job_name` | Number of jobs queued |
| `promgithub_job_in_progress` | Gauge | `runner`, `repository`, `branch`, `workflow_name`, `job_name` | Number of jobs in progress |
| `promgithub_job_completed` | Gauge | `runner`, `repository`, `branch`, `job_conclusion`, `workflow_name`, `job_name` | Number of jobs completed |
| `promgithub_commit_pushed` | Counter | `repository`, `branch`, `commit_author`, `commit_author_email` | Total number of commits pushed |
| `promgithub_pull_request` | Counter | `repository`, `base_branch`, `pull_request_author`, `pull_request_status` | Total number of pull requests |
| Name | Type | Labels | Description |
|------------------------------------|-----------|---------------------------------------------------------------|-------------------------------------------|
| `promgithub_workflow_status` | Counter | `repository`, `branch`, `workflow_name`, `workflow_status`, `conclusion`| Total number of workflow runs with status |
| `promgithub_workflow_duration` | Histogram | `repository`, `branch`, `workflow_name`, `workflow_status`, `conclusion`| Duration of workflow runs |
| `promgithub_workflow_queued` | Gauge | `repository`, `branch`, `workflow_name` | Number of workflow runs queued |
| `promgithub_workflow_in_progress` | Gauge | `repository`, `branch`, `workflow_name` | Number of workflow runs in progress |
| `promgithub_workflow_completed` | Gauge | `repository`, `branch`, `workflow_conclusion`,`workflow_name` | Number of workflow runs completed |
| `promgithub_job_status` | Counter | `repository`, `branch`, `workflow_name`, `job_status`, `job_conclusion` | Total number of jobs with status |
| `promgithub_job_duration` | Histogram | `repository`, `branch`, `workflow_name`, `job_status`, `job_conclusion` | Duration of jobs runs in seconds |
| `promgithub_job_queued` | Gauge | `repository`, `branch`, `workflow_name` | Number of jobs queued |
| `promgithub_job_in_progress` | Gauge | `repository`, `branch`, `workflow_name` | Number of jobs in progress |
| `promgithub_job_completed` | Gauge | `repository`, `branch`, `job_conclusion`, `workflow_name` | Number of jobs completed |
| `promgithub_commit_pushed` | Counter | `repository` | Total number of commits pushed |
| `promgithub_pull_request` | Counter | `repository`, `base_branch`, `pull_request_status` | Total number of pull requests |


## Using `promgithub` service

For usage information see [Usage documentation](./docs/usage.md)

## Contributing to `promgithub` service

For contributing guidelines see [Contributing documentation](./docs/contributing.md)
For contributing guidelines see [Contributing documentation](./docs/contributing.md)
4 changes: 2 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ PROMGITHUB_WEBHOOK_SECRET="<your webhook secret>" PROMGITHUB_SERVICE_PORT="<serv

## Prometheus scraping configuration

Configure prometheus to scrape `promgithub`'s `/metrics` endpoint to extract metrics
Configure prometheus to scrape `promgithub`'s `/metrics` endpoint to extract metrics.

### Prometheus configuration

Expand Down Expand Up @@ -155,4 +155,4 @@ spec:
- path: /metrics
interval: 15s
port: http
```
```
246 changes: 111 additions & 135 deletions src/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ type GithubPullRequest struct {
Repository GithubRepo `json:"repository"`
}

type runMetricDetails struct {
repository string
branch string
name string
status string
conclusion string
startedAt string
endedAt string
}

func validateHMAC(body []byte, signature string, secret []byte) bool {
h := hmac.New(sha256.New, secret)
h.Write(body)
Expand Down Expand Up @@ -121,146 +131,118 @@ func githubEventsHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}

func updateWorkflowMetrics(body []byte) {
var payload GithubWorkflow

if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal workflow_run payload", zap.Error(err))
return
}

workflowStatusCounter.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_name": payload.Workflow.Name,
"workflow_status": payload.Workflow.Status,
"conclusion": payload.Workflow.Conclusion,
}).Inc()
func observeRunMetrics(
details runMetricDetails,
statusCounter *prometheus.CounterVec,
queuedGauge *prometheus.GaugeVec,
inProgressGauge *prometheus.GaugeVec,
completedGauge *prometheus.GaugeVec,
durationHistogram *prometheus.HistogramVec,
) {
statusCounter.WithLabelValues(
details.repository,
details.branch,
details.name,
details.status,
details.conclusion,
).Inc()

// Handle updating the gauges based on workflow status
switch strings.ToLower(payload.Workflow.Status) {
switch strings.ToLower(details.status) {
case "queued":
workflowQueuedGauge.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_name": payload.Workflow.Name,
}).Inc()
queuedGauge.WithLabelValues(
details.repository,
details.branch,
details.name,
).Inc()
case "in_progress":
workflowInProgressGauge.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_name": payload.Workflow.Name,
}).Inc()
workflowQueuedGauge.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_name": payload.Workflow.Name,
}).Dec()
inProgressGauge.WithLabelValues(
details.repository,
details.branch,
details.name,
).Inc()
queuedGauge.WithLabelValues(
details.repository,
details.branch,
details.name,
).Dec()
case "completed":
workflowCompletedGauge.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_conclusion": payload.Workflow.Conclusion,
"workflow_name": payload.Workflow.Name,
}).Inc()
workflowInProgressGauge.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_name": payload.Workflow.Name,
}).Dec()
completedGauge.WithLabelValues(
details.repository,
details.branch,
details.conclusion,
details.name,
).Inc()
inProgressGauge.WithLabelValues(
details.repository,
details.branch,
details.name,
).Dec()

// Update duration histogram when the workflow is completed
createdAt, err1 := time.Parse(time.RFC3339, payload.Workflow.CreatedAt)
updatedAt, err2 := time.Parse(time.RFC3339, payload.Workflow.UpdatedAt)
startedAt, err1 := time.Parse(time.RFC3339, details.startedAt)
endedAt, err2 := time.Parse(time.RFC3339, details.endedAt)
if err1 == nil && err2 == nil {
duration := updatedAt.Sub(createdAt).Seconds()
workflowDurationHistogram.With(prometheus.Labels{
"repository": payload.Workflow.Repository.FullName,
"branch": payload.Workflow.Branch,
"workflow_name": payload.Workflow.Name,
"workflow_status": payload.Workflow.Status,
"conclusion": payload.Workflow.Conclusion,
}).Observe(duration)
duration := endedAt.Sub(startedAt).Seconds()
durationHistogram.WithLabelValues(
details.repository,
details.branch,
details.name,
details.status,
details.conclusion,
).Observe(duration)
}
}
}

func updateJobMetrics(body []byte) {
var payload GithubJob
func updateWorkflowMetrics(body []byte) {
var payload GithubWorkflow

if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal workflow_job payload", zap.Error(err))
logger.Error("Failed to unmarshal workflow_run payload", zap.Error(err))
return
}

jobStatusCounter.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
"job_status": payload.Job.Status,
"job_conclusion": payload.Job.Conclusion,
}).Inc()
observeRunMetrics(
runMetricDetails{
repository: payload.Workflow.Repository.FullName,
branch: payload.Workflow.Branch,
name: payload.Workflow.Name,
status: payload.Workflow.Status,
conclusion: payload.Workflow.Conclusion,
startedAt: payload.Workflow.CreatedAt,
endedAt: payload.Workflow.UpdatedAt,
},
workflowStatusCounter,
workflowQueuedGauge,
workflowInProgressGauge,
workflowCompletedGauge,
workflowDurationHistogram,
)
}

// Handle updating the gauges based on job status
switch strings.ToLower(payload.Job.Status) {
case "queued":
jobQueuedGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Inc()
case "in_progress":
jobInProgressGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Inc()
jobQueuedGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Dec()
case "completed":
jobCompletedGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"job_conclusion": payload.Job.Conclusion,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Inc()
jobInProgressGauge.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
}).Dec()
func updateJobMetrics(body []byte) {
var payload GithubJob

// Update duration histogram when the job is completed
startedAt, err1 := time.Parse(time.RFC3339, payload.Job.StartedAt)
completedAt, err2 := time.Parse(time.RFC3339, payload.Job.CompletedAt)
if err1 == nil && err2 == nil {
duration := completedAt.Sub(startedAt).Seconds()
jobDurationHistogram.With(prometheus.Labels{
"runner": payload.Job.RunnerName,
"repository": payload.Job.Repository.FullName,
"branch": payload.Job.Branch,
"workflow_name": payload.Job.WorkflowName,
"job_name": payload.Job.Name,
"job_status": payload.Job.Status,
"job_conclusion": payload.Job.Conclusion,
}).Observe(duration)
}
if err := json.Unmarshal(body, &payload); err != nil {
logger.Error("Failed to unmarshal workflow_job payload", zap.Error(err))
return
}

observeRunMetrics(
runMetricDetails{
repository: payload.Job.Repository.FullName,
branch: payload.Job.Branch,
name: payload.Job.WorkflowName,
status: payload.Job.Status,
conclusion: payload.Job.Conclusion,
startedAt: payload.Job.StartedAt,
endedAt: payload.Job.CompletedAt,
},
jobStatusCounter,
jobQueuedGauge,
jobInProgressGauge,
jobCompletedGauge,
jobDurationHistogram,
)
}

func updateCommitMetrics(body []byte) {
Expand All @@ -271,13 +253,8 @@ func updateCommitMetrics(body []byte) {
return
}

for _, commit := range payload.Commits {
commitPushedCounter.With(prometheus.Labels{
"repository": payload.Repository.FullName,
"branch": payload.Ref,
"commit_author": commit.Author.Name,
"commit_author_email": commit.Author.Email,
}).Inc()
for range payload.Commits {
commitPushedCounter.WithLabelValues(payload.Repository.FullName).Inc()
}
}

Expand All @@ -289,10 +266,9 @@ func updatePullRequestMetrics(body []byte) {
return
}

pullRequestCounter.With(prometheus.Labels{
"repository": payload.Repository.FullName,
"base_branch": payload.PullRequest.Base.Ref,
"pull_request_author": payload.PullRequest.User.Login,
"pull_request_status": payload.Action,
}).Inc()
pullRequestCounter.WithLabelValues(
payload.Repository.FullName,
payload.PullRequest.Base.Ref,
payload.Action,
).Inc()
}
Loading
Loading