diff --git a/README.md b/README.md index bd64910..d0b0a49 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ Kuvasz (pronounce as [ˈkuvɒs]) is an ancient hungarian breed of livestock & gu - Email notifications through SMTP - Slack notifications through webhoooks - Telegram notifications through the Bot API +- PagerDuty integration with automatic incident resolution - Configurable data retention period ### Future ideas 🚧 - Regular Lighthouse audits for your websites -- Pagerduty, Opsgenie integration ## ⚡️ Quick start guide diff --git a/docs/Integrating-with-PagerDuty.md b/docs/Integrating-with-PagerDuty.md new file mode 100644 index 0000000..a753ae6 --- /dev/null +++ b/docs/Integrating-with-PagerDuty.md @@ -0,0 +1,72 @@ +# PagerDuty + Kuvasz Integration Benefits + +* Notify on-call responders based on alerts sent from Kuvasz if any of your monitors is down or has an invalid SSL certificate +* Incidents will automatically resolve in PagerDuty when the metric in Kuvasz returns to normal (e.g. your monitor is up again, or you renew your site's certificate) + +# How it Works + +* If one of your monitor is down, has an invalid SSL certificate, or the certificate will expire in 30 days, Kuvasz will send an event to a service in PagerDuty. Events from Kuvasz will trigger a new incident on the corresponding PagerDuty service, or group as alerts into an existing incident. +* Once your monitor is up again, or has a valid SSL certificate, a resolve event will be sent to the PagerDuty service to resolve the alert, and associated incident on that service. + +# Requirements +There are no special requirements prior to this setup to successfully integrate PagerDuty and Kuvasz. + +# Support + +If you need help with this integration, please contact the maintainer of Kuvasz, Adam Kobor at adam@akobor.me. + +# Integration Walkthrough +## In PagerDuty + +### Integrating With a PagerDuty Service +1. From the **Configuration** menu, select **Services**. +2. There are two ways to add an integration to a service: + * **If you are adding your integration to an existing service**: Click the **name** of the service you want to add the integration to. Then, select the **Integrations** tab and click the **New Integration** button. + * **If you are creating a new service for your integration**: Please read our documentation in section [Configuring Services and Integrations](https://support.pagerduty.com/docs/services-and-integrations#section-configuring-services-and-integrations) and follow the steps outlined in the [Create a New Service](https://support.pagerduty.com/docs/services-and-integrations#section-create-a-new-service) section, selecting "Kuvasz" as the **Integration Type** in step 4. Continue with the "In Kuvasz" section (below) once you have finished these steps. +3. Enter an **Integration Name** in the format `monitoring-tool-service-name` (e.g. Kuvasz-Shopping-Cart) and select "Kuvasz" from the Integration Type menu. +4. Click the **Add Integration** button to save your new integration. You will be redirected to the Integrations tab for your service. +5. An **Integration Key** will be generated on this screen. Keep this key saved in a safe place, as it will be used when you configure the integration with Kuvasz in the next section. +![](https://pdpartner.s3.amazonaws.com/ig-template-copy-integration-key.png) + +## In Kuvasz + +You have to set up two things in Kuvasz in order to make the integration work: + +1. Provide an environment variable to the application with the name `ENABLE_PAGERDUTY_EVENT_HANDLER` and a value of `true`. You can read more about the available configuration variables of Kuvasz [here](https://github.com/kuvasz-uptime/kuvasz/wiki/Configuration). +2. Set your PagerDuty integration key for the desired monitor in one of the following ways: + +**Providing a key when you create your monitor:** + +```shell +curl --location --request POST 'https://your.kuvasz.host:8080/monitors/' \ +--header 'Authorization: Bearer YourKuvaszAccessToken' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "name": "my_first_monitor", + "url": "https://website.to.check", + "uptimeCheckInterval": 60, + "pagerdutyIntegrationKey": "YourSecretIntegrationKeyFromPagerDuty" +}' +``` + +**Adding/updating a key for an existing monitor:** + +```shell +curl --location --request PUT 'https://your.kuvasz.host:8080/monitors/4/pagerduty-integration-key' \ +--header 'Authorization: Bearer YourKuvaszAccessToken' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "pagerdutyIntegrationKey": "YourSecretIntegrationKeyFromPagerDuty" +}' +``` + +# How to Uninstall + +If you want to disable a monitor's integration with PagerDuty, you can just simply delete the integration key of it with an API call like that: + +```shell +curl --location --request DELETE 'https://your.kuvasz.host:8080/monitors/4/pagerduty-integration-key' \ +--header 'Authorization: Bearer YourKuvaszAccessToken' +``` + +Alternatively you can disable the integration between Kuvasz and PagerDuty globally by setting the value of the `ENABLE_PAGERDUTY_EVENT_HANDLER` to `false` (or by omitting it completely, since its default value is `false` too). diff --git a/examples/docker-compose/docker-compose.yml b/examples/docker-compose/docker-compose.yml index 378aef7..b8b5404 100644 --- a/examples/docker-compose/docker-compose.yml +++ b/examples/docker-compose/docker-compose.yml @@ -31,3 +31,4 @@ services: ENABLE_TELEGRAM_EVENT_HANDLER: 'true' TELEGRAM_API_TOKEN: '1232312321321:GJKGHjhklfdhsklHKLFH' TELEGRAM_CHAT_ID: '1234567890' + ENABLE_PAGERDUTY_EVENT_HANDLER: 'true' diff --git a/examples/k8s/kuvasz.configmap.yml b/examples/k8s/kuvasz.configmap.yml index f7bdc18..2e93516 100644 --- a/examples/k8s/kuvasz.configmap.yml +++ b/examples/k8s/kuvasz.configmap.yml @@ -19,3 +19,4 @@ data: slack_webhook_url: "https://your.slack-webhook.url" data_retention_days: "30" telegram_event_handler_enabled: "true" + pagerduty_event_handler_enabled: "true" diff --git a/examples/k8s/kuvasz.deployment.yaml b/examples/k8s/kuvasz.deployment.yaml index ac6bc3d..ab56bde 100644 --- a/examples/k8s/kuvasz.deployment.yaml +++ b/examples/k8s/kuvasz.deployment.yaml @@ -147,3 +147,8 @@ spec: secretKeyRef: name: telegram-credentials key: chat-id + - name: ENABLE_PAGERDUTY_EVENT_HANDLER + valueFrom: + configMapKeyRef: + name: kuvasz-config + key: pagerduty_event_handler_enabled diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java index 2b34850..186ea2d 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/Monitor.java @@ -17,7 +17,7 @@ import org.jooq.Identity; import org.jooq.Name; import org.jooq.Record; -import org.jooq.Row8; +import org.jooq.Row9; import org.jooq.Schema; import org.jooq.Table; import org.jooq.TableField; @@ -33,7 +33,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class Monitor extends TableImpl { - private static final long serialVersionUID = 1271016748; + private static final long serialVersionUID = 2078797674; /** * The reference instance of monitor @@ -88,6 +88,11 @@ public Class getRecordType() { */ public final TableField SSL_CHECK_ENABLED = createField(DSL.name("ssl_check_enabled"), org.jooq.impl.SQLDataType.BOOLEAN.nullable(false).defaultValue(org.jooq.impl.DSL.field("false", org.jooq.impl.SQLDataType.BOOLEAN)), this, ""); + /** + * The column monitor.pagerduty_integration_key. + */ + public final TableField PAGERDUTY_INTEGRATION_KEY = createField(DSL.name("pagerduty_integration_key"), org.jooq.impl.SQLDataType.VARCHAR, this, ""); + /** * Create a monitor table reference */ @@ -168,11 +173,11 @@ public Monitor rename(Name name) { } // ------------------------------------------------------------------------- - // Row8 type methods + // Row9 type methods // ------------------------------------------------------------------------- @Override - public Row8 fieldsRow() { - return (Row8) super.fieldsRow(); + public Row9 fieldsRow() { + return (Row9) super.fieldsRow(); } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java index 4066485..51cd141 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/daos/MonitorDao.java @@ -165,4 +165,18 @@ public List fetchRangeOfSslCheckEnabled(Boolean lowerInclusive, Boo public List fetchBySslCheckEnabled(Boolean... values) { return fetch(Monitor.MONITOR.SSL_CHECK_ENABLED, values); } + + /** + * Fetch records that have pagerduty_integration_key BETWEEN lowerInclusive AND upperInclusive + */ + public List fetchRangeOfPagerdutyIntegrationKey(String lowerInclusive, String upperInclusive) { + return fetchRange(Monitor.MONITOR.PAGERDUTY_INTEGRATION_KEY, lowerInclusive, upperInclusive); + } + + /** + * Fetch records that have pagerduty_integration_key IN (values) + */ + public List fetchByPagerdutyIntegrationKey(String... values) { + return fetch(Monitor.MONITOR.PAGERDUTY_INTEGRATION_KEY, values); + } } diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java index 5596f0c..89ea8a4 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/pojos/MonitorPojo.java @@ -29,7 +29,7 @@ }) public class MonitorPojo implements Serializable { - private static final long serialVersionUID = -1878969857; + private static final long serialVersionUID = -1828922669; private Integer id; private String name; @@ -39,6 +39,7 @@ public class MonitorPojo implements Serializable { private OffsetDateTime createdAt; private OffsetDateTime updatedAt; private Boolean sslCheckEnabled; + private String pagerdutyIntegrationKey; public MonitorPojo() {} @@ -51,6 +52,7 @@ public MonitorPojo(MonitorPojo value) { this.createdAt = value.createdAt; this.updatedAt = value.updatedAt; this.sslCheckEnabled = value.sslCheckEnabled; + this.pagerdutyIntegrationKey = value.pagerdutyIntegrationKey; } public MonitorPojo( @@ -61,7 +63,8 @@ public MonitorPojo( Boolean enabled, OffsetDateTime createdAt, OffsetDateTime updatedAt, - Boolean sslCheckEnabled + Boolean sslCheckEnabled, + String pagerdutyIntegrationKey ) { this.id = id; this.name = name; @@ -71,6 +74,7 @@ public MonitorPojo( this.createdAt = createdAt; this.updatedAt = updatedAt; this.sslCheckEnabled = sslCheckEnabled; + this.pagerdutyIntegrationKey = pagerdutyIntegrationKey; } @Id @@ -159,6 +163,16 @@ public MonitorPojo setSslCheckEnabled(Boolean sslCheckEnabled) { return this; } + @Column(name = "pagerduty_integration_key") + public String getPagerdutyIntegrationKey() { + return this.pagerdutyIntegrationKey; + } + + public MonitorPojo setPagerdutyIntegrationKey(String pagerdutyIntegrationKey) { + this.pagerdutyIntegrationKey = pagerdutyIntegrationKey; + return this; + } + @Override public boolean equals(Object obj) { if (this == obj) @@ -216,6 +230,12 @@ else if (!updatedAt.equals(other.updatedAt)) } else if (!sslCheckEnabled.equals(other.sslCheckEnabled)) return false; + if (pagerdutyIntegrationKey == null) { + if (other.pagerdutyIntegrationKey != null) + return false; + } + else if (!pagerdutyIntegrationKey.equals(other.pagerdutyIntegrationKey)) + return false; return true; } @@ -231,6 +251,7 @@ public int hashCode() { result = prime * result + ((this.createdAt == null) ? 0 : this.createdAt.hashCode()); result = prime * result + ((this.updatedAt == null) ? 0 : this.updatedAt.hashCode()); result = prime * result + ((this.sslCheckEnabled == null) ? 0 : this.sslCheckEnabled.hashCode()); + result = prime * result + ((this.pagerdutyIntegrationKey == null) ? 0 : this.pagerdutyIntegrationKey.hashCode()); return result; } @@ -246,6 +267,7 @@ public String toString() { sb.append(", ").append(createdAt); sb.append(", ").append(updatedAt); sb.append(", ").append(sslCheckEnabled); + sb.append(", ").append(pagerdutyIntegrationKey); sb.append(")"); return sb.toString(); diff --git a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java index 61fb8ec..a09069e 100644 --- a/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java +++ b/src/jooq/java/com/kuvaszuptime/kuvasz/tables/records/MonitorRecord.java @@ -20,8 +20,8 @@ import org.jooq.Field; import org.jooq.Record1; -import org.jooq.Record8; -import org.jooq.Row8; +import org.jooq.Record9; +import org.jooq.Row9; import org.jooq.impl.UpdatableRecordImpl; @@ -34,9 +34,9 @@ @UniqueConstraint(name = "monitor_pkey", columnNames = {"id"}), @UniqueConstraint(name = "unique_monitor_name", columnNames = {"name"}) }) -public class MonitorRecord extends UpdatableRecordImpl implements Record8 { +public class MonitorRecord extends UpdatableRecordImpl implements Record9 { - private static final long serialVersionUID = 1165953385; + private static final long serialVersionUID = -1258639093; /** * Setter for monitor.id. @@ -172,6 +172,22 @@ public Boolean getSslCheckEnabled() { return (Boolean) get(7); } + /** + * Setter for monitor.pagerduty_integration_key. + */ + public MonitorRecord setPagerdutyIntegrationKey(String value) { + set(8, value); + return this; + } + + /** + * Getter for monitor.pagerduty_integration_key. + */ + @Column(name = "pagerduty_integration_key") + public String getPagerdutyIntegrationKey() { + return (String) get(8); + } + // ------------------------------------------------------------------------- // Primary key information // ------------------------------------------------------------------------- @@ -182,17 +198,17 @@ public Record1 key() { } // ------------------------------------------------------------------------- - // Record8 type implementation + // Record9 type implementation // ------------------------------------------------------------------------- @Override - public Row8 fieldsRow() { - return (Row8) super.fieldsRow(); + public Row9 fieldsRow() { + return (Row9) super.fieldsRow(); } @Override - public Row8 valuesRow() { - return (Row8) super.valuesRow(); + public Row9 valuesRow() { + return (Row9) super.valuesRow(); } @Override @@ -235,6 +251,11 @@ public Field field8() { return Monitor.MONITOR.SSL_CHECK_ENABLED; } + @Override + public Field field9() { + return Monitor.MONITOR.PAGERDUTY_INTEGRATION_KEY; + } + @Override public Integer component1() { return getId(); @@ -275,6 +296,11 @@ public Boolean component8() { return getSslCheckEnabled(); } + @Override + public String component9() { + return getPagerdutyIntegrationKey(); + } + @Override public Integer value1() { return getId(); @@ -315,6 +341,11 @@ public Boolean value8() { return getSslCheckEnabled(); } + @Override + public String value9() { + return getPagerdutyIntegrationKey(); + } + @Override public MonitorRecord value1(Integer value) { setId(value); @@ -364,7 +395,13 @@ public MonitorRecord value8(Boolean value) { } @Override - public MonitorRecord values(Integer value1, String value2, String value3, Integer value4, Boolean value5, OffsetDateTime value6, OffsetDateTime value7, Boolean value8) { + public MonitorRecord value9(String value) { + setPagerdutyIntegrationKey(value); + return this; + } + + @Override + public MonitorRecord values(Integer value1, String value2, String value3, Integer value4, Boolean value5, OffsetDateTime value6, OffsetDateTime value7, Boolean value8, String value9) { value1(value1); value2(value2); value3(value3); @@ -373,6 +410,7 @@ public MonitorRecord values(Integer value1, String value2, String value3, Intege value6(value6); value7(value7); value8(value8); + value9(value9); return this; } @@ -390,7 +428,7 @@ public MonitorRecord() { /** * Create a detached, initialised MonitorRecord */ - public MonitorRecord(Integer id, String name, String url, Integer uptimeCheckInterval, Boolean enabled, OffsetDateTime createdAt, OffsetDateTime updatedAt, Boolean sslCheckEnabled) { + public MonitorRecord(Integer id, String name, String url, Integer uptimeCheckInterval, Boolean enabled, OffsetDateTime createdAt, OffsetDateTime updatedAt, Boolean sslCheckEnabled, String pagerdutyIntegrationKey) { super(Monitor.MONITOR); set(0, id); @@ -401,5 +439,6 @@ public MonitorRecord(Integer id, String name, String url, Integer uptimeCheckInt set(5, createdAt); set(6, updatedAt); set(7, sslCheckEnabled); + set(8, pagerdutyIntegrationKey); } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/config/handlers/SlackEventHandlerConfig.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/config/handlers/SlackEventHandlerConfig.kt deleted file mode 100644 index a57b395..0000000 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/config/handlers/SlackEventHandlerConfig.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.kuvaszuptime.kuvasz.config.handlers - -import com.kuvaszuptime.kuvasz.models.dto.Validation.URI_REGEX -import io.micronaut.context.annotation.ConfigurationProperties -import io.micronaut.core.annotation.Introspected -import javax.inject.Singleton -import javax.validation.constraints.Pattern - -@ConfigurationProperties("handler-config.slack-event-handler") -@Singleton -@Introspected -class SlackEventHandlerConfig { - @Pattern(regexp = URI_REGEX) - var webhookUrl: String? = null -} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt index 73ef137..37fc3e2 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorController.kt @@ -4,9 +4,10 @@ import com.kuvaszuptime.kuvasz.models.MonitorNotFoundError import com.kuvaszuptime.kuvasz.models.ServiceError import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto +import com.kuvaszuptime.kuvasz.models.dto.MonitorDto import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto +import com.kuvaszuptime.kuvasz.models.dto.PagerdutyKeyUpdateDto import com.kuvaszuptime.kuvasz.services.MonitorCrudService -import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller @@ -19,12 +20,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag -import javax.inject.Inject @Controller("/monitors", produces = [MediaType.APPLICATION_JSON]) @Tag(name = "Monitor operations") @SecurityRequirement(name = "bearerAuth") -class MonitorController @Inject constructor( +class MonitorController( private val monitorCrudService: MonitorCrudService ) : MonitorOperations { @@ -58,7 +58,7 @@ class MonitorController @Inject constructor( ApiResponse( responseCode = "201", description = "Successful creation", - content = [Content(schema = Schema(implementation = MonitorPojo::class))] + content = [Content(schema = Schema(implementation = MonitorDto::class))] ), ApiResponse( responseCode = "400", @@ -66,7 +66,10 @@ class MonitorController @Inject constructor( content = [Content(schema = Schema(implementation = ServiceError::class))] ) ) - override fun createMonitor(monitor: MonitorCreateDto): MonitorPojo = monitorCrudService.createMonitor(monitor) + override fun createMonitor(monitor: MonitorCreateDto): MonitorDto { + val updatedPojo = monitorCrudService.createMonitor(monitor) + return MonitorDto.fromMonitorPojo(updatedPojo) + } @Status(HttpStatus.NO_CONTENT) @ApiResponses( @@ -85,7 +88,8 @@ class MonitorController @Inject constructor( @ApiResponses( ApiResponse( responseCode = "200", - description = "Successful update" + description = "Successful update", + content = [Content(schema = Schema(implementation = MonitorDto::class))] ), ApiResponse( responseCode = "400", @@ -98,6 +102,46 @@ class MonitorController @Inject constructor( content = [Content(schema = Schema(implementation = ServiceError::class))] ) ) - override fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorPojo = - monitorCrudService.updateMonitor(monitorId, monitorUpdateDto) + override fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorDto { + val updatedPojo = monitorCrudService.updateMonitor(monitorId, monitorUpdateDto) + return MonitorDto.fromMonitorPojo(updatedPojo) + } + + @ApiResponses( + ApiResponse( + responseCode = "200", + description = "Successful update or create", + content = [Content(schema = Schema(implementation = MonitorDto::class))] + ), + ApiResponse( + responseCode = "400", + description = "Bad request", + content = [Content(schema = Schema(implementation = ServiceError::class))] + ), + ApiResponse( + responseCode = "404", + description = "Not found", + content = [Content(schema = Schema(implementation = ServiceError::class))] + ) + ) + override fun upsertPagerdutyIntegrationKey(monitorId: Int, upsertDto: PagerdutyKeyUpdateDto): MonitorDto { + val updatedPojo = monitorCrudService.updatePagerdutyIntegrationKey(monitorId, upsertDto.pagerdutyIntegrationKey) + return MonitorDto.fromMonitorPojo(updatedPojo) + } + + @Status(HttpStatus.NO_CONTENT) + @ApiResponses( + ApiResponse( + responseCode = "204", + description = "Successful deletion" + ), + ApiResponse( + responseCode = "404", + description = "Not found", + content = [Content(schema = Schema(implementation = ServiceError::class))] + ) + ) + override fun deletePagerdutyIntegrationKey(monitorId: Int) { + monitorCrudService.updatePagerdutyIntegrationKey(monitorId, null) + } } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt index 351cc5b..b25e60d 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorOperations.kt @@ -2,13 +2,15 @@ package com.kuvaszuptime.kuvasz.controllers import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto +import com.kuvaszuptime.kuvasz.models.dto.MonitorDto import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto -import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.models.dto.PagerdutyKeyUpdateDto import io.micronaut.http.annotation.Body import io.micronaut.http.annotation.Delete import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Patch import io.micronaut.http.annotation.Post +import io.micronaut.http.annotation.Put import io.micronaut.http.annotation.QueryValue import io.micronaut.scheduling.TaskExecutors import io.micronaut.scheduling.annotation.ExecuteOn @@ -37,7 +39,7 @@ interface MonitorOperations { @Operation(summary = "Creates a monitor") @Post("/") @ExecuteOn(TaskExecutors.IO) - fun createMonitor(@Valid @Body monitor: MonitorCreateDto): MonitorPojo + fun createMonitor(@Valid @Body monitor: MonitorCreateDto): MonitorDto @Operation(summary = "Deletes a monitor by ID") @Delete("/{monitorId}") @@ -47,5 +49,15 @@ interface MonitorOperations { @Operation(summary = "Updates a monitor by ID") @Patch("/{monitorId}") @ExecuteOn(TaskExecutors.IO) - fun updateMonitor(monitorId: Int, @Valid @Body monitorUpdateDto: MonitorUpdateDto): MonitorPojo + fun updateMonitor(monitorId: Int, @Valid @Body monitorUpdateDto: MonitorUpdateDto): MonitorDto + + @Operation(summary = "Updates or creates a Pagerduty integration key for the given monitor") + @Put("/{monitorId}/pagerduty-integration-key") + @ExecuteOn(TaskExecutors.IO) + fun upsertPagerdutyIntegrationKey(monitorId: Int, @Valid @Body upsertDto: PagerdutyKeyUpdateDto): MonitorDto + + @Operation(summary = "Deletes the Pagerduty integration key of the given monitor") + @Delete("/{monitorId}/pagerduty-integration-key") + @ExecuteOn(TaskExecutors.IO) + fun deletePagerdutyIntegrationKey(monitorId: Int) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt index 39bdac7..632d0b1 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/DatabaseEventHandler.kt @@ -8,13 +8,10 @@ import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.util.transaction import io.micronaut.context.annotation.Context -import io.micronaut.scheduling.TaskExecutors -import io.micronaut.scheduling.annotation.ExecuteOn import org.slf4j.LoggerFactory -import javax.inject.Inject @Context -class DatabaseEventHandler @Inject constructor( +class DatabaseEventHandler( private val eventDispatcher: EventDispatcher, private val uptimeEventRepository: UptimeEventRepository, private val latencyLogRepository: LatencyLogRepository, @@ -28,7 +25,6 @@ class DatabaseEventHandler @Inject constructor( subscribeToEvents() } - @ExecuteOn(TaskExecutors.IO) private fun subscribeToEvents() { eventDispatcher.subscribeToMonitorUpEvents { event -> logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt index cf8b619..7152f0e 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/HandlersInfoSource.kt @@ -6,11 +6,10 @@ import io.micronaut.context.env.PropertySource import io.micronaut.management.endpoint.info.InfoSource import io.reactivex.Flowable import org.reactivestreams.Publisher -import javax.inject.Inject import javax.inject.Singleton @Singleton -class HandlersInfoSource @Inject constructor(private val environment: Environment) : InfoSource { +class HandlersInfoSource(private val environment: Environment) : InfoSource { override fun getSource(): Publisher = Flowable.just(retrieveConfigurationInfo()) @@ -32,6 +31,10 @@ class HandlersInfoSource @Inject constructor(private val environment: Environmen "telegram-event-handler.enabled" to environment.getBooleanProp( "handler-config.telegram-event-handler.enabled", false + ), + "pagerduty-event-handler.enabled" to environment.getBooleanProp( + "handler-config.pagerduty-event-handler.enabled", + false ) ) return MapPropertySource("handlers", mapOf("handlers" to handlerConfigs)) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt index 5878643..7886fdb 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/LogEventHandler.kt @@ -8,11 +8,10 @@ import com.kuvaszuptime.kuvasz.services.EventDispatcher import io.micronaut.context.annotation.Context import io.micronaut.context.annotation.Requires import org.slf4j.LoggerFactory -import javax.inject.Inject @Context @Requires(property = "handler-config.log-event-handler.enabled", value = "true") -class LogEventHandler @Inject constructor(eventDispatcher: EventDispatcher) { +class LogEventHandler(eventDispatcher: EventDispatcher) { companion object { private val logger = LoggerFactory.getLogger(LogEventHandler::class.java) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/PagerdutyEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/PagerdutyEventHandler.kt new file mode 100644 index 0000000..55129fe --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/PagerdutyEventHandler.kt @@ -0,0 +1,141 @@ +package com.kuvaszuptime.kuvasz.handlers + +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLMonitorEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyResolveRequest +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutySeverity +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyTriggerPayload +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyTriggerRequest +import com.kuvaszuptime.kuvasz.services.EventDispatcher +import com.kuvaszuptime.kuvasz.services.PagerdutyAPIClient +import io.micronaut.context.annotation.Context +import io.micronaut.context.annotation.Requires +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import org.slf4j.LoggerFactory + +@Context +@Requires(property = "handler-config.pagerduty-event-handler.enabled", value = "true") +class PagerdutyEventHandler( + private val eventDispatcher: EventDispatcher, + private val apiClient: PagerdutyAPIClient +) { + companion object { + private val logger = LoggerFactory.getLogger(PagerdutyEventHandler::class.java) + } + + init { + subscribeToEvents() + } + + internal fun subscribeToEvents() { + eventDispatcher.subscribeToMonitorUpEvents { event -> + logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToMonitorDownEvents { event -> + logger.debug("A MonitorDownEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLValidEvents { event -> + logger.debug("An SSLValidEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLInvalidEvents { event -> + logger.debug("An SSLInvalidEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + eventDispatcher.subscribeToSSLWillExpireEvents { event -> + logger.debug("An SSLWillExpireEvent has been received for monitor with ID: ${event.monitor.id}") + event.handle() + } + } + + private fun Single.handleResponse(): Disposable = + subscribe( + { + logger.debug("The event has been successfully sent to Pagerduty") + }, + { ex -> + if (ex is HttpClientResponseException) { + val responseBody = ex.response.getBody(String::class.java) + logger.error("The event cannot be sent to Pagerduty: $responseBody") + } + } + ) + + private val UptimeMonitorEvent.deduplicationKey: String + get() = "kuvasz_uptime_${monitor.id}" + + private val SSLMonitorEvent.deduplicationKey: String + get() = "kuvasz_ssl_${monitor.id}" + + private fun UptimeMonitorEvent.handle() { + if (monitor.pagerdutyIntegrationKey != null) { + runWhenStateChanges { event -> + when (event) { + is MonitorUpEvent -> { + if (previousEvent != null) { + val request = event.toResolveRequest(deduplicationKey) + apiClient.resolveAlert(request).handleResponse() + } + } + is MonitorDownEvent -> { + val request = event.toTriggerRequest(deduplicationKey) + apiClient.triggerAlert(request).handleResponse() + } + } + } + } + } + + private fun SSLMonitorEvent.handle() { + if (monitor.pagerdutyIntegrationKey != null) { + runWhenStateChanges { event -> + when (event) { + is SSLValidEvent -> { + if (previousEvent != null) { + val request = event.toResolveRequest(deduplicationKey) + apiClient.resolveAlert(request).handleResponse() + } + } + is SSLInvalidEvent -> { + val request = event.toTriggerRequest(deduplicationKey) + apiClient.triggerAlert(request).handleResponse() + } + is SSLWillExpireEvent -> { + val request = event.toTriggerRequest(deduplicationKey, PagerdutySeverity.WARNING) + apiClient.triggerAlert(request).handleResponse() + } + } + } + } + } + + private fun MonitorEvent.toTriggerRequest( + deduplicationKey: String, + severity: PagerdutySeverity = PagerdutySeverity.CRITICAL + ) = + PagerdutyTriggerRequest( + routingKey = monitor.pagerdutyIntegrationKey, + dedupKey = deduplicationKey, + payload = PagerdutyTriggerPayload( + summary = toStructuredMessage().summary, + source = monitor.url, + severity = severity + ) + ) + + private fun MonitorEvent.toResolveRequest(deduplicationKey: String) = + PagerdutyResolveRequest( + routingKey = monitor.pagerdutyIntegrationKey, + dedupKey = deduplicationKey + ) +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt index 3d395e4..b5598d3 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/RTCMessageEventHandler.kt @@ -5,11 +5,8 @@ import com.kuvaszuptime.kuvasz.models.events.UptimeMonitorEvent import com.kuvaszuptime.kuvasz.models.events.formatters.RichTextMessageFormatter import com.kuvaszuptime.kuvasz.services.EventDispatcher import com.kuvaszuptime.kuvasz.services.TextMessageService -import io.micronaut.http.HttpResponse import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.scheduling.TaskExecutors -import io.micronaut.scheduling.annotation.ExecuteOn -import io.reactivex.Flowable +import io.reactivex.Single import io.reactivex.disposables.Disposable import org.slf4j.Logger @@ -26,7 +23,6 @@ abstract class RTCMessageEventHandler( subscribeToEvents() } - @ExecuteOn(TaskExecutors.IO) internal fun subscribeToEvents() { eventDispatcher.subscribeToMonitorUpEvents { event -> logger.debug("A MonitorUpEvent has been received for monitor with ID: ${event.monitor.id}") @@ -62,7 +58,7 @@ abstract class RTCMessageEventHandler( messageService.sendMessage(message).handleResponse() } - private fun Flowable>.handleResponse(): Disposable = + private fun Single.handleResponse(): Disposable = subscribe( { logger.debug("The message to your configured webhook has been successfully sent") diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt index 836a387..2f8a03e 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandler.kt @@ -9,11 +9,10 @@ import com.kuvaszuptime.kuvasz.services.SMTPMailer import io.micronaut.context.annotation.Context import io.micronaut.context.annotation.Requires import org.slf4j.LoggerFactory -import javax.inject.Inject @Context @Requires(property = "handler-config.smtp-event-handler.enabled", value = "true") -class SMTPEventHandler @Inject constructor( +class SMTPEventHandler( smtpEventHandlerConfig: SMTPEventHandlerConfig, private val smtpMailer: SMTPMailer, private val eventDispatcher: EventDispatcher diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt index 28a5f7d..dc17897 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandler.kt @@ -6,11 +6,10 @@ import com.kuvaszuptime.kuvasz.services.SlackWebhookService import io.micronaut.context.annotation.Context import io.micronaut.context.annotation.Requires import org.slf4j.LoggerFactory -import javax.inject.Inject @Context @Requires(property = "handler-config.slack-event-handler.enabled", value = "true") -class SlackEventHandler @Inject constructor( +class SlackEventHandler( slackWebhookService: SlackWebhookService, eventDispatcher: EventDispatcher ) : RTCMessageEventHandler(eventDispatcher, slackWebhookService) { diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt index 675eb6f..c2542b4 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorCreateDto.kt @@ -20,7 +20,8 @@ data class MonitorCreateDto( @get:Min(MIN_UPTIME_CHECK_INTERVAL) val uptimeCheckInterval: Int, val enabled: Boolean? = true, - val sslCheckEnabled: Boolean? = false + val sslCheckEnabled: Boolean? = false, + val pagerdutyIntegrationKey: String? = null ) { fun toMonitorPojo(): MonitorPojo = MonitorPojo() .setName(name) @@ -28,4 +29,5 @@ data class MonitorCreateDto( .setEnabled(enabled) .setUptimeCheckInterval(uptimeCheckInterval) .setSslCheckEnabled(sslCheckEnabled) + .setPagerdutyIntegrationKey(pagerdutyIntegrationKey) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt index 781cfae..7b6178a 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDetailsDto.kt @@ -26,5 +26,6 @@ data class MonitorDetailsDto( val sslError: String?, val averageLatencyInMs: Int?, val p95LatencyInMs: Int?, - val p99LatencyInMs: Int? + val p99LatencyInMs: Int?, + val pagerdutyKeyPresent: Boolean ) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDto.kt new file mode 100644 index 0000000..4cba5d5 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/MonitorDto.kt @@ -0,0 +1,33 @@ +package com.kuvaszuptime.kuvasz.models.dto + +import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import io.micronaut.core.annotation.Introspected +import java.time.OffsetDateTime + +@Introspected +data class MonitorDto( + val id: Int, + val name: String, + val url: String, + val uptimeCheckInterval: Int, + val enabled: Boolean, + val sslCheckEnabled: Boolean, + val pagerdutyKeyPresent: Boolean, + val createdAt: OffsetDateTime, + val updatedAt: OffsetDateTime? +) { + companion object { + fun fromMonitorPojo(pojo: MonitorPojo) = + MonitorDto( + id = pojo.id, + name = pojo.name, + url = pojo.url, + uptimeCheckInterval = pojo.uptimeCheckInterval, + enabled = pojo.enabled, + sslCheckEnabled = pojo.sslCheckEnabled, + pagerdutyKeyPresent = !pojo.pagerdutyIntegrationKey.isNullOrBlank(), + createdAt = pojo.createdAt, + updatedAt = pojo.updatedAt + ) + } +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/PagerdutyKeyUpdateDto.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/PagerdutyKeyUpdateDto.kt new file mode 100644 index 0000000..12063e2 --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/dto/PagerdutyKeyUpdateDto.kt @@ -0,0 +1,10 @@ +package com.kuvaszuptime.kuvasz.models.dto + +import io.micronaut.core.annotation.Introspected +import javax.validation.constraints.NotBlank + +@Introspected +data class PagerdutyKeyUpdateDto( + @get:NotBlank + val pagerdutyIntegrationKey: String +) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt index fc84931..a68bfff 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/events/StructuredMessage.kt @@ -1,39 +1,41 @@ package com.kuvaszuptime.kuvasz.models.events -sealed class StructuredMessage +sealed class StructuredMessage { + abstract val summary: String +} sealed class StructuredMonitorMessage : StructuredMessage() data class StructuredMonitorUpMessage( - val summary: String, + override val summary: String, val latency: String, val previousDownTime: String? ) : StructuredMonitorMessage() data class StructuredMonitorDownMessage( - val summary: String, + override val summary: String, val error: String, val previousUpTime: String? ) : StructuredMonitorMessage() data class StructuredRedirectMessage( - val summary: String + override val summary: String ) : StructuredMessage() sealed class StructuredSSLMessage : StructuredMessage() data class StructuredSSLValidMessage( - val summary: String, + override val summary: String, val previousInvalidEvent: String? ) : StructuredSSLMessage() data class StructuredSSLInvalidMessage( - val summary: String, + override val summary: String, val error: String, val previousValidEvent: String? ) : StructuredSSLMessage() data class StructuredSSLWillExpireMessage( - val summary: String, + override val summary: String, val validUntil: String ) : StructuredSSLMessage() diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/PagerdutyResolveRequest.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/PagerdutyResolveRequest.kt new file mode 100644 index 0000000..255ba7e --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/PagerdutyResolveRequest.kt @@ -0,0 +1,14 @@ +package com.kuvaszuptime.kuvasz.models.handlers + +import com.fasterxml.jackson.annotation.JsonProperty +import io.micronaut.core.annotation.Introspected + +@Introspected +data class PagerdutyResolveRequest( + @field:JsonProperty("routing_key") + val routingKey: String, + @field:JsonProperty("event_action") + val eventAction: PagerdutyEventAction = PagerdutyEventAction.RESOLVE, + @field:JsonProperty("dedup_key") + val dedupKey: String +) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/PagerdutyTriggerRequest.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/PagerdutyTriggerRequest.kt new file mode 100644 index 0000000..6b6012c --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/PagerdutyTriggerRequest.kt @@ -0,0 +1,35 @@ +package com.kuvaszuptime.kuvasz.models.handlers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonValue +import io.micronaut.core.annotation.Introspected + +@Introspected +data class PagerdutyTriggerRequest( + @field:JsonProperty("routing_key") + val routingKey: String, + @field:JsonProperty("event_action") + val eventAction: PagerdutyEventAction = PagerdutyEventAction.TRIGGER, + @field:JsonProperty("dedup_key") + val dedupKey: String, + val payload: PagerdutyTriggerPayload +) + +@Introspected +enum class PagerdutyEventAction(@field:JsonValue val value: String) { + TRIGGER("trigger"), + RESOLVE("resolve") +} + +@Introspected +data class PagerdutyTriggerPayload( + val summary: String, + val source: String, + val severity: PagerdutySeverity +) + +@Introspected +enum class PagerdutySeverity(@field:JsonValue val value: String) { + CRITICAL("critical"), + WARNING("warning") +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt index 36e4b66..ca8fe9e 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/SlackWebhookMessage.kt @@ -1,13 +1,14 @@ package com.kuvaszuptime.kuvasz.models.handlers +import com.fasterxml.jackson.annotation.JsonProperty import io.micronaut.core.annotation.Introspected import java.net.URI -@Suppress("ConstructorParameterNaming") @Introspected data class SlackWebhookMessage( val username: String = "KuvaszBot", - val icon_url: URI = URI(ICON_URL), + @field:JsonProperty("icon_url") + val iconUrl: URI = URI(ICON_URL), val text: String ) { companion object { diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt index b25b23f..2aa0811 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/models/handlers/TelegramAPIMessage.kt @@ -1,12 +1,15 @@ package com.kuvaszuptime.kuvasz.models.handlers +import com.fasterxml.jackson.annotation.JsonProperty import io.micronaut.core.annotation.Introspected -@Suppress("ConstructorParameterNaming") @Introspected data class TelegramAPIMessage( - val chat_id: String, + @field:JsonProperty("chat_id") + val chatId: String, val text: String, - val disable_web_page_preview: Boolean = true, - val parse_mode: String = "HTML" + @field:JsonProperty("disable_web_page_preview") + val disableWebPagePreview: Boolean = true, + @field:JsonProperty("parse_mode") + val parseMode: String = "HTML" ) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/LatencyLogRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/LatencyLogRepository.kt index 75643b5..f5d57f6 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/LatencyLogRepository.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/LatencyLogRepository.kt @@ -9,11 +9,10 @@ import org.jooq.impl.DSL import org.jooq.impl.DSL.field import org.jooq.impl.DSL.min import java.time.OffsetDateTime -import javax.inject.Inject import javax.inject.Singleton @Singleton -class LatencyLogRepository @Inject constructor(jooqConfig: Configuration) : LatencyLogDao(jooqConfig) { +class LatencyLogRepository(jooqConfig: Configuration) : LatencyLogDao(jooqConfig) { companion object { private const val P95 = .95 diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt index 4b72c02..167198a 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/MonitorRepository.kt @@ -15,15 +15,15 @@ import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp import com.kuvaszuptime.kuvasz.util.toPersistenceError import org.jooq.Configuration import org.jooq.exception.DataAccessException +import org.jooq.impl.DSL.`when` import org.jooq.impl.DSL.avg import org.jooq.impl.DSL.inline import org.jooq.impl.DSL.round import org.jooq.impl.SQLDataType -import javax.inject.Inject import javax.inject.Singleton @Singleton -class MonitorRepository @Inject constructor(jooqConfig: Configuration) : MonitorDao(jooqConfig) { +class MonitorRepository(jooqConfig: Configuration) : MonitorDao(jooqConfig) { private val dsl = jooqConfig.dsl() @@ -80,6 +80,7 @@ class MonitorRepository @Inject constructor(jooqConfig: Configuration) : Monitor .set(MONITOR.ENABLED, updatedPojo.enabled) .set(MONITOR.SSL_CHECK_ENABLED, updatedPojo.sslCheckEnabled) .set(MONITOR.UPDATED_AT, getCurrentTimestamp()) + .set(MONITOR.PAGERDUTY_INTEGRATION_KEY, updatedPojo.pagerdutyIntegrationKey) .where(MONITOR.ID.eq(updatedPojo.id)) .returning(MONITOR.asterisk()) .fetchOne() @@ -110,7 +111,11 @@ class MonitorRepository @Inject constructor(jooqConfig: Configuration) : Monitor SSL_EVENT.ERROR.`as`("sslError"), round(avg(LATENCY_LOG.LATENCY), -1).`as`("averageLatencyInMs"), inline(null, SQLDataType.INTEGER).`as`("p95LatencyInMs"), - inline(null, SQLDataType.INTEGER).`as`("p99LatencyInMs") + inline(null, SQLDataType.INTEGER).`as`("p99LatencyInMs"), + `when`( + MONITOR.PAGERDUTY_INTEGRATION_KEY.isNull.or(MONITOR.PAGERDUTY_INTEGRATION_KEY.eq("")), + "FALSE" + ).otherwise("TRUE").`as`("pagerdutyKeyPresent") ) .from(MONITOR) .leftJoin(UPTIME_EVENT).on(MONITOR.ID.eq(UPTIME_EVENT.MONITOR_ID).and(UPTIME_EVENT.ENDED_AT.isNull)) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt index 4267d52..634a59d 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/SSLEventRepository.kt @@ -7,11 +7,10 @@ import com.kuvaszuptime.kuvasz.tables.daos.SslEventDao import com.kuvaszuptime.kuvasz.tables.pojos.SslEventPojo import org.jooq.Configuration import java.time.OffsetDateTime -import javax.inject.Inject import javax.inject.Singleton @Singleton -class SSLEventRepository @Inject constructor(jooqConfig: Configuration) : SslEventDao(jooqConfig) { +class SSLEventRepository(jooqConfig: Configuration) : SslEventDao(jooqConfig) { private val dsl = jooqConfig.dsl() fun insertFromMonitorEvent(event: SSLMonitorEvent) { diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt index be0ab8d..4ac1614 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/repositories/UptimeEventRepository.kt @@ -8,11 +8,10 @@ import com.kuvaszuptime.kuvasz.tables.daos.UptimeEventDao import com.kuvaszuptime.kuvasz.tables.pojos.UptimeEventPojo import org.jooq.Configuration import java.time.OffsetDateTime -import javax.inject.Inject import javax.inject.Singleton @Singleton -class UptimeEventRepository @Inject constructor(jooqConfig: Configuration) : UptimeEventDao(jooqConfig) { +class UptimeEventRepository(jooqConfig: Configuration) : UptimeEventDao(jooqConfig) { private val dsl = jooqConfig.dsl() fun insertFromMonitorEvent(event: UptimeMonitorEvent) { diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/security/AdminAuthProvider.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/security/AdminAuthProvider.kt index 57b6608..f06f294 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/security/AdminAuthProvider.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/security/AdminAuthProvider.kt @@ -12,11 +12,10 @@ import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.FlowableEmitter import org.reactivestreams.Publisher -import javax.inject.Inject import javax.inject.Singleton @Singleton -class AdminAuthProvider @Inject constructor(private val authConfig: AdminAuthConfig) : AuthenticationProvider { +class AdminAuthProvider(private val authConfig: AdminAuthConfig) : AuthenticationProvider { override fun authenticate( httpRequest: HttpRequest<*>?, diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt index 1a8d665..9e62eed 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/CheckScheduler.kt @@ -15,11 +15,10 @@ import org.slf4j.LoggerFactory import java.time.Duration import java.util.concurrent.ScheduledFuture import javax.annotation.PostConstruct -import javax.inject.Inject import javax.inject.Named @Context -class CheckScheduler @Inject constructor( +class CheckScheduler( @Named(TaskExecutors.SCHEDULED) private val taskScheduler: TaskScheduler, private val monitorRepository: MonitorRepository, private val uptimeChecker: UptimeChecker, diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt index 567d837..a711d6e 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/DatabaseCleaner.kt @@ -9,11 +9,10 @@ import io.micronaut.context.annotation.Requires import io.micronaut.context.env.Environment import io.micronaut.scheduling.annotation.Scheduled import org.slf4j.LoggerFactory -import javax.inject.Inject import javax.inject.Singleton @Singleton -class DatabaseCleaner @Inject constructor( +class DatabaseCleaner( private val appConfig: AppConfig, private val uptimeEventRepository: UptimeEventRepository, private val latencyLogRepository: LatencyLogRepository, diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt index 707c277..69fe498 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/EventDispatcher.kt @@ -14,12 +14,12 @@ import javax.inject.Singleton @Singleton class EventDispatcher { - private val monitorUpEvents = PublishSubject.create() - private val monitorDownEvents = PublishSubject.create() - private val redirectEvents = PublishSubject.create() - private val sslValidEvents = PublishSubject.create() - private val sslWillExpireEvents = PublishSubject.create() - private val sslInvalidEvents = PublishSubject.create() + private val monitorUpEvents = PublishSubject.create().toSerialized() + private val monitorDownEvents = PublishSubject.create().toSerialized() + private val redirectEvents = PublishSubject.create().toSerialized() + private val sslValidEvents = PublishSubject.create().toSerialized() + private val sslWillExpireEvents = PublishSubject.create().toSerialized() + private val sslInvalidEvents = PublishSubject.create().toSerialized() fun dispatch(event: MonitorEvent) = when (event) { diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt index 5b59ac9..f8639c2 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/MonitorCrudService.kt @@ -7,11 +7,10 @@ import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo -import javax.inject.Inject import javax.inject.Singleton @Singleton -class MonitorCrudService @Inject constructor( +class MonitorCrudService( private val monitorRepository: MonitorRepository, private val latencyLogRepository: LatencyLogRepository, private val checkScheduler: CheckScheduler @@ -67,6 +66,7 @@ class MonitorCrudService @Inject constructor( uptimeCheckInterval = monitorUpdateDto.uptimeCheckInterval ?: existingMonitor.uptimeCheckInterval enabled = monitorUpdateDto.enabled ?: existingMonitor.enabled sslCheckEnabled = monitorUpdateDto.sslCheckEnabled ?: existingMonitor.sslCheckEnabled + pagerdutyIntegrationKey = existingMonitor.pagerdutyIntegrationKey } updatedMonitor.saveAndReschedule(existingMonitor) @@ -84,4 +84,10 @@ class MonitorCrudService @Inject constructor( updatedMonitor } ) + + fun updatePagerdutyIntegrationKey(monitorId: Int, integrationKey: String?): MonitorPojo = + monitorRepository.findById(monitorId)?.let { existingMonitor -> + val updatedMonitor = existingMonitor.setPagerdutyIntegrationKey(integrationKey) + updatedMonitor.saveAndReschedule(existingMonitor) + } ?: throw MonitorNotFoundError(monitorId) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/PagerdutyAPIClient.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/PagerdutyAPIClient.kt new file mode 100644 index 0000000..2a887ef --- /dev/null +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/PagerdutyAPIClient.kt @@ -0,0 +1,20 @@ +package com.kuvaszuptime.kuvasz.services + +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyResolveRequest +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyTriggerRequest +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Retryable +import io.reactivex.Single + +@Client("https://events.pagerduty.com/v2") +@Retryable +interface PagerdutyAPIClient { + + @Post("/enqueue") + fun triggerAlert(@Body request: PagerdutyTriggerRequest): Single + + @Post("/enqueue") + fun resolveAlert(@Body request: PagerdutyResolveRequest): Single +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SMTPMailer.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SMTPMailer.kt index c3b4e19..a0aa9f4 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SMTPMailer.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SMTPMailer.kt @@ -5,11 +5,10 @@ import org.simplejavamail.api.email.Email import org.simplejavamail.api.mailer.AsyncResponse import org.simplejavamail.mailer.MailerBuilder import org.slf4j.LoggerFactory -import javax.inject.Inject import javax.inject.Singleton @Singleton -class SMTPMailer @Inject constructor(smtpMailerConfig: SMTPMailerConfig) { +class SMTPMailer(smtpMailerConfig: SMTPMailerConfig) { companion object { private val logger = LoggerFactory.getLogger(SMTPMailer::class.java) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt index f0bbc24..14d36e2 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SSLChecker.kt @@ -7,14 +7,11 @@ import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo import com.kuvaszuptime.kuvasz.util.getCurrentTimestamp -import io.micronaut.scheduling.TaskExecutors -import io.micronaut.scheduling.annotation.ExecuteOn import java.net.URL -import javax.inject.Inject import javax.inject.Singleton @Singleton -class SSLChecker @Inject constructor( +class SSLChecker( private val sslValidator: SSLValidator, private val uptimeEventRepository: UptimeEventRepository, private val eventDispatcher: EventDispatcher, @@ -25,7 +22,6 @@ class SSLChecker @Inject constructor( private const val EXPIRY_THRESHOLD_DAYS = 30L } - @ExecuteOn(TaskExecutors.IO) fun check(monitor: MonitorPojo) { if (uptimeEventRepository.isMonitorUp(monitor.id)) { val previousEvent = sslEventRepository.getPreviousEventByMonitorId(monitorId = monitor.id) diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt index 69463f8..13a06fc 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/SlackWebhookService.kt @@ -1,41 +1,26 @@ package com.kuvaszuptime.kuvasz.services -import com.kuvaszuptime.kuvasz.config.handlers.SlackEventHandlerConfig import com.kuvaszuptime.kuvasz.models.handlers.SlackWebhookMessage import io.micronaut.context.annotation.Requires -import io.micronaut.context.event.ShutdownEvent -import io.micronaut.core.type.Argument -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.client.RxHttpClient -import io.micronaut.runtime.event.annotation.EventListener -import io.reactivex.Flowable -import javax.inject.Inject +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Retryable +import io.reactivex.Single import javax.inject.Singleton -@Singleton -@Requires(property = "handler-config.slack-event-handler.enabled", value = "true") -class SlackWebhookService @Inject constructor( - private val slackEventHandlerConfig: SlackEventHandlerConfig, - private val httpClient: RxHttpClient -) : TextMessageService { - - companion object { - private const val RETRY_COUNT = 3L - } +@Client("\${handler-config.slack-event-handler.webhook-url}") +@Retryable +interface SlackWebhookClient { - override fun sendMessage(content: String): Flowable> { - val message = SlackWebhookMessage(text = content) - val request: HttpRequest = HttpRequest.POST(slackEventHandlerConfig.webhookUrl, message) + @Post("/") + fun sendMessage(@Body message: SlackWebhookMessage): Single +} - return httpClient - .exchange(request, Argument.STRING, Argument.STRING) - .retry(RETRY_COUNT) - } +@Singleton +@Requires(property = "handler-config.slack-event-handler.enabled", value = "true") +class SlackWebhookService(private val client: SlackWebhookClient) : TextMessageService { - @EventListener - @Suppress("UNUSED_PARAMETER") - internal fun onShutdownEvent(event: ShutdownEvent) { - httpClient.close() - } + override fun sendMessage(content: String): Single = + client.sendMessage(SlackWebhookMessage(text = content)) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt index ab259da..805bf16 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TelegramAPIService.kt @@ -3,40 +3,27 @@ package com.kuvaszuptime.kuvasz.services import com.kuvaszuptime.kuvasz.config.handlers.TelegramEventHandlerConfig import com.kuvaszuptime.kuvasz.models.handlers.TelegramAPIMessage import io.micronaut.context.annotation.Requires -import io.micronaut.context.event.ShutdownEvent -import io.micronaut.core.type.Argument -import io.micronaut.http.HttpRequest -import io.micronaut.http.HttpResponse -import io.micronaut.http.client.RxHttpClient -import io.micronaut.runtime.event.annotation.EventListener -import io.reactivex.Flowable -import javax.inject.Inject +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Post +import io.micronaut.http.client.annotation.Client +import io.micronaut.retry.annotation.Retryable +import io.reactivex.Single import javax.inject.Singleton +@Client("https://api.telegram.org/bot\${handler-config.telegram-event-handler.token}") +@Retryable +interface TelegramAPIClient { + + @Post("/sendMessage") + fun sendMessage(@Body message: TelegramAPIMessage): Single +} + @Singleton @Requires(property = "handler-config.telegram-event-handler.enabled", value = "true") -class TelegramAPIService @Inject constructor( +class TelegramAPIService( private val telegramEventHandlerConfig: TelegramEventHandlerConfig, - private val httpClient: RxHttpClient + private val client: TelegramAPIClient ) : TextMessageService { - - companion object { - internal const val RETRY_COUNT = 3L - } - - override fun sendMessage(content: String): Flowable> { - val message = TelegramAPIMessage(chat_id = telegramEventHandlerConfig.chatId, text = content) - val url = "https://api.telegram.org/bot" + telegramEventHandlerConfig.token + "/sendMessage" - val request: HttpRequest = HttpRequest.POST(url, message) - - return httpClient - .exchange(request, Argument.STRING, Argument.STRING) - .retry(RETRY_COUNT) - } - - @EventListener - @Suppress("UNUSED_PARAMETER") - internal fun onShutdownEvent(event: ShutdownEvent) { - httpClient.close() - } + override fun sendMessage(content: String): Single = + client.sendMessage(TelegramAPIMessage(chatId = telegramEventHandlerConfig.chatId, text = content)) } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt index 32e796c..ad22e2e 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/TextMessageService.kt @@ -1,8 +1,7 @@ package com.kuvaszuptime.kuvasz.services -import io.micronaut.http.HttpResponse -import io.reactivex.Flowable +import io.reactivex.Single interface TextMessageService { - fun sendMessage(content: String): Flowable> + fun sendMessage(content: String): Single } diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt index 416d3aa..5183fdc 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/services/UptimeChecker.kt @@ -8,21 +8,21 @@ import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo import com.kuvaszuptime.kuvasz.util.RawHttpResponse import com.kuvaszuptime.kuvasz.util.getRedirectionUri import com.kuvaszuptime.kuvasz.util.isSuccess -import io.micronaut.context.event.ShutdownEvent +import io.micronaut.context.annotation.Factory import io.micronaut.http.HttpHeaders import io.micronaut.http.HttpRequest +import io.micronaut.http.client.DefaultHttpClientConfiguration +import io.micronaut.http.client.HttpClientConfiguration import io.micronaut.http.client.RxHttpClient +import io.micronaut.http.client.annotation.Client import io.micronaut.http.client.exceptions.HttpClientResponseException -import io.micronaut.runtime.event.annotation.EventListener -import io.micronaut.scheduling.TaskExecutors -import io.micronaut.scheduling.annotation.ExecuteOn import java.net.URI -import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton @Singleton -class UptimeChecker @Inject constructor( - private val httpClient: RxHttpClient, +class UptimeChecker( + @Client("uptime-checker") private val httpClient: RxHttpClient, private val eventDispatcher: EventDispatcher, private val uptimeEventRepository: UptimeEventRepository ) { @@ -31,7 +31,6 @@ class UptimeChecker @Inject constructor( private const val RETRY_COUNT = 3L } - @ExecuteOn(TaskExecutors.IO) fun check(monitor: MonitorPojo, uriOverride: URI? = null) { val previousEvent = uptimeEventRepository.getPreviousEventByMonitorId(monitorId = monitor.id) var start = 0L @@ -85,12 +84,6 @@ class UptimeChecker @Inject constructor( ) } - @EventListener - @Suppress("UNUSED_PARAMETER") - internal fun onShutdownEvent(event: ShutdownEvent) { - httpClient.close() - } - private fun sendHttpRequest(uri: URI): RawHttpResponse { val request = HttpRequest.GET(uri) .header(HttpHeaders.ACCEPT, "*/*") @@ -100,3 +93,15 @@ class UptimeChecker @Inject constructor( return httpClient.exchange(request).retry(RETRY_COUNT) } } + +@Factory +class UptimeCheckerHttpClientConfigFactory { + + @Named("uptime-checker") + @Singleton + fun configuration(): HttpClientConfiguration { + val config = DefaultHttpClientConfiguration() + config.eventLoopGroup = "uptime-check" + return config + } +} diff --git a/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Http+.kt b/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Http+.kt index 0afa336..1d080dc 100644 --- a/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Http+.kt +++ b/src/main/kotlin/com/kuvaszuptime/kuvasz/util/Http+.kt @@ -12,7 +12,6 @@ typealias RawHttpResponse = Flowable>> @Suppress("MagicNumber") fun HttpResponse<*>.isSuccess() = this.status.code in 200..299 -@Suppress("MagicNumber") fun HttpResponse<*>.isRedirected() = listOf( HttpStatus.MOVED_PERMANENTLY, @@ -24,7 +23,4 @@ fun HttpResponse<*>.isRedirected() = fun String.toUri() = URI(this) -fun HttpResponse<*>.getRedirectionUri(): URI? = - if (isRedirected()) { - header(HttpHeaders.LOCATION)?.toUri() - } else null +fun HttpResponse<*>.getRedirectionUri(): URI? = if (isRedirected()) header(HttpHeaders.LOCATION)?.toUri() else null diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4493515..0ff60d3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -21,6 +21,8 @@ handler-config: enabled: ${ENABLE_SMTP_EVENT_HANDLER:`false`} from: ${SMTP_FROM_ADDRESS:`noreply@kuvasz.uptime'`} to: ${SMTP_TO_ADDRESS:`recipient@kuvasz.uptime`} + pagerduty-event-handler: + enabled: true --- admin-auth: username: user diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 99b6a14..374abb6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,12 @@ micronaut: http: client: follow-redirects: false + netty: + event-loops: + uptime-check: + num-threads: 20 + prefer-native-transport: true + executor: 'io' endpoints: health: enabled: true @@ -93,6 +99,8 @@ handler-config: enabled: ${ENABLE_TELEGRAM_EVENT_HANDLER:`false`} token: ${TELEGRAM_API_TOKEN} chat-id: ${TELEGRAM_CHAT_ID} + pagerduty-event-handler: + enabled: ${ENABLE_PAGERDUTY_EVENT_HANDLER:`false`} --- admin-auth: username: ${ADMIN_USER} diff --git a/src/main/resources/db/migration/V6__Add_pagerduty_integration.sql b/src/main/resources/db/migration/V6__Add_pagerduty_integration.sql new file mode 100644 index 0000000..b6883ba --- /dev/null +++ b/src/main/resources/db/migration/V6__Add_pagerduty_integration.sql @@ -0,0 +1,2 @@ +ALTER TABLE monitor + ADD COLUMN pagerduty_integration_key VARCHAR NULL; diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/config/SlackEventHandlerConfigTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/config/SlackEventHandlerConfigTest.kt deleted file mode 100644 index 52ca025..0000000 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/config/SlackEventHandlerConfigTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.kuvaszuptime.kuvasz.config - -import io.kotest.assertions.exceptionToMessage -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.string.shouldContain -import io.micronaut.context.ApplicationContext -import io.micronaut.context.env.PropertySource -import io.micronaut.context.exceptions.BeanInstantiationException - -class SlackEventHandlerConfigTest : BehaviorSpec( - { - given("an SlackEventHandlerConfig bean") { - `when`("there is no webhook URL in the configuration") { - val properties = PropertySource.of( - "test", - mapOf( - "handler-config.slack-event-handler.enabled" to "true" - ) - ) - then("ApplicationContext should throw a BeanInstantiationException") { - val exception = shouldThrow { - ApplicationContext.run(properties) - } - exceptionToMessage(exception) shouldContain - "Bean definition [com.kuvaszuptime.kuvasz.handlers.SlackEventHandler] could not be loaded" - } - } - - `when`("there the webhookUrl is not a valid URI") { - val properties = PropertySource.of( - "test", - mapOf( - "handler-config.slack-event-handler.enabled" to "true", - "handler-config.slack-event-handler.webhook-url" to "jklfdjaklfjdalfda" - ) - ) - then("ApplicationContext should throw a BeanInstantiationException") { - val exception = shouldThrow { - ApplicationContext.run(properties) - } - exceptionToMessage(exception) shouldContain - "Bean definition [com.kuvaszuptime.kuvasz.handlers.SlackEventHandler] could not be loaded" - } - } - } - } -) diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt index 58275e5..ae31bec 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/InfoEndpointTest.kt @@ -19,6 +19,7 @@ class InfoEndpointTest( response shouldContain "smtp-event-handler.enabled" response shouldContain "slack-event-handler.enabled" response shouldContain "telegram-event-handler.enabled" + response shouldContain "pagerduty-event-handler.enabled" } } } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt index 3202b61..d573c52 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorClient.kt @@ -2,8 +2,9 @@ package com.kuvaszuptime.kuvasz.controllers import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto import com.kuvaszuptime.kuvasz.models.dto.MonitorDetailsDto +import com.kuvaszuptime.kuvasz.models.dto.MonitorDto import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto -import com.kuvaszuptime.kuvasz.tables.pojos.MonitorPojo +import com.kuvaszuptime.kuvasz.models.dto.PagerdutyKeyUpdateDto import io.micronaut.http.client.annotation.Client @Client("/monitors") @@ -12,9 +13,13 @@ interface MonitorClient : MonitorOperations { override fun getMonitorsWithDetails(enabledOnly: Boolean?): List - override fun createMonitor(monitor: MonitorCreateDto): MonitorPojo + override fun createMonitor(monitor: MonitorCreateDto): MonitorDto override fun deleteMonitor(monitorId: Int) - override fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorPojo + override fun updateMonitor(monitorId: Int, monitorUpdateDto: MonitorUpdateDto): MonitorDto + + override fun upsertPagerdutyIntegrationKey(monitorId: Int, upsertDto: PagerdutyKeyUpdateDto): MonitorDto + + override fun deletePagerdutyIntegrationKey(monitorId: Int) } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt index 502704f..a6c0a31 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/controllers/MonitorControllerTest.kt @@ -9,6 +9,7 @@ import com.kuvaszuptime.kuvasz.mocks.createUptimeEventRecord import com.kuvaszuptime.kuvasz.models.CheckType import com.kuvaszuptime.kuvasz.models.dto.MonitorCreateDto import com.kuvaszuptime.kuvasz.models.dto.MonitorUpdateDto +import com.kuvaszuptime.kuvasz.models.dto.PagerdutyKeyUpdateDto import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository @@ -48,8 +49,8 @@ class MonitorControllerTest( init { given("MonitorController's getMonitorsWithDetails() endpoint") { - `when`("there is any monitor in the database") { - val monitor = createMonitor(monitorRepository) + `when`("there is a monitor in the database") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") latencyLogRepository.insertLatencyForMonitor(monitor.id, 1200) latencyLogRepository.insertLatencyForMonitor(monitor.id, 600) latencyLogRepository.insertLatencyForMonitor(monitor.id, 600) @@ -89,6 +90,7 @@ class MonitorControllerTest( responseItem.sslStatusStartedAt shouldBe now responseItem.lastSSLCheck shouldBe now responseItem.sslError shouldBe null + responseItem.pagerdutyKeyPresent shouldBe true } } @@ -108,6 +110,7 @@ class MonitorControllerTest( responseItem.uptimeStatus shouldBe null responseItem.sslStatus shouldBe null responseItem.createdAt shouldBe enabledMonitor.createdAt + responseItem.pagerdutyKeyPresent shouldBe false } } @@ -121,7 +124,7 @@ class MonitorControllerTest( given("MonitorController's getMonitorDetails() endpoint") { `when`("there is a monitor with the given ID in the database") { - val monitor = createMonitor(monitorRepository) + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") latencyLogRepository.insertLatencyForMonitor(monitor.id, 1200) latencyLogRepository.insertLatencyForMonitor(monitor.id, 600) latencyLogRepository.insertLatencyForMonitor(monitor.id, 600) @@ -155,6 +158,7 @@ class MonitorControllerTest( response.sslStatusStartedAt shouldBe now response.lastSSLCheck shouldBe now response.sslError shouldBe null + response.pagerdutyKeyPresent shouldBe true } } @@ -174,7 +178,8 @@ class MonitorControllerTest( val monitorToCreate = MonitorCreateDto( name = "test_monitor", url = "https://valid-url.com", - uptimeCheckInterval = 6000 + uptimeCheckInterval = 6000, + pagerdutyIntegrationKey = "something" ) val createdMonitor = monitorClient.createMonitor(monitorToCreate) @@ -185,6 +190,7 @@ class MonitorControllerTest( monitorInDb.uptimeCheckInterval shouldBe createdMonitor.uptimeCheckInterval monitorInDb.enabled shouldBe createdMonitor.enabled monitorInDb.createdAt shouldBe createdMonitor.createdAt + monitorInDb.pagerdutyIntegrationKey shouldBe monitorToCreate.pagerdutyIntegrationKey checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } .forOne { it.checkType shouldBe CheckType.UPTIME } } @@ -298,7 +304,8 @@ class MonitorControllerTest( url = "https://valid-url.com", uptimeCheckInterval = 6000, enabled = true, - sslCheckEnabled = true + sslCheckEnabled = true, + pagerdutyIntegrationKey = "something" ) val createdMonitor = monitorClient.createMonitor(createDto) val checks = checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } @@ -323,6 +330,7 @@ class MonitorControllerTest( monitorInDb.sslCheckEnabled shouldBe updateDto.sslCheckEnabled monitorInDb.createdAt shouldBe createdMonitor.createdAt monitorInDb.updatedAt shouldNotBe null + monitorInDb.pagerdutyIntegrationKey shouldBe createDto.pagerdutyIntegrationKey val updatedChecks = checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } updatedChecks.shouldBeEmpty() @@ -334,7 +342,8 @@ class MonitorControllerTest( name = "test_monitor", url = "https://valid-url.com", uptimeCheckInterval = 6000, - enabled = false + enabled = false, + pagerdutyIntegrationKey = "something" ) val createdMonitor = monitorClient.createMonitor(createDto) checkScheduler.getScheduledChecks().forNone { it.monitorId shouldBe createdMonitor.id } @@ -357,6 +366,7 @@ class MonitorControllerTest( monitorInDb.sslCheckEnabled shouldBe updateDto.sslCheckEnabled monitorInDb.createdAt shouldBe createdMonitor.createdAt monitorInDb.updatedAt shouldNotBe null + monitorInDb.pagerdutyIntegrationKey shouldBe createDto.pagerdutyIntegrationKey val checks = checkScheduler.getScheduledChecks().filter { it.monitorId == createdMonitor.id } checks.forOne { it.checkType shouldBe CheckType.UPTIME } @@ -410,6 +420,102 @@ class MonitorControllerTest( } } } + + given("MonitorController's deletePagerdutyIntegrationKey() endpoint") { + + `when`("it is called with an existing monitor ID") { + val monitorToCreate = MonitorCreateDto( + name = "test_monitor", + url = "https://valid-url.com", + uptimeCheckInterval = 6000, + enabled = true, + pagerdutyIntegrationKey = "something" + ) + val createdMonitor = monitorClient.createMonitor(monitorToCreate) + val deleteRequest = HttpRequest.DELETE("/monitors/${createdMonitor.id}/pagerduty-integration-key") + val response = client.toBlocking().exchange(deleteRequest) + val monitorInDb = monitorRepository.findById(createdMonitor.id) + + then("it should delete the integration key of the given monitor") { + response.status shouldBe HttpStatus.NO_CONTENT + monitorInDb.pagerdutyIntegrationKey shouldBe null + } + } + + `when`("it is called with a non existing monitor ID") { + val deleteRequest = HttpRequest.DELETE("/monitors/123232/pagerduty-integration-key") + val response = shouldThrow { + client.toBlocking().exchange(deleteRequest) + } + + then("it should return a 404") { + response.status shouldBe HttpStatus.NOT_FOUND + } + } + } + + given("MonitorController's upsertPagerdutyIntegrationKey() endpoint") { + + `when`("it is called with an existing monitor ID and an integration key") { + val createDto = MonitorCreateDto( + name = "test_monitor", + url = "https://valid-url.com", + uptimeCheckInterval = 6000, + enabled = true, + sslCheckEnabled = true, + pagerdutyIntegrationKey = "originalKey" + ) + val createdMonitor = monitorClient.createMonitor(createDto) + + val updateDto = PagerdutyKeyUpdateDto( + pagerdutyIntegrationKey = "updatedKey" + ) + val updatedMonitor = monitorClient.upsertPagerdutyIntegrationKey(createdMonitor.id, updateDto) + val updatedMonitorInDb = monitorRepository.findById(createdMonitor.id)!! + + then("it should update the integration key") { + createdMonitor.pagerdutyKeyPresent shouldBe true + updatedMonitor.pagerdutyKeyPresent shouldBe true + updatedMonitorInDb.pagerdutyIntegrationKey shouldBe updateDto.pagerdutyIntegrationKey + } + } + + `when`("it is called with an existing monitor, without an integration key") { + val createDto = MonitorCreateDto( + name = "test_monitor", + url = "https://valid-url.com", + uptimeCheckInterval = 6000, + enabled = true, + sslCheckEnabled = true + ) + val createdMonitor = monitorClient.createMonitor(createDto) + + val updateDto = PagerdutyKeyUpdateDto( + pagerdutyIntegrationKey = "updatedKey" + ) + val updatedMonitor = monitorClient.upsertPagerdutyIntegrationKey(createdMonitor.id, updateDto) + val updatedMonitorInDb = monitorRepository.findById(createdMonitor.id)!! + + then("it should create the integration key") { + createdMonitor.pagerdutyKeyPresent shouldBe false + updatedMonitor.pagerdutyKeyPresent shouldBe true + updatedMonitorInDb.pagerdutyIntegrationKey shouldBe updateDto.pagerdutyIntegrationKey + } + } + + `when`("it is called with a non existing monitor ID") { + val updateDto = PagerdutyKeyUpdateDto("something") + val updateRequest = + HttpRequest.PUT("/monitors/123232/pagerduty-integration-key", updateDto) + val response = shouldThrow { + client.toBlocking().exchange(updateRequest) + } + + then("it should return a 404") { + response.status shouldBe HttpStatus.NOT_FOUND + } + } + } } override fun afterTest(testCase: TestCase, result: TestResult) { diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/PagerdutyEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/PagerdutyEventHandlerTest.kt new file mode 100644 index 0000000..52b1292 --- /dev/null +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/PagerdutyEventHandlerTest.kt @@ -0,0 +1,486 @@ +package com.kuvaszuptime.kuvasz.handlers + +import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec +import com.kuvaszuptime.kuvasz.mocks.createMonitor +import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo +import com.kuvaszuptime.kuvasz.models.SSLValidationError +import com.kuvaszuptime.kuvasz.models.events.MonitorDownEvent +import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent +import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent +import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyEventAction +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyResolveRequest +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutySeverity +import com.kuvaszuptime.kuvasz.models.handlers.PagerdutyTriggerRequest +import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository +import com.kuvaszuptime.kuvasz.repositories.MonitorRepository +import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository +import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository +import com.kuvaszuptime.kuvasz.services.EventDispatcher +import com.kuvaszuptime.kuvasz.services.PagerdutyAPIClient +import com.kuvaszuptime.kuvasz.tables.SslEvent.SSL_EVENT +import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.core.test.TestCase +import io.kotest.core.test.TestResult +import io.kotest.matchers.shouldBe +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.test.annotation.MicronautTest +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import io.reactivex.Single + +@MicronautTest +class PagerdutyEventHandlerTest( + private val monitorRepository: MonitorRepository, + private val uptimeEventRepository: UptimeEventRepository, + sslEventRepository: SSLEventRepository, + latencyLogRepository: LatencyLogRepository +) : DatabaseBehaviorSpec() { + private val mockClient = mockk() + + init { + val eventDispatcher = EventDispatcher() + + DatabaseEventHandler(eventDispatcher, uptimeEventRepository, latencyLogRepository, sslEventRepository) + PagerdutyEventHandler(eventDispatcher, mockClient) + + given("the PagerdutyEventHandler - UPTIME events") { + `when`("it receives a MonitorUpEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val event = MonitorUpEvent( + monitor = monitor, + status = HttpStatus.OK, + latency = 1000, + previousEvent = null + ) + + eventDispatcher.dispatch(event) + + then("it should not call the PD API") { + verify(exactly = 0) { mockClient.resolveAlert(any()) } + } + } + + `when`("it receives a MonitorDownEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val event = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.INTERNAL_SERVER_ERROR, + error = Throwable(), + previousEvent = null + ) + mockSuccessfulTriggerResponse() + + eventDispatcher.dispatch(event) + + then("it should trigger an alert on PD") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + slot.captured.dedupKey shouldBe "kuvasz_uptime_${monitor.id}" + slot.captured.payload.severity shouldBe PagerdutySeverity.CRITICAL + slot.captured.payload.source shouldBe monitor.url + slot.captured.payload.summary shouldBe event.toStructuredMessage().summary + } + } + + `when`("it receives a MonitorUpEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = MonitorUpEvent( + monitor = monitor, + status = HttpStatus.OK, + latency = 1000, + previousEvent = null + ) + eventDispatcher.dispatch(firstEvent) + val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = MonitorUpEvent( + monitor = monitor, + status = HttpStatus.OK, + latency = 1200, + previousEvent = firstUptimeRecord + ) + eventDispatcher.dispatch(secondEvent) + + then("it should not call the PD API") { + verify(exactly = 0) { mockClient.resolveAlert(any()) } + } + } + + `when`("it receives a MonitorDownEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.INTERNAL_SERVER_ERROR, + error = Throwable("First error"), + previousEvent = null + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(firstEvent) + val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.NOT_FOUND, + error = Throwable("Second error"), + previousEvent = firstUptimeRecord + ) + eventDispatcher.dispatch(secondEvent) + + then("it should call triggerAlert() only once") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + } + } + + `when`("it receives a MonitorUpEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.INTERNAL_SERVER_ERROR, + previousEvent = null, + error = Throwable() + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(firstEvent) + val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = MonitorUpEvent( + monitor = monitor, + status = HttpStatus.OK, + latency = 1000, + previousEvent = firstUptimeRecord + ) + mockSuccessfulResolveResponse() + eventDispatcher.dispatch(secondEvent) + + then("it should trigger an alert and then resolve it") { + val triggerSlot = slot() + val resolveSlot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(triggerSlot)) } + verify(exactly = 1) { mockClient.resolveAlert(capture(resolveSlot)) } + triggerSlot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + triggerSlot.captured.dedupKey shouldBe resolveSlot.captured.dedupKey + resolveSlot.captured.eventAction shouldBe PagerdutyEventAction.RESOLVE + } + } + + `when`("it receives a MonitorDownEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = MonitorUpEvent( + monitor = monitor, + status = HttpStatus.OK, + latency = 1000, + previousEvent = null + ) + eventDispatcher.dispatch(firstEvent) + val firstUptimeRecord = uptimeEventRepository.fetchOne(UPTIME_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.INTERNAL_SERVER_ERROR, + previousEvent = firstUptimeRecord, + error = Throwable() + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(secondEvent) + + then("it should call only triggerAlert()") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + } + } + + `when`("it should call PD but monitor has no routing key set") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = null) + val event = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.INTERNAL_SERVER_ERROR, + previousEvent = null, + error = Throwable() + ) + eventDispatcher.dispatch(event) + + then("it should not call PD's API") { + verify(exactly = 0) { mockClient.triggerAlert(any()) } + } + } + } + + given("the PagerdutyEventHandler - SSL events") { + `when`("it receives an SSLValidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val event = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = null + ) + eventDispatcher.dispatch(event) + + then("it should not call the PD API") { + verify(exactly = 0) { mockClient.resolveAlert(any()) } + } + } + + `when`("it receives an SSLInvalidEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val event = SSLInvalidEvent( + monitor = monitor, + previousEvent = null, + error = SSLValidationError("ssl error") + ) + mockSuccessfulTriggerResponse() + + eventDispatcher.dispatch(event) + + then("it should trigger an alert on PD") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + slot.captured.dedupKey shouldBe "kuvasz_ssl_${monitor.id}" + slot.captured.payload.severity shouldBe PagerdutySeverity.CRITICAL + slot.captured.payload.source shouldBe monitor.url + slot.captured.payload.summary shouldBe event.toStructuredMessage().summary + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = null + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = firstSSLRecord + ) + eventDispatcher.dispatch(secondEvent) + + then("it should not call the PD API") { + verify(exactly = 0) { mockClient.resolveAlert(any()) } + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = null, + error = SSLValidationError("ssl error1") + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = firstSSLRecord, + error = SSLValidationError("ssl error2") + ) + eventDispatcher.dispatch(secondEvent) + + then("it should call triggerAlert() only once") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + } + } + + `when`("it receives an SSLValidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = null, + error = SSLValidationError("ssl error1") + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = firstSSLRecord + ) + mockSuccessfulResolveResponse() + eventDispatcher.dispatch(secondEvent) + + then("it should trigger an alert and then resolve it") { + val triggerSlot = slot() + val resolveSlot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(triggerSlot)) } + verify(exactly = 1) { mockClient.resolveAlert(capture(resolveSlot)) } + triggerSlot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + triggerSlot.captured.dedupKey shouldBe resolveSlot.captured.dedupKey + resolveSlot.captured.eventAction shouldBe PagerdutyEventAction.RESOLVE + } + } + + `when`("it receives an SSLInvalidEvent and there is a previous event with different status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = null + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLInvalidEvent( + monitor = monitor, + previousEvent = firstSSLRecord, + error = SSLValidationError("ssl error") + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(secondEvent) + + then("it should call only triggerAlert()") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + } + } + + `when`("it receives an SSLWillExpireEvent and there is no previous event for the monitor") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val event = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = null + ) + mockSuccessfulTriggerResponse() + + eventDispatcher.dispatch(event) + + then("it should trigger an alert with WARNING severity") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + slot.captured.dedupKey shouldBe "kuvasz_ssl_${monitor.id}" + slot.captured.payload.summary shouldBe event.toStructuredMessage().summary + slot.captured.payload.source shouldBe event.monitor.url + slot.captured.payload.severity shouldBe PagerdutySeverity.WARNING + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous event with the same status") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = null + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = firstSSLRecord + ) + eventDispatcher.dispatch(secondEvent) + + then("it should call triggerAlert() only once") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + } + } + + `when`("it receives an SSLWillExpireEvent and there is a previous SSLValidEvent") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val firstEvent = SSLValidEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = null + ) + eventDispatcher.dispatch(firstEvent) + val firstSSLRecord = sslEventRepository.fetchOne(SSL_EVENT.MONITOR_ID, monitor.id) + + val secondEvent = SSLWillExpireEvent( + monitor = monitor, + certInfo = generateCertificateInfo(), + previousEvent = firstSSLRecord + ) + mockSuccessfulTriggerResponse() + eventDispatcher.dispatch(secondEvent) + + then("it should call only triggerAlert()") { + val slot = slot() + + verify(exactly = 1) { mockClient.triggerAlert(capture(slot)) } + slot.captured.payload.severity shouldBe PagerdutySeverity.WARNING + slot.captured.eventAction shouldBe PagerdutyEventAction.TRIGGER + } + } + } + + given("the PagerdutyEventHandler - error handling logic") { + `when`("an error happens when it calls the API") { + val monitor = createMonitor(monitorRepository, pagerdutyIntegrationKey = "something") + val event = MonitorDownEvent( + monitor = monitor, + status = HttpStatus.INTERNAL_SERVER_ERROR, + previousEvent = null, + error = Throwable() + ) + mockErrorTriggerResponse() + + then("it should not throw an exception") { + shouldNotThrowAny { eventDispatcher.dispatch(event) } + verify(exactly = 1) { mockClient.triggerAlert(any()) } + } + } + } + } + + override fun afterTest(testCase: TestCase, result: TestResult) { + clearAllMocks() + super.afterTest(testCase, result) + } + + private fun mockSuccessfulTriggerResponse() { + every { + mockClient.triggerAlert(any()) + } returns Single.just("irrelevant") + } + + private fun mockErrorTriggerResponse() { + every { + mockClient.triggerAlert(any()) + } returns Single.error( + HttpClientResponseException("error", HttpResponse.badRequest("bad_request")) + ) + } + + private fun mockSuccessfulResolveResponse() { + every { + mockClient.resolveAlert(any()) + } returns Single.just("irrelevant") + } +} diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt index a56e0bf..0ad446b 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SMTPEventHandlerTest.kt @@ -11,6 +11,7 @@ import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent +import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository @@ -23,7 +24,6 @@ import io.kotest.core.test.TestResult import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldNotContain -import io.micronaut.context.annotation.Property import io.micronaut.http.HttpStatus import io.micronaut.test.annotation.MicronautTest import io.mockk.clearAllMocks @@ -34,19 +34,20 @@ import org.simplejavamail.api.email.Email import java.time.OffsetDateTime @MicronautTest -@Property(name = "handler-config.smtp-event-handler.enabled", value = "true") class SMTPEventHandlerTest( - private val eventDispatcher: EventDispatcher, private val monitorRepository: MonitorRepository, private val uptimeEventRepository: UptimeEventRepository, private val sslEventRepository: SSLEventRepository, + latencyLogRepository: LatencyLogRepository, smtpEventHandlerConfig: SMTPEventHandlerConfig, smtpMailer: SMTPMailer - ) : DatabaseBehaviorSpec() { init { + val eventDispatcher = EventDispatcher() val emailFactory = EmailFactory(smtpEventHandlerConfig) val mailerSpy = spyk(smtpMailer, recordPrivateCalls = true) + + DatabaseEventHandler(eventDispatcher, uptimeEventRepository, latencyLogRepository, sslEventRepository) SMTPEventHandler(smtpEventHandlerConfig, mailerSpy, eventDispatcher) given("the SMTPEventHandler - UPTIME events") { diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt index d92c403..f84022d 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/SlackEventHandlerTest.kt @@ -1,7 +1,6 @@ package com.kuvaszuptime.kuvasz.handlers import com.kuvaszuptime.kuvasz.DatabaseBehaviorSpec -import com.kuvaszuptime.kuvasz.config.handlers.SlackEventHandlerConfig import com.kuvaszuptime.kuvasz.mocks.createMonitor import com.kuvaszuptime.kuvasz.mocks.generateCertificateInfo import com.kuvaszuptime.kuvasz.models.SSLValidationError @@ -10,11 +9,12 @@ import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent -import com.kuvaszuptime.kuvasz.models.handlers.SlackWebhookMessage +import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher +import com.kuvaszuptime.kuvasz.services.SlackWebhookClient import com.kuvaszuptime.kuvasz.services.SlackWebhookService import com.kuvaszuptime.kuvasz.tables.SslEvent.SSL_EVENT import com.kuvaszuptime.kuvasz.tables.UptimeEvent.UPTIME_EVENT @@ -23,10 +23,8 @@ import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldNotContain -import io.micronaut.core.type.Argument import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus -import io.micronaut.http.client.RxHttpClient import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.test.annotation.MicronautTest import io.mockk.clearAllMocks @@ -35,22 +33,24 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify -import io.reactivex.Flowable +import io.reactivex.Single import java.time.OffsetDateTime @MicronautTest class SlackEventHandlerTest( - private val eventDispatcher: EventDispatcher, private val monitorRepository: MonitorRepository, private val uptimeEventRepository: UptimeEventRepository, - private val sslEventRepository: SSLEventRepository + private val sslEventRepository: SSLEventRepository, + latencyLogRepository: LatencyLogRepository ) : DatabaseBehaviorSpec() { - private val mockHttpClient = mockk() + private val mockClient = mockk() init { - val eventHandlerConfig = SlackEventHandlerConfig().apply { webhookUrl = "https://jklfdalda.com/webhook" } - val slackWebhookService = SlackWebhookService(eventHandlerConfig, mockHttpClient) + val eventDispatcher = EventDispatcher() + val slackWebhookService = SlackWebhookService(mockClient) val webhookServiceSpy = spyk(slackWebhookService, recordPrivateCalls = true) + + DatabaseEventHandler(eventDispatcher, uptimeEventRepository, latencyLogRepository, sslEventRepository) SlackEventHandler(webhookServiceSpy, eventDispatcher) given("the SlackEventHandler - UPTIME events") { @@ -443,12 +443,8 @@ class SlackEventHandlerTest( ) mockHttpErrorResponse() - then("it should send a webhook message about the event") { - val slot = slot() - + then("it should not throw an exception") { shouldNotThrowAny { eventDispatcher.dispatch(event) } - verify(exactly = 1) { webhookServiceSpy.sendMessage(capture(slot)) } - slot.captured shouldContain "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (200)" } } } @@ -461,16 +457,14 @@ class SlackEventHandlerTest( private fun mockSuccessfulHttpResponse() { every { - mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) - } returns Flowable.just( - HttpResponse.ok() - ) + mockClient.sendMessage(any()) + } returns Single.just("ok") } private fun mockHttpErrorResponse() { every { - mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) - } returns Flowable.error( + mockClient.sendMessage(any()) + } returns Single.error( HttpClientResponseException("error", HttpResponse.badRequest("bad_request")) ) } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt index b98bb81..cea11b5 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/handlers/TelegramEventHandlerTest.kt @@ -10,11 +10,12 @@ import com.kuvaszuptime.kuvasz.models.events.MonitorUpEvent import com.kuvaszuptime.kuvasz.models.events.SSLInvalidEvent import com.kuvaszuptime.kuvasz.models.events.SSLValidEvent import com.kuvaszuptime.kuvasz.models.events.SSLWillExpireEvent -import com.kuvaszuptime.kuvasz.models.handlers.SlackWebhookMessage +import com.kuvaszuptime.kuvasz.repositories.LatencyLogRepository import com.kuvaszuptime.kuvasz.repositories.MonitorRepository import com.kuvaszuptime.kuvasz.repositories.SSLEventRepository import com.kuvaszuptime.kuvasz.repositories.UptimeEventRepository import com.kuvaszuptime.kuvasz.services.EventDispatcher +import com.kuvaszuptime.kuvasz.services.TelegramAPIClient import com.kuvaszuptime.kuvasz.services.TelegramAPIService import com.kuvaszuptime.kuvasz.tables.SslEvent import com.kuvaszuptime.kuvasz.tables.UptimeEvent @@ -23,10 +24,8 @@ import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult import io.kotest.matchers.string.shouldContain import io.kotest.matchers.string.shouldNotContain -import io.micronaut.core.type.Argument import io.micronaut.http.HttpResponse import io.micronaut.http.HttpStatus -import io.micronaut.http.client.RxHttpClient import io.micronaut.http.client.exceptions.HttpClientResponseException import io.micronaut.test.annotation.MicronautTest import io.mockk.clearAllMocks @@ -35,25 +34,28 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify -import io.reactivex.Flowable +import io.reactivex.Single import java.time.OffsetDateTime @MicronautTest class TelegramEventHandlerTest( - private val eventDispatcher: EventDispatcher, private val monitorRepository: MonitorRepository, private val uptimeEventRepository: UptimeEventRepository, - private val sslEventRepository: SSLEventRepository + private val sslEventRepository: SSLEventRepository, + latencyLogRepository: LatencyLogRepository ) : DatabaseBehaviorSpec() { - private val mockHttpClient = mockk() + private val mockClient = mockk() init { + val eventDispatcher = EventDispatcher() val eventHandlerConfig = TelegramEventHandlerConfig().apply { token = "my_token" chatId = "@channel" } - val telegramAPIService = TelegramAPIService(eventHandlerConfig, mockHttpClient) + val telegramAPIService = TelegramAPIService(eventHandlerConfig, mockClient) val apiServiceSpy = spyk(telegramAPIService, recordPrivateCalls = true) + + DatabaseEventHandler(eventDispatcher, uptimeEventRepository, latencyLogRepository, sslEventRepository) TelegramEventHandler(apiServiceSpy, eventDispatcher) given("the TelegramEventHandler") { @@ -446,12 +448,8 @@ class TelegramEventHandlerTest( ) mockHttpErrorResponse() - then("it should send a webhook message about the event") { - val slot = slot() - + then("it should not throw an exception") { shouldNotThrowAny { eventDispatcher.dispatch(event) } - verify(exactly = 1) { apiServiceSpy.sendMessage(capture(slot)) } - slot.captured shouldContain "Your monitor \"${monitor.name}\" (${monitor.url}) is UP (200)" } } } @@ -464,16 +462,14 @@ class TelegramEventHandlerTest( private fun mockSuccessfulHttpResponse() { every { - mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) - } returns Flowable.just( - HttpResponse.ok() - ) + mockClient.sendMessage(any()) + } returns Single.just("ok") } private fun mockHttpErrorResponse() { every { - mockHttpClient.exchange(any(), Argument.STRING, Argument.STRING) - } returns Flowable.error( + mockClient.sendMessage(any()) + } returns Single.error( HttpClientResponseException("error", HttpResponse.badRequest("bad_request")) ) } diff --git a/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt b/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt index 020e44d..27f72f7 100644 --- a/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt +++ b/src/test/kotlin/com/kuvaszuptime/kuvasz/mocks/TestData.kt @@ -20,13 +20,15 @@ fun createMonitor( sslCheckEnabled: Boolean = true, uptimeCheckInterval: Int = 30000, monitorName: String = "testMonitor", - url: String = "http://irrelevant.com" + url: String = "http://irrelevant.com", + pagerdutyIntegrationKey: String? = null ): MonitorPojo { val monitor = MonitorPojo() .setId(id) .setName(monitorName) .setUptimeCheckInterval(uptimeCheckInterval) .setUrl(url) + .setPagerdutyIntegrationKey(pagerdutyIntegrationKey) .setEnabled(enabled) .setSslCheckEnabled(sslCheckEnabled) .setCreatedAt(getCurrentTimestamp())