Пишем 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!



Поделиться публикацией

Комментарии 2

    0
    Мне кажется для ваших целей лучше подойдёт бесплатная база GeoLite2 от maxmind, не нужно куда то ходить по API. Можно быстро искать по локальной базе.
    Библиотека для Go есть:
    github.com/oschwald/geoip2-golang
      +1
      Да, спасибо, я тоже думал сначала про локальную базу. Единственное, встает тогда вопрос с ее обновлением — хотя, опять же, для грубой оценки геолокаций клиентов наверное это не столь критично…

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое