Привет всем!
Я хочу поделиться с вами тем, как легко можно написать свой экспортер для Prometheus на Golang и покажу как это можно сделать на примере небольшой программы, которая следит за тем, откуда географически установлены текущие TCP соединения.
0. Disclaimer
Хотелось бы сразу в самом начале очертить, так сказать, scope данной публикации и сказать про что она не рассказывает, чтобы потом не возникло вопросов:
- да, это не визуализация клиентов. Это визуализация удаленных соединений. То есть она не делит соединения на те, в которых соединение инициировал удаленный сервер и на те что были иниированы данной машиной, и покажет на карте все подряд — например, сервер с репозиторием, откуда сейчас происходит скачивание обновлений на вашу машину.
- да, я понимаю что есть инструменты анонимизации в сети, которые скрывают реальный IP клиента. Цель данного инструмента не выявить точные GPS-координаты любого клиента, а иметь хотя бы общее представление об их географии.
- whois предоставляет информацию более точную, чем страна IP адреса, но тут я был связан лимитом плагина для Grafan'ы, который визуализирует только страны, но не города.
1. Пишем "back-end": экспортер на go
Итак, первое, что нам необходимо сделать — написать экспортер, который собственно будет собирать данные с нашего сервера и отдавать их в Prometheus. Выбор языков здесь велик: Prometheus имеет клиентские библиотеки для написания экспортеров на многих популярных языках, но я выбрал Go, во-первых, потому что так "нативнее" (раз уж сам Prometheus на нем написан), ну а во-вторых поскольку сам им пользуюсь в своей DevOps практике.
Ну довольно лирики, давайте приступим к коду. Начнем писать "снизу вверх": сначала функции для определения страны по IP и самого списка удаленных IP адресов, а потом уже отправка всего этого в Prometheus.
1.1. Определяем страну по IP адресу
Ну тут совсем все в лоб, я не стал мудрствовать и просто воспользовался сервисом freegeoip.net, API которого к моменту написания данной статьи уже стал deprecated, и теперь они предлагают бесплатно зарегистрироваться и иметь возможность делать 10,000 запросов в месяц (что для наших целей достаточно). Тут все просто: есть endpoint вида http://api.ipstack.com/<IP>?access_key=<API_KEY>
, который просто нам вернет json с нужным нам полем country_code
— это все, что нам потебуется для визуализации.
Итак, напишем пакет для выдергивания страны по IP.
// Package geo implements function for searching
// for a country code by IP address.
package geo
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// Type GeoIP stores whois info.
type GeoIP struct {
Ip string `json:""`
CountryCode string `json:"country_code"`
CountryName string `json:""`
RegionCode string `json:"region_code"`
RegionName string `json:"region_name"`
City string `json:"city"`
Zipcode string `json:"zipcode"`
Lat float32 `json:"latitude"`
Lon float32 `json:"longitude"`
MetroCode int `json:"metro_code"`
AreaCode int `json:"area_code"`
}
// Function GetCode returns country code by IP address.
func GetCode(address string) (string, error) {
response, err = http.Get("http://api.ipstack.com/" + address + "?access_key=<API_KEY>&format=1&legacy=1")
if err != nil {
fmt.Println(err)
return "", err
}
defer response.Body.Close()
body, err = ioutil.ReadAll(response.Body)
if err != nil {
fmt.Println(err)
return "", err
}
err = json.Unmarshal(body, &geo)
if err != nil {
fmt.Println(err)
return "", err
}
return geo.CountryCode, nil
}
Обратите внимание на параметр legacy=1
, мне приходится его использовать для обратной совметимости; вы, конечно, если будете использовать их API, пользуйтесь последней версией.
1.2. Формируем список TCP-соединений
Здесь воспользуемя пакетом github.com/shirou/gopsutil/net
и отфильтруем соединения со статусом ESTABLISHED
, исключив локальные IP-адреса и адреса из кастомного black-листа, который можно передать экспортеру при запуске (например, чтобы исключить все ваши собственные публичные IP адреса)
// Package conn implements function for collecting
// active TCP connections.
package conn
import (
"log"
"github.com/gree-gorey/geoip-exporter/pkg/geo"
"github.com/shirou/gopsutil/net"
)
// Type Connections stores map of active connections: country code -> number of connections.
type Connections struct {
ConnectionsByCode map[string]int `json:"connections_by_code"`
}
// Function RunJob retrieves active TCP connections.
func (c *Connections) RunJob(p *Params) {
if p.UseWg {
defer p.Wg.Done()
}
c.GetActiveConnections(p.BlackList)
}
// Function GetActiveConnections retrieves active TCP connections.
func (c *Connections) GetActiveConnections(blackList map[string]bool) {
cs, err := net.Connections("tcp")
if err != nil {
log.Println(err)
}
c.ConnectionsByCode = make(map[string]int)
for _, conn := range cs {
if _, ok := blackList[conn.Raddr.IP]; !ok && (conn.Status == "ESTABLISHED") && (conn.Raddr.IP != "127.0.0.1") {
code, err := geo.GetCode(conn.Raddr.IP)
if code != "" && err == nil {
_, ok := c.ConnectionsByCode[code]
if ok == true {
c.ConnectionsByCode[code] += 1
} else {
c.ConnectionsByCode[code] = 1
}
}
}
}
}
1.3. И, наконец, отправляем все в Prometheus
Точнее, он сам все заберет. Просто будем слушать порт и отдавать на нем собранные метрики.
Используя github.com/prometheus/client_golang/prometheus
создадим метрику типа Gauge
. На самом деле, можно было создать и Counter
, просто потом мы бы при запросах к базе использовали бы rate
. Возможно, последнее с точки зрения Prometheus эффективнее, но в то время как я писал этот экспортер (полгода назад) я только начинал знакомство с Prometheus и для меня было достаточно Gauge
:
location = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "job_location",
Help: "Location connections number",
},
[]string{"location"},
)
Собрав метрики с помощью предыдущих пунктов, обновляем наш вектор:
for code, number := range c.ConnectionsByCode {
location.With(prometheus.Labels{"location": code}).Set(float64(number))
}
Все это запускаем бесконечным циклом в отдельной горутине, а в основной просто биндим порт и ждем пока наши метрики заберет Prometheus:
prometheus.MustRegister(location)
http.Handle("/metrics", prometheus.Handler())
log.Fatal(http.ListenAndServe(*addr, nil))
Собственно, весь код можно посмотреть в репозитории на GitHub, не хочется здесь копипастить все подряд.
2. "Front-end": Grafana
Но для начала, конечно же, нужно сообщить Prometheus'у, чтобы тот собирал наши метрики:
- job_name: 'GeoIPExporter'
scrape_interval: 10s
static_configs:
- targets: ['127.0.0.1:9300']
(либо используя service discovery, если у вас, например, Kubernetes). Prometheus можно заставить перечитать конфиг, послав ему сигнал HUP
:
$ pgrep "^prometheus$" | xargs -i kill -HUP {}
Сходим к нему в UI и проверим, что метрики собираются:
Отлично, теперь очередь Grafan'ы. Воспользуемся плагином grafana-worldmap-panel
, который нужно предварительно установить:
$ grafana-cli plugins install grafana-worldmap-panel
Далее идем к ней в UI и жмем add panel -> Worldmap Panel. Во вкладке Metrics вводим следующий запрос:
sum(job_location) by (location)
И указываем legend format: {{location}}
. Выглядеть все должно примерно так:
Далее переходим во вкладку Worldmap и настраиваем все как на скриншоте:
И все! Наслаждаемся нашей картой.
Вот таким несложным образом можно сделать красивую карту соединений в Grafan'е.
Спасибо за внимание и жду ваших комментариев.
TODO
Конечно, чтобы использовать инструмент по назначению, нужно его доделать: отфильтровывать адреса локальных подсетей и многое другое. Кстати, если кто заинтересовался и хочет развивать этот экспортер — добро пожаловать в репозиторий на GitHub!
Links
- Prometheus client libraries
- Geolocation API
- psutil for golang
- Worldmap Panel Plugin for Grafana
- Репозиторий проекта на GitHub
Автор: gree-gorey