@@ -0,0 +1,167 @@ |
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | + |
| 3 | +package metrics |
| 4 | + |
| 5 | +import ( |
| 6 | + "context" |
| 7 | + "testing" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/jackc/pgx/v5/pgtype" |
| 11 | + "github.com/prometheus/client_golang/prometheus" |
| 12 | + dto "github.com/prometheus/client_model/go" |
| 13 | + |
| 14 | + actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc" |
| 15 | + reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 16 | + "github.com/tenseleyFlow/shithub/internal/testing/dbtest" |
| 17 | + usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc" |
| 18 | +) |
| 19 | + |
| 20 | +func TestRefreshActionsPublishesQueueRunnerAndStorageGauges(t *testing.T) { |
| 21 | + ctx := context.Background() |
| 22 | + pool := dbtest.NewTestDB(t) |
| 23 | + q := actionsdb.New() |
| 24 | + |
| 25 | + user, err := usersdb.New().CreateUser(ctx, pool, usersdb.CreateUserParams{ |
| 26 | + Username: "metrics-observer", |
| 27 | + DisplayName: "Metrics Observer", |
| 28 | + PasswordHash: "hash", |
| 29 | + }) |
| 30 | + if err != nil { |
| 31 | + t.Fatalf("CreateUser: %v", err) |
| 32 | + } |
| 33 | + repo, err := reposdb.New().CreateRepo(ctx, pool, reposdb.CreateRepoParams{ |
| 34 | + OwnerUserID: pgtype.Int8{Int64: user.ID, Valid: true}, |
| 35 | + Name: "actions-metrics", |
| 36 | + DefaultBranch: "trunk", |
| 37 | + Visibility: reposdb.RepoVisibilityPublic, |
| 38 | + }) |
| 39 | + if err != nil { |
| 40 | + t.Fatalf("CreateRepo: %v", err) |
| 41 | + } |
| 42 | + |
| 43 | + run, err := q.InsertWorkflowRun(ctx, pool, actionsdb.InsertWorkflowRunParams{ |
| 44 | + RepoID: repo.ID, |
| 45 | + RunIndex: 1, |
| 46 | + WorkflowFile: ".shithub/workflows/ci.yml", |
| 47 | + WorkflowName: "CI", |
| 48 | + HeadSha: "0123456789abcdef0123456789abcdef01234567", |
| 49 | + HeadRef: "trunk", |
| 50 | + Event: actionsdb.WorkflowRunEventPush, |
| 51 | + EventPayload: []byte(`{}`), |
| 52 | + ActorUserID: pgtype.Int8{Int64: user.ID, Valid: true}, |
| 53 | + }) |
| 54 | + if err != nil { |
| 55 | + t.Fatalf("InsertWorkflowRun: %v", err) |
| 56 | + } |
| 57 | + job, err := q.InsertWorkflowJob(ctx, pool, actionsdb.InsertWorkflowJobParams{ |
| 58 | + RunID: run.ID, |
| 59 | + JobIndex: 0, |
| 60 | + JobKey: "build", |
| 61 | + JobName: "Build", |
| 62 | + RunsOn: `["ubuntu-latest"]`, |
| 63 | + TimeoutMinutes: 30, |
| 64 | + Permissions: []byte(`{}`), |
| 65 | + JobEnv: []byte(`{}`), |
| 66 | + }) |
| 67 | + if err != nil { |
| 68 | + t.Fatalf("InsertWorkflowJob: %v", err) |
| 69 | + } |
| 70 | + step, err := q.InsertWorkflowStep(ctx, pool, actionsdb.InsertWorkflowStepParams{ |
| 71 | + JobID: job.ID, |
| 72 | + StepIndex: 0, |
| 73 | + StepID: "test", |
| 74 | + StepName: "Test", |
| 75 | + RunCommand: "go test ./...", |
| 76 | + WorkingDirectory: ".", |
| 77 | + StepEnv: []byte(`{}`), |
| 78 | + StepWith: []byte(`{}`), |
| 79 | + }) |
| 80 | + if err != nil { |
| 81 | + t.Fatalf("InsertWorkflowStep: %v", err) |
| 82 | + } |
| 83 | + if _, err := pool.Exec(ctx, `UPDATE workflow_steps SET log_object_key = $1, log_byte_count = $2 WHERE id = $3`, "actions/logs/test.log", int64(123), step.ID); err != nil { |
| 84 | + t.Fatalf("mark step log object: %v", err) |
| 85 | + } |
| 86 | + if _, err := q.AppendStepLogChunk(ctx, pool, actionsdb.AppendStepLogChunkParams{ |
| 87 | + StepID: step.ID, |
| 88 | + Seq: 0, |
| 89 | + Chunk: []byte("hello"), |
| 90 | + }); err != nil { |
| 91 | + t.Fatalf("AppendStepLogChunk: %v", err) |
| 92 | + } |
| 93 | + if _, err := q.InsertArtifact(ctx, pool, actionsdb.InsertArtifactParams{ |
| 94 | + RunID: run.ID, |
| 95 | + Name: "bundle", |
| 96 | + ObjectKey: "actions/artifacts/bundle.zip", |
| 97 | + ByteCount: 2048, |
| 98 | + ExpiresAt: pgtype.Timestamptz{ |
| 99 | + Time: time.Now().UTC().Add(24 * time.Hour), |
| 100 | + Valid: true, |
| 101 | + }, |
| 102 | + }); err != nil { |
| 103 | + t.Fatalf("InsertArtifact: %v", err) |
| 104 | + } |
| 105 | + runner, err := q.InsertRunner(ctx, pool, actionsdb.InsertRunnerParams{ |
| 106 | + Name: "runner-a", |
| 107 | + Labels: []string{"self-hosted", "linux", "ubuntu-latest"}, |
| 108 | + Capacity: 3, |
| 109 | + RegisteredByUserID: pgtype.Int8{Int64: user.ID, Valid: true}, |
| 110 | + }) |
| 111 | + if err != nil { |
| 112 | + t.Fatalf("InsertRunner: %v", err) |
| 113 | + } |
| 114 | + if _, err := pool.Exec(ctx, `UPDATE workflow_runners SET status = 'busy', last_heartbeat_at = now() - interval '75 seconds' WHERE id = $1`, runner.ID); err != nil { |
| 115 | + t.Fatalf("touch runner heartbeat: %v", err) |
| 116 | + } |
| 117 | + |
| 118 | + resetActionsObserverGauges() |
| 119 | + refreshActions(ctx, pool) |
| 120 | + |
| 121 | + assertGauge(t, ActionsQueueDepth, []string{"runs"}, 1) |
| 122 | + assertGauge(t, ActionsQueueDepth, []string{"jobs"}, 1) |
| 123 | + assertGauge(t, ActionsActive, []string{"runs"}, 0) |
| 124 | + assertGauge(t, ActionsActive, []string{"jobs"}, 0) |
| 125 | + assertGauge(t, ActionsRunnerCapacity, []string{"runner-a", "busy"}, 3) |
| 126 | + if got := gaugeValue(t, ActionsRunnerHeartbeatAgeSeconds, []string{"runner-a", "busy"}); got < 60 { |
| 127 | + t.Fatalf("runner heartbeat age = %v, want >= 60", got) |
| 128 | + } |
| 129 | + assertGauge(t, ActionsStorageObjects, []string{"artifacts"}, 1) |
| 130 | + assertGauge(t, ActionsStorageBytes, []string{"artifacts"}, 2048) |
| 131 | + assertGauge(t, ActionsStorageObjects, []string{"step_logs"}, 1) |
| 132 | + assertGauge(t, ActionsStorageBytes, []string{"step_logs"}, 123) |
| 133 | + assertGauge(t, ActionsStorageObjects, []string{"hot_log_chunks"}, 1) |
| 134 | + assertGauge(t, ActionsStorageBytes, []string{"hot_log_chunks"}, 5) |
| 135 | +} |
| 136 | + |
| 137 | +type labeledGauge interface { |
| 138 | + WithLabelValues(lvs ...string) prometheus.Gauge |
| 139 | +} |
| 140 | + |
| 141 | +func resetActionsObserverGauges() { |
| 142 | + ActionsQueueDepth.Reset() |
| 143 | + ActionsActive.Reset() |
| 144 | + ActionsRunnerHeartbeatAgeSeconds.Reset() |
| 145 | + ActionsRunnerCapacity.Reset() |
| 146 | + ActionsStorageObjects.Reset() |
| 147 | + ActionsStorageBytes.Reset() |
| 148 | +} |
| 149 | + |
| 150 | +func assertGauge(t *testing.T, vec labeledGauge, labels []string, want float64) { |
| 151 | + t.Helper() |
| 152 | + if got := gaugeValue(t, vec, labels); got != want { |
| 153 | + t.Fatalf("gauge %v = %v, want %v", labels, got, want) |
| 154 | + } |
| 155 | +} |
| 156 | + |
| 157 | +func gaugeValue(t *testing.T, vec labeledGauge, labels []string) float64 { |
| 158 | + t.Helper() |
| 159 | + var metric dto.Metric |
| 160 | + if err := vec.WithLabelValues(labels...).Write(&metric); err != nil { |
| 161 | + t.Fatalf("read gauge %v: %v", labels, err) |
| 162 | + } |
| 163 | + if metric.Gauge == nil { |
| 164 | + t.Fatalf("gauge %v missing", labels) |
| 165 | + } |
| 166 | + return metric.Gauge.GetValue() |
| 167 | +} |