Skip to content

Commit

Permalink
Merge pull request #118 from lucidsoftware/PROD-2927-enumerate-monito…
Browse files Browse the repository at this point in the history
…ring-teams

Add ability to enumerate monitoring team options in config
  • Loading branch information
tnnrj authored Jan 22, 2025
2 parents 785cf65 + d558507 commit 1759756
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 84 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ src_managed/
project/boot/
project/plugins/project/
project/project/
project/metals.sbt

# Scala-IDE specific
.scala_dependencies
.idea
.idea_modules
.bsp
.metals
.bloop/
project/metals.sbt
.bloop
.vscode
1 change: 1 addition & 0 deletions admin/app/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class PiezoAdminComponents(context: Context) extends BuiltInComponentsFromContex

lazy val schedulerFactory: WorkerSchedulerFactory = new WorkerSchedulerFactory()
lazy val jobFormHelper: JobFormHelper = wire[JobFormHelper]
lazy val monitoringTeams: MonitoringTeams = MonitoringTeams(configuration)

lazy val triggers: Triggers = wire[Triggers]
lazy val jobs: Jobs = wire[Jobs]
Expand Down
7 changes: 4 additions & 3 deletions admin/app/com/lucidchart/piezo/admin/controllers/Jobs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import scala.jdk.CollectionConverters._
import scala.collection.mutable
import scala.Some
import scala.io.Source
import com.lucidchart.piezo.admin.models.MonitoringTeams

trait ImportResult {
val jobKey: Option[JobKey]
Expand All @@ -35,14 +36,14 @@ trait ImportResult {
case class ImportSuccess(val jobKey: Option[JobKey], val errorMessage: String = "", val success: Boolean=true) extends ImportResult
case class ImportFailure(val jobKey: Option[JobKey], val errorMessage: String, val success: Boolean=false) extends ImportResult

class Jobs(schedulerFactory: WorkerSchedulerFactory, jobView: html.job, cc: ControllerComponents) extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport {
class Jobs(schedulerFactory: WorkerSchedulerFactory, jobView: html.job, cc: ControllerComponents, monitoringTeams: MonitoringTeams) extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport {
val scheduler = logExceptions(schedulerFactory.getScheduler())
val properties = schedulerFactory.props
val jobHistoryModel = logExceptions(new JobHistoryModel(properties))
val triggerMonitoringPriorityModel = logExceptions(new TriggerMonitoringModel(properties))

val jobFormHelper = new JobFormHelper()
val triggerFormHelper = new TriggerFormHelper(scheduler)
val triggerFormHelper = new TriggerFormHelper(scheduler, monitoringTeams)

// Allow up to 1M
private val maxFormSize = 1024 * 1024
Expand Down Expand Up @@ -235,7 +236,7 @@ class Jobs(schedulerFactory: WorkerSchedulerFactory, jobView: html.job, cc: Cont
ImportFailure(Some(jobDetail.getKey), "Trigger Import Error:"+errorMessage)
} else {
val triggers = triggersBinding.flatMap(_.value)
triggers.foreach { case (trigger, monitoringPriority, errorTime, monitoringTeam) =>
triggers.foreach { case TriggerFormValue(trigger, monitoringPriority, errorTime, monitoringTeam) =>
scheduler.scheduleJob(trigger)
triggerMonitoringPriorityModel.setTriggerMonitoringRecord(
trigger.getKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.lucidchart.piezo.admin.controllers

import com.lucidchart.piezo.TriggerMonitoringPriority
import com.lucidchart.piezo.TriggerMonitoringPriority.TriggerMonitoringPriority
import com.lucidchart.piezo.admin.models.MonitoringTeams
import com.lucidchart.piezo.admin.utils.CronHelper
import java.text.ParseException
import org.quartz._
Expand All @@ -11,7 +12,8 @@ import play.api.data.format.Formats.parsing
import play.api.data.format.Formatter
import play.api.data.validation.{Constraint, Constraints, Invalid, Valid, ValidationError}

class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
case class TriggerFormValue(trigger: Trigger, priority: TriggerMonitoringPriority, maxErrorTime: Int, monitoringTeam: Option[String])
class TriggerFormHelper(scheduler: Scheduler, monitoringTeams: MonitoringTeams) extends JobDataHelper {

private def simpleScheduleFormApply(repeatCount: Int, repeatInterval: Int): SimpleScheduleBuilder = {
SimpleScheduleBuilder
Expand Down Expand Up @@ -47,7 +49,7 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
triggerMonitoringPriority: String,
triggerMaxErrorTime: Int,
triggerMonitoringTeam: Option[String],
): (Trigger, TriggerMonitoringPriority, Int, Option[String]) = {
): TriggerFormValue = {
val newTrigger: Trigger = TriggerBuilder
.newTrigger()
.withIdentity(name, group)
Expand All @@ -59,15 +61,15 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
.forJob(jobName, jobGroup)
.usingJobData(jobDataMap.getOrElse(new JobDataMap()))
.build()
(
TriggerFormValue(
newTrigger,
TriggerMonitoringPriority.withName(triggerMonitoringPriority),
triggerMaxErrorTime,
triggerMonitoringTeam,
)
}

private def triggerFormUnapply(tp: (Trigger, TriggerMonitoringPriority, Int, Option[String])): Option[
private def triggerFormUnapply(value: TriggerFormValue): Option[
(
String,
String,
Expand All @@ -83,7 +85,7 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
Option[String],
),
] = {
val trigger = tp._1
val trigger = value.trigger
val (triggerType: String, simple, cron) = trigger match {
case cron: CronTrigger => ("cron", None, Some(cron.getScheduleBuilder))
case simple: SimpleTrigger => ("simple", Some(simple.getScheduleBuilder), None)
Expand All @@ -100,9 +102,9 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
simple.asInstanceOf[Option[SimpleScheduleBuilder]],
cron.asInstanceOf[Option[CronScheduleBuilder]],
Some(trigger.getJobDataMap),
tp._2.toString,
tp._3,
tp._4,
value.priority.toString,
value.maxErrorTime,
value.monitoringTeam,
),
)
}
Expand Down Expand Up @@ -132,7 +134,7 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
}
}

def buildTriggerForm: Form[(Trigger, TriggerMonitoringPriority, Int, Option[String])] = Form(
def buildTriggerForm: Form[TriggerFormValue] = Form(
mapping(
"triggerType" -> nonEmptyText(),
"group" -> nonEmptyText(),
Expand All @@ -159,8 +161,14 @@ class TriggerFormHelper(scheduler: Scheduler) extends JobDataHelper {
.verifying(
"Job does not exist",
fields => {
scheduler.checkExists(fields._1.getJobKey)
scheduler.checkExists(fields.trigger.getJobKey)
},
)
.verifying(
"A valid team is required if monitoring is on",
fields => {
!monitoringTeams.teamsDefined || fields.priority == TriggerMonitoringPriority.Off || fields.monitoringTeam.exists(monitoringTeams.value.contains[String])
}
),
)
}
Expand Down
28 changes: 17 additions & 11 deletions admin/app/com/lucidchart/piezo/admin/controllers/Triggers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import scala.util.Try
import play.api.Logging
import play.api.i18n.I18nSupport

class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponents) extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport {
class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponents, monitoringTeams: MonitoringTeams)
extends AbstractController(cc) with Logging with ErrorLogging with play.api.i18n.I18nSupport {

val scheduler = logExceptions(schedulerFactory.getScheduler())
val properties = schedulerFactory.props
val triggerHistoryModel = logExceptions(new TriggerHistoryModel(properties))
val triggerMonitoringPriorityModel = logExceptions(new TriggerMonitoringModel(properties))
val triggerFormHelper = new TriggerFormHelper(scheduler)
val triggerFormHelper = new TriggerFormHelper(scheduler, monitoringTeams)

def firesFirst(time: Date)(trigger1: Trigger, trigger2: Trigger): Boolean = {
val time1 = trigger1.getFireTimeAfter(time)
Expand Down Expand Up @@ -160,17 +161,17 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent
case "simple" => new DummySimpleTrigger(jobGroup, jobName)
}
val newTriggerForm = triggerFormHelper.buildTriggerForm.fill(
(dummyTrigger, TriggerMonitoringPriority.Low, 300, None)
TriggerFormValue(dummyTrigger, TriggerMonitoringPriority.Low, 300, None)
)
Ok(
com.lucidchart.piezo.admin.views.html.editTrigger(
TriggerHelper.getTriggersByGroup(scheduler),
monitoringTeams.value,
newTriggerForm,
formNewAction,
false,
false
)
(request, implicitly)
)
}
}
Expand All @@ -194,28 +195,30 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent
(TriggerMonitoringPriority.Low, 300, None)
}
val editTriggerForm = triggerFormHelper.buildTriggerForm.fill(
(triggerDetail, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam)
TriggerFormValue(triggerDetail, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam)
)
if (isTemplate) {
Ok(
com.lucidchart.piezo.admin.views.html.editTrigger(
TriggerHelper.getTriggersByGroup(scheduler),
monitoringTeams.value,
editTriggerForm,
formNewAction,
false,
isTemplate
)(request, implicitly)
)
)
}
else {
Ok(
com.lucidchart.piezo.admin.views.html.editTrigger(
TriggerHelper.getTriggersByGroup(scheduler),
monitoringTeams.value,
editTriggerForm,
formEditAction(group, name),
true,
isTemplate
)(request, implicitly)
)
)
}
}
Expand All @@ -231,14 +234,15 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent
BadRequest(
com.lucidchart.piezo.admin.views.html.editTrigger(
TriggerHelper.getTriggersByGroup(scheduler),
monitoringTeams.value,
formWithErrors,
formEditAction(group, name),
true,
false
)
),
value => {
val (trigger, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam) = value
val TriggerFormValue(trigger, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam) = value
scheduler.rescheduleJob(trigger.getKey(), trigger)
triggerMonitoringPriorityModel.setTriggerMonitoringRecord(
trigger.getKey,
Expand All @@ -258,14 +262,15 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent
BadRequest(
com.lucidchart.piezo.admin.views.html.editTrigger(
TriggerHelper.getTriggersByGroup(scheduler),
monitoringTeams.value,
formWithErrors,
formNewAction,
false,
false
)
),
value => {
val (trigger, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam) = value
val TriggerFormValue(trigger, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam) = value
try {
scheduler.scheduleJob(trigger)
triggerMonitoringPriorityModel.setTriggerMonitoringRecord(
Expand All @@ -279,17 +284,18 @@ class Triggers(schedulerFactory: WorkerSchedulerFactory, cc: ControllerComponent
} catch {
case alreadyExists: ObjectAlreadyExistsException =>
val form = triggerFormHelper.buildTriggerForm.fill(
(trigger, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam)
TriggerFormValue(trigger, triggerMonitoringPriority, triggerMaxErrorTime, triggerMonitoringTeam)
)
Ok(
com.lucidchart.piezo.admin.views.html.editTrigger(
TriggerHelper.getTriggersByGroup(scheduler),
monitoringTeams.value,
form,
formNewAction,
false,
false,
errorMessage = Some("Please provide unique group-name pair")
)(request, implicitly)
)
)
}
}
Expand Down
39 changes: 39 additions & 0 deletions admin/app/com/lucidchart/piezo/admin/models/MonitoringTeams.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.lucidchart.piezo.admin.models

import play.api.Configuration
import java.nio.file.Files
import play.api.libs.json.Json
import scala.util.Try
import java.io.File
import java.io.FileInputStream
import play.api.libs.json.JsArray
import play.api.Logging
import scala.util.control.NonFatal
import scala.util.Failure

case class MonitoringTeams(value: Seq[String]) {
def teamsDefined: Boolean = value.nonEmpty
}
object MonitoringTeams extends Logging {
def apply(configuration: Configuration): MonitoringTeams = {
val path = configuration.getOptional[String]("com.lucidchart.piezo.admin.monitoringTeams.path")

val value = path.flatMap { p =>
Try {
Json.parse(new FileInputStream(p))
.as[JsArray]
.value
.map(entry => (entry \ "name").as[String])
.toSeq
}.recoverWith {
case NonFatal(e) =>
logger.error(s"Error reading monitoring teams from $p", e)
Failure(e)
}.toOption
}.getOrElse(Seq.empty)

MonitoringTeams(value)
}

def empty: MonitoringTeams = MonitoringTeams(Seq.empty)
}
25 changes: 16 additions & 9 deletions admin/app/com/lucidchart/piezo/admin/views/editTrigger.scala.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
@(
triggersByGroup: scala.collection.mutable.Buffer[(String, scala.collection.immutable.List[org.quartz.TriggerKey])],
triggerForm: Form[(org.quartz.Trigger, com.lucidchart.piezo.TriggerMonitoringPriority.Value, Int, Option[String])],
monitoringTeams: Seq[String],
triggerForm: Form[com.lucidchart.piezo.admin.controllers.TriggerFormValue],
formAction: play.api.mvc.Call,
existing: Boolean,
isTemplate: Boolean,
errorMessage: Option[String] = None,
scripts: List[String] = List[String]("js/jobData.js", "js/typeAhead.js")
scripts: List[String] = List[String]("js/jobData.js", "js/typeAhead.js", "js/triggerMonitoring.js")
)(
implicit
request: play.api.mvc.Request[AnyContent],
messagesProvider: play.api.i18n.MessagesProvider
messagesProvider: play.api.i18n.MessagesProvider,
)

@import com.lucidchart.piezo.TriggerMonitoringPriority
Expand Down Expand Up @@ -67,12 +68,18 @@ <h4 class="text-danger">@triggerForm.errors.filter(_.key == "").map(_.message).m
}
}
@helper.select(triggerForm("triggerMonitoringPriority"), TriggerMonitoringPriority.values.map(tp => tp.name -> tp.name), Symbol("_label") -> "Monitoring Priority", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("class") -> "form-control", Symbol("value") -> triggerForm.data.get("triggerMonitoringPriority").getOrElse(TriggerMonitoringPriority.Low), Symbol("placeholder") -> TriggerMonitoringPriority.Low)
@helper.input(triggerForm("triggerMaxErrorTime"), Symbol("_label") -> "Monitoring - Max Seconds Between Successes", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMaxErrorTime").getOrElse(300)) { (id, name, value, args) =>
<input type="number" class="form-control form-inline-control " name="@name" id="@id" @toHtmlArgs(args)>
}
@helper.input(triggerForm("triggerMonitoringTeam"), Symbol("_label") -> "Monitoring team", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMonitoringTeam").getOrElse(None)) { (id, name, value, args) =>
<input type="text" class="form-control form-inline-control" name="@name" id="@id" @toHtmlArgs(args)>
}
<div id="triggerMonitoringDetails">
@helper.input(triggerForm("triggerMaxErrorTime"), Symbol("_label") -> "Monitoring - Max Seconds Between Successes", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMaxErrorTime").getOrElse(300)) { (id, name, value, args) =>
<input type="number" class="form-control form-inline-control " name="@name" id="@id" @toHtmlArgs(args)>
}
@if(monitoringTeams.nonEmpty) {
@helper.select(triggerForm("triggerMonitoringTeam"), monitoringTeams.map(mt => mt -> mt), Symbol("_default") -> "Select team", Symbol("_label") -> "Monitoring team", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("class") -> "form-control", Symbol("value") -> triggerForm.data.get("triggerMonitoringTeam").getOrElse(""))
} else {
@helper.input(triggerForm("triggerMonitoringTeam"), Symbol("_label") -> "Monitoring team", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-4", Symbol("placeholder") -> "", Symbol("value") -> triggerForm.data.get("triggerMonitoringTeam").getOrElse(None)) { (id, name, value, args) =>
<input type="text" class="form-control form-inline-control" name="@name" id="@id" @toHtmlArgs(args)>
}
}
</div>

@helper.input(triggerForm("description"), Symbol("_label") -> "Description", Symbol("labelClass") -> "col-sm-2 text-right", Symbol("inputDivClass") -> "col-sm-10", Symbol("placeholder") -> "Description", Symbol("value")-> triggerForm.data.get("description").getOrElse("")) { (id, name, value, args) =>
<input type="text" class="form-control form-inline-control " name="@name" id="@id" @toHtmlArgs(args)>
Expand Down
11 changes: 11 additions & 0 deletions admin/conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,14 @@ com.lucidchart.piezo.heartbeatFile="/tmp/piezo/workerHeartbeatFile"
com.lucidchart.piezo.admin.production=false
healthCheck.worker.minutesBetween=5
play.application.loader=com.lucidchart.piezo.admin.PiezoAdminApplicationLoader

# Monitoring teams
# ~~~~~
# Path to a JSON file that fills the "Monitoring Team" dropdown on editTrigger
# in the admin UI with a predefined set of team names. File format:
# [
# {"name": "team1"},
# {"name": "team2"}
# ]
# If this is left blank, monitoring team will be a freeform input.
# com.lucidchart.piezo.admin.monitoringTeams.path = "/etc/piezo/teams.json"
15 changes: 15 additions & 0 deletions admin/public/js/triggerMonitoring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
window.addEventListener('load', () => {
const priorityInput = document.getElementById('triggerMonitoringPriority');
const setMonitoringFieldVisibility = () => {
const priority = priorityInput.value;
const monitoringDetails = document.getElementById('triggerMonitoringDetails');
if (priority == 'Off') {
monitoringDetails.style.display = 'none'; // hide
} else {
monitoringDetails.style.display = 'block'; // show
}
};

priorityInput.addEventListener('change', setMonitoringFieldVisibility);
setMonitoringFieldVisibility();
}, {once: true});
Loading

0 comments on commit 1759756

Please sign in to comment.