| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package telemetry |
| 4 | |
| 5 | import ( |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/jackc/pgx/v5/pgtype" |
| 10 | "github.com/prometheus/client_golang/prometheus" |
| 11 | dto "github.com/prometheus/client_model/go" |
| 12 | |
| 13 | actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc" |
| 14 | "github.com/tenseleyFlow/shithub/internal/infra/metrics" |
| 15 | ) |
| 16 | |
| 17 | func TestRecordRunTerminalBoundsLabelsAndDuration(t *testing.T) { |
| 18 | metrics.ActionsRunsCompletedTotal.Reset() |
| 19 | metrics.ActionsRunDurationSeconds.Reset() |
| 20 | |
| 21 | started := time.Date(2026, 5, 12, 10, 0, 0, 0, time.UTC) |
| 22 | completed := started.Add(75 * time.Second) |
| 23 | RecordRunTerminal(actionsdb.WorkflowRun{ |
| 24 | Event: actionsdb.WorkflowRunEvent("pull_request"), |
| 25 | Status: actionsdb.WorkflowRunStatusCompleted, |
| 26 | Conclusion: actionsdb.NullCheckConclusion{CheckConclusion: actionsdb.CheckConclusionSuccess, Valid: true}, |
| 27 | StartedAt: pgtype.Timestamptz{Time: started, Valid: true}, |
| 28 | CompletedAt: pgtype.Timestamptz{Time: completed, Valid: true}, |
| 29 | }) |
| 30 | |
| 31 | var completedMetric dto.Metric |
| 32 | if err := metrics.ActionsRunsCompletedTotal.WithLabelValues("pull_request", "success").Write(&completedMetric); err != nil { |
| 33 | t.Fatalf("read completed counter: %v", err) |
| 34 | } |
| 35 | if got := completedMetric.GetCounter().GetValue(); got != 1 { |
| 36 | t.Fatalf("completed counter = %v, want 1", got) |
| 37 | } |
| 38 | |
| 39 | histogram, ok := metrics.ActionsRunDurationSeconds.WithLabelValues("pull_request", "success").(prometheus.Histogram) |
| 40 | if !ok { |
| 41 | t.Fatalf("duration metric is not a prometheus.Histogram") |
| 42 | } |
| 43 | var durationMetric dto.Metric |
| 44 | if err := histogram.Write(&durationMetric); err != nil { |
| 45 | t.Fatalf("read duration histogram: %v", err) |
| 46 | } |
| 47 | if got := durationMetric.GetHistogram().GetSampleCount(); got != 1 { |
| 48 | t.Fatalf("duration sample count = %v, want 1", got) |
| 49 | } |
| 50 | if got := durationMetric.GetHistogram().GetSampleSum(); got != 75 { |
| 51 | t.Fatalf("duration sample sum = %v, want 75", got) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func TestRecordStepTerminalUsesBoundedStepTypes(t *testing.T) { |
| 56 | metrics.ActionsStepsCompletedTotal.Reset() |
| 57 | |
| 58 | RecordStepTerminal(actionsdb.WorkflowStep{ |
| 59 | UsesAlias: "actions/checkout@v4", |
| 60 | Status: actionsdb.WorkflowStepStatusCompleted, |
| 61 | Conclusion: actionsdb.NullCheckConclusion{CheckConclusion: actionsdb.CheckConclusionSuccess, Valid: true}, |
| 62 | }) |
| 63 | RecordStepTerminal(actionsdb.WorkflowStep{ |
| 64 | StepName: "user controlled label with high cardinality potential", |
| 65 | UsesAlias: "owner/custom-action@v1", |
| 66 | Status: actionsdb.WorkflowStepStatusCompleted, |
| 67 | Conclusion: actionsdb.NullCheckConclusion{CheckConclusion: actionsdb.CheckConclusionFailure, Valid: true}, |
| 68 | }) |
| 69 | RecordStepTerminal(actionsdb.WorkflowStep{ |
| 70 | RunCommand: "go test ./...", |
| 71 | Status: actionsdb.WorkflowStepStatusCompleted, |
| 72 | Conclusion: actionsdb.NullCheckConclusion{CheckConclusion: actionsdb.CheckConclusionSuccess, Valid: true}, |
| 73 | }) |
| 74 | |
| 75 | assertStepCounter(t, "checkout", "success", 1) |
| 76 | assertStepCounter(t, "uses", "failure", 1) |
| 77 | assertStepCounter(t, "run", "success", 1) |
| 78 | } |
| 79 | |
| 80 | func assertStepCounter(t *testing.T, stepType, conclusion string, want float64) { |
| 81 | t.Helper() |
| 82 | var metric dto.Metric |
| 83 | if err := metrics.ActionsStepsCompletedTotal.WithLabelValues(stepType, conclusion).Write(&metric); err != nil { |
| 84 | t.Fatalf("read step counter %s/%s: %v", stepType, conclusion, err) |
| 85 | } |
| 86 | if got := metric.GetCounter().GetValue(); got != want { |
| 87 | t.Fatalf("step counter %s/%s = %v, want %v", stepType, conclusion, got, want) |
| 88 | } |
| 89 | } |
| 90 |