diff options
Diffstat (limited to 'src/main/kotlin/moe/nea/ledger/utils/telemetry')
12 files changed, 397 insertions, 0 deletions
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) + } + +} |