aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/ledger/utils
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-12-13 17:42:27 +0100
committerLinnea Gräf <nea@nea.moe>2024-12-13 17:42:27 +0100
commitc4e4985b8b96ed156851c6be6d4a8d5e110c3040 (patch)
tree4dc7d19ccff486a9f612738db7ea196beffb38d1 /src/main/kotlin/moe/nea/ledger/utils
parent4e986409ad65aa5c11baf7e8571b78b89ec260a4 (diff)
downloadLocalTransactionLedger-c4e4985b8b96ed156851c6be6d4a8d5e110c3040.tar.gz
LocalTransactionLedger-c4e4985b8b96ed156851c6be6d4a8d5e110c3040.tar.bz2
LocalTransactionLedger-c4e4985b8b96ed156851c6be6d4a8d5e110c3040.zip
Add error reporting framework
Diffstat (limited to 'src/main/kotlin/moe/nea/ledger/utils')
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt37
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt10
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt9
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt57
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt70
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt9
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt38
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt9
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt25
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt5
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt8
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt146
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt11
13 files changed, 434 insertions, 0 deletions
diff --git a/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt b/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt
new file mode 100644
index 0000000..9b6a153
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt
@@ -0,0 +1,37 @@
+package moe.nea.ledger.utils
+
+import moe.nea.ledger.utils.telemetry.ContextValue
+import moe.nea.ledger.utils.telemetry.EventRecorder
+import moe.nea.ledger.utils.telemetry.Span
+
+class ErrorUtil {
+
+ @Inject
+ lateinit var reporter: EventRecorder
+
+ fun report(exception: Throwable, message: String?) {
+ Span.current().recordException(reporter, exception, message)
+ }
+
+ fun <T> Result<T>.getOrReport(): T? {
+ val exc = exceptionOrNull()
+ if (exc != null) {
+ report(exc, null)
+ }
+ return getOrNull()
+ }
+
+ inline fun <T> catch(
+ vararg pairs: Pair<String, ContextValue>,
+ crossinline function: () -> T
+ ): T? {
+ return Span.current().enterWith(*pairs) {
+ try {
+ return@enterWith function()
+ } catch (ex: Exception) {
+ report(ex, null)
+ return@enterWith null
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt
new file mode 100644
index 0000000..5f4ccdf
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt
@@ -0,0 +1,10 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+
+class BooleanContext(val boolean: Boolean) : ContextValue {
+ override fun serialize(): JsonElement {
+ return JsonPrimitive(boolean)
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt
new file mode 100644
index 0000000..004ae9c
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt
@@ -0,0 +1,9 @@
+package moe.nea.ledger.utils.telemetry
+
+object CommonKeys {
+ val EVENT_MESSAGE = "event_message"
+ val EXCEPTION = "event_exception"
+ val COMMIT_VERSION = "version_commit"
+ val VERSION = "version"
+ val PHASE = "phase" // TODO: add a sort of "manual" stacktrace with designated function phases
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt
new file mode 100644
index 0000000..3c30a52
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt
@@ -0,0 +1,57 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.JsonObject
+
+class Context(val data: MutableMap<String, ContextValue> = mutableMapOf()) : ContextValue.Collatable<Context> {
+
+ inline fun <reified T : ContextValue> getT(key: String): T? {
+ return get(key) as? T
+ }
+
+ fun get(key: String): ContextValue? {
+ return data[key]
+ }
+
+ fun add(key: String, value: ContextValue) {
+ data[key] = value
+ }
+
+ @Suppress("NOTHING_TO_INLINE")
+ private inline fun <T : ContextValue.Collatable<T>> cope(
+ left: ContextValue.Collatable<T>,
+ right: ContextValue
+ ): ContextValue {
+ return try {
+ left.combineWith(right as T)
+ } catch (ex: Exception) {
+ // TODO: cope with this better
+ right
+ }
+ }
+
+ override fun combineWith(overrides: Context): Context {
+ val copy = data.toMutableMap()
+ for ((key, overrideValue) in overrides.data) {
+ copy.merge(key, overrideValue) { old, new ->
+ if (old is ContextValue.Collatable<*>) {
+ cope(old, new)
+ } else {
+ new
+ }
+ }
+ }
+ return Context(copy)
+ }
+
+ override fun actualize(): Context {
+ return this
+ }
+
+ override fun serialize(): JsonObject {
+ val obj = JsonObject()
+ data.forEach { (k, v) ->
+ obj.add(k, v.serialize())
+ }
+ return obj
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt
new file mode 100644
index 0000000..b5891fc
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt
@@ -0,0 +1,70 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+
+interface ContextValue {
+ companion object {
+ fun <T : Collatable<T>> lazyCollatable(value: () -> Collatable<T>): Collatable<T> {
+ return LazyCollatable(value)
+ }
+
+ fun lazy(value: () -> ContextValue): ContextValue {
+ return object : ContextValue {
+ val value by kotlin.lazy(value)
+ override fun serialize(): JsonElement {
+ return this.value.serialize()
+ }
+ }
+ }
+
+ fun bool(boolean: Boolean): ContextValue {
+ return BooleanContext(boolean)
+ }
+
+ fun string(message: String): ContextValue {
+ return StringContext(message)
+ }
+
+ fun jsonObject(vararg pairs: Pair<String, JsonElement>): ContextValue {
+ val obj = JsonObject()
+ for ((l, r) in pairs) {
+ obj.add(l, r)
+ }
+ return JsonElementContext(obj)
+ }
+
+ fun compound(vararg pairs: Pair<String, String>): ContextValue {
+ val obj = JsonObject()
+ for ((l, r) in pairs) {
+ obj.addProperty(l, r)
+ }
+ // TODO: should this be its own class?
+ return JsonElementContext(obj)
+ }
+ }
+
+ // TODO: allow other serialization formats
+ fun serialize(): JsonElement
+ interface Collatable<T : Collatable<T>> : ContextValue {
+ fun combineWith(overrides: T): T
+ fun actualize(): T
+ }
+
+ private class LazyCollatable<T : Collatable<T>>(
+ provider: () -> Collatable<T>,
+ ) : Collatable<T> {
+ val value by kotlin.lazy(provider)
+ override fun actualize(): T {
+ return value.actualize()
+ }
+
+ override fun combineWith(overrides: T): T {
+ return value.combineWith(overrides)
+ }
+
+ override fun serialize(): JsonElement {
+ return value.serialize()
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt
new file mode 100644
index 0000000..28b1ab5
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt
@@ -0,0 +1,9 @@
+package moe.nea.ledger.utils.telemetry
+
+interface EventRecorder {
+ companion object {
+ var instance: EventRecorder? = null
+ }
+
+ fun record(event: RecordedEvent)
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt
new file mode 100644
index 0000000..df588a8
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt
@@ -0,0 +1,38 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+
+class ExceptionContextValue(val exception: Throwable) : ContextValue {
+ val stackTrace by lazy {
+ exception.stackTraceToString()
+ }
+
+ override fun serialize(): JsonElement {
+ val jsonObject = JsonObject()
+ jsonObject.addProperty("exception_stackTrace", stackTrace)
+ jsonObject.add("exception_structure", walkExceptions(exception, 6))
+ return jsonObject
+ }
+
+ private fun walkExceptions(exception: Throwable, searchDepth: Int): JsonElement {
+ val obj = JsonObject()
+ obj.addProperty("class", exception.javaClass.name)
+ obj.addProperty("message", exception.message)
+ // TODO: allow exceptions to implement an "extra info" interface
+ if (searchDepth > 0) {
+ if (exception.cause != null) {
+ obj.add("cause", walkExceptions(exception, searchDepth - 1))
+ }
+ val suppressions = JsonArray()
+ for (suppressedException in exception.suppressedExceptions) {
+ suppressions.add(walkExceptions(suppressedException, searchDepth - 1))
+ }
+ if (suppressions.size() > 0) {
+ obj.add("suppressions", suppressions)
+ }
+ }
+ return obj
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt
new file mode 100644
index 0000000..1601f56
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt
@@ -0,0 +1,9 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.JsonElement
+
+class JsonElementContext(val element: JsonElement) : ContextValue {
+ override fun serialize(): JsonElement {
+ return element
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt
new file mode 100644
index 0000000..82a76ed
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt
@@ -0,0 +1,25 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.GsonBuilder
+import org.apache.logging.log4j.Logger
+
+class LoggingEventRecorder(
+ val logger: Logger,
+ val logJson: Boolean
+) : EventRecorder {
+ companion object {
+ private val gson = GsonBuilder().setPrettyPrinting().create()
+ }
+
+ override fun record(event: RecordedEvent) {
+ val exc = event.context.getT<ExceptionContextValue>(CommonKeys.EXCEPTION)
+ var message = "Event Recorded: " + event.context.getT<StringContext>(CommonKeys.EVENT_MESSAGE)?.message
+ if (logJson) {
+ message += "\n" + gson.toJson(event.context.serialize())
+ }
+ if (exc != null)
+ logger.error(message, exc.exception)
+ else
+ logger.warn(message)
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt
new file mode 100644
index 0000000..346417d
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt
@@ -0,0 +1,5 @@
+package moe.nea.ledger.utils.telemetry
+
+class RecordedEvent(val context: Context) {
+
+}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt
new file mode 100644
index 0000000..e9a3b79
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt
@@ -0,0 +1,8 @@
+package moe.nea.ledger.utils.telemetry
+
+enum class Severity {
+ INFO,
+ WARN,
+ ERROR,
+ CRITICAL,
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt
new file mode 100644
index 0000000..0d680a9
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt
@@ -0,0 +1,146 @@
+package moe.nea.ledger.utils.telemetry
+
+class Span(val parent: Span?) : AutoCloseable {
+ companion object {
+ private val _current = object : InheritableThreadLocal<Span>() {
+ override fun initialValue(): Span {
+ return Span(null)
+ }
+
+ override fun childValue(parentValue: Span?): Span {
+ return parentValue?.forkNewRoot() ?: initialValue()
+ }
+ }
+
+ fun current(): Span {
+ return _current.get()
+ }
+ }
+
+ private val data = Context()
+
+ // TODO : replace string key with a SpanKey<T> class
+ fun add(key: String, value: ContextValue) {
+ data.add(key, value)
+ }
+
+ /**
+ * Create a sub span, and [enter] it, with the given values.
+ */
+ fun <T> enterWith(vararg pairs: Pair<String, ContextValue>, block: Span.() -> T): T {
+ return enter().use { span ->
+ pairs.forEach { (k, value) ->
+ span.add(k, value)
+ }
+ block(span)
+ }
+ }
+
+ /**
+ * Create a sub span, to attach some additional context, without modifying the [current] at all.
+ */
+ fun forkWith(vararg pairs: Pair<String, ContextValue?>): Span {
+ val newSpan = fork()
+ for ((key, value) in pairs) {
+ if (value == null) continue
+ newSpan.add(key, value)
+ }
+ return newSpan
+ }
+
+ /**
+ * Create a sub span, to which additional context can be added. This context will receive updates from its parent,
+ * and will be set as the [current]. To return to the parent, either call [exit] on the child. Or use inside of a
+ * [use] block.
+ */
+ fun enter(): Span {
+ require(_current.get() == this)
+ return fork().enterSelf()
+ }
+
+ /**
+ * Force [enter] this span, without creating a subspan. This bypasses checks like parent / child being the [current].
+ */
+ fun enterSelf(): Span {
+ _current.set(this)
+ return this
+ }
+
+ /**
+ * Creates a temporary sub span, to which additional context can be added. This context will receive updates from
+ * its parent, but will not be set as the [current].
+ */
+ fun fork(): Span {
+ return Span(this)
+ }
+
+ /**
+ * Create a new root span, that will not receive any updates from the current span, but will have all the same
+ * context keys associated.
+ */
+ fun forkNewRoot(): Span {
+ val newRoot = Span(null)
+ newRoot.data.data.putAll(collectContext().data)
+ return newRoot
+ }
+
+ /**
+ * Collect the context, including all parent context
+ */
+ fun collectContext(): Context {
+ if (parent != null)
+ return data.combineWith(parent.collectContext())
+ return data
+ }
+
+ /**
+ * Exit an [entered][enter] span, returning back to the parent context, and discard any current keys.
+ */
+ fun exit() {
+ require(parent != null)
+ require(_current.get() == this)
+ _current.set(parent)
+ }
+
+ /**
+ * [AutoCloseable] implementation for [exit]
+ */
+ override fun close() {
+ return exit()
+ }
+
+ /**
+ * Record an empty event given the context. This indicates nothing except for "I was here".
+ * @see recordMessageEvent
+ * @see recordException
+ */
+ fun recordEmptyTrace(recorder: EventRecorder) {
+ recorder.record(RecordedEvent(collectContext()))
+ }
+
+ /**
+ * Record a message with the key `"event_message"` to the recorder
+ */
+ fun recordMessageEvent(
+ recorder: EventRecorder,
+ message: String
+ ) {
+ forkWith(CommonKeys.EVENT_MESSAGE to ContextValue.string(message))
+ .recordEmptyTrace(recorder)
+ }
+
+ /**
+ * Record an exception to the recorder
+ */
+ fun recordException(
+ recorder: EventRecorder,
+ exception: Throwable,
+ message: String? = null
+ ) {
+ forkWith(
+ CommonKeys.EVENT_MESSAGE to message?.let(ContextValue::string),
+ CommonKeys.EXCEPTION to ExceptionContextValue(exception),
+ ).recordEmptyTrace(recorder)
+ }
+
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt
new file mode 100644
index 0000000..2d33075
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt
@@ -0,0 +1,11 @@
+package moe.nea.ledger.utils.telemetry
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+
+class StringContext(val message: String) : ContextValue {
+ override fun serialize(): JsonElement {
+ return JsonPrimitive(message)
+ }
+
+}