Как протестировать регистрацию динамических метрик в пользовательском экспортере Prometheus?

ОБНОВЛЕНИЕ: текущий источник доступен здесь.

В настоящее время я работаю над пользовательским экспортером Prometheus для changetection.io , чтобы отображать как парсинг, так и ценовые показатели для всех зарегистрированных часов.

Получив рабочее доказательство концепции, я пытаюсь сделать проект пригодным для сопровождения и готовым к выпуску для сообщества с открытым исходным кодом (например, добавляя тесты и документацию и делая его максимально полным).

При написании этих тестов я наткнулся на проблему при попытке протестировать динамическую регистрацию новых часов по мере их создания в отслеживаемом экземпляре Changetection.io. Чтобы экспортер мог их забрать без перезапуска, я проверяю API на предмет вновь добавленных часов при каждом запуске сбора.

Вот функция CollectpriceCollector:

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*. Согласно комментариям к функциям, они регистрируют сборщик в недавно созданном педантичном реестре, что может привести к тому, что он не подберет лениво зарегистрированный дескриптор.

Есть мысли по этому поводу?

Из вашего вопроса непонятно, как работает CreateTestApiServer. Похоже, вы создаете сервер с набором данных из 2 записей, подсчитываете метрики, затем создаете второй сервер с 2+1 записями, а затем пытаетесь перенаправить сборщик на новый сервер. Не проще ли для вашего теста добавить запись на карту, которая читается обработчиком сервера? При первом прохождении карты будет содержаться x записей, затем вы добавляете запись на карту так, чтобы при втором прохождении она содержала x+1 записей.

DazWilkin 09.04.2024 23:35

Поскольку это все равно меня беспокоило, я полностью переработал TestApiServer, чтобы больше не использовать файловые приспособления и полагаться на переданную структуру map[string]*data.WatchItem. К моему разочарованию, результат остался прежним (см. обновленный вопрос):( Кроме того, я добавил ссылку на источник на GitHub.

schaermu 12.04.2024 08:55
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
2
2
119
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не уверен, отвечает ли это на ваш вопрос, но вот пример

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 помогли мне не только решить проблему с тестами, но и значительно облегчили понимание и поддержку моего кода! Большое спасибо за ваши усилия, награда будет присуждена как можно скорее :)

schaermu 12.04.2024 22:27

Я рад слышать, что у вас все заработало и что это помогло. Я узнал о prometheus.testutil из вашего вопроса. Итак, спасибо за это! :-)

DazWilkin 12.04.2024 22:28

Другие вопросы по теме