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
6 changes: 6 additions & 0 deletions docs/partials/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ github_runner_repo_busy{owner, id, name, os, status}
github_runner_repo_online{owner, id, name, os, status}
: Static metrics of runner is online or not

github_workflow_job_completed_total{owner, repo, workflow_name, name, conclusion}
: Total number of completed workflow jobs

github_workflow_job_created_timestamp{owner, repo, name, title, branch, sha, identifier, run_id, run_attempt, labels, runner_id, runner_name, runner_group_id, runner_group_name, workflow_name, conclusion}
: Timestamp when the workflow job have been created

Expand All @@ -250,6 +253,9 @@ github_workflow_job_duration_ms{owner, repo, name, title, branch, sha, identifie
github_workflow_job_duration_run_created_minutes{owner, repo, name, title, branch, sha, identifier, run_id, run_attempt, labels, runner_id, runner_name, runner_group_id, runner_group_name, workflow_name, conclusion}
: Duration since the workflow run creation time in minutes

github_workflow_job_duration_seconds_total{owner, repo, workflow_name, name, conclusion}
: Total duration of completed workflow jobs in seconds

github_workflow_job_started_timestamp{owner, repo, name, title, branch, sha, identifier, run_id, run_attempt, labels, runner_id, runner_name, runner_group_id, runner_group_name, workflow_name, conclusion}
: Timestamp when the workflow job have been started

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/lib/pq v1.12.3
github.com/oklog/run v1.2.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/prometheus/exporter-toolkit v0.17.1
github.com/ryanuber/go-glob v1.0.0
github.com/stretchr/testify v1.11.1
Expand Down Expand Up @@ -200,7 +201,6 @@ require (
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.69.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
Expand Down
75 changes: 70 additions & 5 deletions pkg/exporter/workflow_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ type WorkflowJobCollector struct {
duration *prometheus.HistogramVec
config config.Target

Status *prometheus.Desc
Duration *prometheus.Desc
Creation *prometheus.Desc
Created *prometheus.Desc
Started *prometheus.Desc
Status *prometheus.Desc
Duration *prometheus.Desc
Creation *prometheus.Desc
Created *prometheus.Desc
Started *prometheus.Desc
CompletedTotal *prometheus.Desc
DurationSecondsTotal *prometheus.Desc
}

// NewWorkflowJobCollector returns a new WorkflowCollector.
Expand All @@ -33,6 +35,14 @@ func NewWorkflowJobCollector(logger *slog.Logger, client *github.Client, db stor
}

labels := cfg.WorkflowJobs.Labels
completionLabels := []string{
"owner",
"repo",
"workflow_name",
"name",
"conclusion",
}

return &WorkflowJobCollector{
client: client,
logger: logger.With("collector", "workflow_job"),
Expand Down Expand Up @@ -71,6 +81,18 @@ func NewWorkflowJobCollector(logger *slog.Logger, client *github.Client, db stor
labels,
nil,
),
CompletedTotal: prometheus.NewDesc(
"github_workflow_job_completed_total",
"Total number of completed workflow jobs",
completionLabels,
nil,
),
DurationSecondsTotal: prometheus.NewDesc(
"github_workflow_job_duration_seconds_total",
"Total duration of completed workflow jobs in seconds",
completionLabels,
nil,
),
}
}

Expand All @@ -82,6 +104,8 @@ func (c *WorkflowJobCollector) Metrics() []*prometheus.Desc {
c.Creation,
c.Created,
c.Started,
c.CompletedTotal,
c.DurationSecondsTotal,
}
}

Expand All @@ -92,6 +116,8 @@ func (c *WorkflowJobCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.Creation
ch <- c.Created
ch <- c.Started
ch <- c.CompletedTotal
ch <- c.DurationSecondsTotal
}

// Collect is called by the Prometheus registry when collecting metrics.
Expand Down Expand Up @@ -174,6 +200,45 @@ func (c *WorkflowJobCollector) Collect(ch chan<- prometheus.Metric) {
labels...,
)
}

completions, err := c.db.GetWorkflowJobCompletions()

if err != nil {
c.logger.Error("Failed to fetch workflow job completions",
"err", err,
)

c.failures.WithLabelValues("workflow_job").Inc()
return
}

c.logger.Debug("Fetched workflow job completions",
"count", len(completions),
)

for _, completion := range completions {
labels := []string{
completion.Owner,
completion.Repo,
completion.WorkflowName,
completion.Name,
completion.Conclusion,
}

ch <- prometheus.MustNewConstMetric(
c.CompletedTotal,
prometheus.CounterValue,
float64(completion.Count),
labels...,
)

ch <- prometheus.MustNewConstMetric(
c.DurationSecondsTotal,
prometheus.CounterValue,
completion.DurationSecondsTotal,
labels...,
)
}
}

func jobStatusToGauge(conclusion string) float64 {
Expand Down
134 changes: 121 additions & 13 deletions pkg/exporter/workflow_job_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package exporter

import (
"fmt"
"log/slog"
"os"
"reflect"
Expand All @@ -10,24 +9,13 @@ import (

"github.com/google/go-github/v89/github"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/promhippie/github_exporter/pkg/config"
"github.com/promhippie/github_exporter/pkg/store"
)

type StaticStore struct{}

func (s StaticStore) GetWorkflowJobRuns(owner, repo, workflow string) ([]*store.WorkflowRun, error) {
_, _ = fmt.Fprintf(
os.Stdout,
"GetWorkflowJobRuns for %s/%s %s \n",
owner,
repo,
workflow,
)

return nil, nil
}

func (s StaticStore) StoreWorkflowRunEvent(*github.WorkflowRunEvent) error {
return nil
}
Expand All @@ -52,6 +40,10 @@ func (s StaticStore) PruneWorkflowJobs(time.Duration) error {
return nil
}

func (s StaticStore) GetWorkflowJobCompletions() ([]*store.WorkflowJobCompletionAggregate, error) {
return nil, nil
}

func (s StaticStore) Open() (bool, error) {
return true, nil
}
Expand Down Expand Up @@ -118,6 +110,21 @@ func TestWorkflowJobCollector(t *testing.T) {
"Created time of the workflow job",
nil, nil,
),
Started: prometheus.NewDesc(
"github_workflow_job_started_timestamp",
"Timestamp when the workflow job have been started",
nil, nil,
),
CompletedTotal: prometheus.NewDesc(
"github_workflow_job_completed_total",
"Total number of completed workflow jobs",
nil, nil,
),
DurationSecondsTotal: prometheus.NewDesc(
"github_workflow_job_duration_seconds_total",
"Total duration of completed workflow jobs in seconds",
nil, nil,
),
}

if collector.client != mockClient {
Expand All @@ -139,3 +146,104 @@ func TestWorkflowJobCollector(t *testing.T) {
t.Errorf("Expected config to be %v, got %v", mockConfig, collector.config)
}
}

type completionStore struct {
StaticStore
completions []*store.WorkflowJobCompletionAggregate
}

func (s completionStore) GetWorkflowJobCompletions() ([]*store.WorkflowJobCompletionAggregate, error) {
return s.completions, nil
}

func TestWorkflowJobCollectorCounters(t *testing.T) {
mockLogger := slog.New(
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}),
)

mockFailures := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "test_failures_total",
Help: "Total number of test failures",
}, []string{"type"})

mockDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "test_duration_seconds",
Help: "Duration of test",
}, []string{"type"})

completions := []*store.WorkflowJobCompletionAggregate{
{
Owner: "promhippie",
Repo: "github_exporter",
WorkflowName: "CI",
Name: "test",
Conclusion: "success",
Count: 2,
DurationSecondsTotal: 42.5,
},
{
Owner: "promhippie",
Repo: "github_exporter",
WorkflowName: "CI",
Name: "test",
Conclusion: "failure",
Count: 1,
DurationSecondsTotal: 10.0,
},
}

store := completionStore{completions: completions}
collector := NewWorkflowJobCollector(
mockLogger,
nil,
store,
mockFailures,
mockDuration,
config.Target{},
)

registry := prometheus.NewRegistry()
registry.MustRegister(collector)

metrics, err := registry.Gather()
if err != nil {
t.Fatalf("failed to gather metrics: %v", err)
}

expected := map[string]float64{
"github_workflow_job_completed_total": 3,
"github_workflow_job_duration_seconds_total": 52.5,
}

for name, expectedValue := range expected {
value := metricFamilyValue(t, metrics, name)
if value != expectedValue {
t.Errorf("expected %s to be %v, got %v", name, expectedValue, value)
}
}
}

func metricFamilyValue(t *testing.T, metrics []*dto.MetricFamily, name string) float64 {
t.Helper()

for _, mf := range metrics {
if mf.GetName() != name {
continue
}

var total float64

for _, m := range mf.GetMetric() {
if m.Counter != nil {
total += m.Counter.GetValue()
}
}

return total
}

t.Errorf("metric family %s not found", name)
return 0
}
23 changes: 23 additions & 0 deletions pkg/store/chai.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@ var (
PRIMARY KEY(owner, repo, identifier)
);`,
},
{
Version: 4,
Description: "Creating table workflow_job_completions",
Script: `CREATE TABLE workflow_job_completions (
owner TEXT NOT NULL,
repo TEXT NOT NULL,
identifier BIGINT NOT NULL,
run_attempt INTEGER NOT NULL,
workflow_name TEXT,
name TEXT,
conclusion TEXT,
duration_seconds DOUBLE PRECISION,
recorded_at INTEGER,
PRIMARY KEY(owner, repo, identifier, run_attempt)
);
CREATE INDEX idx_workflow_job_completions_aggregate
ON workflow_job_completions(owner, repo, workflow_name, name, conclusion);`,
},
}
)

Expand Down Expand Up @@ -166,6 +184,11 @@ func (s *chaiStore) PruneWorkflowJobs(timeframe time.Duration) error {
return pruneWorkflowJobs(s.handle, timeframe)
}

// GetWorkflowJobCompletions implements the Store interface.
func (s *chaiStore) GetWorkflowJobCompletions() ([]*WorkflowJobCompletionAggregate, error) {
return getWorkflowJobCompletions(s.handle)
}

func (s *chaiStore) dsn() string {
if len(s.meta) > 0 {
return fmt.Sprintf(
Expand Down
Loading