Skip to content
Merged
4 changes: 3 additions & 1 deletion internal/cmd/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
monitorUI "github.com/helvethink/gitlab-ci-exporter/pkg/monitor/ui"
)

var startMonitorUI = monitorUI.Start

// Monitor starts the internal monitoring UI.
func Monitor(ctx *cli.Context) (int, error) {
// Parse global flags from CLI context (e.g., internal monitoring address)
Expand All @@ -15,7 +17,7 @@ func Monitor(ctx *cli.Context) (int, error) {
}

// Start the monitoring UI with app version and configured listener address
monitorUI.Start(
startMonitorUI(
ctx.App.Version,
cfg.InternalMonitoringListenerAddress,
)
Expand Down
83 changes: 83 additions & 0 deletions internal/cmd/monitor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cmd

import (
"net/url"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMonitor(t *testing.T) {
ctx, flags := NewTestContext()
ctx.App.Version = "1.2.3"
flags.String("internal-monitoring-listener-address", "", "")
require.NoError(t, flags.Set("internal-monitoring-listener-address", "http://127.0.0.1:8081"))

var (
called bool
gotVersion string
gotListenerURL *url.URL
)

previousStart := startMonitorUI
startMonitorUI = func(version string, listenerAddress *url.URL) {
called = true
gotVersion = version
gotListenerURL = listenerAddress
}
t.Cleanup(func() {
startMonitorUI = previousStart
})

exitCode, err := Monitor(ctx)
require.NoError(t, err)
assert.Equal(t, 0, exitCode)
assert.True(t, called)
assert.Equal(t, "1.2.3", gotVersion)
require.NotNil(t, gotListenerURL)
assert.Equal(t, "http://127.0.0.1:8081", gotListenerURL.String())
}

func TestMonitorReturnsErrorWhenInternalMonitoringAddressIsInvalid(t *testing.T) {
ctx, flags := NewTestContext()
flags.String("internal-monitoring-listener-address", "", "")
require.NoError(t, flags.Set("internal-monitoring-listener-address", "://bad-url"))

called := false
previousStart := startMonitorUI
startMonitorUI = func(version string, listenerAddress *url.URL) {
called = true
}
t.Cleanup(func() {
startMonitorUI = previousStart
})

exitCode, err := Monitor(ctx)
require.Error(t, err)
assert.Equal(t, 1, exitCode)
assert.False(t, called)
}

func TestMonitorWithoutInternalMonitoringAddress(t *testing.T) {
ctx, flags := NewTestContext()
ctx.App.Version = "dev"
flags.String("internal-monitoring-listener-address", "", "")

called := false
var gotListenerURL *url.URL
previousStart := startMonitorUI
startMonitorUI = func(version string, listenerAddress *url.URL) {
called = true
gotListenerURL = listenerAddress
}
t.Cleanup(func() {
startMonitorUI = previousStart
})

exitCode, err := Monitor(ctx)
require.NoError(t, err)
assert.Equal(t, 0, exitCode)
assert.True(t, called)
assert.Nil(t, gotListenerURL)
}
120 changes: 120 additions & 0 deletions internal/collectors/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package collectors

import (
"io"
"log/slog"
"strings"
"testing"

dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func newTestLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}

func descString(t *testing.T, desc *prometheus.Desc) string {
t.Helper()
require.NotNil(t, desc)

return desc.String()
}

func TestNewMetrics(t *testing.T) {
m := NewMetrics()

require.NotNil(t, m)
assert.Contains(t, descString(t, m.sampleMetric1), `fqName: "gitlab-ci-exporter_sampleMetric1"`)
assert.Contains(t, descString(t, m.sampleMetric1), `variableLabels: {label1}`)
assert.Contains(t, descString(t, m.sampleMetric2), `fqName: "gitlab-ci-exporter_sampleMetric2"`)
assert.Contains(t, descString(t, m.sampleMetric2), `variableLabels: {label2}`)
}

func TestNewExporter(t *testing.T) {
settings := &Settings{
LogLevel: "info",
LogFormat: "text",
MetricsPath: "/metrics",
ListenPort: "8080",
Address: "0.0.0.0",
}
logger := newTestLogger()

exporter, err := NewExporter(settings, logger)
require.NoError(t, err)
require.NotNil(t, exporter)
require.NotNil(t, exporter.metrics)
assert.Same(t, settings, exporter.Settings)
assert.Same(t, logger, exporter.Logger)
}

func TestExporterDescribe(t *testing.T) {
exporter, err := NewExporter(&Settings{}, newTestLogger())
require.NoError(t, err)

ch := make(chan *prometheus.Desc, 2)
exporter.Describe(ch)
close(ch)

var descs []string
for desc := range ch {
descs = append(descs, desc.String())
}

require.Len(t, descs, 2)
assert.True(t, strings.Contains(descs[0], "sampleMetric1") || strings.Contains(descs[1], "sampleMetric1"))
assert.True(t, strings.Contains(descs[0], "sampleMetric2") || strings.Contains(descs[1], "sampleMetric2"))
}

func TestExporterCollect(t *testing.T) {
exporter, err := NewExporter(&Settings{}, newTestLogger())
require.NoError(t, err)

registry := prometheus.NewRegistry()
require.NoError(t, registry.Register(exporter))

families, err := registry.Gather()
require.NoError(t, err)
require.Len(t, families, 2)

familyByName := make(map[string]*dto.MetricFamily, len(families))
for _, family := range families {
familyByName[family.GetName()] = family
}

sample1 := familyByName["gitlab-ci-exporter_sampleMetric1"]
require.NotNil(t, sample1)
require.Len(t, sample1.GetMetric(), 1)
assert.Equal(t, dto.MetricType_GAUGE, sample1.GetType())
require.Len(t, sample1.GetMetric()[0].GetLabel(), 1)
assert.Equal(t, "label1", sample1.GetMetric()[0].GetLabel()[0].GetName())
assert.Equal(t, "labelValue", sample1.GetMetric()[0].GetLabel()[0].GetValue())
assert.GreaterOrEqual(t, sample1.GetMetric()[0].GetGauge().GetValue(), float64(0))
assert.Less(t, sample1.GetMetric()[0].GetGauge().GetValue(), float64(1))

sample2 := familyByName["gitlab-ci-exporter_sampleMetric2"]
require.NotNil(t, sample2)
require.Len(t, sample2.GetMetric(), 1)
assert.Equal(t, dto.MetricType_GAUGE, sample2.GetType())
require.Len(t, sample2.GetMetric()[0].GetLabel(), 1)
assert.Equal(t, "label2", sample2.GetMetric()[0].GetLabel()[0].GetName())
assert.Equal(t, "labelValue", sample2.GetMetric()[0].GetLabel()[0].GetValue())
assert.GreaterOrEqual(t, sample2.GetMetric()[0].GetGauge().GetValue(), float64(0))
assert.Less(t, sample2.GetMetric()[0].GetGauge().GetValue(), float64(1))
}

func TestSampleMetricsReturnValueBetweenZeroAndOne(t *testing.T) {
exporter, err := NewExporter(&Settings{}, newTestLogger())
require.NoError(t, err)

sample1 := exporter.sampleMetric1()
sample2 := exporter.sampleMetric2()

assert.GreaterOrEqual(t, sample1, float64(0))
assert.Less(t, sample1, float64(1))
assert.GreaterOrEqual(t, sample2, float64(0))
assert.Less(t, sample2, float64(1))
}
77 changes: 77 additions & 0 deletions internal/httpServer/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package httpServer

import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/helvethink/gitlab-ci-exporter/internal/collectors"
)

func newTestExporter(t *testing.T) *collectors.Exporter {
t.Helper()

exporter, err := collectors.NewExporter(&collectors.Settings{
MetricsPath: "metrics",
ListenPort: "9191",
Address: "0.0.0.0",
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
require.NoError(t, err)

return exporter
}

func withFreshServeMux(t *testing.T) {
t.Helper()

previousMux := http.DefaultServeMux
http.DefaultServeMux = http.NewServeMux()
t.Cleanup(func() {
http.DefaultServeMux = previousMux
})
}

func TestNewServerSetsExpectedAddr(t *testing.T) {
withFreshServeMux(t)

server := NewServer(newTestExporter(t))

require.NotNil(t, server)
assert.Equal(t, ":9191", server.Addr)
assert.Nil(t, server.Handler)
}

func TestNewServerServesRootPage(t *testing.T) {
withFreshServeMux(t)

_ = NewServer(newTestExporter(t))

req := httptest.NewRequest(http.MethodGet, "/", nil)
rr := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(rr, req)

require.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Gitlab CI Exporter")
assert.Contains(t, rr.Body.String(), "Metrics at:")
assert.Contains(t, rr.Body.String(), "href='metrics'")
assert.Contains(t, rr.Body.String(), "github.com/Helvethink/gitlab-ci-exporter")
}

func TestNewServerServesMetricsEndpoint(t *testing.T) {
withFreshServeMux(t)

_ = NewServer(newTestExporter(t))

req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
rr := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(rr, req)

require.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "gitlab_ci_exporter_sampleMetric1")
assert.Contains(t, rr.Body.String(), "gitlab_ci_exporter_sampleMetric2")
}
81 changes: 81 additions & 0 deletions internal/logging/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package logger

import (
"os"
"testing"

log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func preserveLogrusState(t *testing.T) {
t.Helper()

prevLevel := log.GetLevel()
prevFormatter := log.StandardLogger().Formatter
prevReportCaller := log.StandardLogger().ReportCaller
prevOutput := log.StandardLogger().Out

t.Cleanup(func() {
log.SetLevel(prevLevel)
log.SetFormatter(prevFormatter)
log.SetReportCaller(prevReportCaller)
log.SetOutput(prevOutput)
})
}

func TestConfigureTextFormat(t *testing.T) {
preserveLogrusState(t)

err := Configure(Config{
Level: "debug",
Format: "text",
ReportCaller: true,
})
require.NoError(t, err)

assert.Equal(t, log.DebugLevel, log.GetLevel())
_, ok := log.StandardLogger().Formatter.(*log.TextFormatter)
assert.True(t, ok)
assert.True(t, log.StandardLogger().ReportCaller)
assert.Same(t, os.Stdout, log.StandardLogger().Out)
}

func TestConfigureJSONFormat(t *testing.T) {
preserveLogrusState(t)

err := Configure(Config{
Level: "info",
Format: "json",
ReportCaller: false,
})
require.NoError(t, err)

assert.Equal(t, log.InfoLevel, log.GetLevel())
_, ok := log.StandardLogger().Formatter.(*log.JSONFormatter)
assert.True(t, ok)
assert.False(t, log.StandardLogger().ReportCaller)
assert.Same(t, os.Stdout, log.StandardLogger().Out)
}

func TestConfigureInvalidLevelReturnsError(t *testing.T) {
preserveLogrusState(t)

err := Configure(Config{
Level: "not-a-level",
Format: "text",
})
require.Error(t, err)
}

func TestConfigureInvalidFormatReturnsError(t *testing.T) {
preserveLogrusState(t)

err := Configure(Config{
Level: "info",
Format: "xml",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid log format")
}
Loading
Loading