aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/moe/nea/ledger/DevUtil.kt7
-rw-r--r--src/main/kotlin/moe/nea/ledger/Ledger.kt23
-rw-r--r--src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt66
-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
16 files changed, 524 insertions, 6 deletions
diff --git a/src/main/kotlin/moe/nea/ledger/DevUtil.kt b/src/main/kotlin/moe/nea/ledger/DevUtil.kt
new file mode 100644
index 0000000..d0dd653
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/DevUtil.kt
@@ -0,0 +1,7 @@
+package moe.nea.ledger
+
+import net.minecraft.launchwrapper.Launch
+
+object DevUtil {
+ val isDevEnv = Launch.blackboard["fml.deobfuscatedEnvironment"] as Boolean
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/Ledger.kt b/src/main/kotlin/moe/nea/ledger/Ledger.kt
index b3afd37..f8c8d99 100644
--- a/src/main/kotlin/moe/nea/ledger/Ledger.kt
+++ b/src/main/kotlin/moe/nea/ledger/Ledger.kt
@@ -7,6 +7,7 @@ import moe.nea.ledger.database.Database
import moe.nea.ledger.events.ChatReceived
import moe.nea.ledger.events.LateWorldLoadEvent
import moe.nea.ledger.events.RegistrationFinishedEvent
+import moe.nea.ledger.gen.BuildConfig
import moe.nea.ledger.modules.AuctionHouseDetection
import moe.nea.ledger.modules.BankDetection
import moe.nea.ledger.modules.BazaarDetection
@@ -20,6 +21,7 @@ import moe.nea.ledger.modules.MinionDetection
import moe.nea.ledger.modules.NpcDetection
import moe.nea.ledger.modules.VisitorDetection
import moe.nea.ledger.utils.DI
+import moe.nea.ledger.utils.ErrorUtil
import net.minecraft.client.Minecraft
import net.minecraft.command.ICommand
import net.minecraftforge.client.ClientCommandHandler
@@ -36,7 +38,7 @@ import org.apache.logging.log4j.LogManager
import java.io.File
import java.util.concurrent.ConcurrentLinkedQueue
-@Mod(modid = "ledger", useMetadata = true)
+@Mod(modid = "ledger", useMetadata = true, version = BuildConfig.VERSION)
class Ledger {
/*
You have withdrawn 1M coins! You now have 518M coins in your account!
@@ -88,6 +90,7 @@ class Ledger {
logger.info("Initializing ledger")
val di = DI()
+ TelemetryProvider.setupFor(di)
di.registerSingleton(this)
di.registerSingleton(Minecraft.getMinecraft())
di.registerSingleton(gson)
@@ -101,6 +104,7 @@ class Ledger {
ConfigCommand::class.java,
Database::class.java,
DungeonChestDetection::class.java,
+ ErrorUtil::class.java,
ItemIdProvider::class.java,
KatDetection::class.java,
KuudraChestDetection::class.java,
@@ -111,12 +115,19 @@ class Ledger {
QueryCommand::class.java,
VisitorDetection::class.java,
)
- di.instantiateAll()
- di.getAllInstances().forEach(MinecraftForge.EVENT_BUS::register)
- di.getAllInstances().filterIsInstance<ICommand>()
- .forEach { ClientCommandHandler.instance.registerCommand(it) }
+ val errorUtil = di.provide<ErrorUtil>()
+ errorUtil.catch {
+ di.instantiateAll()
+ di.getAllInstances().forEach(MinecraftForge.EVENT_BUS::register)
+ di.getAllInstances().filterIsInstance<ICommand>()
+ .forEach { ClientCommandHandler.instance.registerCommand(it) }
+ }
+
+ errorUtil.catch {
+ di.provide<Database>().loadAndUpgrade()
+ error("Lol")
+ }
- di.provide<Database>().loadAndUpgrade()
MinecraftForge.EVENT_BUS.post(RegistrationFinishedEvent())
}
diff --git a/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt b/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt
new file mode 100644
index 0000000..04f3fd2
--- /dev/null
+++ b/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt
@@ -0,0 +1,66 @@
+package moe.nea.ledger
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.ledger.gen.BuildConfig
+import moe.nea.ledger.utils.DI
+import moe.nea.ledger.utils.DIProvider
+import moe.nea.ledger.utils.telemetry.CommonKeys
+import moe.nea.ledger.utils.telemetry.ContextValue
+import moe.nea.ledger.utils.telemetry.EventRecorder
+import moe.nea.ledger.utils.telemetry.JsonElementContext
+import moe.nea.ledger.utils.telemetry.LoggingEventRecorder
+import moe.nea.ledger.utils.telemetry.Span
+import net.minecraft.client.Minecraft
+import net.minecraft.util.Session
+import net.minecraftforge.fml.common.Loader
+
+object TelemetryProvider {
+ fun injectTo(di: DI) {
+ di.register(
+ EventRecorder::class.java,
+ if (DevUtil.isDevEnv) DIProvider.singeleton(LoggingEventRecorder(Ledger.logger, true))
+ else DIProvider.singeleton(
+ LoggingEventRecorder(Ledger.logger, false)) // TODO: replace with upload to server
+ )
+ }
+
+ val USER = "minecraft_user"
+ val MINECRAFT_VERSION = "minecraft_version"
+ val MODS = "mods"
+
+ class MinecraftUser(val session: Session) : ContextValue {
+ override fun serialize(): JsonElement {
+ val obj = JsonObject()
+ obj.addProperty("uuid", session.playerID)
+ obj.addProperty("name", session.username)
+ return obj
+ }
+ }
+
+ fun setupDefaultSpan() {
+ val sp = Span.current()
+ sp.add(USER, MinecraftUser(Minecraft.getMinecraft().session))
+ sp.add(MINECRAFT_VERSION, ContextValue.compound(
+ "static" to "1.8.9",
+ "rt" to Minecraft.getMinecraft().version,
+ ))
+ val mods = JsonArray()
+ Loader.instance().activeModList.map {
+ val obj = JsonObject()
+ obj.addProperty("id", it.modId)
+ obj.addProperty("version", it.version)
+ obj.addProperty("displayVersion", it.displayVersion)
+ obj
+ }.forEach(mods::add)
+ sp.add(MODS, JsonElementContext(mods))
+ sp.add(CommonKeys.VERSION, ContextValue.string(BuildConfig.FULL_VERSION))
+ sp.add(CommonKeys.COMMIT_VERSION, ContextValue.string(BuildConfig.GIT_COMMIT))
+ }
+
+ fun setupFor(di: DI) {
+ injectTo(di)
+ setupDefaultSpan()
+ }
+} \ No newline at end of file
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)
+ }
+
+}