@@ -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 | +} |