Раскрытие информации: компания Pusher, которая предоставляет разработчикам API в реальном времени, ранее спонсировала Hacker Noon.

Чтобы следовать этому руководству, необходимо базовое понимание Go и JavaScript.

REST - это популярный архитектурный стиль, обеспечивающий стандарты между компьютерными системами в Интернете, что упрощает взаимодействие систем друг с другом. Он в основном используется API-интерфейсами для предоставления данных другим системам, которым они необходимы.

Иногда поставщики API хотят контролировать его использование. API мониторинга помогают предоставить полезную информацию, например, какие конечные точки вызываются чаще всего или какие регионы являются самой большой аудиторией, использующей IP-адреса запросов. Затем эту информацию можно использовать для оптимизации API.

В этой статье мы реализуем мониторинг в реальном времени небольшого API, созданного с помощью GoLang, с помощью Pusher. Вот пример того, как это должно выглядеть в конце:

Требования

Чтобы следовать этой статье, вам понадобится следующее:

  • IDE по вашему выбору, например Код Visual Studio.
  • Go установлен на вашем компьютере.
  • Базовые знания GoLang.
  • Базовые знания JavaScript (синтаксис ES6) и jQuery.
  • Базовые знания об использовании инструмента или терминала с интерфейсом командной строки.

Когда у вас будут все вышеперечисленные требования, приступим.

Настройка нашей кодовой базы

Для простоты мы будем использовать уже написанный GoLang CRUD API, доступный на GitHub. Мы создадим ответвление репозитория и настроим его в соответствии с инструкциями по установке README.md.

Далее мы настроим Pusher в проекте API. Pusher - это сервис, который обеспечивает простую реализацию функций в реальном времени для наших веб-приложений и мобильных приложений. Мы будем использовать его в этой статье, чтобы предоставлять обновления в реальном времени для нашей панели мониторинга API.

Давайте зайдем на Pusher.com, вы можете создать бесплатную учетную запись, если у вас ее еще нет. На панели управления создайте новое приложение и скопируйте учетные данные приложения (идентификатор приложения, ключ, секрет и кластер). Мы будем использовать эти учетные данные в нашем API.

Теперь, когда у нас есть приложение Pusher, мы установим библиотеку Pusher Go, запустив:

$ go get github.com/pusher/pusher-http-go

Мониторинг нашего API

Пока что мы настроили функциональный CRUD API, и теперь мы реализуем к нему контрольные вызовы. В этой статье мы будем отслеживать:

  • Конечные точки, вызываемые с указанием таких деталей, как имя, тип запроса (GET, POST и т. Д.) И URL-адрес.
  • Для каждого вызова конечной точки мы также будем учитывать:
  • Запрашивающий IP-адрес удалить,
  • Код состояния ответа для конкретного вызова.

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

Создание моделей для мониторинга

Основываясь на наших спецификациях выше, мы создадим два новых файла модели EndPoints.go и EndPointCalls.go. Как и в базовом API, мы будем использовать GORM (GoLang ORM) для управления хранилищем данных.

💡 Наши новые файлы моделей будут существовать в каталоге моделей и принадлежать к пакету моделей.

В EndPoints.go мы определим объект EndPoints и метод для сохранения конечных точек:

package models
import (
    "github.com/jinzhu/gorm"
)
// EndPoints - endpoint model
type EndPoints struct {
    gorm.Model
    Name, URL string
    Type      string          `gorm:"DEFAULT:'GET'"`
    Calls     []EndPointCalls `gorm:"ForeignKey:EndPointID"`
}
// SaveOrCreate - save endpoint called
func (ep EndPoints) SaveOrCreate() EndPoints {
    db.FirstOrCreate(&ep, ep)
    return ep
}

В приведенном выше блоке кода наша модель не повторно инициализировала экземпляр GORM db, но он был использован. Это связано с тем, что экземпляр, определенный в файле Movies.go, был глобальным для всех членов пакета, и поэтому на него могут ссылаться и использовать все члены package models.

💡 Наша модель EndPoints имеет атрибут Calls, который представляет собой массив EndPointCalls объектов. Этот атрибут обозначает отношение один ко многим между EndPoints и EndPointCalls. Дополнительную информацию об ассоциациях и взаимосвязях моделей см. В документации GORM.

Затем мы введем определения модели и методы для нашей EndPointCalls модели в файл EndPointCalls.go:

package models
import (
    "github.com/jinzhu/gorm"
    "github.com/kataras/iris"
)
// EndPointCalls - Object for storing endpoints call details
type EndPointCalls struct {
    gorm.Model
    EndPointID   uint `gorm:"index;not null"`
    RequestIP    string
    ResponseCode int
}
// SaveCall - Save the call details of an endpoint
func (ep EndPoints) SaveCall(context iris.Context) EndPointCalls {
    epCall := EndPointCalls{
        EndPointID:   ep.ID,
        RequestIP:    context.RemoteAddr(),
        ResponseCode: context.GetStatusCode(),
    }
    db.Create(&epCall)
    
    return epCall
}

Как показано выше, наша модель EndPointCalls определяет метод SaveCall, в котором хранится запрашиваемый IP-адрес и код ответа существующего объекта EndPoint.

Наконец, мы обновим миграцию модели в файле index.go, чтобы включить наши новые модели:

// index.go
// ...
func main() {
    // ...
    // Initialize ORM and auto migrate models
    db, _ := gorm.Open("sqlite3", "./db/gorm.db")
    db.AutoMigrate(&models.Movies{}, &models.EndPoints{}, &models.EndPointCalls{})
    // ...
}

Сохранение данных конечной точки для мониторинга

Используя наши недавно созданные модели, мы отредактируем файл MoviesController.go, чтобы сохранить соответствующие данные при вызове конечной точки.

Для этого мы добавим в MoviesController.go частный вспомогательный метод, который будет сохранять данные конечных точек вместе с нашими моделями. Посмотрите, как это делается ниже:

// MoviesController.go
// ...
func (m MoviesController) saveEndpointCall(name string) {
    endpoint := models.EndPoints{
        Name: name,
        URL:  m.Cntx.Path(),
        Type: m.Cntx.Request().Method,
    }
    endpoint = endpoint.SaveOrCreate()
    endpointCall := endpoint.SaveCall(m.Cntx)
}

Метод saveEndpointCall принимает имя конечной точки в качестве параметра. Используя экземпляр iris.Context контроллера, он считывает и сохраняет путь к конечной точке и метод запроса.

Теперь, когда доступен этот вспомогательный метод, мы будем вызывать его в каждом из методов конечной точки в файле MoviesController.go:

// MoviesController.go
// ...
// Get - get a list of all available movies
func (m MoviesController) Get() {
    movie := models.Movies{}
    movies := movie.Get()
    go m.saveEndpointCall("Movies List")
    m.Cntx.JSON(iris.Map{"status": "success", "data": movies})
}
// GetByID - Get movie by ID
func (m MoviesController) GetByID(ID int64) {
    movie := models.Movies{}
    movie = movie.GetByID(ID)
    if !movie.Validate() {
        msg := fmt.Sprintf("Movie with ID: %v not found", ID)
        m.Cntx.StatusCode(iris.StatusNotFound)
        m.Cntx.JSON(iris.Map{"status": "error", "message": msg})
    } else {
        m.Cntx.JSON(iris.Map{"status": "success", "data": movie})
    }
    name := fmt.Sprintf("Single Movie with ID: %v Retrieval", ID)
    go m.saveEndpointCall(name)
}
// ...

Как показано в приведенном выше фрагменте, вспомогательный метод saveEndpointCall будет вызываться в каждом методе CRUD.

💡 Метод saveEndpointCall называется горутиной. Такой вызов вызывает его одновременно с выполнением метода конечной точки и позволяет нашему коду мониторинга не задерживать и не блокировать ответ API.

Создание панели мониторинга монитора конечных точек

Теперь, когда мы реализовали мониторинг вызовов нашего API, мы будем отображать накопленные данные на панели инструментов.

Регистрация нашего шаблонизатора

Фреймворк GoLang, Iris, может реализовывать ряд механизмов шаблонов, которыми мы воспользуемся.

В этом разделе мы реализуем механизм шаблонов Handlebars, а в нашем index.go файле мы зарегистрируем его в экземпляре приложения:

// index.go
package main
import (
    "goggles/controllers"
    "goggles/models"
    "github.com/jinzhu/gorm"
    "github.com/kataras/iris"
)
func main() {
    app := iris.New()
    tmpl := iris.Handlebars("./templates", ".html")  
    app.RegisterView(tmpl)
    // ...
    app.Run(iris.Addr("127.0.0.1:1234"))
}

💡 Мы определили наш механизм шаблонов (Handlebars) для рендеринга .html файлов, содержащихся в templatesdirectory.

Создание маршрута и контроллера приборной панели

Теперь, когда мы зарегистрировали наш механизм шаблонов в приложении, мы добавим маршрут в index.go для отображения панели мониторинга нашего API-монитора:

// index.go
// ...
func main() {
    app := iris.New()
    // ...
    app.Get("/admin/endpoints", func(ctx iris.Context) {
        dashBoard := controllers.DashBoardController{Cntx: ctx}
        dashBoard.ShowEndpoints()
    })
    app.Run(iris.Addr("127.0.0.1:1234"))
}

Выше мы добавили определения для пути / admin / endpoints, где мы собираемся отображать детали наших конечных точек API и их вызовов. Мы также указали, что маршрут должен обрабатываться методом ShowEndpoints DashBoardController.

Чтобы создать DashBoardController, мы создадим файл DashBoardController.go в каталоге контроллеров. И в нашем файле DashBoardController.go мы определим объект DashBoardController и его метод ShowEndpoints:

// DashBoardController.go
package controllers
import (
    "goggles/models"
    "github.com/kataras/iris"
    "github.com/kataras/iris/mvc"
)
// DashBoardController - Controller object for Endpoints dashboard
type DashBoardController struct {
    mvc.BaseController
    Cntx iris.Context
}
// ShowEndpoints - show list of endpoints
func (d DashBoardController) ShowEndpoints() {
    endpoints := (models.EndPoints{}).GetWithCallSummary()
    d.Cntx.ViewData("endpoints", endpoints)
    d.Cntx.View("endpoints.html")
}

В ShowEndpoints() мы получаем наши конечные точки и сводку их вызовов для отображения. Затем мы передаем эти данные нашему представлению, используя d.Cntx.ViewData("endpoints", endpoints), и, наконец, мы визуализируем наш файл представления templates/endpoints.html, используя d.Cntx.View("endpoints.html").

Получение конечных точек и сводок вызовов

Чтобы получить наш список конечных точек и сводку их вызовов, мы создадим в файле EndPoints.go метод с именем GetWithCallSummary.

Наш GetWithCallSummary метод должен возвращать конечные точки и их сводки вызовов, готовые для отображения. Для этого мы определим объект коллекции EndPointWithCallSummary с атрибутами, которые нам нужны для нашего отображения в EndPoints.go файле:

// EndPoints.go
package models
import (
    "github.com/jinzhu/gorm"
)
// EndPoints - endpoint model
type EndPoints struct {
    gorm.Model
    Name, URL string
    Type      string          `gorm:"DEFAULT:'GET'"`
    Calls     []EndPointCalls `gorm:"ForeignKey:EndPointID"`
}
// EndPointWithCallSummary - Endpoint with last call summary
type EndPointWithCallSummary struct {
    ID            uint
    Name, URL     string
    Type          string
    LastStatus    int
    NumRequests   int
    LastRequester string
}

Затем определите метод GetWithCallSummary, чтобы использовать его следующим образом:

// EndPoints.go
// ...
// GetWithCallSummary - get all endpoints with call summary details
func (ep EndPoints) GetWithCallSummary() []EndPointWithCallSummary {
    var eps []EndPoints
    var epsWithDets []EndPointWithCallSummary
    db.Preload("Calls").Find(&eps)
    for _, elem := range eps {
        calls := elem.Calls
        lastCall := calls[len(calls)-1:][0]
        newElem := EndPointWithCallSummary{
            elem.ID,
            elem.Name,
            elem.URL,
            elem.Type,
            lastCall.ResponseCode,
            len(calls),
            lastCall.RequestIP,
        }
        epsWithDets = append(epsWithDets, newElem)
    }
    return epsWithDets
}
// ...

Выше метод GetWithCallSummary использует атрибут Calls EndPoints, который определяет его связь с EndPointCalls. При получении нашего списка конечных точек из базы данных мы стремимся загрузить его EndPointCalls данные, используя db.Preload("Calls").Find(&eps).

Подробнее об активной загрузке в GORM см. Документацию.

GetWithCallSummary инициализирует массив EndPointWithCallSummary и просматривает EndPointsобъектов, возвращаемых из нашей базы данных, для создания EndPointWithCallSummary объектов.

Эти EndPointWithCallSummary объекты добавляются к инициализированному массиву и возвращаются.

💡 EndPointWithCallSummary - это не модель. Это объект коллекции, и ему не обязательно иметь таблицу в нашей базе данных. Вот почему у него нет собственного файла, и он не передается index.go для переноса.

Реализация дашборда и отображение данных

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

Давайте обновим templates/endpoints.html, получив следующий код:

<!-- templates/endpoints.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Endpoints Monitor Dashboard</title>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.3/css/bootstrap.min.css" />
</head>
<body>
    <div>
        <nav class="navbar navbar-default navbar-static-top">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="http://127.0.0.1:1234/">
                        Goggles - A Real-Time API Monitor
                    </a>
                </div>
            </div>
        </nav>
        <div class="container">
            <div class="row">
                <div class="col-xs-12 col-lg-12">
                    <div class="endpoints list-group">
                        {{#each endpoints}}
                            <a id="endpoint-{{ID}}" href="#" class="list-group-item 
                            list-group-item-{{status_class LastStatus}}">
                                <strong>{{name}}</strong>
                                <span class="stats">
                                    {{type}}: <strong>{{url}}</strong> |
                                    Last Status: <span class="last_status">
                                    {{LastStatus}}</span> |
                                    Times Called: <span class="times_called">
                                    {{NumRequests}}</span> |
                                    Last Request IP: <span class="request_ip">
                                    {{LastRequester}}</span>
                                </span>
                            </a>
                        {{/each}}
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.3/js/bootstrap.min.js"></script>
</body>
</html>

Выше мы отображаем наш список конечных точек, используя Bootstrap и наш шаблонизатор Handlebars. Мы также создали и использовали функцию шаблона status_class, чтобы закрасить наш список цветом в зависимости от статуса последнего вызова LastStatus.

Мы определяем функцию шаблона status_class в index.go после инициализации нашего механизма шаблонов:

// index.go
// ...
func main() {
    app := iris.New()
    tmpl := iris.Handlebars("./templates", ".html")
    tmpl.AddFunc("status_class", func(status int) string {
        if status >= 200 && status < 300 {
            return "success"
        } else if status >= 300 && status < 400 {
            return "warning"
        } else if status >= 400 {
            return "danger"
        }
        return "success"
    })
    app.RegisterView(tmpl)
}

Затем в нашем файле представления мы вызываем функцию как:

class="list-group-item list-group-item-{{status_class LastStatus}}"

💡 LastStatus - это параметр функции.

Добавление обновлений в реальном времени на нашу панель управления

До сих пор в этой статье мы отслеживали вызовы API и отображали данные через панель управления. Теперь мы будем использовать Pusher для обновления данных в реальном времени на нашей панели инструментов.

Отправка данных в реальном времени из серверной части

Ранее мы установили библиотеку Pusher Go, которую мы будем использовать для запуска события при вызове конечной точки. В файле MoviesController.go, где обрабатываются запросы API, мы инициализируем клиент Pusher:

// MoviesController.go
    package controllers
    import (
        // ...
        "github.com/pusher/pusher-http-go"
    )
    // MoviesController - controller object to serve movie data
    type MoviesController struct {
        mvc.BaseController
        Cntx iris.Context
    }
    var client = pusher.Client{
        AppId:   "app_id",
        Key:     "app_key",
        Secret:  "app_secret",
        Cluster: "app_cluster",
    }
    // ...

Здесь мы инициализировали клиент Pusher, используя учетные данные из нашего ранее созданного приложения.

⚠️ Замените app_id, app_key, app_secret and app_cluster учетными данными своего приложения.

Затем мы будем использовать наш клиент Pusher для запуска события, которое будет включать данные конечной точки, которые будут отображаться в нашем представлении. Мы сделаем это в методе saveEndpointCall, который регистрирует конечную точку и ее вызов:

// MoviesController.go
    // ...
    func (m MoviesController) saveEndpointCall(name string) {
        endpoint := models.EndPoints{
            Name: name,
            URL:  m.Cntx.Path(),
            Type: m.Cntx.Request().Method,
        }
        endpoint = endpoint.SaveOrCreate()
        endpointCall := endpoint.SaveCall(m.Cntx)
        endpointWithCallSummary := models.EndPointWithCallSummary{
            ID:            endpoint.ID,
            Name:          endpoint.Name,
            URL:           endpoint.URL,
            Type:          endpoint.Type,
            LastStatus:    endpointCall.ResponseCode,
            NumRequests:   1,
            LastRequester: endpointCall.RequestIP,
        }
        client.Trigger("goggles_channel", "new_endpoint_request", endpointWithCallSummary)
    }

Выше мы создаем объект EndPointWithCallSummary из EndPoints (конечная точка) и EndPointCalls. Этот объект EndPointWithCallSummary содержит все данные, необходимые для отображения на приборной панели, поэтому он будет передан в Pusher для передачи.

Отображение данных в реальном времени на панели управления

Чтобы отображать обновления наших конечных точек в реальном времени, мы будем использовать клиент Pusher JavaScript и библиотеки jQuery.

В нашем файле представления templates/endpoints.html мы сначала импортируем и инициализируем экземпляр Pusher, используя учетные данные нашего приложения:

<!-- endpoints.html -->
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.3/js/bootstrap.min.js"></script>
    <script src="//js.pusher.com/4.1/pusher.min.js"></script>
    <script>
      const pusher = new Pusher('app_id', {cluster: "app_cluster"});
    </script>

⚠️ Замените app_id and app_cluster значениями из учетных данных вашего приложения.

Далее мы определим следующее:

  • Шаблон для добавления новых конечных точек в наше представление.
  • Функции для добавления новой конечной точки и получения класса состояния конечной точки.

Наконец, мы подпишемся на goggles_channel и будем прослушивать событие new_endpoint_request, куда будут передаваться обновления наших конечных точек:

<!-- endpoints.html -->
    <script>
    // ...

    const channel = pusher.subscribe("goggles_channel");

    channel.bind('new_endpoint_request', function(data) {
        let end_point_id = data.ID;
        if ( $('#endpoint-' + end_point_id).length > 0 ) {
            let status_class = getItemStatusClass(data['LastStatus']),
                endpoint     = $('#endpoint-' + end_point_id);
            let calls = 1 * endpoint.find('span.times_called').text()
            endpoint.find('span.last_status').text(data['LastStatus']);
            endpoint.find('span.times_called').text( (calls + 1) )
            endpoint.removeClass('list-group-item-success');
            endpoint.removeClass('list-group-item-danger');
            endpoint.removeClass('list-group-item-warning');
            endpoint.addClass('list-group-item-' + status_class);
        } else {
            addNewEndPoint(data);
        }
    });

    // ...

В обработчике событий new_endpoint_request данные конечной точки подразделяются на сценарий обновления (где конечная точка уже существует на панели мониторинга) или сценарий создания (где создается и добавляется новый элемент списка).

Наконец, вы можете создать свое приложение, и когда вы запустите его, вы должны увидеть что-то похожее на то, что мы видели в предварительном просмотре:

Заключение

В этой статье мы смогли отслеживать запросы REST API в реальном времени и продемонстрировать, как Pusher работает с приложениями GoLang.

Этот пост впервые появился в Pusher blog.