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

в 7:42, , рубрики: devops, Go, Grafana, monitoring, prometheus, визуализация данных

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

Привет всем!

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

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

Отлично, теперь очередь 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}}. Выглядеть все должно примерно так:

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

Далее переходим во вкладку Worldmap и настраиваем все как на скриншоте:

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

И все! Наслаждаемся нашей картой.

Вот таким несложным образом можно сделать красивую карту соединений в Grafan'е.

Спасибо за внимание и жду ваших комментариев.

TODO

Конечно, чтобы использовать инструмент по назначению, нужно его доделать: отфильтровывать адреса локальных подсетей и многое другое. Кстати, если кто заинтересовался и хочет развивать этот экспортер — добро пожаловать в репозиторий на GitHub!

Автор: gree-gorey

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js