Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Автоматизация экспериментов с помощью Kotlin DS...

AvitoTech
November 25, 2017

Автоматизация экспериментов с помощью Kotlin DSL — Александр Тарасов (Одноклассники)

В большом проекте нельзя просто так взять и сделать фичу доступной всем клиентам. Поэтому проводятся эксперименты, которые требуют внесения изменений вручную, что ведёт к временным издержкам и порой ошибкам. Чтобы убрать эти факторы, в Odnoklassniki.ru решили автоматизировать подготовку и запуск экспериментов. В докладе Александр Тарасов расскажет, почему для этого был выбран Kotlin, а не классические инструменты управления конфигурацией (такие, как Ansible), почему хороший DSL и инструментарий критически важны для этой задачи и какие проблемы пришлось преодолеть, чтобы всё работало, как изначально задумано.

Kotlin Night Moscow
25/11/2017

AvitoTech

November 25, 2017
Tweet

More Decks by AvitoTech

Other Decks in Technology

Transcript

  1. Мнение докладчика может не совпадать с официальной позицией его работодателя,

    начальника, коллег или других специалистов. 
 Все представленные в докладе сведения, примеры, выводы и другую информацию вы можете использовать на свой страх и риск. За все ваши действия ответственность несёте только вы сами. 
 Этот доклад не совсем про Kotlin, хотя его в нём более чем достаточно. 3 Disclaimer
  2. 4 71 млн Посещают нас в месяц 8000 Серверов Одноклассники

    в цифрах 10 тыс Выполненных экспериментов
  3. • Нельзя просто так взять и выкатить фичу на клиента

    • Технические риски • Продуктовые риски • Запуск новых фич происходит поэтапно • Наполнение кэшей • А/B/…/Z-тестирование • Процесс запуска фичи называется экспериментом 6 Зачем нужны эксперименты?
  4. 11 Эксперимент в деталях Code Branches if / switch Config

    by partition Logs/ Monitoring observe by server by geo
  5. 17 Всё очень просто one partition //step#1 app.mood.withFriendsEnabled: 0 //step#2

    app.mood.withFriendsEnabled: 0-63 //step#3 app.mood.withFriendsEnabled: 0-255 several partitions all partitions
  6. • Релизный цикл • Нужно вспоминать, где и какие настройки

    нужно включить/прописать • Хранение экспериментов • Разные нотации обозначения настроек • Отдельный тип задач в Jira • Запуск экспериментов • руками через UI • комментарий в Jira • сообщение в ТамТам 19 Проблемы
  7. • Консистентность настроек между средами • Непонятно в какой стадии

    эксперимент • Забыли написать комментарий? 21 Проблемы #2
  8. • Описание эксперимента в виде некоторого DSL • Храним в

    git-е как код • Автоматизированный накат/откат настроек • Запускаем через CLI • Используем API системы хранения конфигурации • Автоматизация сопутствующих действий • Постим комментарии в Jira, TamTam 23 Чего бы хотелось?
  9. • Быстро делать прототипы по автоматизации • Три универсальных модуля

    • sh • uri • template • Готовый DSL и механизмы запуска 25 Ansible - почти идеальный кандидат
  10. 26 Описание эксперимента --- - include: step0.yml tags: - step0

    - include: step1.yml tags: - step1 … - include: step5.yml tags: - step5
  11. 27 Каждый шаг имеет своё описание --- - include: step0.yml

    tags: - step0 - include: step1.yml tags: - step1 … - include: step5.yml tags: - step5 --- - hosts: local gather_facts: no tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "" anotherProperty: name: "app.mood.anotherProperty" value: "" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  12. 28 Сокращённый вариант - hosts: local tasks: - name: Cleanup

    all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: ""
  13. 29 Общая часть всех экспериментов - hosts: local tasks: -

    name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: ""
  14. 30 Ansible Role - name: update properties uri: url: "{{

    pms_host }}/api/property" method: POST user: "{{ pms_username }}" password: "{{ pms_password }}" body: applicationName: "{{ application_name }}" hostName: "{{ host_name }}" propertyName: "{{ item.value.name }}" propertyValue: "{{ item.value.value }}" body_format: json status_code: 200 headers: Content-Type: "application/json" with_dict: "{{ properties }}"
  15. 31 Куда - name: update properties uri: url: "{{ pms_host

    }}/api/property" method: POST user: "{{ pms_username }}" password: "{{ pms_password }}" body: applicationName: "{{ application_name }}" hostName: "{{ host_name }}" propertyName: "{{ item.value.name }}" propertyValue: "{{ item.value.value }}" body_format: json status_code: 200 headers: Content-Type: "application/json" with_dict: "{{ properties }}"
  16. 32 Что - name: update properties uri: url: "{{ pms_host

    }}/api/property" method: POST user: "{{ pms_username }}" password: "{{ pms_password }}" body: applicationName: "{{ application_name }}" hostName: "{{ host_name }}" propertyName: "{{ item.value.name }}" propertyValue: "{{ item.value.value }}" body_format: json status_code: 200 headers: Content-Type: "application/json" with_dict: "{{ properties }}"
  17. 33 Properties - hosts: local tasks: - name: Cleanup all

    include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: ""
  18. 40 Проблема копипаста. Step#1 --- - hosts: local gather_facts: no

    tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "проинициализирую настройки" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "" anotherProperty: name: "app.mood.anotherProperty" value: "" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  19. 41 Проблема копипаста. Step#2 --- - hosts: local gather_facts: no

    tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "на одну партицию" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "63" anotherProperty: name: "app.mood.anotherProperty" value: "63" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  20. 42 Проблема копипаста. Step#N --- - hosts: local gather_facts: no

    tasks: - name: Cleanup all include_role: name: pms vars: step_definition: "на всех" application_name: odnoklassniki-web properties: withFriends: name: "app.mood.withFriends" value: "0-255" anotherProperty: name: "app.mood.anotherProperty" value: "0-255" with_items: - "{{ host_common }}" loop_control: loop_var: host_name
  21. • Нужно писать модули • на питоне • я не

    знаю питон :) • Обобщённый DSL это хорошо для решения общих задач • Частное решение позволяет писать меньше кода 43 Проблемы использования Ansible-а
  22. 45 DSL Design. Список шагов эксперимента steps { empty("off") {}

    user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  23. 46 Инициализация/Отключка steps { empty("off") {} user("onebot") { bot(prop("botId")) }

    partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  24. 47 Открываем на бота steps { empty("off") {} user("onebot") {

    bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  25. 48 Открытие по партициям steps { empty("off") {} user("onebot") {

    bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  26. 49 На всех steps { empty("off") {} user("onebot") { bot(prop("botId"))

    } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  27. 50 Настройки. Отделяем мух от котлет experiment { steps {

    empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  28. 51 Step + Property experiment { steps { empty("off") {}

    user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  29. 52 Компактно и понятно experiment { steps { empty("off") {}

    user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  30. • Java - знаем, любим, но вербозно • кто-то пишет

    скриптинг на Java? • JVM - очень здорово • интероп с уже написанными библиотеками • знакомая платформа 54 Что же выбрать для реализации?
  31. • Groovy, JRuby, Javascript, … • синтаксический сахар • JSR223

    • Но мы хотим: • в идеале писать скрипты и их обработку на одном языке • иметь статическую типизацию, • проверки на этапе компиляции • близкую к идеальной поддержку в IDE 55 А что вообще есть то?
  32. 56 JSR223. Как заиспользовать Java-класс //javascript var exp = Java.type("one.exps.Experiment");

    //JRuby require "java" java_import "one.exps.Experiment" exp = Experiment.new //Groovy def exp = new Experiment()
  33. • Kotlin потому что: • синтаксический сахар • JSR223 •

    компиляция • статическая типизация • хороший code completion • хотелось попробовать новое и интересное ;) 57 Почему Kotlin?
  34. 59 Как реализовать такой DSL? experiment { steps { empty("off")

    {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  35. 60 Древовидная структура @DslMarker annotation class ElementMarker @ElementMarker abstract class

    Element { val children = arrayListOf<Element>() fun <T : Element> make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }
  36. 61 Init function experiment { steps { // call init

    here partition("half"){ // call init here } } ... }
  37. 62 Init function @DslMarker annotation class ElementMarker @ElementMarker abstract class

    Element { val children = arrayListOf<Element>() fun <T : Element> make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }
  38. 63 Safe-Type experiment { steps { // call init here

    partition("half"){ // call init here } } ... } fun partition(name: String, init: PartitionStep.() -> Unit) = make(PartitionStep(name), init)
  39. 64 Lateinit vars experiment { description = "very cool experiment”

    steps { // call init here partition("half"){ // call init here } } … }
  40. • Кто знает зачем он нужен? • Валидация скоупа внутри

    DSL • вложенность элементов 65 @DSLMarker
  41. 66 @DSLMarker @DslMarker annotation class ElementMarker @ElementMarker abstract class Element

    { val children = arrayListOf<Element>() fun <T : Element> make(element: T, init: T.() -> Unit): T { element.init() children.add(element) return element } }
  42. 67 Без @DSLMarker experiment { steps { steps { }

    } ... } Шаги могут быть вложенными (?)
  43. 68 С @DSLMarker experiment { steps { steps { }

    } ... } Шаги не могут быть вложенными
  44. 71 Оптимизация DSL experiment { steps { empty("off") {} user("onebot")

    { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  45. 72 Засахарим немного experiment { steps { openByPartitionSteps() } properties("odnoklassniki-web",

    prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  46. 73 Extension Function - это очень удобно //extension function fun

    Steps.openByPartitionSteps() { empty("off") {} user("onebot") { bot(prop("botId")) } partition("partition") { part("0") } partition("quarter") { part("0-63") } partition("half") { part("0-63") part("64-127") } partition("all") { part("0-255") } }
  47. 74 Кастомные свойства experiment { steps { custom("messages", "сообщения для

    настроения 'Хочу общаться'") { properties("odnoklassniki-web", prop("hosts")) { property("app.property.custom", "i_want_comm=SEND_MESSAGE") } } openByPartitionSteps() } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  48. 75 Это всё-таки язык программирования custom("42everywhere", "всем по 42") {

    properties("odnoklassniki-web", prop("hosts")) { calcProperty("app.complex.property", { PMS.property("odnoklassniki-web", "app.complex.property") .lines() .map { Converter.LIST_STRING.convert(it) .minus("42").plus("42") .joinToString(",") } .joinToString("\n") }) } }
  49. • Файл с расширением .kts • Имплицитные аргументы • args[0]

    • Запускается через command line • kotlinc -script <script_name> -classpath <> • Запускается с помощью IDE 77 Kotlin Script
  50. 78 Kotlin Script. Ожидание import runner.* runner.run { exp {

    steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } } }
  51. • Нужно как-то подключить классы проекта • Нужно как-то подключить

    все зависимости проекта • Gradle не умеет (по крайней мере из коробки) • IDEA не умеет* 79 Kotlin Script. CMD kotlinc -script <script_name> -classpath <>
  52. 81 Kotlin Script. IDEA java -Dfile.encoding=UTF-8 \ -classpath "kotlin-compiler.jar:kotlin-reflect.jar: kotlin-stdlib.jar:kotlin-script-runtime.jar"

    org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -script ~/experiments/src/main/kotlin/one/exps/scripts/XPRM-10222.kts XPRM-10222.kts:1:8: error: unresolved reference: one import one.exps.*
  53. 82 Kotlin Script. IDEA java -Dfile.encoding=UTF-8 \ -classpath "kotlin-compiler.jar:kotlin-reflect.jar: kotlin-stdlib.jar:kotlin-script-runtime.jar"

    org.jetbrains.kotlin.cli.jvm.K2JVMCompiler -script ~/experiments/src/main/kotlin/one/exps/scripts/XPRM-10222.kts XPRM-10222.kts:1:8: error: unresolved reference: one import one.exps.*
  54. 83 https://github.com/andrewoma/kotlin-script #!/bin/sh exec kotlins -cp `mvncp <lib>` !# import

    runner.* runner.run { exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }
  55. 84 https://github.com/andrewoma/kotlin-scripting-kickstarter #!/usr/bin/env kotlin-script.sh import runner.* fun main(args: Array<String>) {

    runner.run { exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } } }
  56. 85 https://github.com/holgerbrandl/kscript #!/usr/bin/env kscript //DEPS one:experiments:1.0.0 import runner.* runner.run {

    exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }
  57. • Необходимо писать обвязки • Нельзя запустить из IDE (ну

    кроме как console) • Непонятно как подебажить скрипт, если очень нужно 86 Проблемы Kotlin Script
  58. 88 Когда нету Engine-a //JSR223 //ConsoleExecutor.kt fun main(args : Array<String>)

    { ScriptEngineManager().getEngineByExtension("kts")!! .eval(getResource("${args[0]}.kts").readText()) } Exception in thread "main" kotlin.KotlinNullPointerException
  59. 90 А что если в зависимостях Guava? //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version") compile("com.google.guava:guava:$guava_version")

    //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
  60. 91 Метод есть, но нам говорят что нет //build.gradle compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version")

    compile("com.google.guava:guava:$guava_version") //META-INF/services/javax.script.ScriptEngineFactory org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkState(...)
  61. 94 Теперь уже не хватает класса //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version")

    { exclude module: 'kotlin-compiler' } compile("com.google.guava:guava:$guava_version") java.lang.NoClassDefFoundError: com/intellij/openapi/util/Disposer
  62. 95 Класс есть, а пакет другой //build.gradle compile "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version" compile("org.jetbrains.kotlin:kotlin-script-util:$kotlin_version")

    { exclude module: 'kotlin-compiler' } compile("com.google.guava:guava:$guava_version") org.jetbrains.kotlin.com.intellij.openapi.util.Disposer
  63. 99 Апгрейдим Console Executor fun main(args : Array<String>) { val

    parsedArgs = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }
  64. 100 Парсим аргументы fun main(args : Array<String>) { val parsedArgs

    = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }
  65. 101 Знакомая фабрика fun main(args : Array<String>) { val parsedArgs

    = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }
  66. 102 Выполнение эксперимента fun main(args : Array<String>) { val parsedArgs

    = Args(ArgParser(args)) val experimentName = parsedArgs.experiment val factory = ScriptEngineManager().getEngineByExtension("kts")!! factory.eval(getResource("$experimentName.kts").readText()) val executionPlan = ExecutionPlan(expRegistry.find(experimentName)) ConsoleExecutor(executionPlan).execute(parsedArgs.stage) }
  67. 105 Передаём аргументы через Gradle //build.gradle processResources { from ‘src/main/kotlin/one/exps/scripts'

    } mainClassName = 'one.exps.execution.ConsoleExecutorKt' run { standardInput = System.in args System.getProperty("exec.args", "").split() }
  68. 109 CMD + Docker docker run --rm -it one.experiment/experiments:1.0 -e

    dev -s all -x XPRM-9756 https://github.com/bmuschko/gradle-docker-plugin
  69. 114 Разделение ответственностей user model experiment { steps { openByPartitionSteps()

    } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") property("app.mood.anotherProperty") } }
  70. 116 “Плоская” модель исполнения execution model execution { steps {

    init { description='на никого' property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.mood.withFriendsEnabled", "propertyValue": "" } property { ... } } ... all { ... } } definition='возможность отмечать друзей в настроении' }
  71. 117 Модель исполнения. Один шаг init { description='на никого' property

    { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.mood.withFriendsEnabled", "propertyValue": "" } property { "applicationName": "odnoklassniki-web", "hostName": "host-odnoklassniki-web", "propertyName": "app.mood.anotherProperty", "propertyValue": "" } }
  72. 121 Сверка реального и ожидаемого состояния 1 2 3 C

    validation model app.mood.withFriendsEnabled: 0-63 app.mood.anotherProperty: 0-63 check state C
  73. 123 Основная модель - дополнительные модели • Модель исполнения •

    Модель валидации • Модель планирования • создание задач в Jira • визуализация плана • ??? • любые хотелки
  74. 126 Spek. Пиши как пользователь exp { steps { empty("off")

    {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }
  75. 127 Тестовый эксперимент val experiment = exp { steps {

    empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } }
  76. 128 Spek. Новая спецификация object ExperimentToExecutionPlanSpek : Spek ({ describe("Experiment")

    { val experiment = exp { steps { empty("off") {} ... partition("all") { part("0-255") } } properties("odnoklassniki-web", prop("hosts")) { property("app.mood.withFriendsEnabled") } } } })
  77. 129 Добавляем действия и проверки object ExperimentToExecutionPlanSpek : Spek ({

    describe("Experiment") { ... } on("Make execution plan") { val execPlan = ExecutionPlan(experiment) it("should has execution steps") { assertTrue(execPlan.steps.isNotEmpty()) } it("something else") { ... } } })
  78. 130 Spek + build.gradle buildscript { ext.kotlin_version = '1.1.51' ext.spek_version

    = '1.1.5' ext.junit_runner_version = '1.0.1' repositories { ... } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:$junit_runner_version" } }
  79. 131 Нужно согласовать версии buildscript { ext.kotlin_version = '1.1.51' ext.spek_version

    = '1.1.5' ext.junit_runner_version = '1.0.1' repositories { ... } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:$junit_runner_version" } }
  80. • Скрипты можно хранить в SCM • относимся к экспериментам

    как к коду • Унифицированный DSL • Автоматизированный запуск • из IDE или командной строки • меньше ручных действий • Приятный язык для создания и поддержки DSL 135 Что мы получили?
  81. • Kotlin Script пока что “сыроват” • есть способы преодоления

    • Инструменты не идеальны • а так хотелось • Не всё и не сразу можно покрыть базовыми сценариями • постоянные доработки • свой синтаксический сахар • баланс между сложностью освоения и гибкостью инструмента 136 Trade-Offs
  82. 137 Мы ещё в начале пути ;) t MVP CLI

    Jira, TamTam Auto Launch Jira custom fields and charts Auto Feedback Charts Smart monitoring