indexer/metrics/metrics.go
Ricardo (XenGi) Band 63c6f4b761
Some checks failed
CI pipeline for ffd-indexer / checks (go test + golangci) (push) Failing after 26s
CI pipeline for ffd-indexer / build binaries + containers (push) Failing after 3s
CI pipeline for ffd-indexer / format (gofmt + nixfmt) (push) Failing after 3s
CI pipeline for ffd-indexer / integration (live scrape + Typesense) (push) Failing after 20s
🔥
2026-10-01 00:37:44 +02:00

127 lines
3.5 KiB
Go

// Package metrics exposes the state of the federated forge index to
// Prometheus.
//
// The ffd-metrics daemon is a long-running process that reads the SQLite
// store on each scrape and renders it as Prometheus text metrics at
// /metrics. It never touches the forges itself: the indexer's daily run
// populates the store, and this daemon merely reflects the current DB state.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"git.xengi.de/ffd/indexer/store"
)
// Collector implements prometheus.Collector by querying the store on every
// Prometheus scrape. Gather is therefore always fresh but also does one
// SQLite read per scrape.
type Collector struct {
store *store.Store
forgeInfo *prometheus.Desc
forgeOnline *prometheus.Desc
repositoryInfo *prometheus.Desc
repositoryLangs *prometheus.Desc
}
// NewCollector creates a Collector backed by st.
func NewCollector(st *store.Store) *Collector {
return &Collector{
store: st,
forgeInfo: prometheus.NewDesc(
"ffd_forge_info",
"Constant gauge with the forge's type and detected software version",
[]string{"forge_url", "forge_type", "forge_version"},
nil,
),
forgeOnline: prometheus.NewDesc(
"ffd_forge_online",
"Whether a forge was reachable and scraped successfully on its last run (1) or not (0).",
[]string{"forge_url", "forge_type"},
nil,
),
repositoryInfo: prometheus.NewDesc(
"ffd_repository_info",
"Constant gauge, one series per stored repository. Count by forge_url to get repositories per forge.",
[]string{"repo_url", "forge_url", "forge_type"},
nil,
),
repositoryLangs: prometheus.NewDesc(
"ffd_repository_languages",
"Constant gauge, one series per (repository, language) pair. Sum by language to get language usage.",
[]string{"repo_url", "language", "forge_url", "forge_type"},
nil,
),
}
}
// Describe implements prometheus.Collector.
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.forgeInfo
ch <- c.forgeOnline
ch <- c.repositoryInfo
ch <- c.repositoryLangs
}
// Collect implements prometheus.Collector. Query errors are surfaced as
// invalid metrics for each family so the scrape still produces a value (with
// a 500 if the handler is configured to propagate errors) instead of silently
// dropping data.
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
statuses, err := c.store.ForgeStatuses()
if err != nil {
c.emitInvalid(ch, err)
return
}
for _, st := range statuses {
online := 0.0
if st.Online {
online = 1
}
ch <- prometheus.MustNewConstMetric(
c.forgeInfo,
prometheus.GaugeValue,
1,
st.URL, st.ForgeType, st.Version,
)
ch <- prometheus.MustNewConstMetric(
c.forgeOnline,
prometheus.GaugeValue,
online,
st.URL, st.ForgeType,
)
}
repos, err := c.store.RepoIdentities()
if err != nil {
c.emitInvalid(ch, err)
return
}
for _, r := range repos {
forge := []string{r.ForgeURL, r.ForgeType}
ch <- prometheus.MustNewConstMetric(
c.repositoryInfo,
prometheus.GaugeValue,
1,
append([]string{r.URL}, forge...)...,
)
for _, lang := range r.Languages {
ch <- prometheus.MustNewConstMetric(
c.repositoryLangs,
prometheus.GaugeValue,
1,
append([]string{r.URL, lang}, forge...)...,
)
}
}
}
// emitInvalid emits one invalid metric per metric family for scrape errors.
func (c *Collector) emitInvalid(ch chan<- prometheus.Metric, err error) {
for _, d := range []*prometheus.Desc{c.forgeInfo, c.forgeOnline, c.repositoryInfo, c.repositoryLangs} {
ch <- prometheus.NewInvalidMetric(d, err)
}
}