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
6 changes: 6 additions & 0 deletions docs/partials/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ github_status_pull_requests_up{}
github_status_webhooks_up{}
: Current health status of Webhooks on GitHub status page

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 @@ -280,6 +283,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
76 changes: 71 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
DurationTotal *prometheus.Desc
}

// NewWorkflowJobCollector returns a new WorkflowCollector.
Expand All @@ -33,6 +35,15 @@ 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 +82,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,
),
DurationTotal: prometheus.NewDesc(
"github_workflow_job_duration_seconds_total",
"Total duration of completed workflow jobs in seconds",
completionLabels,
nil,
),
}
}

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

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

// Collect is called by the Prometheus registry when collecting metrics.
Expand Down Expand Up @@ -180,6 +207,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.DurationTotal,
prometheus.CounterValue,
completion.DurationSecondsTotal,
labels...,
)
}
}

func jobStatusToGauge(conclusion string) float64 {
Expand Down
146 changes: 127 additions & 19 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/v92/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 @@ -99,23 +91,38 @@ func TestWorkflowJobCollector(t *testing.T) {
duration: mockDuration,
config: mockConfig,
Status: prometheus.NewDesc(
"workflow_job_status",
"github_workflow_job_status",
"Status of the workflow job",
nil, nil,
),
Duration: prometheus.NewDesc(
"workflow_job_duration_seconds",
"github_workflow_job_duration_ms",
"Duration of the workflow job",
nil, nil,
),
Creation: prometheus.NewDesc(
"workflow_job_creation_timestamp_seconds",
"Creation time of the workflow job",
"github_workflow_job_duration_run_created_minutes",
"Duration since the workflow run creation time in minutes",
nil, nil,
),
Created: prometheus.NewDesc(
"workflow_job_created_timestamp_seconds",
"Created time of the workflow job",
"github_workflow_job_created_timestamp",
"Timestamp when the workflow job have been created",
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,
),
DurationTotal: prometheus.NewDesc(
"github_workflow_job_duration_seconds_total",
"Total duration of completed workflow jobs in seconds",
nil, nil,
),
}
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
}
27 changes: 27 additions & 0 deletions pkg/store/chai.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ 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)
);`,
},
{
Version: 5,
Description: "Creating index for workflow_job_completions aggregate",
Script: `CREATE INDEX idx_workflow_job_completions_aggregate
ON workflow_job_completions(owner, repo, workflow_name, name, conclusion);`,
},
}
)

Expand Down Expand Up @@ -166,6 +188,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
Loading