Анализ логов

Ура! У нас есть накапливаемая БД в логами! Дальше только анализ логов и разбор полетов…

Графики логов

Ну вот не знаю что с этим делать и как дальше жить. Из готового особо ничего такого не обнаружил (может все же плохо искал). Из того что есть — это графики логов, т.е. пишем специальный запрос и смотрим как это «красиво» рисуется.

Чем же смотреть

Из того что я нашел более простое в освоении и «красивое» — это Grafana.

Пример графика
Пример графика

Я думаю многие видели это решение, по этому не очень хочется заострять на нем внимание. Единственный момент — это работа с ClickHouse.

Подключение ClickHouse

Тут все предельно просто:

  1. Ставим плагин
Плагин в Grafana
Плагин в Grafana

2. Настраиваем подключение к БД

DataSource
DataSource

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 пользователей.

ТОП-100 доменов по пользователю за день
ТОП-100 доменов по пользователю за день

Поделиться
Вы можете оставить комментарий, или ссылку на Ваш сайт.

Оставить комментарий

Вы должны быть авторизованы, чтобы разместить комментарий.