Пишем GeoIP exporter для Prometheus с визуализаций в Grafana за 15 минут

от автора

Привет всем!

Я хочу поделиться с вами тем, как легко можно написать свой экспортер для 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.

Импортируем нужные либы и создаем структуру, в которую будет 'распаковываться' полученный json-объект.

// 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 адреса)

Пакет с функцией, возвращоющей map[string]int: кол-во соединений от страны.

// 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!


ссылка на оригинал статьи https://habr.com/post/420633/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *