ОБНОВЛЕНИЕ: текущий источник доступен здесь.
В настоящее время я работаю над пользовательским экспортером Prometheus для changetection.io , чтобы отображать как парсинг, так и ценовые показатели для всех зарегистрированных часов.
Получив рабочее доказательство концепции, я пытаюсь сделать проект пригодным для сопровождения и готовым к выпуску для сообщества с открытым исходным кодом (например, добавляя тесты и документацию и делая его максимально полным).
При написании этих тестов я наткнулся на проблему при попытке протестировать динамическую регистрацию новых часов по мере их создания в отслеживаемом экземпляре Changetection.io. Чтобы экспортер мог их забрать без перезапуска, я проверяю API на предмет вновь добавленных часов при каждом запуске сбора.
Вот функция Collect
priceCollector
:
func (c *priceCollector) Collect(ch chan<- prometheus.Metric) {
// check for new watches before collecting metrics
watches, err := c.ApiClient.getWatches()
if err != nil {
log.Errorf("error while fetching watches: %v", err)
} else {
for id, watch := range watches {
if _, ok := c.priceMetrics[id]; !ok {
// create new metric and register it on the DefaultRegisterer
c.priceMetrics[id] = newPriceMetric(prometheus.Labels{"title": watch.Title}, c.ApiClient, id)
prometheus.MustRegister(c.priceMetrics[id])
log.Infof("Picked up new watch %s, registered as metric %s", watch.Title, id)
}
}
}
// collect all registered metrics
for _, metric := range c.priceMetrics {
metric.Collect(ch)
}
}
Функция newPriceMetric
просто создает новый объект priceMetric
, состоящий из prometheus.Desc
, ApiClient
(класса, обеспечивающего доступ к API Changetection.io) и UUID
:
func newPriceMetric(labels prometheus.Labels, apiClient *ApiClient, uuid string) priceMetric {
return priceMetric{
desc: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "watch", "price"),
"Current price of an offer type watch",
nil, labels,
),
apiClient: apiClient,
UUID: uuid,
}
}
Тестирование поведения по умолчанию работает отлично и проходит все тесты, но когда я пытаюсь протестировать поведение при добавлении новых часов (когда экспортер работает без его перезапуска), тест завершается неудачно.
Примечания: И
expectMetrics
, иexpectMetricCount
являются функциями-обертками вокруг собственныхtestutil.CollectAndCompare
иtestutil.CollectAndCount
Prometheus. ПомощникCreateTestApiServer
создает завернутыйhttptest
сервер, который возвращает полезные данные JSON на основе переданнойmap[string]*data.WatchItem
структуры.
func TestAutoregisterPriceCollector(t *testing.T) {
watchDb := createCollectorTestDb()
server := testutil.CreateTestApiServer(t, watchDb)
defer server.Close()
c, err := NewPriceCollector(server.URL(), "foo-bar-key")
if err != nil {
t.Fatal(err)
}
expectMetricCount(t, c, 2, "changedetectionio_watch_price")
// now add a new watch and expect the collector to pick it up
uuid, newItem := testutil.NewTestItem("Item 3", 300, "USD")
watchDb[uuid] = newItem
expectMetrics(t, c, "price_metrics_autoregister.prom", "changedetectionio_watch_price")
expectMetricCount(t, c, 3, "changedetectionio_watch_price")
}
При запуске этого теста запуск завершается неудачно со следующей ошибкой:
Collector_test.go:23: Возвращены неожиданные метрики: не удалось собрать метрики: собранная метрика изменена. help: «Текущая цена для просмотра типа предложения», constLabels: {title="Item 3"},variableLabels: {}}
В настоящее время я предполагаю, что эта ошибка связана с внутренней работой testutil.CollectAnd*
. Согласно комментариям к функциям, они регистрируют сборщик в недавно созданном педантичном реестре, что может привести к тому, что он не подберет лениво зарегистрированный дескриптор.
Есть мысли по этому поводу?
Поскольку это все равно меня беспокоило, я полностью переработал TestApiServer, чтобы больше не использовать файловые приспособления и полагаться на переданную структуру map[string]*data.WatchItem
. К моему разочарованию, результат остался прежним (см. обновленный вопрос):( Кроме того, я добавил ссылку на источник на GitHub.
Я не уверен, отвечает ли это на ваш вопрос, но вот пример
package main
import (
"flag"
"fmt"
"log/slog"
"net/http"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/client_golang/prometheus/testutil"
)
var (
endpoint = flag.String(
"endpoint",
"0.0.0.0:8080",
"The endpoint of the HTTP server",
)
)
type TestCollector struct {
sync.RWMutex
values []string
foo *prometheus.Desc
}
func NewTestCollector() *TestCollector {
return &TestCollector{
foo: prometheus.NewDesc(
"foo",
"foo",
[]string{
"labels",
},
nil,
),
}
}
func (c *TestCollector) Collect(ch chan<- prometheus.Metric) {
c.RLock()
defer c.RUnlock()
for _, value := range c.values {
ch <- prometheus.MustNewConstMetric(
c.foo,
prometheus.CounterValue,
1,
value,
)
}
}
func (c *TestCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.foo
}
func main() {
flag.Parse()
c := NewTestCollector()
registry := prometheus.NewRegistry()
registry.MustRegister(c)
go func() {
for i := range 20 {
value := fmt.Sprintf("value-%02d", i)
slog.Info("Adding value", "value", value)
c.Lock()
c.values = append(c.values, value)
c.Unlock()
slog.Info("testutil",
"count", testutil.CollectAndCount(c, "foo"))
time.Sleep(15 * time.Second)
}
}()
http.Handle(
"/metrics",
promhttp.HandlerFor(
registry, promhttp.HandlerOpts{}))
slog.Error("unable to listen",
"err", http.ListenAndServe(*endpoint, nil))
}
Метрика foo
имеет растущий (каждые 15 секунд) набор (0..19) меток (value-xx
).
CollectAndCount
увеличивается с каждой итерацией:
журналы:
2024/04/12 10:43:37 INFO Adding value value=value-00
2024/04/12 10:43:37 INFO testutil count=1
2024/04/12 10:43:52 INFO Adding value value=value-01
2024/04/12 10:43:52 INFO testutil count=2
2024/04/12 10:44:07 INFO Adding value value=value-02
2024/04/12 10:44:07 INFO testutil count=3
2024/04/12 10:44:22 INFO Adding value value=value-03
2024/04/12 10:44:22 INFO testutil count=4
2024/04/12 10:44:37 INFO Adding value value=value-04
2024/04/12 10:44:37 INFO testutil count=5
2024/04/12 10:44:52 INFO Adding value value=value-05
И:
curl --silent --get http://localhost:8080/metrics
# HELP foo foo
# TYPE foo counter
foo{labels = "value-00"} 1
foo{labels = "value-01"} 1
foo{labels = "value-02"} 1
foo{labels = "value-03"} 1
foo{labels = "value-04"} 1
foo{labels = "value-05"} 1
Рефакторинг сборщика и избавление от этой странной структуры priceMetric
помогли мне не только решить проблему с тестами, но и значительно облегчили понимание и поддержку моего кода! Большое спасибо за ваши усилия, награда будет присуждена как можно скорее :)
Я рад слышать, что у вас все заработало и что это помогло. Я узнал о prometheus.testutil
из вашего вопроса. Итак, спасибо за это! :-)
Из вашего вопроса непонятно, как работает
CreateTestApiServer
. Похоже, вы создаете сервер с набором данных из 2 записей, подсчитываете метрики, затем создаете второй сервер с 2+1 записями, а затем пытаетесь перенаправить сборщик на новый сервер. Не проще ли для вашего теста добавить запись на карту, которая читается обработчиком сервера? При первом прохождении карты будет содержаться x записей, затем вы добавляете запись на карту так, чтобы при втором прохождении она содержала x+1 записей.