Hi, I want to measure the time of the Effect execu...
# arrow
d
Hi, I want to measure the time of the Effect execution and report it via Micrometer to Prometheus. I want to have some metric tags to be “calculated” based on the result of Effect evaluation - I want to know the Result - Success, Error, Exception, in case of error, the ErrorType, in case of exception, the ExceptionType, and a few additional “static” tags. I have something similar for functions returning Either. I’ve come up with the following solution.
Copy code
import arrow.core.raise.Effect
import arrow.core.raise.Raise
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Timer

sealed interface Outcome<out Error, out Value> {
    data class Value<out Value>(val value: Value) : Outcome<Nothing, Value>
    data class Error<out Error>(val error: Error) : Outcome<Error, Nothing>
    data class Throwable(val throwable: kotlin.Throwable) : Outcome<Nothing, Nothing>
}

class TimedRaise<Error>(private val delegate: Raise<Error>) : Raise<Error> by delegate {
    var error: Error? = null

    override fun raise(r: Error): Nothing {
        error = r
        delegate.raise(r)
    }
}

typealias GenerateTags<E, V> = (Outcome<E, V>) -> List<Tag>

class TimedEffect<E, A>(
        private val meterRegistry: MeterRegistry,
        private val metricName: String,
        private val tags: List<Tag>,
        private val generateTags: GenerateTags<E, A>,
        private val delegate: Effect<E, A>,
) : Effect<E, A> by delegate {
    override suspend fun invoke(raise: Raise<E>): A {
        val timedRaise = TimedRaise(raise)
        val sample = Timer.start(meterRegistry)
        val result = runCatching { delegate.invoke(timedRaise) }
        val outcome = outcome(result, timedRaise.error)
        val outcomeTags = generateTags(outcome)
        val timer = meterRegistry.timer(metricName, tags + outcomeTags)
        sample.stop(timer)
        result.fold(
                { value -> return value },
                { e -> throw e }
        )
    }

    private fun outcome(result: Result<A>, error: E?): Outcome<E, A> = when {
        error != null -> Outcome.Error(error)
        else -> result.fold({ Outcome.Value(it) }, { Outcome.Throwable(it) })
    }
}

fun <E, A> Effect<E, A>.timed(
        meterRegistry: MeterRegistry,
        metricName: String,
        tags: List<Tag> = emptyList(),
        generateTags: GenerateTags<E, A> = { emptyList() }
): Effect<E, A> = TimedEffect(meterRegistry, metricName, tags, generateTags, this)
Is it correct? Will it cover all possible scenarios - result, error, exception? Will it be possible to write a generic proxy with multiple context receivers?
s
Hey @dnowak, That is actually a very interesting use-case 🤔 Might be interesting to do some light integrations between micrometer and Arrow typed errors. I clean up the code a bit ☺️ I think it can be done simpler than you did it now, without such a custom `Raise`/`Effect` type just re-using & composing some of the builder inside Arrow.
Copy code
import arrow.core.raise.Effect
import arrow.core.raise.Raise
import arrow.core.raise.effect
import arrow.core.raise.recover
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Timer
import kotlin.experimental.ExperimentalTypeInference

sealed interface Outcome<out Error, out Value> {
  data class Value<out Value>(val value: Value) : Outcome<Nothing, Value>
  data class Error<out Error>(val error: Error) : Outcome<Error, Nothing>
  data class Throwable(val throwable: Throwable) : Outcome<Nothing, Nothing>
}

typealias GenerateTags<E, V> = (Outcome<E, V>) -> List<Tag>

fun <E, A> Effect<E, A>.timed(
  meterRegistry: MeterRegistry,
  metricName: String,
  tags: List<Tag> = emptyList(),
  generateTags: GenerateTags<E, A> = { emptyList() }
): Effect<E, A> = effect {
  timed(meterRegistry, metricName, tags, generateTags) {
    bind()
  }
}

@OptIn(ExperimentalTypeInference::class)
inline fun <E, A> outcome(@BuilderInference block: Raise<E>.() -> A): Outcome<E, A> =
  recover( // can also be arrow.core.raise.fold
    { Outcome.Value(block(this)) },
    { e: E -> Outcome.Error(e) }
  ) { t: Throwable -> Outcome.Throwable(t) }

// context(Raise<E>, MeterRegistry)
@OptIn(ExperimentalTypeInference::class)
inline fun <E, A> Raise<E>.timed(
  meterRegistry: MeterRegistry,
  metricName: String,
  tags: List<Tag> = emptyList(),
  generateTags: GenerateTags<E, A> = { emptyList() },
  @BuilderInference block: Raise<E>.() -> A
): A {
  val sample = Timer.start(meterRegistry)
  val outcome = outcome { block(this) }
  val outcomeTags = generateTags(outcome)
  val timer = meterRegistry.timer(metricName, tags + outcomeTags)
  sample.stop(timer)
  return when (outcome) {
    is Outcome.Error -> raise(outcome.error)
    is Outcome.Throwable -> throw outcome.throwable
    is Outcome.Value -> outcome.value
  }
}

// With context receivers
context(Raise<E>)
fun <E, A> Outcome<E, A>.bind(): A =
  when (this) {
    is Outcome.Error -> raise(error)
    is Outcome.Throwable -> throw throwable
    is Outcome.Value -> value
  }
Hope that helps you out! Let me know if anything is unclear
t
maybe use this kotlin feature to time?
s
This timer from micrometer is like a logging system, it's typically used in combination with more specialised instrumentation, etc and then used to create dashboards etc