Сбор статистики MTProto Proxy

    Содержание
    • Предыстория
    • Сбор статистики
    • Отображение статистики
    • Визуализация и ведение статистики
    • Развертка
    • Заключение


    Предыстория


    Привет хабр, телеграм сейчас на пике популярности, все скандалы, интриги, блокировки вертятся вокруг него, в связи с чем телеграм выкатил свой вариант прокси под названием MTProto Proxy который призван помочь с обходом блокировки. Однако предоставленные телеграмом сервисы для мониторинга MTProto Proxy не дают возможности наблюдать статистику в реальном времени и собирать её для наблюдения за её изменениями, потому мы будем решать проблему своими силами.

    Сбор статистики


    На официальной странице MTProto Proxy на Docker Hub указано что мы можем использовать команду docker exec mtproto-proxy curl http://localhost:2398/stats для получения статистики напрямую от MTProto Proxy который находится в контейнере, так что наш код будет выглядеть следующим образом.

    package main
    
    import (
    	"io/ioutil"
    	"net/http"
    	"strings"
    	"time"
    )
    
    type User struct {
    	Num string
    }
    
    var Users User
    
    func CurrenUsers() (err error) {
    	// Тянем статистику
    	response, err := http.Get(`http://localhost:2398/stats`)
    	if err != nil {
    		return
    	}
    	body, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return
    	}
    	defer response.Body.Close()
    	stat := strings.Split(string(body), "\n")
    	for _, item := range stat {
    		// Проверяем что у нас есть нужное поле
    		// которое содержит количество пользователей
    		if strings.HasPrefix(item, `total_special_connections`) {
    			Users.Num = strings.Split(item, "\t")[1]
    		}
    	}
    	return nil
    }
    
    func main() {
    	for t := time.Tick(10 * time.Second); ; <-t {
    		if err := CurrenUsers(); err != nil {
    			continue
    		}
    	}
    }
    

    total_special_connections указано на том же Docker Hub как число входящих подключений клиентов

    Отображение статистики


    Далее нам нужно в простой и удобной форме выводить текущее количество пользователей, мы будем выводить её в браузер.

    package main
    
    import (
    	"html/template"
    	"io/ioutil"
    	"net/http"
    	"strings"
    	"time"
    )
    
    type User struct {
    	Num string
    }
    
    type HTML struct {
    	IndexPage string
    }
    
    var Users User
    
    var IndexTemplate = HTML{
    	IndexPage: `<!DOCTYPE html>
        <html>
        
        <head>
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4"
                crossorigin="anonymous">
            <title>Stats</title>
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        </head>
        
        <body>
            <div class="container-fluid">
                <div class="row justify-content-center text-center" style="margin-top: 20%">
                    <h1>Count of current users of MTProto Proxy: {{.Num}}</h1>
                </div>
            </div>
        </body>
        
        </html>`,
    }
    
    func CurrenUsers() (err error) {
    	// Тянем статистику
    	response, err := http.Get(`http://localhost:2398/stats`)
    	if err != nil {
    		return
    	}
    	body, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return
    	}
    	defer response.Body.Close()
    	stat := strings.Split(string(body), "\n")
    	for _, item := range stat {
    		// Проверяем что у нас есть нужное поле
    		// которое содержит количество пользователей
    		if strings.HasPrefix(item, `total_special_connections`) {
    			Users.Num = strings.Split(item, "\t")[1]
    		}
    	}
    	return nil
    }
    
    func sendStat(w http.ResponseWriter, r *http.Request) {
    	if r.Method == "GET" {
    		t := template.Must(template.New("indexpage").Parse(IndexTemplate.IndexPage))
    		t.Execute(w, Users)
    	}
    }
    
    func init() {
    	go func() {
    		for t := time.Tick(10 * time.Second); ; <-t {
    			if err := CurrenUsers(); err != nil {
    				continue
    			}
    		}
    	}()
    }
    
    func main() {
    	http.HandleFunc("/", sendStat)
    	http.ListenAndServe(":80", nil)
    }
    

    что такое init
    init в любом случае будет вызван перед вызовом main

    Теперь перейдя по IP адресу нашего MTProto Proxy мы сможем увидеть текущее количество клиентов.

    image

    Визуализация и ведение статистики


    Есть много вариантов для визуализации и ведения статистики Datadog, Zabbix, Grafana, Graphite. Я буду использовать Datadog. С помощью команды go get -u github.com/DataDog/datadog-go/statsd импортируем библиотеку statsd и используем её в коде.

    package main
    
    import (
    	"html/template"
    	"io/ioutil"
    	"net/http"
    	"os"
    	"strconv"
    	"strings"
    	"time"
    
    	"github.com/DataDog/datadog-go/statsd"
    )
    
    var (
    	datadogIP = os.Getenv("DDGIP")
    	tagName   = os.Getenv("TGN")
    	t, _      = strconv.Atoi(os.Getenv("TIMEOUT"))
    	timeout   = time.Duration(t) * time.Second
    )
    
    type User struct {
    	Num string
    }
    
    type HTML struct {
    	IndexPage string
    }
    
    var Users User
    
    var IndexTemplate = HTML{
    	IndexPage: `<!DOCTYPE html>
        <html>
        
        <head>
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4"
                crossorigin="anonymous">
            <title>Stats</title>
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        </head>
        
        <body>
            <div class="container-fluid">
                <div class="row justify-content-center text-center" style="margin-top: 20%">
                    <h1>Count of current users of MTProto Proxy: {{.Num}}</h1>
                </div>
            </div>
        </body>
        
        </html>`,
    }
    
    func (u User) convert() int64 {
    	num, _ := strconv.Atoi(u.Num)
    	return int64(num)
    }
    
    func CurrenUsers() (err error) {
    	// Тянем статистику
    	response, err := http.Get(`http://localhost:2398/stats`)
    	if err != nil {
    		return
    	}
    	body, err := ioutil.ReadAll(response.Body)
    	if err != nil {
    		return
    	}
    	defer response.Body.Close()
    	stat := strings.Split(string(body), "\n")
    	for _, item := range stat {
    		// Проверяем что у нас есть нужное поле
    		// которое содержит количество пользователей
    		if strings.HasPrefix(item, `total_special_connections`) {
    			Users.Num = strings.Split(item, "\t")[1]
    		}
    	}
    	return nil
    }
    
    func sendStat(w http.ResponseWriter, r *http.Request) {
    	if r.Method == "GET" {
    		t := template.Must(template.New("indexpage").Parse(IndexTemplate.IndexPage))
    		t.Execute(w, Users)
    	}
    }
    
    func init() {
    	if t == 0 {
    		timeout = 10 * time.Second
    	}
    
    	go func() {
    		for t := time.Tick(timeout); ; <-t {
    			if err := CurrenUsers(); err != nil {
    				continue
    			}
    		}
    	}()
    
    	// Отправляем статистику агенту Datadog
    	go func() error {
    		c, err := statsd.New(datadogIP + ":8125")
    		if err != nil || len(datadogIP) == 0 {
    			return err
    		}
    		c.Namespace = "mtproto."
    		c.Tags = append(c.Tags, tagName)
    		for t := time.Tick(timeout); ; <-t {
    			c.Count("users.count", Users.convert(), nil, 1)
    		}
    	}()
    }
    
    func main() {
    	http.HandleFunc("/", sendStat)
    	http.ListenAndServe(":80", nil)
    }
    


    Осталось собрать всё в докер образ

    FROM telegrammessenger/proxy
    COPY mtproto_proxy_stat .
    RUN echo "$(tail -n +2 run.sh)" > run.sh && echo '#!/bin/bash\n./mtproto_proxy_stat & disown' | cat - run.sh > temp && mv temp run.sh
    CMD [ "/bin/sh", "-c", "/bin/bash /run.sh"] 

    Развертка


    Сначала нам нужно запустить контейнер с агентом Datadog

    docker run -d --name dd-agent -v /var/run/docker.sock:/var/run/docker.sock:ro -v /proc/:/host/proc/:ro -v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro -e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=true -e DD_API_KEY=ВАШ_КЛЮЧ datadog/agent:latest

    ВАЖНО для того чтобы мы могли слать агенту наши данные нужно установить значение true для переменной окружения DD_DOGSTATSD_NON_LOCAL_TRAFFIC

    Далее с помощью команды docker inspect dd-agent нам нужно посмотреть IP контейнера чтобы слать ему данные

    image

    и запустить наш MTProto Proxy соединив его мостом с контейнером агента

    docker run -d -p 443:443 -p 80:80 -e WORKERS=16 -e DDGIP=172.17.0.2 -e TGN=mtproto:main --link=dd-agent --name=mtproto --restart=always -v proxy-config:/data trigun117/mtproto_proxy_stat

    И через пару минут мы уже можем построить график выбрав нужную метрику и источник (тег который указан при запуске контейнера с MTProto Proxy)

    image

    и отобразить на нём нашу статистику

    image

    Живой пример

    Заключение


    Для себя я открыл новые инструменты для удобной работы с данными, познакомился с их большим разнообразием и выбрал что-то подходящее на свой вкус.

    Спасибо за уделенное внимание, предлагаю всем поделится мнением, замечаниями и предложениями в комментариях.

    GitHub репозиторий
    Share post

    Similar posts

    Comments 11

      0
      Не переусложненный ли способ? Можно ведь смотреть статистику в Теле-боте, где регистрируется прокси. А если это умеет один бот, значит можно научить и другой, Или своим ботом опрашивать бота Телеги, а потом постить статистику куда хочется.
        0

        В боте статистика по уникальным пользователям, а в прокси сервере — по tcp-соединениям. И то, и другое полезно

          0
          как уже отметил alexbers удобно смотреть именно количество tcp-соединений, а @MTProxybot от телеграма часто не работает
          0
          Интересный вопрос хочется задать: часто вижу комменты, что сервер MTProto на Сишнике сильно грузит проц. Насколько это правда? Хочется тоже поднять себе в довесок к SoftEther на сервере.
            0

            Авторы MTProto на C предприняли много усилий чтобы всё работало быстро на большой нагрузке. Но CPU грузит удивительно много, при отсутствующей нагрузке будет процентов 5%.

            +1

            В init 'defer runtime.GC()' смахивает на memory leak, ибо в цикле мы будем добавлять новые и новые defer в stack, а вызовутся они, скорее всего никогда.
            Да и не нужен он. Гошный рантайм сам с этим справится.

              0
              trigun117 Отправил вас PR с фиксом. хорошо бы еще растоту обновлений статистики вынести в переменные окружения или флага.
                0
                Видел, только там из-за defer sendStats.Stop()(возможно, не проверил) сразу завершается всё
                  0
                  вот семсем вас не понял про `defer sendStats.Stop()`
                    0
                    не туда глянул, это возможно проблема в горутинах которые после defer sendStats.Stop(), точнее не скажу ибо сейчас не дома
              +2

              У меня в Erlang реализации тоже метрики собираются. Преимущество собственной реализации — можно метрики сделать какие угодно:
              image
              image
              image

              Only users with full accounts can post comments. Log in, please.