Типичный юзкейс для Kibana — смотрим логи, видим ошибки, создаем тикеты по ним. Логов у нас довольно много, места для их хранения мало. Поэтому просто вставить ссылку на документ из Elasticsearch/Kibana недостаточно, особенно для низкоприоритетных задач: пока доберемся до нее, индекс с логом может быть уже удален. Соответственно, приходится документ сохранять в файл и прикреплять к тикету.
Если один раз это делать, то это еще куда ни шло, но создавать уже десять тикетов подряд будет тупо лень, поэтому я решил это «быстренько» (ха-ха) автоматизировать.
Под катом: статья для пятницы, экспериментальная фича javascript, пара грязных хаков, небольшая регулярка с галочками, reverse proxy, проигрыш безопасности удобству, костыли и очевидная картинка из xkcd.
Предупреждаю: я далеко не специалист в web-технологиях, и поэтому специалистам, скорее всего, покажутся мои проблемы очень очевидными, а решения — тупыми. Но это не продакшн-решение, а просто мелкий скрипт «для своих». Все происходит в доверенной локальной сети и поэтому скрипт имеет много проблем с безопасностью.
Сходу можно придумать достаточно много решений проблемы. Во-первых, можно пихать сразу все логи в RM (внезапно, для этого даже есть плагин logstash), предварительно их фильтруя/агрегируя — знай себе меняй описание да исполнителя. Это, конечно, прикольно, но надо будет долго отлаживать/настраивать и появится много новой рутинной работы — давать описания/удалять лишнее.
Второй вариант — намастрячить какой-нибудь скрипт, который получает ссылки на логи, скачивает их, спрашивает дополнительные параметры у пользователя и через API Redmine создает новый тикет. Но к этому надо будет нормальный интерфейс пилить, да и дублировать часть функций RM…
Можно извратиться и сделать кликер или с помощью selenium подготовить тикет, чтобы потом в привычном интерфейсе дозаполнить что надо, но нельзя будет трогать мышку… Да и редактирование может вдруг понадобиться.
Плагин для браузера? Окститесь, его еще регистрировать и поддерживать, да еще хотя бы под два браузера делать.
Плагин Redmine? Не, это ж API надо будет изучать, да и лезть в кишки RM… Простого дополнительного поля недостаточно будет.
В итоге приходим к букмарклету (выполнению javascript из закладок) и/или пользовательскому скрипту (greasemonkey/tampermonkey и т.п.) — javascript‘ом вроде можно и интерфейс нарисовать, и логи скачать через ajax-запрос, да и вообще почти все что угодно со страницей сделать.
Пока самая неясная часть — это загрузка файлов. Все остальное вроде можно легко сделать… За загрузку файлов на странице создания тикета RM отвечает обычный
По идее, надо всего лишь изменить список файлов у этого элемента и дернуть этот метод. Есть только одна мааааленькая проблема:
Сделано это ради того, чтобы нельзя было отправить на сервер
Т.е. тут имитируется добавление файлов из буфера обмена, которые мы потом получаем списком. Сам по себе «файл» из текста создается элементарно:
Тут все просто, как топор: делаем в нужном месте надпись, поле ввода и кнопку загрузки. Поскольку делается «для своих» с форматом ввода (и его валидацией) особо не стал заморачиваться — пусть будет текстовое поле, одна строка — один лог (ссылка и имя создаваемого файла через пробел).
Для букмарклета еще пригодилось предварительное удаление себя по id.
Здесь тоже все просто: разбиваем текст из поля ввода на пары «ссылка»-«имя файла», скачиваем все из эластика, потому что Kibana так просто данные не отдаст, заливаем на RM, изменяем описание тикета и все. Благо к RM уже подключен jquery и ajax-запросы легко создаются.
Отлично, все готово! Делаем тестовый запуск и…
Для тех кто не в курсе, запрашивать http-данные, находясь на https ресурсе, — очень плохо, потому что вам могут подпихнуть левые данные через MITM атаку. Более того, какой-нибудь Firefox даже если вам и разрешит это сделать, просить у него разрешение надо будет каждый раз — и белого списка никогда не будет. Это все правильно и хорошо с точки зрения пользователя, но для скрипта на коленке это только палки в колеса.
Что ж, покупать X-Pack для Elasticsearch ради вшивого скрипта не хочется, поэтому придется сделать прокси https -> http. Он же reverse proxy. Вариантов тут достаточно много, от монструозного squid до питонячего скрипта. Самым подходящим мне показался haproxy — он и прост в настройке/установке, и ресурсы не жрет.
Достаточно лишь сгенерить самоподписанный сертификат (прости, let‘s encrypt, но мы в траст-зоне)
и, собственно, настроить haproxy:
Теперь на порту 9243 будет прозрачная прокси до эластика (соответственно, меняем порт в регулярке и добавляем https).
Однако и это не удовлетворит наш браузер, который печется о безопасности пользователя. На этот раз проблема в том, что нельзя запрашивать данные с другого домена, если он это не разрешил. Решается это с помощью механизма CORS. Хорошо хоть, что Elasticsearch это сам умеет:
Напомню, что мы все ещевтирали делали эту дичь в формате букмарклета. В принципе, ничего страшного, но кому-то даже лишний раз кликнуть лень (например, мне). Поэтому будем делать userscript. Тут заодно встает проблема его обновления (делаем-то на века!). Поэтому воспользуемся механизмом обновления юзерскриптов кого я обманываю, конечно, очередным костылем:
Зато и в букмарклетах у параноиков будет обновляться. Для раздачи этой фигни нам понадобится https-сервер. Тут я уже откровенно заленился и взял первый попавшийся (да еще и на python 2.7) *посыпаю голову пеплом*:
Вот теперь пользователям осталось только создать юзерскрипт/букмарклет, добавить в исключения сертификат и все будет работать.
Суть первой проблемы заключается в следующем: когда нужно обработать результаты сразу нескольких ajax-запросов, в функцию передается столько аргументов, сколько было запросов. Но когда запрос один, jquery «любезно» его раскрывает в три аргумента. Поэтому пришлось писать такой костыль:
Второй баг связан с тем, что при смене трекера или при смене статуса заявки Redmine сохраняет все введенные данные, запрашивает новый интерфейс (прямо html cо встроенным js), пересоздает интерфейс и перезаполняет поля с помощью функциикостыль ad-hoc решение:
Т.е. просто делаем хук на оригинальную функцию и делаем аналогичные ей действия для своего поля.
Полную версию скрипта можно посмотреть в моем gist. Вот картинка, которую должно большинство ожидать к концу этой статьи:
А вообще автоматизировать вещи — весело и полезно, и позволяет изучить что-то новое в другой области. Пользователи скрипта довольны, создание тикетов по логам в кибане теперь не так сильно напрягает.
Если один раз это делать, то это еще куда ни шло, но создавать уже десять тикетов подряд будет тупо лень, поэтому я решил это «быстренько» (ха-ха) автоматизировать.
Под катом: статья для пятницы, экспериментальная фича javascript, пара грязных хаков, небольшая регулярка с галочками, reverse proxy, проигрыш безопасности удобству, костыли и очевидная картинка из xkcd.
Предупреждаю: я далеко не специалист в web-технологиях, и поэтому специалистам, скорее всего, покажутся мои проблемы очень очевидными, а решения — тупыми. Но это не продакшн-решение, а просто мелкий скрипт «для своих». Все происходит в доверенной локальной сети и поэтому скрипт имеет много проблем с безопасностью.
Варианты решения
Сходу можно придумать достаточно много решений проблемы. Во-первых, можно пихать сразу все логи в RM (внезапно, для этого даже есть плагин logstash), предварительно их фильтруя/агрегируя — знай себе меняй описание да исполнителя. Это, конечно, прикольно, но надо будет долго отлаживать/настраивать и появится много новой рутинной работы — давать описания/удалять лишнее.
Второй вариант — намастрячить какой-нибудь скрипт, который получает ссылки на логи, скачивает их, спрашивает дополнительные параметры у пользователя и через API Redmine создает новый тикет. Но к этому надо будет нормальный интерфейс пилить, да и дублировать часть функций RM…
Можно извратиться и сделать кликер или с помощью selenium подготовить тикет, чтобы потом в привычном интерфейсе дозаполнить что надо, но нельзя будет трогать мышку… Да и редактирование может вдруг понадобиться.
Плагин для браузера? Окститесь, его еще регистрировать и поддерживать, да еще хотя бы под два браузера делать.
Плагин Redmine? Не, это ж API надо будет изучать, да и лезть в кишки RM… Простого дополнительного поля недостаточно будет.
В итоге приходим к букмарклету (выполнению javascript из закладок) и/или пользовательскому скрипту (greasemonkey/tampermonkey и т.п.) — javascript‘ом вроде можно и интерфейс нарисовать, и логи скачать через ajax-запрос, да и вообще почти все что угодно со страницей сделать.
Загрузка файлов
Пока самая неясная часть — это загрузка файлов. Все остальное вроде можно легко сделать… За загрузку файлов на странице создания тикета RM отвечает обычный
<input type="file">
, при изменении которого вызывается функция addInputFiles(this)
.По идее, надо всего лишь изменить список файлов у этого элемента и дернуть этот метод. Есть только одна мааааленькая проблема:
Сделано это ради того, чтобы нельзя было отправить на сервер
/etc/passwd
, /etc/shadow/
или фото вашего кота с рабочего стола. В принципе, разумно, но надо это как-то обойти. Впрочем, если нельзя, но очень хочется, то можно заиспользовать такой грязный хак, который основан экспериментальной фиче — Clipboard API.function createFileList(files){
const dt = new ClipboardEvent("").clipboardData || new DataTransfer();
for (let file of files) {
dt.items.add(file);
}
return dt.files;
}
Т.е. тут имитируется добавление файлов из буфера обмена, которые мы потом получаем списком. Сам по себе «файл» из текста создается элементарно:
function createFile(text, fileName){
let blob = new Blob([text], {type: 'text/plain'});
let file = new File([blob], fileName);
return file;
}
Пользовательский интерфейс
Тут все просто, как топор: делаем в нужном месте надпись, поле ввода и кнопку загрузки. Поскольку делается «для своих» с форматом ввода (и его валидацией) особо не стал заморачиваться — пусть будет текстовое поле, одна строка — один лог (ссылка и имя создаваемого файла через пробел).
Для букмарклета еще пригодилось предварительное удаление себя по id.
Элементарные вещи
function removeSelf(){
let old = document.getElementById(ui_id);
if (old != null) old.remove();
}
function createUi(){
removeSelf();
let ui = document.createElement('p');
ui.id = ui_id;
let label = document.createElement('label');
label.innerHTML = "Logs data:";
ui.appendChild(label);
let textarea = document.createElement('textarea');
textarea.id = data_id;
textarea.cols = 60;
textarea.rows = 10;
textarea.name = "issue[logs_data]";
ui.appendChild(textarea);
let button = document.createElement('button');
button.type = "button";
button.onclick = addLogsData;
button.innerHTML = "Add logs data";
ui.appendChild(button);
let attributesBlock = document.querySelector("#attributes");
attributesBlock.parentNode.insertBefore(ui, attributesBlock);
}
Основная работа
Здесь тоже все просто: разбиваем текст из поля ввода на пары «ссылка»-«имя файла», скачиваем все из эластика, потому что Kibana так просто данные не отдаст, заливаем на RM, изменяем описание тикета и все. Благо к RM уже подключен jquery и ajax-запросы легко создаются.
Скучный код, регулярку искать здесь
function addLogsData(){
let text = document.getElementById(data_id).value;
let lines = text.split('\n');
let urlsAndNames = lines
.filter(x => x.length > 2)
.map(line => line.split(/\s+/, 2));
downloadUrlsToFiles(urlsAndNames);
}
const kibana_pattern = /http:\/\/([^:]*):\d+\/app\/kibana#\/doc\/[^\/]*\/([^\/]*)\/([^\/]*)\/?\?id=(.*?)(&.*)?$/;
const es_pattern = 'http://$1:9200/$2/$3/$4';
function downloadUrlsToFiles(urlsAndNames){
let requests = urlsAndNames.map((splitted) => {
let url = splitted[0].replace(kibana_pattern, es_pattern);
return $.ajax({
url: url,
dataType: 'json'
});
});
$.when(...requests).done(function(...responses){
let files = responses.map((responseRaw, index) => {
let response = responseRaw[0];
checkError(response);
let fileName = urlsAndNames[index][1];
return createFile(JSON.stringify(response._source), fileName + '.json');
});
uploadFiles(files, urlsAndNames);
}).fail((error) => {
let errorString = JSON.stringify(error);
alert(errorString);
throw errorString;
});
}
function uploadFiles(files, urlsAndNames){
pseudoUpload(files);
changeDescription(urlsAndNames);
removeSelf();
}
Отлично, все готово! Делаем тестовый запуск и…
Безопасность
Для тех кто не в курсе, запрашивать http-данные, находясь на https ресурсе, — очень плохо, потому что вам могут подпихнуть левые данные через MITM атаку. Более того, какой-нибудь Firefox даже если вам и разрешит это сделать, просить у него разрешение надо будет каждый раз — и белого списка никогда не будет. Это все правильно и хорошо с точки зрения пользователя, но для скрипта на коленке это только палки в колеса.
Что ж, покупать X-Pack для Elasticsearch ради вшивого скрипта не хочется, поэтому придется сделать прокси https -> http. Он же reverse proxy. Вариантов тут достаточно много, от монструозного squid до питонячего скрипта. Самым подходящим мне показался haproxy — он и прост в настройке/установке, и ресурсы не жрет.
Достаточно лишь сгенерить самоподписанный сертификат (прости, let‘s encrypt, но мы в траст-зоне)
openssl genrsa -out dummy.key 1024
openssl req -new -key dummy.key -out dummy.csr
openssl x509 -req -days 3650 -in dummy.csr -signkey dummy.key -out dummy.crt
cat dummy.crt dummy.key > dummy.pem
и, собственно, настроить haproxy:
frontend https-in
mode tcp
bind *:9243 ssl crt /etc/ssl/localcerts/dummy.pem alpn http/1.1
http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"
default_backend nodes-http
backend nodes-http
server node1 localhost:9200 check
Теперь на порту 9243 будет прозрачная прокси до эластика (соответственно, меняем порт в регулярке и добавляем https).
Однако и это не удовлетворит наш браузер, который печется о безопасности пользователя. На этот раз проблема в том, что нельзя запрашивать данные с другого домена, если он это не разрешил. Решается это с помощью механизма CORS. Хорошо хоть, что Elasticsearch это сам умеет:
http.cors.allow-headers: X-Requested-With, Content-Type, Content-Length
http.cors.allow-origin: "/.*/"
http.cors.enabled: true
Userscript
Напомню, что мы все еще
// ==UserScript==
// @name KIBANA_LOGS
// @grant none
// @include https://<rm-address>/*issues*
// ==/UserScript==
(function(){document.body.appendChild(document.createElement('script')).src='https://<kibana-address>:4443/kibana_logs_rm.js';})();
Зато и в букмарклетах у параноиков будет обновляться. Для раздачи этой фигни нам понадобится https-сервер. Тут я уже откровенно заленился и взял первый попавшийся (да еще и на python 2.7) *посыпаю голову пеплом*:
import BaseHTTPServer, SimpleHTTPServer
import ssl
httpd = BaseHTTPServer.HTTPServer(('0.0.0.0', 4443),
SimpleHTTPServer.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket(httpd.socket, certfile='/etc/ssl/localcerts/dummy.pem',
server_side=True)
httpd.serve_forever()
Вот теперь пользователям осталось только создать юзерскрипт/букмарклет, добавить в исключения сертификат и все будет работать.
Пара багов
Суть первой проблемы заключается в следующем: когда нужно обработать результаты сразу нескольких ajax-запросов, в функцию передается столько аргументов, сколько было запросов. Но когда запрос один, jquery «любезно» его раскрывает в три аргумента. Поэтому пришлось писать такой костыль:
let responses;
if (requests.length == 1){
responses = [arguments];
} else {
responses = Array.from(arguments);
}
Второй баг связан с тем, что при смене трекера или при смене статуса заявки Redmine сохраняет все введенные данные, запрашивает новый интерфейс (прямо html cо встроенным js), пересоздает интерфейс и перезаполняет поля с помощью функции
replaceIssueFormWith
. Звучит немного дико, но это сделано для реализации workflow (а там на разных стадиях поля для ввода потенциально могут отличаться). Тут тоже пришлось сделать function installReplaceHook(){
let original = window.replaceIssueFormWith;
window.replaceIssueFormWith = function(html){
let logs_data = document.getElementById(data_id).value;
let ret = original(html);
createUi();
document.getElementById(data_id).value = logs_data;
return ret;
};
}
Т.е. просто делаем хук на оригинальную функцию и делаем аналогичные ей действия для своего поля.
Заключение
Полную версию скрипта можно посмотреть в моем gist. Вот картинка, которую должно большинство ожидать к концу этой статьи:
А вообще автоматизировать вещи — весело и полезно, и позволяет изучить что-то новое в другой области. Пользователи скрипта довольны, создание тикетов по логам в кибане теперь не так сильно напрягает.