Задача скрапинга Prometheus не может получить доступ к метрикам

У меня установлена работающая версия Discourse (на самом деле две: одна на этапе тестирования, другая в продакшене, на разных виртуальных машинах и так далее). Я провожу тестирование в тестовой среде. Установка выполнена согласно официальному руководству.

В настоящее время на той же виртуальной машине, где уже развернута установка Discourse, с помощью docker compose развернут стек Grafana/Prometheus/Node Exporter.

Вот файл docker-compose.yaml:

version: "3"

services:
    cadvisor:
        image: gcr.io/cadvisor/cadvisor:latest
        container_name: cadvisor
        restart: unless-stopped
        volumes:
            - /:/rootfs:ro
            - /var/run:/var/run:ro
            - /sys:/sys:ro
            - /var/lib/docker/:/var/lib/docker:ro
            - /dev/disk/:/dev/disk:ro
        networks:
            - prometheus-cadvisor

    node_exporter:
        image: quay.io/prometheus/node-exporter:latest
        container_name: node_exporter
        command:
            - '--path.rootfs=/host'
        pid: host
        restart: unless-stopped
        volumes:
            - '/:/host:ro,rslave'
        networks:
            - prometheus-node_exporter

    prometheus:
        image: prom/prometheus:latest
        restart: unless-stopped
        container_name: prometheus
        ports:
            - "9090:9090"
        volumes:
            - ./prometheus:/app.cfg
        networks:
            - world
            - prometheus-cadvisor
            - prometheus-node_exporter
            - discourse
            - grafana-prometheus
        command: >-
            --config.file=/app.cfg/prometheus.yaml
            --storage.tsdb.path=/prometheus
            --web.console.libraries=/usr/share/prometheus/console_libraries
            --web.console.templates=/usr/share/prometheus/consoles

    grafana:
        image: grafana/grafana:latest
        container_name: grafana
        restart: unless-stopped
        ports:
            - "3000:3000"
        environment:
            GF_SECURITY_ADMIN_USER: [OMITTED]
            GF_SECURITY_ADMIN_PASSWORD: [OMITTED]
            GF_PATHS_PROVISIONING: '/app.cfg/provisioning'
        volumes:
            - ./grafana:/app.cfg
            - ./grafana/provisioning:/etc/grafana/provisioning
        networks:
            - world
            - grafana-prometheus

networks:
    world:
    grafana-prometheus:
        internal: true
    prometheus-cadvisor:
        internal: true
    prometheus-node_exporter:
        internal: true
    discourse:
        external: true

Я пересобрал контейнер Discourse, указав сеть, чтобы он не разворачивался в bridge, и подключил Prometheus к той же сети.

docker network create -d bridge discourse
/var/discourse/launcher rebuild app --docker-args '--network discourse'

Я протестировал соединение, зайдя в контейнер Prometheus и выполнив ping контейнера Discourse по внутреннему алиасу сети; соединение установлено.

Однако при настройке задачи в Prometheus для сбора метрик с использованием внутреннего IP-адреса я вижу только сообщение server returned HTTP status 404 Not Found.

Вот конфигурация Prometheus:

global:
  scrape_interval: 30s
  scrape_timeout: 10s

rule_files:

scrape_configs:
  - job_name: prometheus
    metrics_path: /metrics
    static_configs:
      - targets:
        - 'prometheus:9090'
  - job_name: node_exporter
    static_configs:
      - targets:
        - 'node_exporter:9100'
  - job_name: discourse_exporter
    static_configs:
      - targets:
        - 'vmuniqueID-app:80'

vmuniqueID — это заменённое фактическое имя виртуальной машины.

Согласно документации здесь, доступ к маршруту metrics через внутренний IP-адрес должен быть разрешён:

По умолчанию мы разрешаем доступ к маршруту metrics администраторам и частным IP-адресам.

Пожалуйста, помогите понять, что я упускаю :stuck_out_tongue:

Чтобы продолжить расследование, я попытался сгенерировать ключ API в Discourse и обратиться к нему через внутреннее имя хоста. Ответ не был 301, что правильно, так как каждый запрос должен перенаправляться на https.

Проблема, как я думаю, в том, что входящие запросы, даже если они поступают с внутреннего IP-адреса, считаются неавторизованными, и из-за этого возвращается 404.

У вас точно установлен и включен плагин Prometheus? Он должен разрешать запросы с частных адресов, но вы можете попробовать установить переменную окружения, чтобы разрешить доступ с IP-адреса, с которого вы делаете запрос.

Да, Prometheus работает на той же виртуальной машине и развернут как контейнер Docker. Всё работает (я также развернул другие экспортеры), но по какой-то причине плагин Discourse Prometheus, несмотря на то что явно запущен и работает, не принимает запросы.\n\nКогда вы говорите о переменной окружения, вы имеете в виду окружение в файле app.yaml для Discourse, верно?\n\nТак, что-то вроде этого:\n\n\nenv:\n DISCOURSE_PROMETHEUS_TRUSTED_IP_ALLOWLIST_REGEX: 172.20.0.3\n\n\n172.20.0.3 — это текущий внутренний IP-адрес, который будет у Prometheus в виртуальной сети Docker, к которой также подключён Discourse.\n\nЯ уже пробовал использовать внешний IP-адрес, которым в любом случае пользуются все контейнеры (статический IP-адрес ВМ), но так как они находятся в одной сети, при попытке обращения одного к другому соединение идёт через внутренний IP-адрес.\n\nКоманды ./launcher restart app должно быть достаточно для того, чтобы переменные окружения были применены, верно?\n\nВ этом случае я получаю:\n\n\nGet "http://vmi1187507-app:80/metrics": dial tcp: lookup vmi1187507-app on 127.0.0.11:53: server misbehaving\n\n\n

\n\nvmi1187507-app — это имя контейнера в его сети. Имя верное, я могу сделать ping этого адреса из работающего контейнера Prometheus.\n\nЧестно говоря, не имею представления, откуда берётся 127.0.0.11:53 :thinking:\n\nСообщение остаётся тем же, даже если закомментировать переменную окружения.

Я думаю, что да, но не уверен на сто процентов. Вы можете проверить это изнутри контейнера и посмотреть, сможете ли вы выполнить curl оттуда.

Запуск wget из контейнера prometheus возвращает:

/prometheus # wget http://vmi1229594-app:80/metrics
Connecting to vmi1229594-app:80 (172.20.0.2:80)
Connecting to [public URL] (172.67.69.84:443)
wget: note: TLS certificate validation not implemented
wget: server returned error: HTTP/1.1 404 Not Found

Я предполагаю, что это автоматическое перенаправление из контейнера nginx Discourse?
Суть в том, что запрос перенаправляется на https публичного доменного имени, которое указывает на внутренний IP-адрес Cloudflare, и тот, естественно, отклоняет запрос.

Однако это не главное, поскольку такое перенаправление не должно происходить для пути http://yourwebsite.com/metrics, если запрос поступает с внутреннего IP-адреса. Я ожидал, что плагин решит эту проблему, добавив конфигурацию nginx с соответствующим правилом, но, похоже, этого не происходит.

Может ли кто-то из разработчиков Discourse подключиться к обсуждению? Я не хочу случайно уведомлять людей, и странно, что об этой проблеме раньше никто не сообщал.

Редактирование: Я пересобрал контейнер, указав также статическое имя хоста для конфигурации сети, так как заметил, что при каждой пересборке контейнеру присваивался новый случайный хост.
После этого я также попробовал настроить задачу Prometheus для доступа к версии https метрик, но проблема вернулась к исходному шагу:

global:
  scrape_interval: 30s
  scrape_timeout: 10s

rule_files:

scrape_configs:
# other jobs
# [...]
  - job_name: discourse_exporter
    scheme: https
    tls_config:
      insecure_skip_verify: true
    static_configs:
      - targets:
        - 'discourse_app'
/prometheus # wget https://discourse_app/metrics
Connecting to discourse_app (172.20.0.2:443)
wget: note: TLS certificate validation not implemented
Connecting to [public URL] (104.26.4.193:443)
wget: server returned error: HTTP/1.1 404 Not Found

На данном этапе это похоже на проблему самого плагина.

Верно. Вам нужно обращаться к нему по имени хоста, а не по имени контейнера.

Я использую имя хоста. Я много писал и поздно, возможно, это было запутанно, но я точно использую внутреннее имя хоста сети.

Это не моя сфера компетенции, но я порылся в сообщениях, которые таймер темы удалил, чтобы проверить, нет ли среди них чего-то полезного, и, возможно, нашёл вот это? (Извините, если я сильно ошибся :slight_smile: :pray: )

Getting Discourse to see the Prometheus server IP - #5 by ishan
Using Prometheous with Cloudflare

Спасибо @JammyDodger, но, к сожалению, эти материалы не помогли.

У них похожая проблема, но с небольшими отличиями, из-за чего они неприменимы в данном случае.
Только чтобы убедиться, я попробовал то, что предлагалось в одном из этих обсуждений (а также @pfaffman), и поэкспериментировал с переменной окружения DISCOURSE_PROMETHEUS_TRUSTED_IP_ALLOWLIST_REGEX.

Я тестировал:

  • Закомментировал её
  • Добавил значение с внутренним IP-адресом
  • Добавил значение с внешним IP-адресом

Также пробовал изменить задачу скрапинга Prometheus для обращения к установке Discourse следующими способами:

  • Прямой внутренний IP-адрес
  • Внутреннее имя хоста Docker
  • Прямой внешний IP-адрес
  • Публичное доменное имя

В каждом случае пробовал как http, так и https.

Во всех случаях я получаю ошибку 404.
Ожидается, что я получу ответ со страницы, так как запрос поступает с внутреннего IP-адреса.

То, что имел в виду Джей, заключается в том, что необходимо использовать настроенное имя хоста (DISCOURSE_HOSTNAME в определении вашего контейнера в файле .yml), а не любое имя хоста, которое может разрешаться в правильный IP-адрес.

Это сделано намеренно, чтобы нельзя было тривиально настроить обратный прокси для публичного экземпляра из любого места, и чтобы принималось только настроенное имя хоста:

$ curl -I https://try.discourse.org/about.json
HTTP/2 200
server: nginx
date: Mon, 15 May 2023 16:25:05 GMT
content-type: application/json; charset=utf-8
[...]

# следующее действие эквивалентно созданию DNS-записи для
# try.somebogusreverseproxy.com, указывающей на тот же IP-адрес, что и try.discourse.org,
# а затем запросу https://try.somebogusreverseproxy.com/about.json
$ curl -H 'Host: try.somebogusreverseproxy.com' -I https://try.discourse.org/about.json
HTTP/2 404
cache-control: no-cache
content-length: 1427
content-type: text/html
cdck-proxy-id: app-router-tiehunter02.sea1
cdck-proxy-id: app-balancer-tieinterceptor1b.sea1

И наоборот, если вы попробуете следующее:

curl -H 'Host: ВАШЕ_НАСТРОЕННОЕ_ИМЯ_ХОСТА' -I https://discourse_app/metrics

это должно сработать, но это обходной путь. Ожидается, что вы настроите DNS необходимым образом, чтобы к Discourse можно было получить доступ по его настроенному имени хоста прозрачно:

curl -I https://ВАШЕ_НАСТРОЕННОЕ_ИМЯ_ХОСТА/metrics

Как это сделать, сильно зависит от ваших требований, но самый простой вариант — настроить алиас в /etc/hosts с того места, откуда исходят ваши HTTP-запросы.

Экспортер Prometheus не работает на порту 80 — он слушает свой собственный порт. По умолчанию порт 9405.

Хорошее решение, но если я пытаюсь обратиться к этому конкретному порту, получаю сообщение «connection refused».

Get "http://discourse_app:9405/metrics": dial tcp 172.20.0.2:9405: connect: connection refused

Для уверенности также протестировал через wget изнутри контейнера prometheus.

/prometheus # ping discourse_app
PING discourse_app (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.223 ms
64 bytes from 172.20.0.2: seq=1 ttl=64 time=0.270 ms
^C
--- discourse_app ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.223/0.246/0.270 ms
/prometheus # wget discourse_app:9405/metrics
Connecting to discourse_app:9405 (172.20.0.2:9405)
wget: can't connect to remote host (172.20.0.2): Connection refused

Да, протестировал через wget (контейнер prometheus — минималистичный busybox), но метрики всё же получил.

То есть вы предлагаете найти способ добавить запись в файл /etc/hosts контейнера, на котором запущен prometheus, чтобы он мог разрешать… Извините, я вас потерял :slight_smile:

Я поступил иначе: запустил ещё один Docker-контейнер с простым nginx и настроил прокси-перенаправление, которое добавляет заголовок Host к входящим запросам. Он не открывает никаких портов, поэтому доступен только из внутренней виртуальной сети.

Так как же теперь всё работает?

Задача Prometheus:

  - job_name: discourse_exporter_proxy
    scheme: http
    static_configs:
      - targets:
        - 'discourse_forward_proxy:8080'

docker-compose.yaml (только часть с прокси)

version: "3"

services:
# [...]
    discourse_forward_proxy:
        image: nginx:latest
        container_name: discourse_forward_proxy
        restart: unless-stopped
        volumes:
            - ./discourse_forward_proxy/:/etc/nginx/conf.d
        networks:
            - prometheus-discourse_forward_proxy
            - discourse
# [...]

networks:
    prometheus-discourse_forward_proxy:
        internal: true
    discourse:
        external: true

В директории, где находится ваш docker-compose.yaml, создайте файл ./discourse_forward_proxy/discourse_forward_proxy.conf со следующим содержимым:

server {
    listen 8080;

    location /metrics {
      proxy_set_header Host "YOUR_DOMAIN_HERE.COM";
      proxy_pass https://discourse_app/metrics;
    }
}

Вот и всё:

Для истории: у меня есть репозиторий, в котором я настроил всё необходимое.

Там есть несколько жёстко заданных значений (например, FQDN нашего сайта в конфигурационном файле обратного прокси), которые потребуется изменить, если кто-то другой захочет его использовать, но, возможно, это может быть полезно кому-то ещё.

Там есть всё: от docker-compose до конфигурации nginx и настройки Grafana для ресурсов и дашбордов.

Это происходит из-за следующей строки:

GlobalSetting.add_default :prometheus_collector_port, 9405
GlobalSetting.add_default :prometheus_webserver_bind, "localhost"
GlobalSetting.add_default :prometheus_trusted_ip_allowlist_regex, ""

Привязка к localhost означает, что соединение возможно только с IP-адреса localhost, поэтому попытка подключиться к 172.20.0.2 завершается ошибкой. Это мера безопасности, чтобы случайно не открыть доступ к шире, чем предполагалось.

Если вы установите в файле определения контейнера:

  DISCOURSE_PROMETHEUS_WEBSERVER_BIND: '*'

то сервис будет прослушивать все IP-адреса, и вы сможете подключиться к нему из другого контейнера.

Причина, по которой это сработало в вашем случае:

server {
    listen 8080;

    location /metrics {
      proxy_set_header Host "YOUR_DOMAIN_HERE.COM";
      proxy_pass https://discourse_app/metrics;
    }
}

заключается в том, что этот контейнер nginx теперь обращается к Prometheus через IP-адрес localhost.

Если вы не уверены, на каких IP-адресах и портах работают службы, используйте команды ss -ltp или netstat -ltp (внутри контейнера! необходимые пакеты — net-tools и iproute2 соответственно), чтобы проверить это. Например, я только что пересобрал контейнер с плагином Prometheus и увидел следующее:

root@discourse-docker-app:/# ss -ltp
State      Recv-Q     Send-Q           Local Address:Port                 Peer Address:Port     Process                             
LISTEN     0          128                  127.0.0.1:3000                      0.0.0.0:*                                            
LISTEN     0          128                    0.0.0.0:postgresql                0.0.0.0:*                                            
LISTEN     0          128                    0.0.0.0:https                     0.0.0.0:*         users:(("nginx",pid=555,fd=7))     
LISTEN     0          128                  127.0.0.1:9405                      0.0.0.0:*                                            
LISTEN     0          128                    0.0.0.0:redis                     0.0.0.0:*                                            
LISTEN     0          128                    0.0.0.0:http                      0.0.0.0:*         users:(("nginx",pid=555,fd=6))     
LISTEN     0          128                       [::]:postgresql                   [::]:*                                            
LISTEN     0          128                       [::]:https                        [::]:*         users:(("nginx",pid=555,fd=8))     
LISTEN     0          128                       [::]:redis                        [::]:*

root@discourse-docker-app:/# curl http://172.17.0.2:9405/metrics
curl: (7) Failed to connect to 172.17.0.2 port 9405: Connection refused

root@discourse-docker-app:/# curl http://localhost:9405/metrics
# HELP discourse_collector_working Is the master process collector able to collect metrics
# TYPE discourse_collector_working gauge
discourse_collector_working 1


# HELP discourse_collector_rss total memory used by collector process
# TYPE discourse_collector_rss gauge
discourse_collector_rss 38178816
…

Это имя сервера отклоняет запрос на поиск IP для vmi1187507-app. Порт 53 — это DNS.

Это отличная работа, Майкл, спасибо, что уделили время и записали это.

Я протестирую это в выходные, так как на этой неделе я уже потратил слишком много рабочего времени на это :stuck_out_tongue:

Во время моих попыток я пробовал добавить внутренний IP-адрес, с которого контейнер с Prometheus будет выглядеть как запрос метрик, в DISCOURSE_PROMETHEUS_TRUSTED_IP_ALLOWLIST_REGEX, но это не сработало.

Вы предлагаете DISCOURSE_PROMETHEUS_WEBSERVER_BIND. Могу ли я спросить, откуда вы это взяли? Я предполагаю, что это ещё одна переменная окружения, которую нужно добавить в файл app.yml, верно?

Как именно это не сработало?

Если соединение не удалось установить, то настройка allowlist не имеет значения, так как она применяется после установления L4-соединения.

В коде Discourse есть магия :magic_wand: — если установить переменную окружения DISCOURSE_SITE_OR_GLOBAL_SETTING_NAME, она переопределит значение.

Таким образом, установка этой переменной переопределит:

GlobalSetting.add_default :prometheus_webserver_bind, "localhost"