[orx-crash-handler] Add crash handler with Slack reporter
This commit is contained in:
@@ -45,6 +45,7 @@ val demo: SourceSet by project.sourceSets.creating {
|
|||||||
"orx-runway",
|
"orx-runway",
|
||||||
"orx-syphon",
|
"orx-syphon",
|
||||||
"orx-video-profiles",
|
"orx-video-profiles",
|
||||||
|
"orx-crash-handler"
|
||||||
)
|
)
|
||||||
if (project.name !in skipDemos) {
|
if (project.name !in skipDemos) {
|
||||||
collectScreenshots(project, this@creating) { }
|
collectScreenshots(project, this@creating) { }
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ jsoup = "1.21.2"
|
|||||||
mockk = "1.14.2"
|
mockk = "1.14.2"
|
||||||
processing = "4.4.10"
|
processing = "4.4.10"
|
||||||
nmcp = "1.1.0"
|
nmcp = "1.1.0"
|
||||||
|
okhttp = "5.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
|
kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
|
||||||
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
|
||||||
kotlin-scriptingJvm = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-jvm", version.ref = "kotlin" }
|
kotlin-scriptingJvm = { group = "org.jetbrains.kotlin", name = "kotlin-scripting-jvm", version.ref = "kotlin" }
|
||||||
|
|||||||
3
orx-jvm/orx-crash-handler/README.md
Normal file
3
orx-jvm/orx-crash-handler/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# orx-crash-handler
|
||||||
|
|
||||||
|
Extension for reporting unhandled exceptions
|
||||||
11
orx-jvm/orx-crash-handler/build.gradle.kts
Normal file
11
orx-jvm/orx-crash-handler/build.gradle.kts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.openrndr.extra.convention.kotlin-jvm")
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(sharedLibs.kotlin.serialization.json)
|
||||||
|
implementation(openrndr.application.core)
|
||||||
|
implementation(openrndr.math)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.extra.crashhandler.CrashHandler
|
||||||
|
import org.openrndr.extra.crashhandler.slack
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
application {
|
||||||
|
configure {
|
||||||
|
width = 1280
|
||||||
|
height = 720
|
||||||
|
}
|
||||||
|
program {
|
||||||
|
extend(CrashHandler()) {
|
||||||
|
name = "jump-scare"
|
||||||
|
vncHost = "localhost"
|
||||||
|
slack {
|
||||||
|
authToken = System.getenv("SLACK_AUTH_TOKEN")
|
||||||
|
channelId = System.getenv("SLACK_CHANNEL_ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extend {
|
||||||
|
error("something bad happened")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
orx-jvm/orx-crash-handler/src/main/kotlin/CrashHandler.kt
Normal file
49
orx-jvm/orx-crash-handler/src/main/kotlin/CrashHandler.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package org.openrndr.extra.crashhandler
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.openrndr.Extension
|
||||||
|
import org.openrndr.Program
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class CrashHandler : Extension {
|
||||||
|
override var enabled: Boolean = true
|
||||||
|
|
||||||
|
var name: String? = null
|
||||||
|
var vncHost: String? = null
|
||||||
|
|
||||||
|
|
||||||
|
val reporters = mutableListOf<Reporter>()
|
||||||
|
|
||||||
|
|
||||||
|
override fun setup(program: Program) {
|
||||||
|
if (name == null)
|
||||||
|
name = program.name
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { t, e: Throwable ->
|
||||||
|
logger.error(e) { "Uncaught exception in thread $t" }
|
||||||
|
|
||||||
|
for (reporter in reporters) {
|
||||||
|
try {
|
||||||
|
reporter.reportCrash(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("error while reporting")
|
||||||
|
logger.error(e) { "reporter threw an exception" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val crashFile = File("${program.name}.crash")
|
||||||
|
val lastCrash = if (crashFile.exists()) crashFile.readText().toLongOrNull() ?: 0L else 0L
|
||||||
|
|
||||||
|
crashFile.writeText("${System.currentTimeMillis()}")
|
||||||
|
if (System.currentTimeMillis() - lastCrash < 60 * 1000) {
|
||||||
|
logger.info { "crashed less than 60 seconds ago, sleeping for 60 seconds" }
|
||||||
|
Thread.sleep(60 * 1000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
5
orx-jvm/orx-crash-handler/src/main/kotlin/Reporter.kt
Normal file
5
orx-jvm/orx-crash-handler/src/main/kotlin/Reporter.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package org.openrndr.extra.crashhandler
|
||||||
|
|
||||||
|
abstract class Reporter(val handler: CrashHandler) {
|
||||||
|
abstract fun reportCrash(throwable: Throwable)
|
||||||
|
}
|
||||||
141
orx-jvm/orx-crash-handler/src/main/kotlin/SlackReporter.kt
Normal file
141
orx-jvm/orx-crash-handler/src/main/kotlin/SlackReporter.kt
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package org.openrndr.extra.crashhandler
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class BlockResponse(
|
||||||
|
val type: String,
|
||||||
|
val text: TextElementResponse? = null,
|
||||||
|
val elements: List<BlockElementResponse> = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class TextElementResponse(
|
||||||
|
val text: String,
|
||||||
|
val type: String,
|
||||||
|
val emoji: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class BlockElementResponse(
|
||||||
|
val type: String,
|
||||||
|
val text: TextElementResponse,
|
||||||
|
val url: String? = null,
|
||||||
|
val style: String = "primary",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ChatPostMessageRequest(
|
||||||
|
val channel: String,
|
||||||
|
val blocks: List<BlockResponse>? = null,
|
||||||
|
val text: String? = null,
|
||||||
|
val thread_ts: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class ChatPostMessageResponse(
|
||||||
|
val ok: Boolean,
|
||||||
|
val ts: String? = null,
|
||||||
|
val channel: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class SlackReporter(handler: CrashHandler) : Reporter(handler) {
|
||||||
|
|
||||||
|
var channelId: String = ""
|
||||||
|
var authToken: String = ""
|
||||||
|
|
||||||
|
private val monitorJson = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeRequest(client: OkHttpClient, messageRequest: ChatPostMessageRequest): Response {
|
||||||
|
val body = monitorJson.encodeToString(messageRequest)
|
||||||
|
val requestBody = body.toRequestBody("application/json".toMediaType())
|
||||||
|
|
||||||
|
val replySlackRequest: Request = Request.Builder()
|
||||||
|
.url("https://slack.com/api/chat.postMessage")
|
||||||
|
.method("POST", requestBody)
|
||||||
|
.addHeader("Content-Type", "application/json")
|
||||||
|
.addHeader("Authorization", "Bearer $authToken")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(replySlackRequest).execute()
|
||||||
|
require(response.isSuccessful) {
|
||||||
|
"request failed: ${response.code} ${response.message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun plainText(text: String): TextElementResponse {
|
||||||
|
return TextElementResponse(text, "plain_text", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun slackMessage(client: OkHttpClient, endpoint: String, error: Boolean = false, errorLog: String? = null) {
|
||||||
|
|
||||||
|
val messageRequest = if (error) {
|
||||||
|
ChatPostMessageRequest(channel = channelId!!, thread_ts = null,
|
||||||
|
blocks = listOf(
|
||||||
|
BlockResponse("section", plainText("There is a problem with $endpoint. Please check.")),
|
||||||
|
BlockResponse("actions", elements = listOfNotNull(
|
||||||
|
|
||||||
|
if (handler.vncHost != null) {
|
||||||
|
BlockElementResponse(
|
||||||
|
type = "button",
|
||||||
|
text = plainText("VNC into $endpoint"),
|
||||||
|
url = "vnc://${handler.vncHost}"
|
||||||
|
)
|
||||||
|
} else { null }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ChatPostMessageRequest(channel = channelId!!, thread_ts = null,
|
||||||
|
blocks = listOf(BlockResponse("section", plainText("$endpoint is back online!")))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = makeRequest(client, messageRequest)
|
||||||
|
|
||||||
|
|
||||||
|
if (error && response.isSuccessful) {
|
||||||
|
val cmr = monitorJson.decodeFromString<ChatPostMessageResponse>(response.body?.string() ?: "")
|
||||||
|
|
||||||
|
val logMessage = errorLog ?: "No log could be retrieved. Machine is likely unreachable"
|
||||||
|
|
||||||
|
val replyRequest = ChatPostMessageRequest(channel = channelId, thread_ts = cmr.ts,
|
||||||
|
blocks = listOf(
|
||||||
|
BlockResponse(
|
||||||
|
type = "section",
|
||||||
|
text = TextElementResponse("```${logMessage}```", "mrkdwn", false)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
makeRequest(client, replyRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun reportCrash(throwable: Throwable) {
|
||||||
|
logger.info { "reporting " }
|
||||||
|
val client = OkHttpClient().newBuilder().build()
|
||||||
|
slackMessage(client, handler.name ?: "no name", true, throwable.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CrashHandler.slack(config: SlackReporter.() -> Unit) {
|
||||||
|
reporters.add(SlackReporter(this).apply(config))
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ include(
|
|||||||
"orx-color",
|
"orx-color",
|
||||||
"orx-composition",
|
"orx-composition",
|
||||||
"orx-compositor",
|
"orx-compositor",
|
||||||
|
"orx-jvm:orx-crash-handler",
|
||||||
"orx-delegate-magic",
|
"orx-delegate-magic",
|
||||||
"orx-jvm:orx-dnk3",
|
"orx-jvm:orx-dnk3",
|
||||||
"orx-easing",
|
"orx-easing",
|
||||||
|
|||||||
Reference in New Issue
Block a user