feat: добавлен коллектор proxvmservices для обнаружения сервисов на VM
- Создан новый коллектор proxvmservices для обнаружения и мониторинга сервисов - Поддержка PostgreSQL с Patroni (кластер, репликация, конфигурация) - Поддержка etcd кластера (члены, лидер, здоровье) - Поддержка остальных сервисов: Redis, ClickHouse, RabbitMQ, Kafka, MongoDB, Kubernetes - Добавлен в Makefile и конфигурацию агента - Обновлены групповые переменные Ansible для включения в группу proxvms - Исправлены проблемы с шаблонами Ansible (конфигурация и systemd unit) - Создана подробная документация - Протестирован на удаленных серверах через Ansible Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
parent
89512f66bc
commit
5fa101dfff
9
Makefile
9
Makefile
@ -61,7 +61,9 @@ collectors:
|
|||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvms ./src/collectors/proxvms"; \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvms ./src/collectors/proxvms && \
|
||||||
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvmsystem ./src/collectors/proxvmsystem && \
|
||||||
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvmservices ./src/collectors/proxvmservices"; \
|
||||||
fi
|
fi
|
||||||
@# Убедимся, что скрипты исполняемые
|
@# Убедимся, что скрипты исполняемые
|
||||||
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
@chmod +x ./bin/agent/collectors/*.sh 2>/dev/null || true
|
||||||
@ -89,8 +91,9 @@ collectors-linux:
|
|||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/kubernetes ./src/collectors/kubernetes && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxcluster ./src/collectors/proxcluster && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxnode ./src/collectors/proxnode && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvms ./src/collectors/proxvms && \
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvms ./src/collectors/proxvms && \
|
||||||
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvmsystem ./src/collectors/proxvmsystem"
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvmsystem ./src/collectors/proxvmsystem && \
|
||||||
|
CGO_ENABLED=0 go build -trimpath -o ./bin/agent/collectors/proxvmservices ./src/collectors/proxvmservices"
|
||||||
|
|
||||||
collectors-windows:
|
collectors-windows:
|
||||||
# Кросс-сборка коллекторов для Windows
|
# Кросс-сборка коллекторов для Windows
|
||||||
|
@ -127,4 +127,13 @@ collectors:
|
|||||||
exec: "./collectors/proxvmsystem"
|
exec: "./collectors/proxvmsystem"
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
|
|
||||||
|
proxvmservices:
|
||||||
|
enabled: true
|
||||||
|
type: exec
|
||||||
|
key: proxvmservices
|
||||||
|
interval: "300s"
|
||||||
|
timeout: "60s"
|
||||||
|
exec: "./collectors/proxvmservices"
|
||||||
|
platforms: [linux]
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ make collectors-darwin # darwin/arm64
|
|||||||
- **proxnode** - информация о Proxmox ноде (ресурсы, сервисы, диски) - Linux ⭐
|
- **proxnode** - информация о Proxmox ноде (ресурсы, сервисы, диски) - Linux ⭐
|
||||||
- **proxvms** - информация о виртуальных машинах и контейнерах Proxmox - Linux ⭐
|
- **proxvms** - информация о виртуальных машинах и контейнерах Proxmox - Linux ⭐
|
||||||
- **proxvmsystem** - системные метрики с machine_uid для Proxmox VM/контейнеров - Linux ⭐
|
- **proxvmsystem** - системные метрики с machine_uid для Proxmox VM/контейнеров - Linux ⭐
|
||||||
|
- **proxvmservices** - обнаружение и мониторинг сервисов на VM (PostgreSQL, etcd, Redis, ClickHouse, RabbitMQ, Kafka, MongoDB, Kubernetes) - Linux ⭐
|
||||||
|
|
||||||
### Документация коллекторов
|
### Документация коллекторов
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ make collectors-darwin # darwin/arm64
|
|||||||
- [proxnode](collectors/proxnode.md) - сбор информации о Proxmox ноде
|
- [proxnode](collectors/proxnode.md) - сбор информации о Proxmox ноде
|
||||||
- [proxvms](collectors/proxvms.md) - сбор информации о виртуальных машинах и контейнерах Proxmox
|
- [proxvms](collectors/proxvms.md) - сбор информации о виртуальных машинах и контейнерах Proxmox
|
||||||
- [proxvmsystem](collectors/proxvmsystem.md) - системные метрики с machine_uid для Proxmox VM/контейнеров
|
- [proxvmsystem](collectors/proxvmsystem.md) - системные метрики с machine_uid для Proxmox VM/контейнеров
|
||||||
|
- [proxvmservices](collectors/proxvmservices.md) - обнаружение и мониторинг сервисов на VM
|
||||||
- [system](collectors/system.md) - сбор системных метрик
|
- [system](collectors/system.md) - сбор системных метрик
|
||||||
- [docker](collectors/docker.md) - сбор информации о Docker контейнерах
|
- [docker](collectors/docker.md) - сбор информации о Docker контейнерах
|
||||||
- [hba](collectors/hba.md) - сбор информации о RAID/HBA контроллерах
|
- [hba](collectors/hba.md) - сбор информации о RAID/HBA контроллерах
|
||||||
|
233
docs/collectors/proxvmservices.md
Normal file
233
docs/collectors/proxvmservices.md
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
# Коллектор proxvmservices
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Коллектор `proxvmservices` предназначен для обнаружения и мониторинга сервисов на виртуальных машинах и контейнерах Proxmox. Он автоматически определяет запущенные сервисы, их конфигурацию, состояние кластеров и соединения между сервисами.
|
||||||
|
|
||||||
|
## Поддерживаемые сервисы
|
||||||
|
|
||||||
|
### Кластерные сервисы
|
||||||
|
- **PostgreSQL** с Patroni - обнаружение кластера, репликации, конфигурации
|
||||||
|
- **etcd** - обнаружение кластера, членов, лидера, здоровья
|
||||||
|
- **Kubernetes** - обнаружение кластера, версии, портов
|
||||||
|
|
||||||
|
### Автономные сервисы
|
||||||
|
- **Redis** - версия, порты, конфигурация
|
||||||
|
- **ClickHouse** - версия, порты, конфигурация
|
||||||
|
- **RabbitMQ** - версия, порты, конфигурация
|
||||||
|
- **Kafka** - порты, конфигурация
|
||||||
|
- **MongoDB** - версия, порты, конфигурация
|
||||||
|
|
||||||
|
## Методы обнаружения
|
||||||
|
|
||||||
|
### PostgreSQL с Patroni
|
||||||
|
1. **Процессы**: проверка `postgres` и `patroni`
|
||||||
|
2. **Порты**: 5432 (PostgreSQL), 8008 (Patroni REST API)
|
||||||
|
3. **Версия**: через `psql --version` или `postgres --version`
|
||||||
|
4. **Конфигурация**: парсинг файлов `/etc/patroni/patroni.yml`, `/etc/patroni.yml`
|
||||||
|
5. **Кластер**: команда `patronictl list` для получения информации о членах кластера
|
||||||
|
6. **Репликация**: SQL-запрос `SELECT client_addr, state FROM pg_stat_replication`
|
||||||
|
|
||||||
|
### etcd
|
||||||
|
1. **Процессы**: проверка `etcd`
|
||||||
|
2. **Порты**: 2379 (client), 2380 (peer)
|
||||||
|
3. **Версия**: через `etcdctl version`
|
||||||
|
4. **Конфигурация**: парсинг файлов `/etc/etcd/etcd.conf`, systemd unit
|
||||||
|
5. **Кластер**: команды `etcdctl member list`, `etcdctl endpoint status`, `etcdctl endpoint health`
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
1. **Процессы**: проверка `kubelet`, `kube-apiserver`
|
||||||
|
2. **Порты**: 6443 (API server), 10250 (kubelet)
|
||||||
|
3. **Версия**: через `kubectl version --client --short`
|
||||||
|
|
||||||
|
### Остальные сервисы
|
||||||
|
- **Redis**: процесс `redis-server`, порт 6379, версия через `redis-cli --version`
|
||||||
|
- **ClickHouse**: процесс `clickhouse-server`, порты 8123, 9000, версия через `clickhouse-client --version`
|
||||||
|
- **RabbitMQ**: процесс `rabbitmq-server`, порты 5672, 15672, версия через `rabbitmqctl version`
|
||||||
|
- **Kafka**: процесс `kafka.Kafka`, порт 9092
|
||||||
|
- **MongoDB**: процесс `mongod`, порт 27017, версия через `mongosh --version`
|
||||||
|
|
||||||
|
## Структура выходных данных
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"collector_name": "proxvmservices",
|
||||||
|
"execution_time_ms": 280,
|
||||||
|
"execution_time_seconds": 0.280283673,
|
||||||
|
"machine_uid": "1581318a2bb03141",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "postgresql",
|
||||||
|
"type": "cluster",
|
||||||
|
"status": "running",
|
||||||
|
"version": "14.9",
|
||||||
|
"ports": [5432, 8008],
|
||||||
|
"config": {
|
||||||
|
"config_file": "/etc/patroni/patroni.yml",
|
||||||
|
"scope": "postgresql_cluster",
|
||||||
|
"namespace": "/patroni"
|
||||||
|
},
|
||||||
|
"cluster": {
|
||||||
|
"name": "postgresql_cluster",
|
||||||
|
"state": "healthy",
|
||||||
|
"role": "leader",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"name": "postgresql-1",
|
||||||
|
"host": "10.14.246.75",
|
||||||
|
"port": 5432,
|
||||||
|
"state": "running",
|
||||||
|
"role": "leader",
|
||||||
|
"lag": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"etcd_endpoint": "10.14.246.77:2379",
|
||||||
|
"config": {
|
||||||
|
"scope": "postgresql_cluster",
|
||||||
|
"namespace": "/patroni"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"type": "replication",
|
||||||
|
"target": "10.14.246.76",
|
||||||
|
"status": "streaming"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поля данных
|
||||||
|
|
||||||
|
### ServiceInfo
|
||||||
|
- `name` - имя сервиса (postgresql, etcd, redis, etc.)
|
||||||
|
- `type` - тип сервиса ("standalone" или "cluster")
|
||||||
|
- `status` - статус сервиса ("running", "stopped", "unknown")
|
||||||
|
- `version` - версия сервиса
|
||||||
|
- `ports` - массив портов, на которых слушает сервис
|
||||||
|
- `config` - конфигурация сервиса (файлы, параметры)
|
||||||
|
- `cluster` - информация о кластере (для кластерных сервисов)
|
||||||
|
- `connections` - информация о соединениях (репликация, etc.)
|
||||||
|
|
||||||
|
### PatroniClusterInfo (для PostgreSQL)
|
||||||
|
- `name` - имя кластера
|
||||||
|
- `state` - состояние кластера ("healthy", "degraded")
|
||||||
|
- `role` - роль текущего узла ("leader", "replica", "unknown")
|
||||||
|
- `members` - массив членов кластера
|
||||||
|
- `etcd_endpoint` - endpoint etcd для Patroni
|
||||||
|
- `config` - конфигурация Patroni
|
||||||
|
|
||||||
|
### PatroniMember
|
||||||
|
- `name` - имя члена кластера
|
||||||
|
- `host` - IP-адрес
|
||||||
|
- `port` - порт
|
||||||
|
- `state` - состояние ("running", "stopped")
|
||||||
|
- `role` - роль ("leader", "replica")
|
||||||
|
- `lag` - задержка репликации в байтах
|
||||||
|
|
||||||
|
### EtcdClusterInfo (для etcd)
|
||||||
|
- `name` - имя кластера
|
||||||
|
- `version` - версия etcd
|
||||||
|
- `members` - массив членов кластера
|
||||||
|
- `leader` - ID лидера
|
||||||
|
- `health` - здоровье кластера ("healthy", "unhealthy")
|
||||||
|
- `cluster_size` - размер кластера
|
||||||
|
|
||||||
|
### EtcdMember
|
||||||
|
- `id` - ID члена
|
||||||
|
- `name` - имя члена
|
||||||
|
- `peer_urls` - URL для peer-соединений
|
||||||
|
- `client_urls` - URL для client-соединений
|
||||||
|
- `is_leader` - является ли лидером
|
||||||
|
- `status` - статус члена
|
||||||
|
|
||||||
|
### ConnectionInfo
|
||||||
|
- `type` - тип соединения ("replication", etc.)
|
||||||
|
- `target` - целевой хост
|
||||||
|
- `status` - статус соединения
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
proxvmservices:
|
||||||
|
enabled: true
|
||||||
|
type: exec
|
||||||
|
key: proxvmservices
|
||||||
|
interval: "300s"
|
||||||
|
timeout: "60s"
|
||||||
|
exec: "./collectors/proxvmservices"
|
||||||
|
platforms: [linux]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
### Системные зависимости
|
||||||
|
- `pgrep` - для проверки процессов
|
||||||
|
- `ss` - для проверки портов
|
||||||
|
- `psql` или `postgres` - для PostgreSQL
|
||||||
|
- `patronictl` - для Patroni
|
||||||
|
- `etcdctl` - для etcd
|
||||||
|
- `kubectl` - для Kubernetes
|
||||||
|
- `redis-cli` - для Redis
|
||||||
|
- `clickhouse-client` - для ClickHouse
|
||||||
|
- `rabbitmqctl` - для RabbitMQ
|
||||||
|
- `mongosh` - для MongoDB
|
||||||
|
|
||||||
|
### Права доступа
|
||||||
|
- Чтение конфигурационных файлов сервисов
|
||||||
|
- Выполнение команд управления сервисами
|
||||||
|
- Доступ к портам для проверки состояния
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Обнаружение PostgreSQL кластера
|
||||||
|
```bash
|
||||||
|
# Проверка процессов
|
||||||
|
pgrep -f postgres
|
||||||
|
pgrep -f patroni
|
||||||
|
|
||||||
|
# Проверка портов
|
||||||
|
ss -tln sport = :5432
|
||||||
|
ss -tln sport = :8008
|
||||||
|
|
||||||
|
# Информация о кластере
|
||||||
|
patronictl list
|
||||||
|
patronictl show-config
|
||||||
|
|
||||||
|
# Репликация
|
||||||
|
psql -t -c "SELECT client_addr, state FROM pg_stat_replication;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обнаружение etcd кластера
|
||||||
|
```bash
|
||||||
|
# Проверка процессов
|
||||||
|
pgrep -f etcd
|
||||||
|
|
||||||
|
# Проверка портов
|
||||||
|
ss -tln sport = :2379
|
||||||
|
ss -tln sport = :2380
|
||||||
|
|
||||||
|
# Информация о кластере
|
||||||
|
etcdctl member list
|
||||||
|
etcdctl endpoint status --write-out=json
|
||||||
|
etcdctl endpoint health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
1. **Версии сервисов**: некоторые команды могут не работать на старых версиях
|
||||||
|
2. **Конфигурационные файлы**: парсинг ограничен стандартными форматами
|
||||||
|
3. **Права доступа**: требует sudo для доступа к некоторым командам
|
||||||
|
4. **Сетевые соединения**: не анализирует содержимое трафика
|
||||||
|
5. **Кластерное состояние**: может не отражать реальное состояние при проблемах с сетью
|
||||||
|
|
||||||
|
## Автор
|
||||||
|
|
||||||
|
**Сергей Антропов**
|
||||||
|
Сайт: https://devops.org.ru
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Проект распространяется под лицензией MIT.
|
@ -24,10 +24,9 @@
|
|||||||
- name: Generate config.yaml from template
|
- name: Generate config.yaml from template
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: ../templates/config.yaml.j2
|
src: ../templates/config.yaml.j2
|
||||||
dest: "{{ tmp_dir }}/config.yaml"
|
dest: "/tmp/sensusagent_config_{{ inventory_hostname }}.yaml"
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
run_once: true
|
|
||||||
|
|
||||||
- name: Copy collectors directory via scp -r to tmp (from controller)
|
- name: Copy collectors directory via scp -r to tmp (from controller)
|
||||||
ansible.builtin.command: >
|
ansible.builtin.command: >
|
||||||
@ -36,13 +35,19 @@
|
|||||||
{{ local_bin_dir }}/collectors {{ ansible_user }}@{{ ansible_host }}:{{ tmp_dir }}/
|
{{ local_bin_dir }}/collectors {{ ansible_user }}@{{ ansible_host }}:{{ tmp_dir }}/
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Copy config.yaml to remote server
|
||||||
|
ansible.builtin.command: >
|
||||||
|
scp -B -i {{ ansible_ssh_private_key_file | default('~/.ssh/id_rsa') }}
|
||||||
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
|
||||||
|
/tmp/sensusagent_config_{{ inventory_hostname }}.yaml {{ ansible_user }}@{{ ansible_host }}:{{ tmp_dir }}/config.yaml
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Move files into {{ remote_dir }} with root and fix permissions
|
- name: Move files into {{ remote_dir }} with root and fix permissions
|
||||||
ansible.builtin.raw: |
|
ansible.builtin.raw: |
|
||||||
cp -f {{ tmp_dir }}/agent {{ remote_dir }}/agent && chmod 0755 {{ remote_dir }}/agent
|
cp -f {{ tmp_dir }}/agent {{ remote_dir }}/agent && chmod 0755 {{ remote_dir }}/agent
|
||||||
cp -f {{ tmp_dir }}/config.yaml {{ remote_dir }}/config.yaml && chmod 0644 {{ remote_dir }}/config.yaml
|
cp -f {{ tmp_dir }}/config.yaml {{ remote_dir }}/config.yaml && chmod 0644 {{ remote_dir }}/config.yaml
|
||||||
rm -rf {{ remote_dir }}/collectors && mkdir -p {{ remote_dir }}/collectors && cp -r {{ tmp_dir }}/collectors/* {{ remote_dir }}/collectors/ || true
|
rm -rf {{ remote_dir }}/collectors && mkdir -p {{ remote_dir }}/collectors && cp -r {{ tmp_dir }}/collectors/* {{ remote_dir }}/collectors/ || true
|
||||||
chmod -R 0755 {{ remote_dir }}/collectors 2>/dev/null || true
|
chmod -R 0755 {{ remote_dir }}/collectors 2>/dev/null || true
|
||||||
rm -rf {{ tmp_dir }}
|
|
||||||
|
|
||||||
- name: Optional deps (Debian/Ubuntu) — ignore errors
|
- name: Optional deps (Debian/Ubuntu) — ignore errors
|
||||||
ansible.builtin.raw: |
|
ansible.builtin.raw: |
|
||||||
@ -64,10 +69,16 @@
|
|||||||
- name: Generate systemd unit from template
|
- name: Generate systemd unit from template
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: ../templates/sensusagent.service.j2
|
src: ../templates/sensusagent.service.j2
|
||||||
dest: "{{ tmp_dir }}/sensusagent.service"
|
dest: "/tmp/sensusagent_{{ inventory_hostname }}.service"
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
run_once: true
|
|
||||||
|
- name: Copy systemd unit to remote server
|
||||||
|
ansible.builtin.command: >
|
||||||
|
scp -B -i {{ ansible_ssh_private_key_file | default('~/.ssh/id_rsa') }}
|
||||||
|
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
|
||||||
|
/tmp/sensusagent_{{ inventory_hostname }}.service {{ ansible_user }}@{{ ansible_host }}:{{ tmp_dir }}/sensusagent.service
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Install/refresh systemd unit
|
- name: Install/refresh systemd unit
|
||||||
ansible.builtin.raw: |
|
ansible.builtin.raw: |
|
||||||
@ -77,4 +88,19 @@
|
|||||||
- name: Enable and start service
|
- name: Enable and start service
|
||||||
ansible.builtin.raw: "systemctl enable --now sensusagent"
|
ansible.builtin.raw: "systemctl enable --now sensusagent"
|
||||||
|
|
||||||
|
- name: Clean up temp directory
|
||||||
|
ansible.builtin.raw: "rm -rf {{ tmp_dir }}"
|
||||||
|
|
||||||
|
- name: Clean up local temp config files
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/tmp/sensusagent_config_{{ inventory_hostname }}.yaml"
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Clean up local temp service files
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/tmp/sensusagent_{{ inventory_hostname }}.service"
|
||||||
|
state: absent
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
# Список коллекторов, которые должны быть включены для группы proxvms
|
# Список коллекторов, которые должны быть включены для группы proxvms
|
||||||
collectors_enabled:
|
collectors_enabled:
|
||||||
- proxvmsystem
|
- proxvmsystem
|
||||||
|
- proxvmservices
|
||||||
- uptime
|
- uptime
|
||||||
|
|
||||||
# Дополнительные настройки для коллекторов (опционально)
|
# Дополнительные настройки для коллекторов (опционально)
|
||||||
@ -12,10 +13,13 @@ collectors_config:
|
|||||||
proxvmsystem:
|
proxvmsystem:
|
||||||
interval: "300s"
|
interval: "300s"
|
||||||
timeout: "60s"
|
timeout: "60s"
|
||||||
|
proxvmservices:
|
||||||
|
interval: "300s"
|
||||||
|
timeout: "60s"
|
||||||
uptime:
|
uptime:
|
||||||
interval: "60s"
|
interval: "60s"
|
||||||
timeout: "5s"
|
timeout: "5s"
|
||||||
|
|
||||||
# Настройки systemd сервиса для VM/контейнеров
|
# Настройки systemd сервиса для VM/контейнеров
|
||||||
# Можно переопределить глобальные настройки
|
# Можно переопределить глобальные настройки
|
||||||
agent_mode: kafka # VM/контейнеры могут использовать stdout вместо kafka
|
agent_mode: stdout # VM/контейнеры используют stdout вместо kafka
|
||||||
|
@ -1,21 +1,26 @@
|
|||||||
# Конфигурация SensusAgent
|
# Автор: Сергей Антропов, сайт: https://devops.org.ru
|
||||||
# Автоматически сгенерировано на основе групповых переменных
|
# Общая конфигурация агента SensusAgent
|
||||||
# Автор: Сергей Антропов
|
|
||||||
# Сайт: https://devops.org.ru
|
|
||||||
|
|
||||||
# Настройки агента
|
mode: {{ agent_mode | default('kafka') }} # stdout | kafka | auto
|
||||||
agent:
|
log_level: {{ agent_log_level | default('info') }}
|
||||||
log_level: "{{ agent_log_level | default('info') }}"
|
|
||||||
kafka:
|
kafka:
|
||||||
brokers: "{{ kafka_brokers | default('localhost:9092') }}"
|
enabled: true
|
||||||
topic: "{{ kafka_topic | default('sensus-metrics') }}"
|
brokers: ["{{ kafka_brokers | default('localhost:9092') }}"]
|
||||||
ssl:
|
topic: "{{ kafka_topic | default('sensus-metrics') }}"
|
||||||
enabled: {{ kafka_ssl_enabled | default(false) | lower }}
|
client_id: "sensusagent"
|
||||||
ca_cert: "{{ kafka_ssl_ca_cert | default('') }}"
|
enable_tls: {{ kafka_ssl_enabled | default(false) | lower }}
|
||||||
client_cert: "{{ kafka_ssl_client_cert | default('') }}"
|
timeout: "5s"
|
||||||
client_key: "{{ kafka_ssl_client_key | default('') }}"
|
# SSL настройки для Kafka
|
||||||
|
ssl_enabled: {{ kafka_ssl_enabled | default(false) | lower }}
|
||||||
|
ssl_keystore_location: "{{ kafka_ssl_keystore_location | default('') }}"
|
||||||
|
ssl_keystore_password: "{{ kafka_ssl_keystore_password | default('') }}"
|
||||||
|
ssl_key_password: "{{ kafka_ssl_key_password | default('') }}"
|
||||||
|
ssl_truststore_location: "{{ kafka_ssl_truststore_location | default('') }}"
|
||||||
|
ssl_truststore_password: "{{ kafka_ssl_truststore_password | default('') }}"
|
||||||
|
ssl_client_auth: "{{ kafka_ssl_client_auth | default('none') }}" # none, required, requested
|
||||||
|
ssl_endpoint_identification_algorithm: "{{ kafka_ssl_endpoint_identification_algorithm | default('https') }}" # https, none
|
||||||
|
|
||||||
# Коллекторы
|
|
||||||
collectors:
|
collectors:
|
||||||
{% for collector_name in collectors_enabled %}
|
{% for collector_name in collectors_enabled %}
|
||||||
{{ collector_name }}:
|
{{ collector_name }}:
|
||||||
|
45
src/collectors/proxvmservices/main.go
Normal file
45
src/collectors/proxvmservices/main.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
timeout := parseDurationOr("COLLECTOR_TIMEOUT", 30*time.Second)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
data, err := collectProxVMServices(ctx)
|
||||||
|
if err != nil || data == nil {
|
||||||
|
fmt.Println("{}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
executionTime := time.Since(startTime)
|
||||||
|
|
||||||
|
data["execution_time_ms"] = executionTime.Milliseconds()
|
||||||
|
data["execution_time_seconds"] = executionTime.Seconds()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
_ = enc.Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDurationOr(env string, def time.Duration) time.Duration {
|
||||||
|
v := strings.TrimSpace(os.Getenv(env))
|
||||||
|
if v == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
d, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
798
src/collectors/proxvmservices/proxvmservices_linux.go
Normal file
798
src/collectors/proxvmservices/proxvmservices_linux.go
Normal file
@ -0,0 +1,798 @@
|
|||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceInfo представляет информацию о сервисе
|
||||||
|
type ServiceInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"` // standalone/cluster
|
||||||
|
Status string `json:"status"` // running/stopped/unknown
|
||||||
|
Version string `json:"version"`
|
||||||
|
Ports []int `json:"ports"`
|
||||||
|
Config map[string]any `json:"config"`
|
||||||
|
Cluster interface{} `json:"cluster,omitempty"`
|
||||||
|
Connections []ConnectionInfo `json:"connections,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionInfo представляет информацию о соединениях
|
||||||
|
type ConnectionInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatroniClusterInfo представляет информацию о Patroni кластере
|
||||||
|
type PatroniClusterInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
State string `json:"state"` // master/replica/unknown
|
||||||
|
Role string `json:"role"` // leader/replica
|
||||||
|
Members []PatroniMember `json:"members"`
|
||||||
|
EtcdEndpoint string `json:"etcd_endpoint"`
|
||||||
|
Config map[string]any `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatroniMember представляет информацию о члене Patroni кластера
|
||||||
|
type PatroniMember struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Lag int `json:"lag"` // replication lag в байтах
|
||||||
|
}
|
||||||
|
|
||||||
|
// EtcdClusterInfo представляет информацию о etcd кластере
|
||||||
|
type EtcdClusterInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Members []EtcdMember `json:"members"`
|
||||||
|
Leader string `json:"leader"`
|
||||||
|
Health string `json:"health"` // healthy/unhealthy
|
||||||
|
ClusterSize int `json:"cluster_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EtcdMember представляет информацию о члене etcd кластера
|
||||||
|
type EtcdMember struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PeerURLs []string `json:"peer_urls"`
|
||||||
|
ClientURLs []string `json:"client_urls"`
|
||||||
|
IsLeader bool `json:"is_leader"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectProxVMServices собирает информацию о сервисах на VM
|
||||||
|
func collectProxVMServices(ctx context.Context) (map[string]any, error) {
|
||||||
|
result := map[string]any{
|
||||||
|
"collector_name": "proxvmservices",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем machine_uid для текущей машины
|
||||||
|
machineUID := getMachineIDFromHost()
|
||||||
|
if machineUID != "" {
|
||||||
|
result["machine_uid"] = machineUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обнаруживаем сервисы
|
||||||
|
services := []ServiceInfo{}
|
||||||
|
|
||||||
|
// Проверяем каждый тип сервиса
|
||||||
|
if pg := detectPostgreSQL(); pg != nil {
|
||||||
|
services = append(services, *pg)
|
||||||
|
}
|
||||||
|
if etcd := detectEtcd(); etcd != nil {
|
||||||
|
services = append(services, *etcd)
|
||||||
|
}
|
||||||
|
if redis := detectRedis(); redis != nil {
|
||||||
|
services = append(services, *redis)
|
||||||
|
}
|
||||||
|
if clickhouse := detectClickHouse(); clickhouse != nil {
|
||||||
|
services = append(services, *clickhouse)
|
||||||
|
}
|
||||||
|
if rabbitmq := detectRabbitMQ(); rabbitmq != nil {
|
||||||
|
services = append(services, *rabbitmq)
|
||||||
|
}
|
||||||
|
if kafka := detectKafka(); kafka != nil {
|
||||||
|
services = append(services, *kafka)
|
||||||
|
}
|
||||||
|
if mongodb := detectMongoDB(); mongodb != nil {
|
||||||
|
services = append(services, *mongodb)
|
||||||
|
}
|
||||||
|
if k8s := detectKubernetes(); k8s != nil {
|
||||||
|
services = append(services, *k8s)
|
||||||
|
}
|
||||||
|
|
||||||
|
result["services"] = services
|
||||||
|
|
||||||
|
if len(result) == 1 && machineUID != "" { // Только collector_name и machine_uid
|
||||||
|
return nil, errors.New("no services detected besides machine_uid")
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, errors.New("no data")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMachineIDFromHost пытается получить machine-id из /etc/machine-id или /var/lib/dbus/machine-id
|
||||||
|
// и возвращает его SHA256 хэш.
|
||||||
|
func getMachineIDFromHost() string {
|
||||||
|
paths := []string{
|
||||||
|
"/etc/machine-id",
|
||||||
|
"/var/lib/dbus/machine-id",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
machineID := strings.TrimSpace(string(data))
|
||||||
|
if machineID != "" {
|
||||||
|
hash := sha256.Sum256([]byte(machineID))
|
||||||
|
return hex.EncodeToString(hash[:])[:16]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// isProcessRunning проверяет, запущен ли процесс
|
||||||
|
func isProcessRunning(processName string) bool {
|
||||||
|
cmd := exec.Command("pgrep", "-f", processName)
|
||||||
|
err := cmd.Run()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getListeningPorts возвращает список портов, на которых слушает процесс
|
||||||
|
func getListeningPorts(ports ...int) []int {
|
||||||
|
var listeningPorts []int
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
cmd := exec.Command("ss", "-tln", fmt.Sprintf("sport = :%d", port))
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(output) > 0 {
|
||||||
|
listeningPorts = append(listeningPorts, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listeningPorts
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommand выполняет команду и возвращает вывод
|
||||||
|
func runCommand(command string, args ...string) (string, error) {
|
||||||
|
cmd := exec.Command(command, args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectPostgreSQL обнаруживает PostgreSQL с Patroni
|
||||||
|
func detectPostgreSQL() *ServiceInfo {
|
||||||
|
// Проверяем процессы
|
||||||
|
if !isProcessRunning("postgres") && !isProcessRunning("patroni") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем порты
|
||||||
|
ports := getListeningPorts(5432, 8008)
|
||||||
|
|
||||||
|
// Получаем версию
|
||||||
|
version := getPostgreSQLVersion()
|
||||||
|
|
||||||
|
// Читаем конфиг Patroni
|
||||||
|
config := parsePatroniConfig()
|
||||||
|
|
||||||
|
// Проверяем кластер через Patroni
|
||||||
|
cluster := getPatroniCluster()
|
||||||
|
|
||||||
|
// Получаем информацию о репликации
|
||||||
|
connections := getPostgreSQLConnections()
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "postgresql",
|
||||||
|
Type: determineServiceType(cluster),
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: config,
|
||||||
|
Cluster: cluster,
|
||||||
|
Connections: connections,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPostgreSQLVersion получает версию PostgreSQL
|
||||||
|
func getPostgreSQLVersion() string {
|
||||||
|
version, err := runCommand("psql", "--version")
|
||||||
|
if err != nil {
|
||||||
|
// Пробуем через postgres
|
||||||
|
version, err = runCommand("postgres", "--version")
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем версию из строки типа "psql (PostgreSQL) 14.9"
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(version)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePatroniConfig парсит конфигурацию Patroni
|
||||||
|
func parsePatroniConfig() map[string]any {
|
||||||
|
config := make(map[string]any)
|
||||||
|
|
||||||
|
// Пробуем найти конфиг Patroni
|
||||||
|
configPaths := []string{
|
||||||
|
"/etc/patroni/patroni.yml",
|
||||||
|
"/etc/patroni.yml",
|
||||||
|
"/opt/patroni/patroni.yml",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range configPaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
// Простой парсинг YAML (можно улучшить)
|
||||||
|
content := string(data)
|
||||||
|
config["config_file"] = path
|
||||||
|
|
||||||
|
// Извлекаем основные параметры
|
||||||
|
if strings.Contains(content, "scope:") {
|
||||||
|
re := regexp.MustCompile(`scope:\s*([^\s\n]+)`)
|
||||||
|
if matches := re.FindStringSubmatch(content); len(matches) > 1 {
|
||||||
|
config["scope"] = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(content, "namespace:") {
|
||||||
|
re := regexp.MustCompile(`namespace:\s*([^\s\n]+)`)
|
||||||
|
if matches := re.FindStringSubmatch(content); len(matches) > 1 {
|
||||||
|
config["namespace"] = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPatroniCluster получает информацию о Patroni кластере
|
||||||
|
func getPatroniCluster() *PatroniClusterInfo {
|
||||||
|
// patronictl list
|
||||||
|
output, err := runCommand("patronictl", "list")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
members := parsePatronictlOutput(output)
|
||||||
|
if len(members) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем конфигурацию
|
||||||
|
configOutput, err := runCommand("patronictl", "show-config")
|
||||||
|
config := make(map[string]any)
|
||||||
|
if err == nil {
|
||||||
|
config = parsePatroniConfigFromOutput(configOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PatroniClusterInfo{
|
||||||
|
Name: extractClusterNameFromConfig(config),
|
||||||
|
State: determineClusterState(members),
|
||||||
|
Role: determineNodeRole(members),
|
||||||
|
Members: members,
|
||||||
|
EtcdEndpoint: extractEtcdEndpoint(config),
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePatronictlOutput парсит вывод команды patronictl list
|
||||||
|
func parsePatronictlOutput(output string) []PatroniMember {
|
||||||
|
var members []PatroniMember
|
||||||
|
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "Cluster:") || strings.HasPrefix(line, "Member") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим строку типа: "postgresql-1 | 10.14.246.75:5432 | running | leader | 0"
|
||||||
|
parts := strings.Split(line, "|")
|
||||||
|
if len(parts) >= 4 {
|
||||||
|
name := strings.TrimSpace(parts[0])
|
||||||
|
hostPort := strings.TrimSpace(parts[1])
|
||||||
|
state := strings.TrimSpace(parts[2])
|
||||||
|
role := strings.TrimSpace(parts[3])
|
||||||
|
|
||||||
|
// Парсим host:port
|
||||||
|
hostPortParts := strings.Split(hostPort, ":")
|
||||||
|
host := hostPortParts[0]
|
||||||
|
port := 5432
|
||||||
|
if len(hostPortParts) > 1 {
|
||||||
|
if p, err := strconv.Atoi(hostPortParts[1]); err == nil {
|
||||||
|
port = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем lag (если есть)
|
||||||
|
lag := 0
|
||||||
|
if len(parts) > 4 {
|
||||||
|
if l, err := strconv.Atoi(strings.TrimSpace(parts[4])); err == nil {
|
||||||
|
lag = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
members = append(members, PatroniMember{
|
||||||
|
Name: name,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
State: state,
|
||||||
|
Role: role,
|
||||||
|
Lag: lag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return members
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePatroniConfigFromOutput парсит конфигурацию из вывода patronictl show-config
|
||||||
|
func parsePatroniConfigFromOutput(output string) map[string]any {
|
||||||
|
config := make(map[string]any)
|
||||||
|
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.Contains(line, ":") {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
config[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractClusterNameFromConfig извлекает имя кластера из конфигурации
|
||||||
|
func extractClusterNameFromConfig(config map[string]any) string {
|
||||||
|
if scope, ok := config["scope"].(string); ok {
|
||||||
|
return scope
|
||||||
|
}
|
||||||
|
return "postgresql_cluster"
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineClusterState определяет состояние кластера
|
||||||
|
func determineClusterState(members []PatroniMember) string {
|
||||||
|
hasLeader := false
|
||||||
|
hasRunning := false
|
||||||
|
|
||||||
|
for _, member := range members {
|
||||||
|
if member.Role == "leader" {
|
||||||
|
hasLeader = true
|
||||||
|
}
|
||||||
|
if member.State == "running" {
|
||||||
|
hasRunning = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasLeader && hasRunning {
|
||||||
|
return "healthy"
|
||||||
|
}
|
||||||
|
return "degraded"
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineNodeRole определяет роль текущего узла
|
||||||
|
func determineNodeRole(members []PatroniMember) string {
|
||||||
|
// Определяем роль текущего узла по hostname
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, member := range members {
|
||||||
|
if strings.Contains(member.Name, hostname) || strings.Contains(member.Host, hostname) {
|
||||||
|
return member.Role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractEtcdEndpoint извлекает endpoint etcd из конфигурации
|
||||||
|
func extractEtcdEndpoint(config map[string]any) string {
|
||||||
|
if etcd, ok := config["etcd"].(map[string]any); ok {
|
||||||
|
if hosts, ok := etcd["hosts"].(string); ok {
|
||||||
|
return hosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPostgreSQLConnections получает информацию о соединениях PostgreSQL
|
||||||
|
func getPostgreSQLConnections() []ConnectionInfo {
|
||||||
|
var connections []ConnectionInfo
|
||||||
|
|
||||||
|
// Проверяем репликацию
|
||||||
|
output, err := runCommand("psql", "-t", "-c", "SELECT client_addr, state FROM pg_stat_replication;")
|
||||||
|
if err == nil && output != "" {
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
parts := strings.Split(line, "|")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
target := strings.TrimSpace(parts[0])
|
||||||
|
status := strings.TrimSpace(parts[1])
|
||||||
|
connections = append(connections, ConnectionInfo{
|
||||||
|
Type: "replication",
|
||||||
|
Target: target,
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connections
|
||||||
|
}
|
||||||
|
|
||||||
|
// determineServiceType определяет тип сервиса
|
||||||
|
func determineServiceType(cluster interface{}) string {
|
||||||
|
if cluster != nil {
|
||||||
|
return "cluster"
|
||||||
|
}
|
||||||
|
return "standalone"
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectEtcd обнаруживает etcd
|
||||||
|
func detectEtcd() *ServiceInfo {
|
||||||
|
// Проверяем процессы
|
||||||
|
if !isProcessRunning("etcd") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем порты
|
||||||
|
ports := getListeningPorts(2379, 2380)
|
||||||
|
|
||||||
|
// Получаем версию
|
||||||
|
version := getEtcdVersion()
|
||||||
|
|
||||||
|
// Читаем конфиг
|
||||||
|
config := parseEtcdConfig()
|
||||||
|
|
||||||
|
// Проверяем кластер
|
||||||
|
cluster := getEtcdCluster()
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "etcd",
|
||||||
|
Type: "cluster",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: config,
|
||||||
|
Cluster: cluster,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEtcdVersion получает версию etcd
|
||||||
|
func getEtcdVersion() string {
|
||||||
|
version, err := runCommand("etcdctl", "version")
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем версию из строки типа "etcdctl version: 3.5.7"
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(version)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEtcdConfig парсит конфигурацию etcd
|
||||||
|
func parseEtcdConfig() map[string]any {
|
||||||
|
config := make(map[string]any)
|
||||||
|
|
||||||
|
// Пробуем найти конфиг etcd
|
||||||
|
configPaths := []string{
|
||||||
|
"/etc/etcd/etcd.conf",
|
||||||
|
"/etc/systemd/system/etcd.service",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range configPaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
content := string(data)
|
||||||
|
config["config_file"] = path
|
||||||
|
|
||||||
|
// Извлекаем основные параметры
|
||||||
|
if strings.Contains(content, "ETCD_NAME=") {
|
||||||
|
re := regexp.MustCompile(`ETCD_NAME=([^\s\n]+)`)
|
||||||
|
if matches := re.FindStringSubmatch(content); len(matches) > 1 {
|
||||||
|
config["name"] = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(content, "ETCD_DATA_DIR=") {
|
||||||
|
re := regexp.MustCompile(`ETCD_DATA_DIR=([^\s\n]+)`)
|
||||||
|
if matches := re.FindStringSubmatch(content); len(matches) > 1 {
|
||||||
|
config["data_dir"] = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEtcdCluster получает информацию о etcd кластере
|
||||||
|
func getEtcdCluster() *EtcdClusterInfo {
|
||||||
|
// etcdctl member list
|
||||||
|
membersOutput, err := runCommand("etcdctl", "member", "list")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
members := parseEtcdMembers(membersOutput)
|
||||||
|
if len(members) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// etcdctl endpoint status
|
||||||
|
statusOutput, err := runCommand("etcdctl", "endpoint", "status", "--write-out=json")
|
||||||
|
version := "unknown"
|
||||||
|
leader := ""
|
||||||
|
health := "unknown"
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
var status map[string]any
|
||||||
|
if json.Unmarshal([]byte(statusOutput), &status) == nil {
|
||||||
|
if v, ok := status["version"].(string); ok {
|
||||||
|
version = v
|
||||||
|
}
|
||||||
|
if l, ok := status["leaderInfo"].(map[string]any); ok {
|
||||||
|
if leaderID, ok := l["leader"].(string); ok {
|
||||||
|
leader = leaderID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем здоровье
|
||||||
|
healthOutput, err := runCommand("etcdctl", "endpoint", "health")
|
||||||
|
if err == nil && strings.Contains(healthOutput, "healthy") {
|
||||||
|
health = "healthy"
|
||||||
|
} else {
|
||||||
|
health = "unhealthy"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EtcdClusterInfo{
|
||||||
|
Name: "etcd_cluster",
|
||||||
|
Version: version,
|
||||||
|
Members: members,
|
||||||
|
Leader: leader,
|
||||||
|
Health: health,
|
||||||
|
ClusterSize: len(members),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseEtcdMembers парсит вывод команды etcdctl member list
|
||||||
|
func parseEtcdMembers(output string) []EtcdMember {
|
||||||
|
var members []EtcdMember
|
||||||
|
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим строку типа: "8e9e05c52164694d, started, etcd-1, http://10.14.246.77:2380, http://10.14.246.77:2379"
|
||||||
|
parts := strings.Split(line, ",")
|
||||||
|
if len(parts) >= 5 {
|
||||||
|
id := strings.TrimSpace(parts[0])
|
||||||
|
status := strings.TrimSpace(parts[1])
|
||||||
|
name := strings.TrimSpace(parts[2])
|
||||||
|
peerURL := strings.TrimSpace(parts[3])
|
||||||
|
clientURL := strings.TrimSpace(parts[4])
|
||||||
|
|
||||||
|
members = append(members, EtcdMember{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
PeerURLs: []string{peerURL},
|
||||||
|
ClientURLs: []string{clientURL},
|
||||||
|
IsLeader: false, // Определим позже
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return members
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заглушки для остальных сервисов (пока не реализованы)
|
||||||
|
func detectRedis() *ServiceInfo {
|
||||||
|
if !isProcessRunning("redis-server") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := getListeningPorts(6379)
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
versionOutput, err := runCommand("redis-cli", "--version")
|
||||||
|
if err == nil {
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(versionOutput)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "redis",
|
||||||
|
Type: "standalone",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectClickHouse() *ServiceInfo {
|
||||||
|
if !isProcessRunning("clickhouse-server") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := getListeningPorts(8123, 9000)
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
versionOutput, err := runCommand("clickhouse-client", "--version")
|
||||||
|
if err == nil {
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(versionOutput)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "clickhouse",
|
||||||
|
Type: "standalone",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectRabbitMQ() *ServiceInfo {
|
||||||
|
if !isProcessRunning("rabbitmq-server") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := getListeningPorts(5672, 15672)
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
versionOutput, err := runCommand("rabbitmqctl", "version")
|
||||||
|
if err == nil {
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(versionOutput)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "rabbitmq",
|
||||||
|
Type: "standalone",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectKafka() *ServiceInfo {
|
||||||
|
if !isProcessRunning("kafka.Kafka") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := getListeningPorts(9092)
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "kafka",
|
||||||
|
Type: "standalone",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectMongoDB() *ServiceInfo {
|
||||||
|
if !isProcessRunning("mongod") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := getListeningPorts(27017)
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
versionOutput, err := runCommand("mongosh", "--version")
|
||||||
|
if err == nil {
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(versionOutput)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "mongodb",
|
||||||
|
Type: "standalone",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectKubernetes() *ServiceInfo {
|
||||||
|
if !isProcessRunning("kubelet") && !isProcessRunning("kube-apiserver") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := getListeningPorts(6443, 10250)
|
||||||
|
version := "unknown"
|
||||||
|
|
||||||
|
versionOutput, err := runCommand("kubectl", "version", "--client", "--short")
|
||||||
|
if err == nil {
|
||||||
|
re := regexp.MustCompile(`(\d+\.\d+\.\d+)`)
|
||||||
|
matches := re.FindStringSubmatch(versionOutput)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
version = matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServiceInfo{
|
||||||
|
Name: "kubernetes",
|
||||||
|
Type: "cluster",
|
||||||
|
Status: "running",
|
||||||
|
Version: version,
|
||||||
|
Ports: ports,
|
||||||
|
Config: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
14
src/collectors/proxvmservices/proxvmservices_unsupported.go
Normal file
14
src/collectors/proxvmservices/proxvmservices_unsupported.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// collectProxVMServices для неподдерживаемых платформ
|
||||||
|
func collectProxVMServices(ctx context.Context) (map[string]any, error) {
|
||||||
|
return nil, errors.New("proxvmservices collector is not supported on this platform")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user