Ура! У нас есть накапливаемая БД в логами! Дальше только анализ логов и разбор полетов…
Графики логов
Ну вот не знаю что с этим делать и как дальше жить. Из готового особо ничего такого не обнаружил (может все же плохо искал). Из того что есть — это графики логов, т.е. пишем специальный запрос и смотрим как это «красиво» рисуется.
Чем же смотреть
Из того что я нашел более простое в освоении и «красивое» — это Grafana.
Я думаю многие видели это решение, по этому не очень хочется заострять на нем внимание. Единственный момент — это работа с ClickHouse.
Подключение ClickHouse
Тут все предельно просто:
- Ставим плагин
2. Настраиваем подключение к БД
3. Пишем запрос для отображения
В принципе тут все. Все стандартненько.
Свое решение
Другой вариант — это собственное решение. Нам важнее было анализировать работу через proxy-сервер пользователей за какой-то период. Например нам нужно было узнать кто потратил трафик за период, на каких доменах тусовался, может даже ссылочки посмотреть какие видюшки рассматривал и т.д. Может я что-то в Grafana и не осилил, но собственное решение появилось раньше.
Так как это все тот же ClickHouse, и в нем выполняется самый обычный SQL, то для меня (а уже и для коллег, которые с SQL-запросами не работали, но научились) это проще. Ну лень мне учить очередной язык чего-то там супер модного.
Для начала можно написать запрос в каком-нибудь готовом UI, чтобы его отладить. Далее можно было бы сделать какую-то оболочку, в которую «загоняется» написанный запрос и при его вызове появлялась бы простенькая форма для ввода нужных значений и получения результата.
Само решение приводить не буду, так как оно ну оооочень и слиииишком сырое, но работает уже около полутора лет. Приведу только некоторые моменты…
Сама система написана в связке NodeJS (backend) + Angular и используется 2 коннектора для СУБД: SQLite (ORM Sequelize) и ClickHouse. Собственно в SQLite хранятся разделы и непосредственно запросы. Тут тоже ничего сверхъестественного нет (2 таблицы). group.js:
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Group extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
Group.hasMany(models.Query, {
foreignKey: 'id_group'
});
}
};
Group.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: DataTypes.TEXT
}, {
sequelize,
modelName: 'Group',
});
return Group;
};
и query.js:
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Query extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
Query.belongsTo(models.Group, {
foreignKey: 'id_group'
});
}
};
Query.init({
id: {
type: DataTypes.UUID,
primaryKey: true,
allowNull: false,
defaultValue: DataTypes.UUIDV4
},
name: {
type: DataTypes.STRING,
allowNulld: false
},
description: DataTypes.TEXT,
querytext: {
type: DataTypes.TEXT,
allowNull: false
}
}, {
sequelize,
modelName: 'Query',
});
return Query;
};
Немного express.js по вкусу и в принципе всё (даже приводить не буду — все стандартно).
Собственно нужно выполнить запрос с параметрами. Для этого нужно как-то запрос параметризировать, да еще и вменяемые параметры для отображения использовать.
Решение получилось такое: берется самый обычный запрос и в месте, где должен быть параметр водставляется строка вида {{Название параметра}}. Такой параметр всегда будет принимать строку, а в коде будет самая обычная конкатенация строк (для простоты):
const re = /\{\{[\s\t]*[a-zа-я_]+[a-zа-я_0-9]+[\s\t]*\}\}/ig;
/**
* Найдем все параметры в строке запроса
* @param {string} querytext Текст запроса
* @return Возвращает массив с параметрами
*/
function parsingQueryParams(querytext) {
let m;
let arr = [];
do {
m = re.exec(querytext);
if (m) {
console.log(m[0]);
let tmp = /[a-zа-я_]+[a-zа-я_0-9]/i.exec(m[0])[0];
if (arr.length === 0) {
arr.push(tmp);
} else {
// Проверим не присутствует ли уже этот параметр
let search = false;
for (const str of arr) {
if (str === tmp) {
search = true;
break;
}
}
if (!search) {
arr.push(tmp);
}
}
// arr.push(/[a-zа-я_]+[a-zа-я_0-9]/i.exec(m[0])[0]);
}
} while (m);
return arr;
}
/**
* Подготовка запроса для выполнения
* @param {string} querytext Текст запроса
* @param {Object} params Параметры ключ=знаяение
* @return Возвращает строку с замененными параметрами
*/
function replaceQueryParams(querytext, params) {
let m;
do {
m = re.exec(querytext);
if (m) {
let reTmp = m[0];
let param = reTmp.replace(/[\{\{\}\}\s\t]+/gi, '');
if (params.hasOwnProperty(param)) {
querytext = querytext.replace(reTmp, params[param]);
}
}
} while (m);
return querytext;
}
module.exports = {
parsingQueryParams: parsingQueryParams,
replaceQueryParams: replaceQueryParams
}
Клиентский код тоже достаточно прост:
import { Component, Inject, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import * as parsingQuery from '../../../../../server/libs/parsingQuery';
export interface DisplayField {
name: string;
display?: string;
}
@Component({
selector: 'app-query-exec',
templateUrl: './query-exec.component.html',
styleUrls: ['./query-exec.component.styl']
})
export class QueryExecComponent implements OnInit {
fields = new Array(0);
form: FormGroup = new FormGroup({});
constructor(
public dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) public data: string
) { }
ngOnInit(): void {
const params = parsingQuery.parsingQueryParams(this.data);
this.fields = new Array();
for (const str of params) {
let display: string = str.replace(/_{1,1000}/gi, ' ').trim().toLowerCase();
display = display.trim().substr(0, 1).toUpperCase() + display.substr(1);
this.fields.push({
name: str,
display
});
}
for (const control of Object.keys(this.form.controls)) {
this.form.removeControl(control);
}
for (const field of this.fields) {
this.form.addControl(field.name, new FormControl());
}
}
private prepareParams(): any {
const result: any = {};
result.params = {};
for (const field of this.fields) {
result.params[field.name] = this.form.value[field.name];
/*result.params.push({
name: field.name,
value: this.form.value[field.name]
});*/
}
return result;
}
queryTable(): void {
const result: any = this.prepareParams();
result.typeQuery = 'dataset';
this.dialogRef.close(result);
}
queryGraph(): void {
const result: any = this.prepareParams();
result.typeQuery = 'graph';
this.dialogRef.close(result);
// TODO
}
}
Ну и форма к ней:
<mat-card>
<mat-card-title>Параметры запроса</mat-card-title>
<form [formGroup]="form">
<mat-card-content>
<p *ngFor="let field of fields">
<mat-form-field>
<mat-label>{{ field.display }}</mat-label>
<input matInput [formControlName]="field.name">
</mat-form-field>
</p>
</mat-card-content>
<mat-card-footer>
<button mat-raised-button color="primary" (click)="queryTable()">Таблица</button>
<button mat-raised-button color="primary" (click)="queryGraph()">График</button>
</mat-card-footer>
</form>
</mat-card>
Собственно всё!
Результаты
В итоге свою задачу мы решили и пользуемся. Нам хватает с головой, при учете 6 серверов и около 200 пользователей.