Проверка состояния веб-сервиса средствами Go
Понадобилось мне тут автоматизировать мониторинг одного корпоративного сервиса. Если говорить упрощенно, то сервис представляет собой веб-адрес, на который другое приложение отправляет некие данные. Сервис иногда падает, несмотря на заявленную отказоустойчивость, поэтому потребовалось сделать мониторинг его доступности с сохранением истории проверок в файл.
Ввиду имеющегося у меня опыта написания программ на Python скрипт с нужным функционалом был написан быстро, однако захотелось сделать примерно то же самое, но средствами Go.
Примечание.
Будем считать, что Go у вас уже стоит. Если нет, инструкцию по установке для разных операционных систем можно глянуть тут: https://golang.org/doc/install
Также вы должны знать основы языка. Познакомиться с ним можно на официальном сайте. Рекомендуются к изучению:
A Tour of Go
How to write Go code
Effective Go
Введение в программирование на Go
Исходный код программы можно набирать при помощи любого текстового редактора, который вы предпочитаете, а затем компилировать исполняемый файл из командной строки. А можно воспользоваться LiteIDE X
Сразу оговорюсь, что статья написана только для закрепления знаний по языку Go, а про профессиональные инструменты для мониторинга веб-ресурсов и серверов я знаю. Just for fun. Также отмечу, что я владею только базовыми знаниями Go, поэтому некоторые реализации кода кому-то могут показаться не go-way.
Для отправки http запросов методом Get, будем использовать пакет net/http. Проверять будем адрес https://golang.org/
Сначала мы создадим простую версию программы и постепенно будем её усложнять, добавляя возможности, а также расширяя функциональность.
Начало
Итак, создаем файл web_check.go
Программа состоит из нескольких “блоков”. В начале файла мы импортируем модули/библиотеки (4–7), которые будут использоваться в программе. Затем создаем текстовую константу (9) и присваиваем адрес, который программа будет проверять. Объявляем функцию main (11) и функцию check (15), которая будет принимать в качестве аргумента проверяемый адрес.
Внутри check вызывается функция Get (17) из пакета net/http, которой передаётся адрес, на куда будет отправлен запрос. Она возвращает ссылку на объект типа Response и ошибку error.
Затем мы анализируем переменную err (19) и статус ответа (25) и выводим соответствующие сообщения в консоль.
Если http-статус (см. коды состояния HTTP) не равен 200, то считаем, что сервис не работает, например, из-за внутренней ошибки сервера (статусы 5хх) или из-за перегрузки на сайте или по другим причинам. Если, например, с сервисом нет связи, тогда переменная err не будет пустой. Для простоты, остальные значения кодов состояния HTTP не будем учитывать. Точнее, они будут считаться ошибкой в работе сервиса.
Компилируем код, командой ниже или из среды LiteIDE (Ctrl-B):
go build web_check.go
И запускаем полученный исполняемый файл. Для Windows это будет web_check.exe
[16:31] D:\work\go\src\web_check>web_check.exe
Проверяем адрес https://golang.org/
Онлайн. http-статус: 200
Отлично. Сайт доступен.
Теперь попробуем изменить адрес, который проверяем, на что-нибудь несуществующее.
const url = “https://golang123.org/"
Компилируем и запускаем.
[16:33] D:\work\go\src\web_check>web_check.exe
Проверяем адрес https://golang123.org/
Ошибка соединения. Get https://golang123.org/: dial tcp: GetAddrInfoW: No such host is known.
Сайта такого нет, поэтому http.Get вторым аргументом возвращает ошибку в переменную err.
Если наш сайт/сервис, который мы в будущем будем проверять, упадёт или с ним пропадёт связь, мы получим примерно такую же ошибку на этом шаге программы.
Периодическая проверка
Чтобы программа делала проверку периодически, необходимо функцию check поместить внутрь бесконечного цикла, в конце которого добавить паузу перед следующей проверкой. Вынесем такой цикл в отдельную функцию check_loop.
import (
“fmt”
“net/http”
“time”
)
const url = “https://golang.org/"
func main() {
check_loop()
}func check_loop() {
for {
check(url)
time.Sleep(1 * time.Minute)
}
}
Полный текст программы с периодической проверкой можно взять тут.
Опять компилируем, запускаем:
Проверяем адрес https://golang.org/
Онлайн. http-статус: 200
Проверяем адрес https://golang.org/
Онлайн. http-статус: 200
Проверяем адрес https://golang.org/
Онлайн. http-статус: 200
Проверяем адрес https://golang.org/
Онлайн. http-статус: 200
История проверок
Сделаем так, чтобы результат проверки адреса сохранялся в файле на диске. Для этого, добавим функцию, принимающую два параметра. Первый — дата в формате yyyy-mm-dd HH24:MI:SS (хотя в Go формат даты задается немного иначе) в виде текстовой переменной, второй — сообщение, которое надо сохранить в файл.
func log_to_file(tm, s string) {
// Сохраняет сообщения в файл
f, err := os.OpenFile(“web_check.log”, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
fmt.Println(tm, err)
return
}
defer f.Close()
if _, err = f.WriteString(fmt.Sprintln(tm, s)); err != nil {
fmt.Println(tm, err)
}
}
Также изменим функцию check, чтобы она вместо вывода результатов проверки на экран возвращала текстовое сообщение для дальнейшей его передачи в функцию log_to_file.
func check(url string) (bool, string) {
// возвращает true — если сервис доступен, false, если нет и текст сообщения
fmt.Println(“Проверяем адрес “, url)
resp, err := http.Get(url)if err != nil {
return false, fmt.Sprintf(“Ошибка соединения. %s”, err)
} defer resp.Body.Close()
if resp.StatusCode != 200 {
return false, fmt.Sprintf(“Ошибка. http-статус: %s”, resp.StatusCode)
}
return true, fmt.Sprintf(“Онлайн. http-статус: %d”, resp.StatusCode)
}
И изменим функцию check_loop, чтобы она получала текущую дату и вызывала функцию log_to_file
func check_loop() {
for {
tm := time.Now().Format(“2006–01–02 15:04:05”)
// статус, который возвращает check, пока не используем, поэтому ставим _
_, msg := check(url)
log_to_file(tm, msg)
fmt.Println(tm, msg)
time.Sleep(1 * time.Minute)
}
}
Полный вариант программы тут.
Компилируем, запускаем. Видим, что рядом с программой создался файл web_check.log и в нем есть сообщения с результатами проверки.
2015–06–02 23:31:06 Онлайн. http-статус: 200
Веб-интерфейс
Теперь добавим возможность просматривать историю проверки в браузере. Это должно выглядеть примерно так:
Сделаем так, чтобы программа сохраняла последние 10 результатов проверки в массиве, который и будем отображать в браузере. А в файле лога на диске будет храниться полная история проверок.
Добавляем новую константу для определения того, сколько записей сохранять в массиве с историей проверок и объявляем сам массив.
const (
url = “https://golang.org/"
hist_length = 10 // сколько хранить записей в массиве с историей проверок
)var check_history []string
Пишем функцию, которая будет добавлять записи в историю, при этом удалять лишнее.
func save_history(tm, s string) {
// добавляет запись в массив с историей проверок
check_history = append(check_history, fmt.Sprintf(“%s %s”, tm, s))
if len(check_history) > hist_length {
check_history = check_history[1:]
}}
Добавляем сохранение в функцию check_loop
log_to_file(tm, msg)
save_history(tm, msg)
fmt.Println(tm, msg)
Теперь у нас есть массив с результатами проверок, в котором сохраняются последние 10 результатов и можно добавлять веб-интерфейс.
Чтобы программа стала доступна из браузера по определенному порту, воспользуемся функционалом модуля “net/http”
Во-первых, сделаем так, чтобы цикл проверки запускался в отдельном потоке, (потоки в Go называются goroutines, подробнее тут и тут), во-вторых, добавим функции, которые позволят приложению прослушивать определенный порт и выдавать историю проверок при подключении к нему браузером.
func main() {
go check_loop()
http.HandleFunc(“/”, indexHandler)
http.ListenAndServe(“:8088”, nil)
}func indexHandler(w http.ResponseWriter, r *http.Request) {
// Выдает историю проверок в браузер
var html = `<html><head><title>Проверка веб-службы</title></head><body><h1>История проверки</h1><div>%s</div></body></html>`
s := “”
for _, h := range check_history {
s += fmt.Sprintf(“%s<br/>\n”, h)
}
fmt.Fprintf(w, fmt.Sprintf(html, s))
}
Итак, при помощи ключевого слова go мы сделали так, что цикл проверки запускается в отдельном потоке. Без этого ключевого слова функция check_loop() никогда бы не вернулась в main после вызова и следующие две строки кода, запускающие веб-сервер не выполнились бы.
Как видно, чтобы программа превратилась в простой веб-сервер, надо совсем немного кода. Необходимо добавить обработчик запроса (HandleFunc), который первым параметром принимает шаблон адреса, а вторым функцию, формирующую ответ для браузера. В данном случае мы пишем обработчик для адреса по умолчанию: http://127.0.0.1:8088/ Если мы в первый параметр сделаем “/test”, тогда в браузере надо будет открывать адрес: http://127.0.0.1:8088/test
В функции, которую запускает обработчик мы просто формируем html код, считывая данные из массива с историей проверок. На самом деле, то же самое можно сделать более наглядно при помощи шаблонов, но для нашего простого случая сойдет и так.
Полный текст программы можно взять тут.
Итак. Компилируем и запускаем. В консоли у нас все, как было до этого. Открываем в браузере адрес http://127.0.0.1:8088/ и видим там историю проверки. Немного видоизмененной ссылкой можно поделиться с коллегами. http://ваш_ip_в_сети:8088/
Флаги командной строки
Программа практически готова. Остался один момент. Если мы захотим сменить проверяемый адрес, время проверки или количество сообщений, выводимых в браузер, то после изменения кода нам потребуется компилировать его заново. Понятное дело, это не то, что нам надо.
Смену параметров без перекомпиляции можно сделать двумя способами. Первый — это считывание настроек из конфигурационного файла, второй — использование ключей командной строки. Остановимся на втором варианте.
Для получения аргументов командной строки воспользуемся пакетом flag.
import (
“flag”
“fmt”
“net/http”
“os”
“time”
)
Напишем функцию, которая будет разбирать командную строку и заполнять переменные.
func parse_args() bool {
flag.StringVar(&url, “url”, “”, “Адрес для проверки. Например, http://golang.org/")
flag.IntVar(&timeout, “t”, 5, “Период проверки в минутах. Должен быть больше 2”)
flag.IntVar(&hist_length, “l”, 30, “Длина истории в браузере”)
flag.StringVar(&ip, “i”, “:8090”, “ip:port для веб-статистики”)
flag.Parse() if url == “” {
fmt.Println(“Не задан параметр -url”, url)
return false
}
if timeout < 2 {
fmt.Println(“Значение -i должно быть больше 2. Задано: “, timeout)
return false
}
if hist_length < 1 {
fmt.Println(“Значение -l должно быть больше 1. Задано: “, hist_length)
return false
}
return true
}
Функции flag.StringVar и flag.IntVar работают примерно одинаково. Первая используется для строк, вторая для чисел. Обе читают ключ из командной строки и сохраняют его в переменную. Если ключ не задан, то переменная получит значение по умолчанию.
func StringVar(&переменная, ключ_командной_строки, значение_по_умолчанию, описание_ключа)
Объявим необходимые переменные. Обратите внимание, что url теперь не константа (эту строчку надо удалить), а строковая переменная.
var (
check_history []string
url string
hist_length int
timeout int
ip string
)
Заменим константу в функции check_loop на переменную.
time.Sleep(time.Duration(timeout) * time.Minute)
Функция main будет выглядеть так. Если произошла ошибка при чтении командной строки, то просто завершаем работу программы.
func main() {
if !parse_args() {
return
}
go check_loop()
http.HandleFunc(“/”, indexHandler)
http.ListenAndServe(“:8088”, nil)
}
Полный текст можно скачать тут.
Компилируем и запускаем с флагом -h. Увидим справку по аргументам командной строки.
Usage of C:\go_proj\src\web_check\web_check.exe:
-i=”:8090": ip:port для веб-статистики
-l=30: Длина истории в браузере
-t=5: Период проверки в минутах. Должен быть больше 2
-url=””: Адрес для проверки. Например, http://golang.org/
Если запустить программу без параметров, то она выдаст сообщение: Не задан параметр -url
Единственный обязательный параметр командной строки — url. Запускаем программу, указав его.
web_check.exe -url=http://golang.org
Все работает, как и раньше, только теперь можно менять адрес без необходимости компилировать программу.
Заключение
Окончательный вариант программы размещён на github.
Что можно еще усовершенствовать? Например, можно в веб-интерфейс выводить ссылку на проверяемый ресурс, можно воспользоваться шаблонами (html/template) для генерации html, тогда дизайн страницы можно будет менять без необходимости еще раз компилировать программу. Еще хотелось бы получать уведомления на email.
Итого, вся программа у нас заняла 82 строки кода. Писать код было так же легко, как и на Python, особых затруднений не возникло.