Привет всем!
Я хочу поделиться с вами тем, как легко можно написать свой экспортер для 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!
CPro
Мне кажется для ваших целей лучше подойдёт бесплатная база GeoLite2 от maxmind, не нужно куда то ходить по API. Можно быстро искать по локальной базе.
Библиотека для Go есть:
github.com/oschwald/geoip2-golang
gree-gorey Автор
Да, спасибо, я тоже думал сначала про локальную базу. Единственное, встает тогда вопрос с ее обновлением — хотя, опять же, для грубой оценки геолокаций клиентов наверное это не столь критично…