180 lines
6.2 KiB
Go
180 lines
6.2 KiB
Go
package metrics
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/common/expfmt"
|
|
"github.com/prometheus/common/model"
|
|
)
|
|
|
|
func fakeModuleServer(metricName string) *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
|
fmt.Fprintf(w, "# HELP %s ein test-zaehler\n# TYPE %s counter\n%s 42\n", metricName, metricName, metricName)
|
|
}))
|
|
}
|
|
|
|
// Akzeptanzkriterium 1: Core liefert unter dem Handler valides
|
|
// Prometheus-Textformat mit den eigenen Kennzahlen.
|
|
func TestHandler_ServesCoreMetricsInPrometheusFormat(t *testing.T) {
|
|
agg := NewAggregator(NewCoreRegistry(), func(ctx context.Context) ([]Source, error) { return nil, nil })
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
|
rec := httptest.NewRecorder()
|
|
agg.Handler()(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
|
|
parser := expfmt.NewTextParser(model.LegacyValidation)
|
|
families, err := parser.TextToMetricFamilies(strings.NewReader(rec.Body.String()))
|
|
if err != nil {
|
|
t.Fatalf("antwort ist kein valides prometheus-textformat: %v", err)
|
|
}
|
|
if _, ok := families["nexarch_core_up"]; !ok {
|
|
t.Fatalf("erwartet 'nexarch_core_up' unter den core-metriken, habe: %v", keysOf(families))
|
|
}
|
|
}
|
|
|
|
func keysOf[V any](m map[string]V) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Akzeptanzkriterium 2 + Pruefung 2: ein NEU registriertes Modul erscheint
|
|
// in der Aggregation, OHNE dass dieses Paket oder der Aufrufer Code
|
|
// aendern muss — die Quelle kommt ausschliesslich aus sourceProvider.
|
|
func TestHandler_NewlyRegisteredModuleAppearsWithoutCodeChange(t *testing.T) {
|
|
moduleServer := fakeModuleServer("requests_total")
|
|
defer moduleServer.Close()
|
|
|
|
// Simuliert eine sich zur Laufzeit aendernde Modul-Liste (z.B. aus
|
|
// SourceStore.Provide) — zunaechst LEER, dann mit einem Eintrag.
|
|
var sources []Source
|
|
var mu sync.Mutex
|
|
provider := func(ctx context.Context) ([]Source, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
out := make([]Source, len(sources))
|
|
copy(out, sources)
|
|
return out, nil
|
|
}
|
|
|
|
agg := NewAggregator(NewCoreRegistry(), provider)
|
|
|
|
// Vor der Registrierung: Modul-Metrik nicht vorhanden.
|
|
rec1 := httptest.NewRecorder()
|
|
agg.Handler()(rec1, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
|
if strings.Contains(rec1.Body.String(), "requests_total") {
|
|
t.Fatal("modul-metrik haette vor registrierung nicht erscheinen duerfen")
|
|
}
|
|
|
|
// Modul wird "registriert" (kein Code hier oder in metrics.go aendert sich).
|
|
mu.Lock()
|
|
sources = append(sources, Source{ModuleName: "dms", MetricsURL: moduleServer.URL})
|
|
mu.Unlock()
|
|
|
|
rec2 := httptest.NewRecorder()
|
|
agg.Handler()(rec2, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
|
body := rec2.Body.String()
|
|
if !strings.Contains(body, "nexarch_module_dms_requests_total") {
|
|
t.Fatalf("erwartet umbenannte modul-metrik 'nexarch_module_dms_requests_total' nach registrierung, body:\n%s", body)
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 3: Namenskonvention "nexarch_module_<modul>_<name>"
|
|
// wird tatsaechlich angewendet.
|
|
func TestFetchAndRename_AppliesNamingConvention(t *testing.T) {
|
|
moduleServer := fakeModuleServer("queue_depth")
|
|
defer moduleServer.Close()
|
|
|
|
agg := NewAggregator(NewCoreRegistry(), nil)
|
|
families, err := agg.fetchAndRename(context.Background(), Source{ModuleName: "mail", MetricsURL: moduleServer.URL})
|
|
if err != nil {
|
|
t.Fatalf("fetchAndRename: %v", err)
|
|
}
|
|
if len(families) != 1 || families[0].GetName() != "nexarch_module_mail_queue_depth" {
|
|
t.Fatalf("erwartet genau 1 metrik 'nexarch_module_mail_queue_depth', habe: %+v", families)
|
|
}
|
|
}
|
|
|
|
// Ein nicht erreichbares Modul darf die Aggregation der uebrigen und die
|
|
// Gesamtantwort nicht verhindern (dieselbe Resilienz wie OPS-02).
|
|
func TestHandler_UnreachableModuleDoesNotBreakAggregation(t *testing.T) {
|
|
reachable := fakeModuleServer("healthy_metric")
|
|
defer reachable.Close()
|
|
unreachable := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
unreachableURL := unreachable.URL
|
|
unreachable.Close() // sofort schliessen -> Verbindung schlaegt fehl
|
|
|
|
provider := func(ctx context.Context) ([]Source, error) {
|
|
return []Source{
|
|
{ModuleName: "ok", MetricsURL: reachable.URL},
|
|
{ModuleName: "kaputt", MetricsURL: unreachableURL},
|
|
}, nil
|
|
}
|
|
agg := NewAggregator(NewCoreRegistry(), provider)
|
|
|
|
rec := httptest.NewRecorder()
|
|
agg.Handler()(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200 trotz einem nicht erreichbaren modul", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "nexarch_module_ok_healthy_metric") {
|
|
t.Fatal("erreichbares modul haette trotz ausfall des anderen aggregiert werden sollen")
|
|
}
|
|
if strings.Contains(body, "kaputt") {
|
|
t.Fatal("nicht erreichbares modul haette keine metrik beitragen duerfen")
|
|
}
|
|
}
|
|
|
|
// Pruefung 1: Endpunkt antwortet unter mehreren gleichzeitigen Anfragen
|
|
// innerhalb definierter Zeit — kein unbeschraenktes Blockieren durch
|
|
// langsame Module (FetchTimeout begrenzt jeden Scrape).
|
|
func TestHandler_RespondsWithinBoundedTimeUnderLoad(t *testing.T) {
|
|
hangingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(10 * time.Second) // wuerde ohne timeout jede anfrage blockieren
|
|
}))
|
|
defer hangingServer.Close()
|
|
|
|
provider := func(ctx context.Context) ([]Source, error) {
|
|
return []Source{{ModuleName: "haengend", MetricsURL: hangingServer.URL}}, nil
|
|
}
|
|
agg := NewAggregator(NewCoreRegistry(), provider)
|
|
|
|
const concurrentRequests = 10
|
|
var wg sync.WaitGroup
|
|
start := time.Now()
|
|
for i := 0; i < concurrentRequests; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
rec := httptest.NewRecorder()
|
|
agg.Handler()(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200", rec.Code)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
elapsed := time.Since(start)
|
|
|
|
if elapsed > FetchTimeout+3*time.Second {
|
|
t.Fatalf("%d gleichzeitige anfragen brauchten %s, erwartet deutlich unter %s durch FetchTimeout",
|
|
concurrentRequests, elapsed, FetchTimeout+3*time.Second)
|
|
}
|
|
}
|