Custom metrics

The custom metrics extension provides an API whereby a developer can create one of six custom metric types for capturing arbitrary metric data. Also, the API provides the ability to create the custom metrics at the system-level, the actor-level, or the application-level. For example, at the actor-level, you could build and use a Counter metric to capture the number of times an actor receives a particular message.

Custom metric types

The custom metric types we currently support are as follows:

  • Counter: provides an integer to increment and decrement.
  • Gauge Double: provides a settable double.
  • Gauge Long: provides a settable long value.
  • Providing Gauge Long: takes a LongValueProvider as the underlying metric.
  • Providing Gauge Double: takes a DoubleValueProvider as the underlying metric.
  • Rate: marks an occurrence for measuring the rate at which it occurs.
  • Recorder: records a long value which can be used to measure distribution.

The underlying behavior of the metric type is defined by the backend used in configuration. For example, if you use chmetrics as your backend, then the underlying metric for a Recorder is a CodaHale Histogram. For more information on backends see Backend Plugins.

Developer API

Accessing CinnamonMetrics

To start using the Cinnamon developer API you will first want to import CinnamonMetrics and the associated metric interfaces:

Scala
import com.lightbend.cinnamon.akka.CinnamonMetrics
import com.lightbend.cinnamon.metric._
Java
import com.lightbend.cinnamon.akka.CinnamonMetrics;
import com.lightbend.cinnamon.metric.*;

Accessing CinnamonMetrics from Akka Typed

For Akka Typed, there is a separate extension that can be used with typed actor systems or actor contexts.

First add the Cinnamon Akka Typed module dependency to your build file:

sbt
libraryDependencies += Cinnamon.library.cinnamonAkkaTyped
Maven
<dependency>
  <groupId>com.lightbend.cinnamon</groupId>
  <artifactId>cinnamon-akka-typed_2.13</artifactId>
  <version>2.20.4</version>
</dependency>
Gradle
dependencies {
  implementation group: 'com.lightbend.cinnamon', name: 'cinnamon-akka-typed_2.13', version: '2.20.4'
}

Then import the typed extension for CinnamonMetrics:

Scala
import com.lightbend.cinnamon.akka.typed.CinnamonMetrics
import com.lightbend.cinnamon.metric._
Java
import com.lightbend.cinnamon.akka.typed.CinnamonMetrics;
import com.lightbend.cinnamon.metric.*;

Creating system-level custom metrics

System level custom metrics provide an excellent way to capture metric data at the ActorSystem level. To create custom metrics at this level, you use CinnamonMetrics with the ActorSystem and call the appropriate create-method for the desired metric as follows:

Scala
val sysCounter: Counter = CinnamonMetrics(system).createCounter("sysCounter")
val sysGaugeDouble: GaugeDouble = CinnamonMetrics(system).createGaugeDouble("sysGaugeDouble")
val sysGaugeLong: GaugeLong = CinnamonMetrics(system).createGaugeLong("sysGaugeLong")
val sysProvidingGaugeDouble: ProvidingGaugeDouble = CinnamonMetrics(system).createProvidingGaugeDouble("sysProvidingGaugeDouble", doubleValueProvider)
val sysProvidingGaugeLong: ProvidingGaugeLong = CinnamonMetrics(system).createProvidingGaugeLong("sysProvidingGaugeLong", longValueProvider)
val sysRate: Rate = CinnamonMetrics(system).createRate("sysRate")
val sysRecorder: Recorder = CinnamonMetrics(system).createRecorder("sysRecorder")
Java
Counter sysCounter = CinnamonMetrics.get(getSystem()).createCounter("sysCounter");
GaugeDouble sysGaugeDouble = CinnamonMetrics.get(getSystem()).createGaugeDouble("sysGaugeDouble");
GaugeLong sysGaugeLong = CinnamonMetrics.get(getSystem()).createGaugeLong("sysGaugeLong");
ProvidingGaugeDouble sysProvidingGaugeDouble =
    CinnamonMetrics.get(getSystem())
        .createProvidingGaugeDouble("sysProvidingGaugeDouble", doubleValueProvider);
ProvidingGaugeLong sysProvidingGaugeLong =
    CinnamonMetrics.get(getSystem())
        .createProvidingGaugeLong("sysProvidingGaugeLong", longValueProvider);
Rate sysRate = CinnamonMetrics.get(getSystem()).createRate("sysRate");
Recorder sysRecorder = CinnamonMetrics.get(getSystem()).createRecorder("sysRecorder");

Creating actor-level custom metrics

Actor level custom metrics provide an excellent way to capture actor-specific behavior as metric data. To create custom metrics at this level, you use CinnamonMetrics with the ActorContext and call the appropriate create-method for the desired metric as follows:

Scala
val counter: Counter = CinnamonMetrics(context).createCounter("counter")
val gaugeDouble: GaugeDouble = CinnamonMetrics(context).createGaugeDouble("gaugeDouble")
val gaugeLong: GaugeLong = CinnamonMetrics(context).createGaugeLong("gaugeLong")
val providingGaugeDouble: ProvidingGaugeDouble = CinnamonMetrics(context).createProvidingGaugeDouble("providingGaugeDouble", doubleValueProvider)
val providingGaugeLong: ProvidingGaugeLong = CinnamonMetrics(context).createProvidingGaugeLong("providingGaugeLong", longValueProvider)
val rate: Rate = CinnamonMetrics(context).createRate("rate")
val recorder: Recorder = CinnamonMetrics(context).createRecorder("recorder")
Java
Counter counter = CinnamonMetrics.get(getContext()).createCounter("counter");
GaugeDouble gaugeDouble = CinnamonMetrics.get(getContext()).createGaugeDouble("gaugeDouble");
GaugeLong gaugeLong = CinnamonMetrics.get(getContext()).createGaugeLong("gaugeLong");
ProvidingGaugeDouble providingGaugeDouble =
    CinnamonMetrics.get(getContext())
        .createProvidingGaugeDouble("providingGaugeDouble", doubleValueProvider);
ProvidingGaugeLong providingGaugeLong =
    CinnamonMetrics.get(getContext())
        .createProvidingGaugeLong("providingGaugeLong", longValueProvider);
Rate rate = CinnamonMetrics.get(getContext()).createRate("rate");
Recorder recorder = CinnamonMetrics.get(getContext()).createRecorder("recorder");

Aggregation style

Custom metrics on the actor-level are aggregated in the same way as Lightbend Telemetry actor metrics.

For example if report-by = class is configured for a specific actor then any custom metrics created in this actor will be aggregated by actor class. And the same goes for instance and group aggregation. More information about how to configure actor aggregation can be found here.

Creating application-level custom metrics

Application level custom metrics are associated with the application as a whole, rather than with the actor system. See Cinnamon metadata for setting the application name. To create custom metrics at the application level, you first need to use the CinnamonMetrics extension with the ActorSystem or with the ActorContext, and then access the application-level metrics using the metricsForApplication method. You can then call the appropriate create method to create custom metrics:

Scala
val appMetrics = CinnamonMetrics(system).metricsForApplication()

val appCounter: Counter = appMetrics.createCounter("appCounter")
val appGaugeDouble: GaugeDouble = appMetrics.createGaugeDouble("appGaugeDouble")
val appGaugeLong: GaugeLong = appMetrics.createGaugeLong("appGaugeLong")
val appProvidingGaugeDouble: ProvidingGaugeDouble = appMetrics.createProvidingGaugeDouble("appProvidingGaugeDouble", doubleValueProvider)
val appProvidingGaugeLong: ProvidingGaugeLong = appMetrics.createProvidingGaugeLong("appProvidingGaugeLong", longValueProvider)
val appRate: Rate = appMetrics.createRate("appRate")
val appRecorder: Recorder = appMetrics.createRecorder("appRecorder")
Java
CinnamonMetrics appMetrics = CinnamonMetrics.get(getSystem()).metricsForApplication();

Counter appCounter = appMetrics.createCounter("appCounter");
GaugeDouble appGaugeDouble = appMetrics.createGaugeDouble("appGaugeDouble");
GaugeLong appGaugeLong = appMetrics.createGaugeLong("appGaugeLong");
ProvidingGaugeDouble appProvidingGaugeDouble =
    appMetrics.createProvidingGaugeDouble("appProvidingGaugeDouble", doubleValueProvider);
ProvidingGaugeLong appProvidingGaugeLong =
    appMetrics.createProvidingGaugeLong("appProvidingGaugeLong", longValueProvider);
Rate appRate = appMetrics.createRate("appRate");
Recorder appRecorder = appMetrics.createRecorder("appRecorder");

Adding tags to custom metrics

Tags can be added to custom metrics by passing a map of tags to the metric create-method. For example:

Scala
val taggedRecorder: Recorder = CinnamonMetrics(context).createRecorder("tagged-recorder", tags = Map("key" -> "value"))
Java
Map<String, String> tags = ImmutableMap.of("key", "value");
Recorder taggedRecorder =
    CinnamonMetrics.get(getContext()).createRecorder("tagged-recorder", tags);

ProvidingGaugeLong and ProvidingGaugeDouble value providers

In the examples above you will note that the ProvidingGaugeLong takes an additional parameter, provider. The ProvidingGaugeLong is a special custom metric that takes an external provider of a Long value and reports that value when asked by the underlying metrics backend.

Scala
val longValueProvider: LongValueProvider = new LongValueProvider {
  val cnt: AtomicLong = new AtomicLong(0)
  override def currentValue(): Long = cnt.getAndIncrement()
}
Java
LongValueProvider longValueProvider =
    new LongValueProvider() {
      AtomicLong cnt = new AtomicLong(0);

      @Override
      public long currentValue() {
        return cnt.getAndIncrement();
      }
    };

There is also a ProvidingGaugeDouble that can be used:

Scala
val doubleValueProvider: DoubleValueProvider = new DoubleValueProvider {
  val cnt: AtomicReference[Double] = new AtomicReference(0.0)
  override def currentValue(): Double = cnt.get()
}
Java
DoubleValueProvider doubleValueProvider =
    new DoubleValueProvider() {
      AtomicReference<Double> cnt = new AtomicReference(0.0);

      @Override
      public double currentValue() {
        return cnt.get();
      }
    };

Using custom metrics

Using the custom metrics involves calling one of the methods supplied by the metric interface as follows:

Scala
counter.increment()

counter.decrement()

gaugeDouble.set(4.2)

gaugeLong.set(12L)

rate.mark()

recorder.record(42L)
Java
counter.increment();

counter.decrement();

gaugeDouble.set(4.2);

gaugeLong.set(12L);

rate.mark();

recorder.record(42L);

Destroying custom metrics

When finished with the metric(s), you should call destroy. Calling destroy will, in turn, invoke the underlying metric backend to run any cleanup tasks if required.

Scala
counter.destroy()

gaugeDouble.destroy()

gaugeLong.destroy()

providingGaugeLong.destroy()

providingGaugeDouble.destroy()

rate.destroy()

recorder.destroy()
Java
counter.destroy();

gaugeDouble.destroy();

gaugeLong.destroy();

providingGaugeDouble.destroy();

providingGaugeLong.destroy();

rate.destroy();

recorder.destroy();

Custom metric timer utility

In addition to the custom metric types, the developer API also provides a timer utility which you can use to time a block of code in nanoseconds. The elapsed time for the code block to run is captured to an underlying Recorder with the name used in the createTimer(...) method.

Scala
val timer: Timer = CinnamonMetrics(context).createTimer("timer")

timer.time { 1 to 10000 sum }
Java
Timer timer = CinnamonMetrics.get(getContext()).createTimer("timer");

timer.time(() -> IntStream.rangeClosed(1, 100000).sum());

For a timer that can be used over asynchronous flows, such as across actors, see the Stopwatch extension.

Custom metric example

Here is an example of actor-level custom metrics by using a Counter to keep track of particular messages that an actor might receive.

import com.lightbend.cinnamon.akka.CinnamonMetrics
import com.lightbend.cinnamon.metric._

case object Init
case object In
case object Out
case object Done

class CustomerCountActor extends Actor {
  val counter: Counter = CinnamonMetrics(context).createCounter("customerCounter")

  def receive: PartialFunction[Any, Unit] = {
    case Init =>
      sender() ! Init
    case In =>
      counter.increment()
      sender() ! In
    case Out =>
      counter.decrement()
      sender() ! Out
    case Done =>
      counter.destroy()
      sender() ! Done
      context.stop(self)
  }
}