Jsonnet
Learning
Tutorial
Syntax
Любой JSON это валидная Jsonnet программа, поэтому сфокусируемся на том что jsonnet добавляет в json
Начнем с примера в котором не происходит никаких вычислений но используется новый синтаксис:
-
Поля которые являются допустимыми идентификаторами - не нуждаются в кавычках
-
Завершающие запятые на концах массивов и объектов (в голом json нельзя написать вот так
{"a":1,"b":2,"c":3,}
(запятая после тройки))
Пример:
Имеем jsonnet с запятой после тройки🚀 cat commas.jsonnet { a: 1, b: 2, c: 3, }
Рендерим (запятой нет)
🚀 jsonnet commas.jsonnet | jq -c {"a":1,"b":2,"c":3}
То есть получается валидный json (json не позволяет иметь конечную запятую)
🚀 cat commas.json { "a": 1, "b": 2, "c": 3, } 🚀 cat commas.json | jq parse error: Expected another key-value pair at line 5, column 1
-
Комментарии
/* A C-style comment. */ # A Python-style comment. // Comment
-
Можно использовать как двойные так и одинарные кавычки
Их можно миксовать, например как тут - использованы двойные -"Farmer's Gin"
вместо одинарных с экранированием'Farmer\'s Gin'
-
Текстовые блоки позволяют описывать дословные многострочные тексты -
|||
🚀 cat main.jsonnet [ ||| A clear \tred drink. |||, ] 🚀 jsonnet main.jsonnet [ "A clear\n\\tred drink.\n" ] 🚀 jsonnet main.jsonnet | jq .[] -r A clear \tred drink.
-
@
перед строкой превращает однострочную строку в дословную (aka сырая строка)🚀 cat main.jsonnet [ @'1. A clear \n\tred drink.', '2. A clear \n\tred drink.', ] 🚀 jsonnet main.jsonnet | jq .[] -r 1. A clear \n\tred drink. 2. A clear red drink.
Variables
Переменные это простейший путь избежать дублирования
Ключевое слово local
определяет переменную
Объявление переменной рядом с другими полями оканчивается запятой, в остальных случаях точкой с запятой
🚀 cat main.jsonnet
// A regular definition.
local house_rum = 'Banks Rum';
{
// A definition next to fields.
local pour = 1.5,
Daiquiri: {
ingredients: [
{ kind: house_rum, qty: pour },
],
served: 'Straight Up',
}
}
🚀 jsonnet main.jsonnet
{
"Daiquiri": {
"ingredients": [
{
"kind": "Banks Rum",
"qty": 1.5
}
],
"served": "Straight Up"
}
}
References
Другой способ избежать дублирования - ссылаться на объекты:
self
ссылается на текущий объект$
ссылается на внешний объект['foo']
обращаемся к полю объекта.foo
можно использовать если имя поля является идентификатором[10]
обращаемся к элементу массива по номеру- Разрешены произвольно длинные пути
- Разрешены срезы массивов как
arr[10:20:2]
(как в Python) - Строки можно искать/нарезать по юникодным кодам
🚀 cat main.jsonnet
local full_name = {firstname: 'Ivan', lastname: 'Dudin'};
{
local age = 24,
local username = 'vandud',
list: [
{
full_name: full_name,
age: age,
username: username
},
$['list'][0].username,
],
list2: self.list
}
🚀 jsonnet main.jsonnet
{
"list": [
{
"age": 24,
"full_name": {
"firstname": "Ivan",
"lastname": "Dudin"
},
"username": "vandud"
},
"vandud"
],
"list2": [
{
"age": 24,
"full_name": {
"firstname": "Ivan",
"lastname": "Dudin"
},
"username": "vandud"
},
"vandud"
]
}
Arithmetic
Арифметика включает в себя числовые операции такие как умножение, а так же различные операции над другими типами:
- Арифметика с плавающей точкой, побитовые операции, булева логика
- Строки можно конкатенировать через оператор
+
, который неявно преобразует один из операндов к строке если потребуется - Две строки можно сравнить через оператор
<
(unicode codepoint order) - Объекты могут быть скомбинированы через оператор
+
где правосторонний объект победит при конфликте полей - Тест есть ли поле в объекте через
in
==
глубокое равенство значений- Python-совместимое форматирование строк доступно через
%
. При комбинировании с|||
может быть использовано для темплейтинга текстовых файлов
{ | {
concat_array: [1, 2, 3] + [4], | "concat_array": [
| 1,
| 2,
| 3,
| 4
| ],
concat_string: '123' + 4, | "concat_string": "1234",
equality1: 1 == '1', | "equality1": false,
equality2: [{}, { x: 3 - 1 }] | "equality2": true,
== [{}, { x: 2 }], |
ex1: 1 + 2 * 3 / (4 + 5), | "ex1": 1.6666666666666665,
// Bitwise operations first cast to int. |
ex2: self.ex1 | 3, | "ex2": 3,
// Modulo operator. |
ex3: self.ex1 % 2, | "ex3": 1.6666666666666665,
// Boolean logic |
ex4: (4 > 3) && (1 <= 3) || false, | "ex4": true,
// Mixing objects together |
obj: { a: 1, b: 2 } + { b: 3, c: 4 }, | "obj": {
| "a": 1,
| "b": 3,
| "c": 4
| },
// Test if a field is in an object |
obj_member: 'foo' in { foo: 1 }, | "obj_member": true,
// String formatting |
str1: 'The value of self.ex2 is ' | "str1": "The value of self.ex2 is 3.",
+ self.ex2 + '.', |
str2: 'The value of self.ex2 is %g.' | "str2": "The value of self.ex2 is 3.",
% self.ex2, |
str3: 'ex1=%0.2f, ex2=%0.2f' | "str3": "ex1=1.67, ex2=3.00",
% [self.ex1, self.ex2], |
// By passing self, we allow ex1 and ex2 to |
// be extracted internally. |
str4: 'ex1=%(ex1)0.2f, ex2=%(ex2)0.2f' | "str4": "ex1=1.67, ex2=3.00",
% self, |
// Do textual templating of entire files: |
str5: ||| | "str5": "ex1=1.67\nex2=3.00\n"
ex1=%(ex1)0.2f |
ex2=%(ex2)0.2f |
||| % self, |
} | }
local is_even(x) = x % 2 == 0;
{ | {
result1: is_even(5), | "result1": false,
result2: is_even(4) | "result2": true
} | }
Functions
Как в питоне, функции имеют позиционные параметры, именованные параметры и дефолтные аргументы
Прерывания также поддерживаются
Множество функций уже реализовано в stdlib - https://jsonnet.org/ref/stdlib.html
// Define a local function.
// Default arguments are like Python:
local my_function(x, y=10) = x + y;
// Define a local multiline function.
local multiline_function(x) =
// One can nest locals.
local temp = x * 2;
// Every local ends with a semi-colon.
[temp, temp + 1];
local object = {
// A method
my_method(x): x * x,
};
{
// Functions are first class citizens.
call_inline_function:
(function(x) x * x)(5),
call_multiline_function: multiline_function(4),
// Using the variable fetches the function,
// the parens call the function.
call: my_function(2),
// Like python, parameters can be named at
// call time.
named_params: my_function(x=2),
// This allows changing their order
named_params2: my_function(y=3, x=2),
// object.my_method returns the function,
// which is then called like any other.
call_method1: object.my_method(3),
standard_lib:
std.join(' ', std.split('foo/bar', '/')),
len: [
std.length('hello'),
std.length([1, 2, 3]),
],
}
Conditionals
Условные конструкции выглядят как if b then e else e
. Ветвь 'else' опциональна и по умолчанию возвращает 'null'
🚀 cat main.jsonnet
local my_func(s) =
if std.asciiLower(s) == 'vandud' then {
username: s,
firstname: 'Ivan',
lastname: 'Dudin',
age: '24',
position: 'DevOps'
} else {
username: s,
};
[
my_func('test'),
my_func('vAnDuD')
]
🚀 jsonnet main.jsonnet
[
{
"username": "test"
},
{
"age": "24",
"firstname": "Ivan",
"lastname": "Dudin",
"position": "DevOps",
"username": "vAnDuD"
}
]
Computed Field Names
Jsonnet объекты могут быть использованы как std::map
или как похожие структуры данных из обычных языков
- Напомним что поиск по полю можно сделать так
obj[e]
- Определить поле в объекте можно так
{[e]: ... }
self
или локальные переменные объекта не могут быть использованы при вычислении полей, поскольку объект еще не создан- Если при вычислении объекта значение поля становится
null
, то поле опускается. Это хорошо работает с условным ветвлением по умолчанию
🚀 cat main.jsonnet
local func(s) = {
[if s == 'a' then 'Abracadabra'
else if s == 'b' then 'Bob'
else if s == 'c' then 'Cucumber']: s,
};
{
a: func('a'),
b: func('b'),
c: func('c'),
}
🚀 jsonnet main.jsonnet
{
"a": {
"Abracadabra": "a"
},
"b": {
"Bob": "b"
},
"c": {
"Cucumber": "c"
}
}
Array and Object Comprehension
Что если ты хочешь создать массив или объект и ты не знаешь как много элементов/полей они будут содержать в рантайме. Jsonnet имеет Python-style конструкторы для массивов и объектов
- Может быть использована любая вложенность для
for
иif
- Вложение ведет себя как вложенный цикл, хотя тело будет записано первым
Imports
Можно импортировать и код и данные из файлов
- Конструкция
import
работает как копипаст Jsonnet-кода - Jsonnet-файлы созданные для импорта должны иметь расширение
.libsonnet
- Сырой JSON можно импортировать тем же путем
importstr
иimportbit
для импорта utf-8 и бинарных данных соответственно
Обычно импортированный Jsonnet контент складывается в локальную переменную верхнего уровня. Это похоже на способ хранения модулей из других языков программирования. Jsonnet-библиотеки обычно возвращают объект, так они могут быть легко расширены. Независимо от того соблюдается ли описанная конвенция
🚀 cat mylib.libsonnet
{
name: 'vandud',
age: 25,
gender: 'male'
}
🚀 cat textfile.txt
Jsonnet - это очень круто!
🚀 cat main.jsonnet
local mylib = import 'mylib.libsonnet';
{
name: mylib.name,
text: importstr 'textfile.txt'
}
🚀 jsonnet main.jsonnet
{
"name": "vandud",
"text": "Jsonnet - это очень круто!\n"
}
Errors
Ошибки могут возникать из-за работы языка или из-за логики кода. Трассировка даст контекст ошибки
- Чтобы вызвать ошибку:
error "foo"
- Для утверждения условия перед выражением:
assert "foo";
- Кастомный текст ошибки:
assert "foo" : "message";
- Assert поля имеют свойства:
assert self.f == 10
Parameterize Entire Config
Jsonnet герметичен: он всегда генерирует одинаковые данные вне зависимости от среды выполнения. Это важное свойство, но бывают моменты когда ты хочешь иметь выбираемые параметры на верхнем уровне. Есть два способа достичь этого:
- External variables - переменные которые доступны по всему конфигу через
std.extVar("foo")
- Top-level arguments - когда конфиг представлен в виде функции
External variables
Следующий пример привязывает две внешние переменные. Любое Jsonnet-значение может быть привязано к внешней переменной, даже функции:
prefix
привязано к строке"Happy Hour "
brunch
привязано кtrue
Значения конфигурируются когда виртуальная машина Jsonnet инициализируется за счет прокидывания либо 1) Jsonnet кода (который вычислит значение) 2) сырой строки
jsonnet --ext-str prefix="Happy Hour " \
--ext-code brunch=true ...
А в коде переменная может учитываться как-то так:
local fizz = if std.extVar('brunch') then
'Cheap Sparkling Wine'
else
'Champagne';
Top-level arguments
Альтернативно этот же самый код может быть написан с использованием аргументов верхнего уровня, когда весь конфиг описан в виде функции
Отличается следующим:
- Значения должны быть протянуты через файлы
- Должны иметься дефолтные значения
- Конфиг с аргументами верхнего уровня также может быть импортирован как библиотека и вызван как функция с прокинутыми параметрами
В общем, аргументы верхнего уровня это более безопасный и простой способ параметризации всего конфига, потому что переменные не глобальны и ясно, какие части конфигурации зависят от их окружения. Однако они требуют более явного переноса значений в другой импортируемый код. Вот эквивалентный вызов Jsonnet cli:
jsonnet --tla-str prefix="Happy Hour " \
--tla-code brunch=true ...
В коде выглядит так:
local fizz = if brunch then
'Cheap Sparkling Wine'
else
'Champagne',
(Почти то же самое)
Object-Orientation
Обычно ориентированность на объекты позволяет легко определять множество вариаций от единой "базы"
В отличие от Java, C++ и Python, где классы расширяют другие классы, в Jsonnet объекты расширяют другие объекты
- Объекты (которые мы наследуем от JSON)
- Оператор компоновки объектов
+
, который объединяет два объекта, выбирая правую часть при коллизиях - Ключевое слово
self
, ссылка на текущий объект
Когда эти функции объединяются вместе со следующими новыми функциями, все становится намного интереснее:
- Скрытые поля, определенные с помощью
::
, которые не отображаются в сгенерированном JSON-файле - Ключевое слово
super
используется для доступа к полям в базовом объекте - Синтаксис
+:
field для переопределения глубоко вложенных полей
Grafana JSON Model
Дашборды в графане представляются json-объектами которые хранят метаданные дашборда. Метаданные включают в себя свойства дашборда, метаданные из панелей, переменные для шаблонизирования, запросы панелей итд
Когда пользователь создает новый дашборд, инициализируется json со следующими полями:
id
- не известен пока дашборд не сохранят (айди назначает сервер)
{
"id": null,
"uid": "cLV5GDCkz",
"title": "New dashboard",
"tags": [],
"timezone": "browser",
"editable": true,
"graphTooltip": 1,
"panels": [],
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"time_options": [],
"refresh_intervals": []
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": "5s",
"schemaVersion": 17,
"version": 0,
"links": []
}
Name | Usage |
---|---|
id | уникальный числовой идентификатор дашборда (выдается базой) |
uid | уникальный идентификатор дашборда который может быть сгенерирован кем угодно (например создателем дашборда) |
title | заголовок дашборда |
tags | тэги дашборда, массив строк |
style | тема дашборда dark /light |
timezone | таймзона дашборда utc /browser |
editable | можно ли редактировать дашборд |
graphTooltip | 0 - без общего прицела и подсказки 1 - общий прицел 2 - общий прицел и общая подсказка |
time | временной диапазон отображаемый дашбордом |
timepicker | timepicker metadata |
templating | templating metadata |
annotations | annotations metadata |
refresh | интервал автообновления |
schemaVersion | версия json схемы |
version | версия дашборда (число), инкрементируется после каждого обновления дашборда |
panels | массив панелей |
Панели это строительные блоки дашборда, они содержат запросы к датасорсам, типы графиков, алиасы итд. JSON секции panels
содержит массив JSON объектов, каждый из которых отражает отдельную панель
Многие поля являются общими для всех панелей, но некоторые поля зависят от типа панели
Ниже пример текстовой панели:
"panels": [
{
"type": "text",
"title": "Panel Title",
"gridPos": {
"x": 0,
"y": 0,
"w": 12,
"h": 9
},
"id": 4,
"mode": "markdown",
"content": "# title"
}
]
Размер панели и позиционирование
w
- 1-24 - ширина панели (ширина дашборда - 24)h
- высота панели в юнитах, каждый юнит по 30pxx
- позиция по оси X, те же единицы измерения что и уw
y
- позиция по оси Y, те же единицы измерения что и уh
Сетка имеет негативную гравитацию (панели двигаются вверх если их ничего не ограничивает в этом)
Пример: (click to expand)
...
"panels": [
{
"type": "text",
"title": "Panel Custom Title",
"gridPos": {
"x": 0,
"y": 0,
"w": 12,
"h": 9
},
"id": 1,
"mode": "markdown",
"content": "# title \n__bold text__"
},
{
"type": "text",
"title": "Panel Custom Title #2",
"gridPos": {
"x": 6,
"y": 1,
"w": 12,
"h": 9
},
"id": 1,
"mode": "markdown",
"content": "# title \n__bold text__"
}
],
...
timepicker
Это autorefresh выпадашка
"timepicker": {
"collapse": false,
"enable": true,
"notice": false,
"now": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"status": "Stable",
"type": "timepicker"
}
templating
Секция templating
содержит массив переменных для шаблонизации с их сохраненными значениями и с какими-то метаданными
Пример: (click to expand)
...
"templating": {
"enable": true,
"list": [
{
"allFormat": "wildcard",
"current": {
"tags": [],
"text": "prod",
"value": "prod"
},
"datasource": null,
"includeAll": true,
"name": "env",
"options": [
{
"selected": false,
"text": "All",
"value": "*"
},
{
"selected": false,
"text": "stage",
"value": "stage"
},
{
"selected": false,
"text": "test",
"value": "test"
}
],
"query": "tag_values(cpu.utilization.average,env)",
"refresh": false,
"type": "query"
},
{
"allFormat": "wildcard",
"current": {
"text": "apache",
"value": "apache"
},
"datasource": null,
"includeAll": false,
"multi": false,
"multiFormat": "glob",
"name": "app",
"options": [
{
"selected": true,
"text": "tomcat",
"value": "tomcat"
},
{
"selected": false,
"text": "cassandra",
"value": "cassandra"
}
],
"query": "tag_values(cpu.utilization.average,app)",
"refresh": false,
"regex": "",
"type": "query"
}
]
},
...
Name | Usage |
---|---|
enable | включен ли темплейтинг |
list | массив объектов, каждый отражает переменную |
allFormat | какой использовать формат для стягивания всех значений из датасорса: wildcard /glob /regex /pipe /etc |
current | задает текущее выбранное значение переменной |
data source | датасорс для переменной |
includeAll | доступно ли значение All |
multi | доступен ли выбор множества значений |
multiFormat | какой использовать формат для стягивания таймсерий из датасорса |
name | имя переменной |
options | массив доступных для выбора значений переменной |
query | запрос в датасорс для получения значений переменной |
refresh | |
regex | |
type | тип переменной custom /query /interval |
Переменные
Переменная это плейсхолдер для значения
Можно использовать переменные в запросах метрик и в заголовках панелей
Когда ты выбираешь новое значение переменной с помощью выпадашки сверху дашборда - все панели перерисуются в соответствии с новым значением
Темплейт это запрос в котором используется переменная
Типы переменных
Variable Type | Description |
---|---|
Query | список значений полученный с помощью запроса в датасорс |
Custom | заранее описанный через запятую список возможных значений |
Text box | поле для ввода значения с опциональным дефолтным значением |
Constant | скрытая константа |
Data source | позволяет быстро менять датасорс для всего дашборда |
Interval | отображает временные отрезки |
Ad hoc filters | ключ=значение фильтр который применится ко всем запросам (работает не для всех датасорсов) |
Global variables | встроенные переменные которые могут быть использованы в запросах |
Chained variables | запрос переменной может содержать другую переменную |
Чтобы применить переменную можно использовать два синтаксиса
$varname
- этот синтаксис простой но не позволяет использовать переменную посреди слова${var_name}
- этот синтаксис уже позволяет использовать переменную внутри какого-то слова${var_name:<format>}
- этот синтаксис дает больше контроля над тем как графана будет раскрывать значение тут описаны возможные форматы
Grafonnet
Раньше была библиотека Grafonnet-lib - https://github.com/grafana/grafonnet-lib
Она писалась людьми вручную поэтому отставала от актуальных возможностей графаны
Поэтому появилась новая библиотека Grafonnet - https://github.com/grafana/grafonnet
Она генерируется автоматически из Grafana SDK, поэтому она лучше
Спера делаем в репе
jb init
jb install github.com/grafana/grafonnet/gen/grafonnet-latest@main
init
создаст пустой jsonnetfile.json
Далее jb init
установит указанную библиотеку и пропишет ее в этот файл (а также будет создан jsonnetfile.lock.json
с конкретными версиями и хэшами)
Теперь у нас есть папка vendor
со всеми зависимостями
Пример простейшего дашборда:
🚀 cat test-dashboard.jsonnet
local grafonnet = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';
grafonnet.dashboard.new('My Test Dashboard')
🚀 jsonnet -J vendor/ test-dashboard.jsonnet
{
"schemaVersion": 39,
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "utc",
"title": "My Test Dashboard"
}
Импорт библиотеки происходит следующей строкой:
local grafonnet = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet';
Если открыть этот файл, то видно что latest
ссылается на последнюю версию
🚀 cat vendor/github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet
import 'github.com/grafana/grafonnet/gen/grafonnet-v11.0.0/main.libsonnet'
Важно помнить что библиотека генерится из SDK, и версия библиотеки совпадает с версией графаны
У меня Grafana 10.4.8, поэтому я беру не latest
, а v10.4.0
local grafonnet = import 'github.com/grafana/grafonnet/gen/grafonnet-v10.4.0/main.libsonnet';
Также нужно убедиться что в jsonnetfile.json
прописана правильная версия и если изменял jsonnetfile.json
то надо сделать jb update