Парсим Google Analytics на GO #1

GO это уже даже не "стильно модно молодежно", а просто норма. Он постепенно становится стандартом для написания демонов и других мелких и больших системных утилит. На нем пишут апи, которые обрабатывают кучу запросов и многое другое.

Ссылки на базовые туториалы и другие классные статьи "как начать писать" проще найти в гугле. Здесь же опишу задачу и решение.

Задача

Написать парсер данных из Google Analytics. В первом приближении вытащить только просмотры (pageViews) у заданного массива ссылок.

Прототип

  1. Авторизуемся через OAuth2 в Google
  2. Запрашиваем список доступных View из аналитики
  3. Выбираем нужный View
  4. Получаем от пользователя список ссылок и даты начала и конца поиска

Проблемы по пути

  • Никогда ничего не писал на GO
  • Уродливая документация Google Analytics
  • Библиотеки от Google без нормальных примеров использования

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

Начало

Установить GO. Ставил через Homebrew версию 1.6.2 – последняя доступная на момент написания статьи.

Далее нужно создать приложение в Google Developers Console. Включить для него Analytics API и Analytics Reporting API V4. Для тестов callback url можно указать на localhost. У меня было указано localhost:3000.

Сохранить куда-то ClientID и ClientSecret. Их можно найти в учетных данных во вкладке слева.

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

Для авторизации надо поднять простенький http сервер и сделать несколько страниц. Начать можно со статьи на golang.org – Writing Web Applications. Приятно удивила обширность функционала в стандартных пакетах. Для большинства задач можно не импортировать сторонние библиотеки, а использовать только базовые пакеты и утилиты в комплекте с языком.

Отдельная боль – пакетный менеджер. Его просто нет. На Github можно найти список пакетных менеджеров сторонних разработчиков и выбрать на свой вкус. В статье не буду подробно на нем останавливаться, потому что не использовал и не могу толком сказать какие плюсы и минусы у каждого. С кем я общался – советуют gvt.

К коду. Простой http сервер:

package main

import (  
  "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {  
  fmt.Frintf(w, "Hello from Index")
}

func main() {  
  http.HandleFunc("/", indexHandler)
  http.HandleAndServe(":3000", nil)
}

Слушаем порт 3000 и по адресу http://localhost:3000/ – отдаем текстом Hello from Index.

Для OAuth 2.0 нам нужно сделать редирект на страницу в Google API, чтобы пользователь разрешил приложению делать разные манипуляции с данными и получить обратно в запросе access_token, который будем использовать в запросах к API.

func authHandler(w http.ResponseWriter, r *http.Request) {  
}

func authHandler(w http.ResponseWriter, r *http.Request) {  
}

func main() {  
  http.HandleFunc("/authorize", authHandler)
  http.HandleFunc("/connect", connectHandler)

  http.HandleFunc("/", indexHandler)
  http.HandleAndServe(":3000", nil)
}

На главной странице выведем ссылку на /authorize, откуда сделаем редирект в Google, чтобы пользователь выдал нам права. На /connect будем ждать ответа от Google с кодом авторизации, по которому получим access_token.

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

Для работы с токеном будем пользоваться библиотекой от Google oauth2, а для работы с сессиями библиотекой sessions из GorillaToolkit.

Напомню, что в этой статье не описана начальная настройка окружения GO, предполагаю, что с этим можно справиться самостоятельно после прочтения нескольких статей. Поэтому укачиваем библиотеки себе в workspace с помощью команд:

go get github.com/gorilla/sessions  
go get golang.org/x/oauth2  

Импортируем их в проекте.

import(  
  "net/http"
  "github.com/gorilla/sessions"
  "golang.rog/x/oauth2"
  "golang.rog/x/oauth2/google"
)

...

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

import(  
 ...
)

const (  
  clientID        = "your_client_id"
  clientSecret    = "your_client_secret"
  applicationName = "your_application_name"
)

var store = sessions.NewCookieStore([]byte("gaIntegrationStore"))

var config = &oauth2.Config{  
  ClientID: clientID,
  ClientSecret: clientSecret,
  Scopes: []string{"https://www.googleapis.com/auth/analytics", "https://www.googleapis.com/auth/analytics.readonly"},
  Endpoint: google.Endpoint,
  RedirectUrl: "http://localhost:3000/connect",
}

oauth2 – это набор методов для работы с OAuth 2.0 авторизацией, а в папках репозитория лежат в основном Endpoints для разных провайдеров – Google, Slack и т.д..
Сессии мы делаем через cookie без сохранения где-либо еще. При желании их можно хранить в memcache, redis и т.д..

Далее разберемся с шаблонами и отрендерим простую страницу с ссылкой для авторизации. Для этого положим в папку с проектом файл index.html:

<html>  
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="x-ua-compatible" content="ie=edge">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css" integrity="sha384-y3tfxAZXuh4HwSYylfB+J125MxIs6mR5FOHamPBG064zB+AFeWH94NdvaCBm8qnd" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-sm-4">
          <a href="/authorize" class="btn btn-primary"> Authorize GA </a>
        </div>
        <div class="col-sm-4">
        </div>
        <div class="col-sm-4">
        </div>
      </div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/js/bootstrap.min.js" integrity="sha384-vZ2WRJMwsjRMW/8U7i6PWi6AlO1L79snBrmgiDpgIWJ82z8eA5lenwvxbMV1PAh7" crossorigin="anonymous"></script>
  </body>
</html>  

Естественно подключили вездесущий Bootstrap. Далее отрендерим шаблон, обратимся к сессии и сделаем логирование:

import(  
  "log"
  "html/template"
  ...
)

...

func indexHandler(w http.ResponseWriter, r *http.Request) {  
  session, err := store.Get(r, "GASession")
  if err != nil {
    log.Println("IH.Error while retrieving session: ", err)
  }
  log.Println("Session: ", session)


  t, _ := template.ParseFiles("index.html")
  t.Execute(w, struct{}{})
}

Что мы сделали – получили сессию, считали index.html и отрендерили страницу. С сессией пока ничего не делаем, понадобится дальше.

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

func authorizeHandler(w http.ResponseWriter, r *http.Request) {  
  url := config.AuthCodeURL("state")
  http.Redirect(w, r, url, http.StatusFound)
}

После того как пользователь нажмет кнопку "Allow", сервис переправит его на урл, который мы указали в настройках /connect, поэтому напишу реализацию обработчика этого действия.

func connectHandler(w http.ResponseWriter, r *http.Request) {  
  code := r.FormValue("code")
  log.Println("AH.Code: ", code)

  tok, err := config.Exchange(oauth2.NoContext, code)
  if err != nil {
    log.Fatalln("AH.Error: ", err)
  }
  log.Println("AH.AccessToken: ", tok)

  session, err := store.Get(r, "GASession")
  if err != nil {
    log.Println("AH.Error :", err)
  }
  session.Values["access_token"] = tok
  session.Save(r, w)

  http.Redirect(w, r, "/", http.StatusFound)
}

Разберу подробнее.

code := r.FormValue("code")  

Когда пользователя средиректит обратно к нам, в параметрах запроса в ссылке будет поле code – проверочный код, по которому мы получим access_token с помощью метода Exchange из библиотеки oauth2.

tok, err := config.Exchange(oauth2.NoContext, code)  

К сожалению складывать все подряд в объект сессии у нас не получиться. Чтобы сохранить там что-то отличное от стандартных типов нужно зарегистрировать тип, который мы хотим сохранить. Регистрируется он в функции init(). Она вызывается после всех определений констант и переменных и до вызова функции main().

func init() {  
  gob.Register(&oauth2.Token{})
}

Далее сохраним объект сессии до отправки ответа:

func connectHandler(w http.ResponseWriter, r *http.Request) {  
  ...
  session.Values["access_token"] = tok
  session.Save(r, w)
  log.Println("AH.Session: ", session)

  http.Redirect(w, r, "/", http.StatusFound)
}

Теперь в indexHandler мы можем обратиться к сессии и вытащить access_token для дальнейшей работы. В других обработчиках мы также можем обращаться к access_token и использовать его для инициализации клиента для запросов в API Google Analytics.

На этом закончу первую часть. Во второй расскажу как использовать библиотеку google.golang.org/api/ для запросов в API Google.

Прочитать

SSH Config

Сегодня расскажу про упрощение жизни с ssh config. По работе приходится ходить по множеству разных серверов и довольно часто. И как бы это не было смешно, но ssh config открыл для себя пару дней назад.

Представим ситуацию:

ssh -p 22 user@host1  

Для 1 сервера набирать недолго, запомнить легко. А теперь представим 30-40-50 разных машин, с разными портами, именами пользователя и ip адресами. В итоге имеем файлик:

ssh -p PORT_NUMBER user1@host1 # dev  
ssh -p PORT_NUMBER user2@host2 # prod  
...
ssh -p PORT_NUMBER user100@host100 # prod100  

Ctrl+C и Ctrl+V и погнали. Но это не круто, поэтому в .bashrc или .zshrc добавляются алиасы:

alias dev='ssh -p PORT_NUMBER user1@host1'  
alias prod1='ssh -p PORT_NUMBER user2@host2'  
...
alias prod100='ssh -p PORT_NUMBER user100@host100'  

Уже лучше, но оказывается можно и еще круче. Тут нам поможет ssh config, подробная документация тут.

2 варианта использования:

  • /etc/ssh/ssh_config – на всю систему
  • ~/.ssh/config – для пользователя

Рассмотрим последний. Создаем и заполняем:

# dev machine
Host dev  
  HostName 127.0.0.1
  Port 22
  User username

Теперь мы можем набрать ssh dev и попасть на машину. Если надо пробросить ключи на сервер добавляем:

# dev machine
Host dev  
  HostName 127.0.0.1
  Port 22
  ForwardAgent yes
  User username

Можно указать настройки для группы серверов. Предположим на всех production серверах у вас перевешен порт ssh со стандартного 22 на 2222. Пользователь везде одинаковый и везде хотим пробрасывать ключи на сервер.

Host prod*  
  ForwardAgent yes
  Port 2222
  User username

Host prod1  
  HostName 10.0.0.1

Host prod2  
  HostName 10.0.0.2
...
Host prod100  
  HostName 10.0.0.100

Бонус - автокомплит. Набираем ssh, нажимаем Tab и показывается список хостов из конфига. Профит. Из минусов - нельзя делать include других файлов конфига, тогда можно было бы отлично комбинировать файлы с личными серверами и рабочими.

Прочитать

Neovim

На новогодних праздниках хочется выбраться из синей ямы и заняться чем-то полезным. Давнее желание - собрать свой Vim с блэкджеком, подошло идеально. Задача полезнее бутылки пива (подставь любой алкогольный напиток), поэтому поехали. По ходу переезда вспомнил про Neovim и решил попробовать.

С самого начала я был на сборке Janus. Отличная вещь для начинающих и незаморачивающихся. Взял велосипед, обновил немного рулевое управление и переключение скоростей, и гоняй себе на здоровье.

Но настоящие "пацаны" делают свою сборку. Перетаскивать сразу весь список плагинов из Janus бессмысленно. Решил, что буду добавлять нужные по мере необходимости. Необходимость возникла сразу и тут же пришлось выбирать между менеджерами зависимостей. Поэтому напишу немного про них, прежде чем перейти к списку плагинов и остальным премудростям.

Не разбирался в различиях между менеджерами плагинов (зависимостей), выбирать пришлось между Vundle и Vim-Plug. Первый многими используется, но мало поддерживается своим создателем. Второй - изначальный форк, но с кучей приятных улучшений, типо lazyload плагинов по необходимости, а также параллельной загрузкой и установкой. Последняя фича - достаточно сомнительное преимущество. Актуальна только при переезде на новый комп.

Как устанавливать Neovim рассказывать бессмысленно, я ставил через brew.

А вот при первоначальной загрузке отловил несколько интересных вещей.

  1. конфигурация лежит в ~/.config/nvim/init.vim
  2. без создания файла ~/.nvimrc мой конфиг не читался (почему так произошло до сих пор для меня загадка)

2ой пункт решил созданием symbol link на init.vim.

Еще одна проблема - неправильный код при нажатии Ctrl + h. Это нужно для переключение между окнами, например с NERDTree. В iTerm сделал так.

Вот так это решается.

Пока у меня не получилось прикрутить YCM, но работать уже можно.

Выводы - Neovim хорош, но сыроват. Сложно найти решение проблем применительно к нему. С Vimом полно обсуждений и блогов. Стоит ли обновляться - решать вам.

P.S. Разработчик neovim сделал очень круто. Вбросил на kickstarter (или схожем ресурсе) про новый vim, собрал начальный капитал, чтобы работать над ним full-time, а теперь каждый месяц собирает по $4-5k, чтобы дальше над ним работать. Учитесь.

Прочитать

Uber в путешествии по Азии

Сегодня начну писать на новую для блога тему — путешествия. Мы программисты — народ закрытый, но путешествовать любим не только по просторам интернета.

Начну рассказ о том, что меня поразило в этот раз. Думаю компанию Uber уже представлять никому не нужно. О них много написано. Таксисты начали с ними войны и переворачивают их мерседесы, что забавно. Это свидетельство того, что компания действительно делает революционные вещи. Но хватит лирики, перейдем к сути.

Москву и Питер Uber'ом уже не удивить, но в провинциях нашей великой родины о нем все еще не слышно. Прилетели в Ханой, первым делом купили симку и узнали стоимость такси до отеля — $24. И тут я вспомнил про Uber. Оказывается в столице социалистического государства Вьетнам работает Uber black и Uber X.

Вип, лакшери, делюкс и премиум - это классно, но мы решили попробовать второй вариант. Не знаю, есть ли у водителей X лицензия, но на тот момент это не особо волновало. Итак, вызвали, первый таксист спустя 5 минут отменил заказ, а второй приехал через 8. Машина чистая, с кондиционером, водитель не мешает своей музыкой и сносно объясняется по-английски. Т.к. в приложении уже выбрана точка назначения, объяснять ему как ехать не надо, Google это сделал за нас.

К слову, ехать из аэропорта до центра города ~ 1 час и 25 км (до нашего отеля). За все — $8. Эта сумма снимается с привязанной карты, наличка не нужна. На время путешествий мы постарались отстраниться от текущего курса доллара и думать о ценах именно в зеленой валюте. Это дешево, серьезно, оченьs дешево.

Начиная с этого момента и до самого конца путешествия, мы пользовались только Uber. Правда не во всех городах он был :(

Дополнительные траты водителя также вносятся в счет. Это удобно, потому что в Ханое, например, въезд в аэропорт стоит 15000 донгов (чуть меньше $1) и тебе не надо искать местную наличку или $1, их просто спишут с карты.

Немного о техподдержке. Уезжали из Ханоя и водитель привез нас не к тому терминалу (нужен был international, а привезли к domestic). Плата у каждого терминала своя. Списали за оба терминала. Ради интереса, обратился в поддержку и попросил откорректировать счет, указав что водитель ошибся. В течение 1 дня поддержка отписалась, что действительно водитель ошибся и средства за въезд на один из терминалов вернули на карту.

Следующей нашей точкой был Куала Лумпур, и там мы тоже пользовались Uber. Из аэропорта брать не стали, т.к. очень большие пробки в городе и быстрее добраться на местном аэроэкспрессе, а уже в городе за пару долларов добраться до отеля. На Uber X через весь город с пробками мы однажды проехали за $7. Бизнес ланч в Куала Лумпуре стоит дороже. Ни разу не пользовались общественным транспортом. Остановки находятся на больших расстояниях, а на улице +30 градусов. Проезд на двоих стоит примерно как такси, так что выбор очевиден.

Резюме. Там где есть Uber — им реально стоит пользоваться. Проверено на собственном опыте.

P.S. Куда же без реферальной ссылки: бесплатная поездка для тебя и меня.

Прочитать

[Отзыв]: Искусство войны

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

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

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

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

А теперь самое главное — все это очень классно ложится на управление большой компанией.

Резюме — обязательно к прочтению.

Книга "Искусство войны" Сунь-Цзы - The Art of War ISBN 978-5-9910-2473-0Книга "Искусство войны" Сунь-Цзы - The Art of War ISBN 978-5-9910-2473-0

Прочитать