package api import ( "net/http" "regexp" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/internal/service/cluster" reg "github.com/photoprism/photoprism/internal/service/cluster/registry" "github.com/photoprism/photoprism/pkg/http/header" "github.com/photoprism/photoprism/pkg/rnd" ) func TestGetMetrics(t *testing.T) { t.Run("ExposeCountStatistics", func(t *testing.T) { app, router, _ := NewApiTest() GetMetrics(router) resp := PerformRequestWithStream(app, "GET", "/api/v1/metrics") if resp.Code != http.StatusOK { t.Fatal(resp.Body.String()) } body := resp.Body.String() floatPattern := `[-+]?\d+(?:\.\d+)?(?:e[-+]?\d+)?` stats := []string{ "all", "photos", "media", "animated", "live", "videos", "audio", "documents", "albums", "private_albums", "folders", "private_folders", "files", "hidden", "favorites", "private", "people", "labels", "label_max_photos", } for _, stat := range stats { assert.Regexp(t, regexp.MustCompile(`photoprism_statistics_media_count{stat="`+stat+`"} `+floatPattern), body) } }) t.Run("ExposeBuildInformation", func(t *testing.T) { app, router, _ := NewApiTest() GetMetrics(router) resp := PerformRequestWithStream(app, "GET", "/api/v1/metrics") if resp.Code != http.StatusOK { t.Fatal(resp.Body.String()) } body := resp.Body.String() assert.Regexp(t, regexp.MustCompile(`photoprism_build_info{edition=".+",goversion=".+",version=".+"} 1`), body) }) t.Run("ExposeUsageMetrics", func(t *testing.T) { app, router, _ := NewApiTest() GetMetrics(router) resp := PerformRequestWithStream(app, "GET", "/api/v1/metrics") if resp.Code != http.StatusOK { t.Fatal(resp.Body.String()) } body := resp.Body.String() floatPattern := `[-+]?\d+(?:\.\d+)?(?:e[-+]?\d+)?` assert.Regexp(t, regexp.MustCompile(`photoprism_usage_files_bytes{state="used"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_files_bytes{state="free"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_files_bytes{state="total"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_files_ratio{state="used"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_files_ratio{state="free"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_accounts_active{state="users"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_accounts_active{state="guests"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_accounts_ratio{state="used"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_usage_accounts_ratio{state="free"} `+floatPattern), body) t.Run("RatiosSumToOne", func(t *testing.T) { parseValue := func(pattern string) float64 { m := regexp.MustCompile(pattern).FindStringSubmatch(body) if len(m) != 2 { t.Fatalf("metric not found: %s", pattern) } v, err := strconv.ParseFloat(m[1], 64) if err != nil { t.Fatalf("parse metric: %v", err) } return v } filesUsed := parseValue(`photoprism_usage_files_ratio{state="used"} (` + floatPattern + `)`) filesFree := parseValue(`photoprism_usage_files_ratio{state="free"} (` + floatPattern + `)`) assert.InEpsilon(t, 1.0, filesUsed+filesFree, 0.01) accountsUsed := parseValue(`photoprism_usage_accounts_ratio{state="used"} (` + floatPattern + `)`) accountsFree := parseValue(`photoprism_usage_accounts_ratio{state="free"} (` + floatPattern + `)`) assert.InEpsilon(t, 1.0, accountsUsed+accountsFree, 0.01) }) }) t.Run("ExposeClusterMetricsForPortal", func(t *testing.T) { app, router, conf := NewApiTest() conf.Options().NodeRole = cluster.RolePortal GetMetrics(router) regy, err := reg.NewClientRegistryWithConfig(conf) assert.NoError(t, err) nodeDefs := []struct { name string role string }{ {"metrics-app-1", string(cluster.RoleApp)}, {"metrics-service-1", string(cluster.RoleService)}, } var cleanupIDs []string for _, def := range nodeDefs { n := ®.Node{Node: cluster.Node{Name: def.name, Role: def.role, UUID: rnd.UUIDv7()}} assert.NoError(t, regy.Put(n)) cleanupIDs = append(cleanupIDs, n.UUID) } t.Cleanup(func() { for _, uuid := range cleanupIDs { _ = regy.Delete(uuid) } }) resp := PerformRequestWithStream(app, "GET", "/api/v1/metrics") if resp.Code != http.StatusOK { t.Fatal(resp.Body.String()) } body := resp.Body.String() floatPattern := `[-+]?\d+(?:\.\d+)?(?:e[-+]?\d+)?` assert.Regexp(t, regexp.MustCompile(`photoprism_cluster_nodes{role="total"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_cluster_nodes{role="app"} `+floatPattern), body) assert.Regexp(t, regexp.MustCompile(`photoprism_cluster_nodes{role="service"} `+floatPattern), body) infoPattern := `photoprism_cluster_info\{(?:cidr="[^"]*",[^}]*uuid="[^"]*"|uuid="[^"]*",[^}]*cidr="[^"]*")\} 1` assert.Regexp(t, regexp.MustCompile(infoPattern), body) }) t.Run("HasPrometheusExpositionFormatAsContentType", func(t *testing.T) { app, router, _ := NewApiTest() GetMetrics(router) resp := PerformRequestWithStream(app, "GET", "/api/v1/metrics") if resp.Code != http.StatusOK { t.Fatal(resp.Body.String()) } assert.Equal(t, header.ContentTypePrometheus, resp.Result().Header.Get("Content-Type")) }) }