[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-syphon",
|
||||
"orx-video-profiles",
|
||||
"orx-crash-handler"
|
||||
)
|
||||
if (project.name !in skipDemos) {
|
||||
collectScreenshots(project, this@creating) { }
|
||||
|
||||
@@ -24,8 +24,10 @@ jsoup = "1.21.2"
|
||||
mockk = "1.14.2"
|
||||
processing = "4.4.10"
|
||||
nmcp = "1.1.0"
|
||||
okhttp = "5.2.1"
|
||||
|
||||
[libraries]
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
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-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-composition",
|
||||
"orx-compositor",
|
||||
"orx-jvm:orx-crash-handler",
|
||||
"orx-delegate-magic",
|
||||
"orx-jvm:orx-dnk3",
|
||||
"orx-easing",
|
||||
|
||||
Reference in New Issue
Block a user