Jsonnet

Learning

Learning

Tutorial

Syntax

Любой JSON это валидная Jsonnet программа, поэтому сфокусируемся на том что jsonnet добавляет в json

Начнем с примера в котором не происходит никаких вычислений но используется новый синтаксис:

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

Другой способ избежать дублирования - ссылаться на объекты:

🚀 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

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

{                                             | {
  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 или как похожие структуры данных из обычных языков

🚀 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 конструкторы для массивов и объектов

Imports

Можно импортировать и код и данные из файлов

Обычно импортированный 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

Ошибки могут возникать из-за работы языка или из-за логики кода. Трассировка даст контекст ошибки

Parameterize Entire Config

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

External variables

Следующий пример привязывает две внешние переменные. Любое Jsonnet-значение может быть привязано к внешней переменной, даже функции:

Значения конфигурируются когда виртуальная машина 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 объекты расширяют другие объекты

Когда эти функции объединяются вместе со следующими новыми функциями, все становится намного интереснее:

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"
  }
]

Размер панели и позиционирование

Сетка имеет негативную гравитацию (панели двигаются вверх если их ничего не ограничивает в этом)

Пример: (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__"
  }
],
...

Screenshot_2021_02_02-12_49_03-2023-11-20-at-16grafana-panels.png


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"
  }

Можно задать свои интервалы
Screenshot_2021_02_02-12_49_03-2023-11-20-at-16timepicker.png


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"
    }
  ]
},
...

Screenshot_2021_02_02-12_49_03-2023-11-20-at-16templating-result.png

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 запрос переменной может содержать другую переменную

Чтобы применить переменную можно использовать два синтаксиса

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