diff options
Diffstat (limited to 'src/main/kotlin')
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) + } + +} |