О языке VRL

Vector Remap Language (VRL) — язык на основе выражений, разработанный для безопасной и эффективной работы с данными наблюдения (журналами и метриками). У него простой синтаксис, богатый набор встроенных функций, предназначенных для работы с данными наблюдения, и многочисленные возможности, которые выгодно отличают его от других языков. Начиная с версии 0.12 VRL стал общедоступным языком.

До версии 0.12 Vector поддерживал два типа преобразований: статические преобразования и преобразования во время выполнения.

  • Статические преобразования — выполняют только одно конкретное действие и основаны на конфигурации. Например, не использующееся более преобразование remove_fields удаляет поля, указанные в конфигурации Vector.

  • Преобразования в ходе выполнения — позволяют изменять данные о событиях с помощью полноценных исполняющих сред, таких как Lua.

Оба этих варианта позволяли пользователям успешно преобразовывать данные, но имели серьезные ограничения, которые необходимо было устранить:

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

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

Классический квадрант-анализ выглядит так:

VRL diagram

VRL устраняет этот компромисс. Рассмотрим подробнее каждый из этих пунктов.

Языки конфигурации плохо подходят для выражения преобразований данных

Обычно процессы наблюдаемости включают жесткий список статических преобразований, которые используют синтаксис конфигурации. Их называют "преобразованиями", "фильтрами", "функциями". Если присмотреться, они важны — выполняют единую задачу и предотвращают выполнение неправильных действий на стороне пользователя. При более общем рассмотрении преобразования похожи на плохо продуманный язык программирования, пытающийся быть одновременно и языком конфигурации, и языком преобразования данных.

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

Для примера возьмем очень распространенный случай использования — преобразуем общий журнал (Apache), поступающий из Docker.

{
"time": "2021-02-03T21:13:54.713161211Z",
"stream": "stdout",
"log": "5.86.210.12 - zieme4647 [03/Feb/2021:21:13:55 -0200] \"GET /embrace/supply-chains/dynamic/vertical HTTP/1.0\" 201 20574"
}

Анализ этого журнала без VRL в Vector выглядит следующим образом:

# ... источники ...
# Анализ внутреннего журнала Syslog
[transforms.parse_syslog]
type = "regex_parser"
inputs = ["parse_docker"]
patterns = ['^(?P<host>\S+) (?P<client>\S+) (?P<user>\S+) \[(?<timestamp>[\w:/]+\s[+\-]\d{4})\] "(?<method>\S+) (?<resource>.+?) (?<protocol>\S+)" (?<status>\d{3}) (?<bytes_out>\S+)$']
field = "log"


# Удаление дублирующихся полей времени и журнала Docker

[transform.remove_log]
type = "remove_fields"
inputs = ["parse_syslog"]
fields = ["time", "log"]

# Принудительная обработка полей
[transforms.coerce_fields]
type = "coercer"
inputs = ["remove_log"]
types.timestamp = "timestamp"
types.status = "int"
types.bytes_out = "int"

# ... приемники данных ...`

В результате получается такой общий вывод:

{
"bytes_out": 20574,
"host": "5.86.210.12",
"method": "GET",
"resource": "/embrace/supply-chains/dynamic/vertical",
"protocol": "HTTP/1.0",
"status": 201,
"timestamp": "2021-02-03T23:13:55Z",
"client": "-",
"user": "zieme4647"
}

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

Вместо того чтобы объединять эти задачи, как делают Logstash, Fluentd и другие языки, Vector разделяет их, позволяя вам выбирать предпочтительный язык конфигурации (TOML, YAML или JSON) и предлагая специально разработанный язык для преобразования данных (VRL). Но так ли необходим VRL? Разве нельзя использовать Lua, JavaScript или любой другой существующий язык?

Преобразования времени выполнения медленные и небезопасные

Преобразования времени выполнения позволяют изменять данные о событиях, используя всю мощь исполняющей среды языка программирования (например, Lua или JavaScript). Они достаточно надежны, чтобы справиться даже с самыми сложными ситуациями, но имеют существенные недостатки. Это делает их весьма опасными для работы в критически важных инфраструктурах, таких как процессы наблюдаемости:

  1. Во-первых, дорого обходится производительность. Например, в среднем Lua работает примерно на 60% медленнее, чем статические преобразования Vector на основе Rust. Другие исполняющие среды, такие как JavaScript, работают еще медленнее.

  2. Во-вторых, существуют серьезные риски безопасности и защиты. Это делает неэффективным использование преобразований в критически важных инфраструктурах (например, процессы наблюдаемости). Также надо учитывать отсутствие безопасности памяти, динамическую оценку кода, доступ к устройствам ввода-вывода, непроверяемые деревья зависимостей, отсутствие "песочницы" и все риски безопасности, связанные со временем обработки.

  3. Наконец, крайне высокая опциональность влечет за собой проблемы производительности и надежности, что делает преобразования трудноуправляемыми.

Преобразования времени выполнения помогли пользователям Vector справиться со многими проблемами. Однако имеются и недостатки их использования:

  • Непредусмотренные искажения данных, приводящие к сбоям в работе процессов.

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

  • Низкая производительность, неспособная справиться с колеблющимся объемом данных.

  • Сложный код, делающий процессы неуправляемыми.

Решение: язык Vector Remap Language

Возьмем следующий фрагмент:

{
"time": "2021-02-03T21:13:54.713161211Z",
"stream": "stdout",
"log": "5.86.210.12 - zieme4647 [03/Feb/2021:21:13:55 -0200] \"GET /embrace/supply-chains/dynamic/vertical HTTP/1.0\" 201 20574"
}

Его можно проанализировать с помощью языка VRL:

. = parse_common_log!(.log)
.total_bytes = del(.size)
.internal_request = ip_cidr_contains("5.86.0.0/16", .host) ?? false`

На выходе имеем:

{
"host": "5.86.210.12",
"internal_request": true,
"user": "zieme4647",
"timestamp": "2021-02-03T23:13:55Z",
"message": "GET /embrace/supply-chains/dynamic/vertical HTTP/1.0",
"method": "GET",
"path": "/embrace/supply-chains/dynamic/vertical",
"protocol": "HTTP/1.0",
"total_bytes": 20574,
"status": 201
}

Как видно, приведенная выше конфигурация значительно проще в написании, чтении и управлении. Кроме того, она поддерживает оптимальную производительность и не создает проблем с безопасностью, возникающих при использовании преобразований во время выполнения, таких как Lua и JavaScript. Добавлено дополнительное поле (internal_request) чтобы показать, как VRL решает сложные задачи для статических преобразований.

Принципы и характеристики языка VRL

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

Типобезопасность и надежность

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

{
"time":"2021-02-03T21:13:54.713161211Z",
"stream": "stdout",
"log": "5.86.210.12 - zieme4647 [03/Feb/2021:21:13:55 -0200] \"GET /embrace/supply-chains/dynamic/vertical HTTP/1.0\" 201 20574"
}

Его необходимо проанализировать и привести к результату:

{
"host": "5.86.210.12",
"user": "zieme4647",
"timestamp": "2021-02-03T23:13:55Z",
"message": "GET /embrace/supply-chains/dynamic/vertical HTTP/1.0",
"method": "GET",
"path": "/embrace/supply-chains/dynamic/vertical",
"protocol": "HTTP/1.0",
"total_bytes": 20574,
"status": 201
}

Пользователь, только начавший работать с VRL, может написать следующую программу:

. = parse_common_log(.log)
.total_bytes = del(.size)

При компиляции он получит следующее сообщение об ошибке (Vector):

error[E103]: unhandled fallible assignment
┌─ :1:5
│
1 │ . = parse_common_log(.log)
│ --- ^^^^^^^^^^^^^^^^^^^^^^
│ │   │
│ │   this expression is fallible
│ │   update the expression to be infallible
│ or change this to an infallible assignment:
│ ., err = parse_common_log(.log)
│
= see documentation about error handling at https://errors.vrl.dev/#handling
= learn more about error code 103 at https://errors.vrl.dev/103
= see language documentation at https://vrl.dev

Как видно, VRL требует от пользователя обработки любого выражения, которое может привести к ошибке во время выполнения. В нашем случае .log может не быть строкой, поэтому нужно либо указать тип поля .log, либо обработать ошибку в случае, если .log не является строкой. Чтобы устранить эту ошибку, пользователь должен выполнить одно из трех действий:

  1. Обработка ошибки

    ., err = parse_common_log(.log)
    if err != null {
    .malformed = true
    log("Failed to parse common-log: " + err, level: "error")
    } else {
    .total_bytes = del(.size)
    }

    Если событие имеет неправильную форму (.log не является отформатированной строкой common-log), мы не выводим ошибку в лог и добавляем поле .malformed. Это сохраняет исходные данные и позволяет легко направить искаженные данные на проверку. Часто обработкой этой ошибки пренебрегают, а это приводит к потере данных и простою системы.

  2. Выявление ошибки и прерывание программы

    . = parse_common_log!(.log)
    .total_bytes = del(.size)

    Иногда наличие искаженных данных бывает неприемлемым, и программа должна быть прервана. В этом случае VRL предлагает варианты ошибочных функций. Здесь указывается, что добавление к вызову функции постфикса ! вызовет сбой и прервет выполнение программы. При этом Vector перейдет к обработке следующего события, но одновременно запишет в журнал сообщение об ошибке. Таким образом, пользователям предлагается выбрать метод обработки ошибок, а не удивляться их появлению.

  3. Указание типов

    .log = to_string!(.log)
    
    ., err = parse_common_log(.log)
    if err != null {
    # This error only occurs for malformed *strings*
    log("Failed to parse common-log: " + err, level: "error")
    } else {
    .total_bytes = del(.size)
    }

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