Для выпуска Unomaly 2.28 мы полностью переработали наш токенизатор, чтобы улавливать вложенные структуры и пары ключ-значение в неструктурированных данных журнала, которые мы принимаем - без какой-либо спецификации схемы. Чтобы было понятнее, мы немного рассмотрим, как работает Unomaly, а затем углубимся в технические детали нового структурного токенизатора.

TL;DR

  • Unomaly автоматически группирует журналы событий и классифицирует их как нормальные или аномальные в соответствии с базовыми показателями всех журналов, которые он видел ранее.
  • Алгоритм не зависит от формата, и простая эвристика того, как обычно выглядят сообщения журнала, прошла долгий путь и оказалась более гибкой, чем можно было бы подумать.
  • Однако мы хотели лучше понять фрагменты структурированных данных в журналах, например: sent message {“from”:”jane”,”to”:[”joe”,”anne”,”peter”]}
  • Для этого мы создали структурный токенизатор, который обнаруживает и анализирует структурированные данные, объединенные в неструктурированные сообщения журнала.
  • В процессе мы узнали новые способы взглянуть на проблему как один из сопоставления строк и последовательностей - открыв еще много путей для дальнейшего улучшения.

Превращение воды в вино - необычный способ

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

if ok {
    log.Info(“This is fine.”)
} else {
    log.Fatalf(“This is not fine.”)
}

В результате получится что-то вроде этого:

INFO 2018–01–24T10:49:54Z This is fine. pid=39067 pkg=main

Со временем программа может генерировать миллионы This is fine. событий. Но поскольку каждый раз он выглядит немного по-другому (в данном случае метка времени и PID), сопоставить эти экземпляры с одним каноническим событием - одной строкой исходного кода - нетривиально. Вам потребуется систематизация всех событий, которые может генерировать ваша система, которая должна охватывать все сторонние инструменты и каждое программное обеспечение, которое вы пишете каждой командой, и постоянно обновлять ее. Много работы!

И это одна из основных вещей, которую делает Unomaly: он выполняет онлайн-кластеризацию событий, которые он получает, и изучает различные типы событий, которые он видел статистически, а не на основе определения. Как только он увидит достаточно данных, он может вместо этого непрерывно описывать поток журнала следующим образом:

  • «Событие X произошло 94 раза за последние 13 секунд» - совокупность обычных событий.
  • «Событие Y произошло в момент T: никогда раньше не наблюдалось ..» - обнаружение аномалий.

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

Как мы это делаем?

Все начинается с поиска любой структуры в необработанных данных, которые она получает. Это делается с помощью токенизатора, который превращает необработанное сообщение журнала в последовательность токенов. Пример:

INFO 2018–01–24T10:49:54Z This is fine. pid=39067 pkg=main

токенизируется в:

{
    P(“INFO”), D(“ “), P(“2018–01–24T10:49:54Z“), D(“ “),
    P(“This”), D(“ “), P(“is”), D(“ “), P(“fine”), D(“. “),
    P(“pid=39067”), D(“ “), P(“pkg=main”)
}

У нас есть два типа токенов: параметры (P) и разделители (D). Параметры содержат фактическое содержимое сообщения журнала, в то время как разделители фиксируют знаки препинания и более полезны для создания сигнатуры структуры для события, описанного ниже. Для краткости мы будем использовать следующий синтаксис:

INFO 2018–01–24T10:49:54Z This is fine. pid=39067 pkg=main
^~~~_^~~~~~~~~~~~~~~~~~~~_^~~~_^~_^~~~__^~~~~~~~~_^~~~~~~~
where ^~~~~~~ = parameter
           __ = delimiter

Из последовательности токенов мы можем получить:

  1. Структура события: хэш, характерный для типа события. В приведенном выше примере это, по сути, хеш следующего:
{P(type=word), D(“ “), P(type=timestamp), P(type=word), D(“ “), P(type=word), D(“ “), P(type=word), D(“. “), P(type=word), D(“ “), P(type=word)}

2. Профиль события: значения параметров для конкретного случая события. Для того же примера это будет:

 {“INFO”, “2018–01–24T10:49:54Z”, “This”, “is”, “fine”, “pid”, 39067, “pkg”, “main”}

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

Наш роман с Рагелем

В начале времен в основе Unomaly вы найдете это регулярное выражение, составляющее наш первый токенизатор:

(\s*\S*?)(\w+\S*\w|\w+)(\S*)|(\s\S+)

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

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

hex_number = ( ‘0’ [xX] [a-fA-F0–9]+ | [0–9]+ [0–9a-fA-F]+ );

Которые затем можно комбинировать, как блоки LEGO, для создания более сложных машин. Настоящая сила заключается в возможности взаимодействия с основным языком, в нашем случае Go, для управления машиной или хранения вещей, которые вы подобрали. Например:

// This is Ragel.
(hex_number | dec_number | oct_number)+ => {
    // This is Go code.
    state.tokens.add(ts, te, data, numberToken)
};

Обнаружив Ragel, мы переписали в нем наш токенизатор, что привело к ускорению на несколько порядков по сравнению со старым сопоставлением регулярных выражений. Написание токенизатора как конечного автомата дало нам большую гибкость и соблюдение нашего принципа разработки: старайтесь писать как можно меньше кода и быть максимально независимым от различных форматов журналов. И этот принцип прошел долгий путь - но, как и в случае с любым типом упрощения 80/20: иногда он требует рассмотрения крайних случаев.

Проблемы со старым токенизатором

Рассмотрим этот журнал:

sudo neo : TTY=pts/1 ; PWD=/home/neo ; USER=root ; COMMAND=/bin/bash
^~~~_^~~___^~~~~~~~~___^~~~~~~~~~~~~___^~~~~~~~~___^~~~~~~~~~~~~~~~~

Здесь обратите внимание, как пара KEY = value становится одним токеном - хотя ключи и значения - это две разные вещи: одна статическая, а другая переменная, они будут анализироваться как одно целое.

Другая проблема, если журнал содержит JSON:

sent message {“from”:”jane”,”to”:[”joe”,”anne”,”peter”]}
^~~~_^~~~~~~___^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~___

Большой двоичный объект JSON становится единым токеном, хотя было бы разумнее анализировать поля to и from по отдельности. Если бы он содержал пробелы, у нас была бы другая проблема:

sent message {“from”: ”jane”, ”to”: [”joe”, ”anne”, ”peter”]}
^~~~_^~~~~~~___^~~~____^~~~____^~_____^~~____^~~~____^~~~~___

Обратите внимание, что значение to представлено не в виде списка вещей, а в виде совершенно не связанных параметров, даже если мы хотим зафиксировать его как что-то вроде отправленного сообщения {“from”: ”jane”, ”to”: <recipients>}.

Похожая проблема связана с глубоко вложенными структурами JSON, которые также неконтролируемо создают токены, что приводит к ложным срабатываниям. Мы предпочитаем некий предел глубины, за пределами которого мы рассматриваем все остальное как единый токен. Единственное средство от этого - превратить весь фрагмент JSON в один токен, независимо от того, сколько информации он содержит. Это сдерживало аномалии, но, по сути, помогало им справляться и говорить: мы просто назовем это случайным и забудем об этом, даже если внутри них могут быть какие-то ценные данные.

С учетом этих примеров и нуждающихся клиентов стало ясно, что токенизатору необходимо понимать такие вещи, как объекты JSON, пары ключ-значение или списки. Нам нужен был структурный токенизатор.

Представляем: структурная токенизация

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

, ; : = # | * “ ‘ { } ( ) [ ] < >

Мы решили придерживаться Ragel and Go, но добавили в токенизатор две новые концепции:

  1. Распознавание "ключ-значение": ключи теперь являются отдельным типом токенов.
  2. Объединение структурных разделителей: мы пытаемся найти открытые и закрывающие пары разделителей, такие как { } или ( ), и пытаемся проанализировать то, что находится внутри, как вложенную структуру. Это также позволяет нам устанавливать (настраиваемый) предел глубины, уменьшая количество ложных срабатываний.

Полужирный части следующих примеров обозначены как ключевые:

  • key=value non_key = value not/a/key=value “is/a/key”=value
  • (key = value, non_key: value)
  • { ‘key’: “value”, key: {non_key = value} }

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

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

  • {key: {key: {key: value}}}
  • {key: [item, item, item], key: (item (item item item))}
  • Non-json lists [are, not, collapsed] {[item, item]}
  • Any (type [of <structure can be collapsed>])
  • {structures (are [balanced (inside ] collapsed ) ] tokens ) }

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

[before]
sudo neo : TTY=pts/1 ; PWD=/home/neo ; USER=root ; COMMAND=/bin/bash
^~~~_^~~___^~~~~~~~~___^~~~~~~~~~~~~___^~~~~~~~~___^~~~~~~~~~~~~~~~~
[after]
sudo neo : TTY=pts/1 ; PWD=/home/neo ; USER=root ; COMMAND=/bin/bash
^~~~_^~~___^~~_^~~~~___^~~_^~~~~~~~~___^~~~_^~~~___^~~~~~~_^~~~~~~~~

Обратите внимание, как теперь разделены ключи и значения. Теперь давайте рассмотрим пример с включенным JSON:

[before]
sent message {“from”:”jane”,”to”:[”joe”,”anne”,”peter”]}
^~~~_^~~~~~~___^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~___
[after]
sent message {“from”:”jane”,”to”:[”joe”,”anne”,”peter”]}
^~~~_^~~~~~~__^~~~~~_^~~~~~_^~~~_^~~~~~~~~~~~~~~~~~~~~~_

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

Заключение

Как гласит известная поговорка, если вы умеете правильно сформулировать проблему - вы уже на полпути к ее решению. Для нас это было осознанием того, что поиск текстовых аномалий сводится к проблеме сопоставления строк и последовательностей. Если мы сможем создать хорошие кластеры журналов и быстро сопоставить с ними новые журналы - у нас есть то, что нам нужно. Это открыло всю совокупность существующих знаний, на которые мы могли смотреть. Нужна ли вообще токенизация? Может быть нет! Это привело нас к экспериментам с новыми методами, менее связанными с нашим существующим подходом:

  • Запускаем его через алгоритмы секвенирования ДНК - медленно на тренировку, но отличные результаты!
  • Локально чувствительное хеширование
  • Поиск ближайшего соседа - с большим пространством для творчества в векторах признаков, которые мы ему добавляем!

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

Это была наша первая публикация о выпуске, в которой мы выбираем то, что нам особенно нравится в выпуске, и публикуем сообщения о нем.

При этом следите за обновлениями, и, если вы живете в северном полушарии, обязательно наслаждайтесь весной!

- Эмиль