sbt Example: Lagom

Here are instructions for how to take a sample application and add telemetry to it for Lagom. In this example you will add Cinnamon and a Coda Hale Console reporter will be used to print telemetry output to the terminal window.

Prerequisites

The following must be installed for these instructions to work:

  • Java
  • sbt
  • Bintray credentials

Bintray credentials

Follow these instructions to set up your Bintray credentials for sbt.

Sample application

We will use a stripped down version of the Hello Service from the Java with sbt version of the Lagom getting started guide.

Start off by creating a folder lagom-example with the following files (content of files will be added later):

  • build.sbt
  • project/build.properties
  • project/plugins.sbt
  • hello-service/src/main/java/cinnamon/lagom/api/HelloService.java
  • hello-service/src/main/java/cinnamon/lagom/api/Hello2Service.java
  • hello-service-impl/src/main/resources/application.conf
  • hello-service-impl/src/main/java/cinnamon/lagom/impl/HelloServiceImpl.java
  • hello-service-impl/src/main/java/cinnamon/lagom/impl/Hello2ServiceImpl.java
  • hello-service-impl/src/main/java/cinnamon/lagom/impl/HelloModule.java

Next step is to add content to the files created.

Add to build.sbt:

organization in ThisBuild := "cinnamon.lagom"

// the Scala version that will be used for cross-compiled libraries
scalaVersion in ThisBuild := "2.11.11"

lazy val cinnamonDependencies = Seq(
  // Use Coda Hale Metrics and Lagom instrumentation
  Cinnamon.library.cinnamonCHMetrics,
  Cinnamon.library.cinnamonLagom
)

lazy val helloServiceApi = project("hello-service-api")
  .settings(
    version := "0.1-SNAPSHOT",
    // Add the resolver for the cinnamon dependencies
    resolvers += Cinnamon.resolver.commercial,
    libraryDependencies ++= Seq(lagomJavadslApi) ++ cinnamonDependencies
  )

lazy val helloServiceImpl = project("hello-service-impl")
  .enablePlugins(LagomJava)
  .settings(version := "0.1-SNAPSHOT")
  .settings(lagomForkedTestSettings: _*)
  .dependsOn(helloServiceApi)

def project(id: String) = Project(id, base = file(id))
  .settings(eclipseSettings: _*)
  .settings(javacOptions in compile ++= Seq("-encoding", "UTF-8", "-source", "1.8", "-target", "1.8", "-Xlint:unchecked", "-Xlint:deprecation"))
  .settings(jacksonParameterNamesJavacSettings: _*) // applying it to every project even if not strictly needed.

// See https://github.com/FasterXML/jackson-module-parameter-names
lazy val jacksonParameterNamesJavacSettings = Seq(
  javacOptions in compile += "-parameters"
)

// do not delete database files on start
lagomCassandraCleanOnStart in ThisBuild := false

Add to project/build.properties:

sbt.version=0.13.16

Add to project/plugins.sbt:

// The Lagom plugin
addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.3.6")
// Needed for importing the project into Eclipse
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.1.0")
// The Cinnamon Telemetry plugin
addSbtPlugin("com.lightbend.cinnamon" % "sbt-cinnamon" % "2.5.0")
// Credentials and resolver to download the Cinnamon Telemetry libraries
credentials += Credentials(Path.userHome / ".lightbend" / "commercial.credentials")
resolvers += Resolver.url("lightbend-commercial", url("https://repo.lightbend.com/commercial-releases"))(Resolver.ivyStylePatterns)

Make sure that the credentials setting points to where you chose to create the commercial credentials file.

Add to hello-service/src/main/java/cinnamon/lagom/api/HelloService.java:

package cinnamon.lagom.api;

import akka.NotUsed;

import com.lightbend.lagom.javadsl.api.*;
import static com.lightbend.lagom.javadsl.api.Service.*;

public interface HelloService extends Service {
    /**
     * Example: curl http://localhost:9000/api/hello/Alice
     */
    ServiceCall<NotUsed, String> hello(String id);

    @Override
    default Descriptor descriptor() {
        return named("hello").withCalls(
            pathCall("/api/hello/:id", this::hello)
        ).withAutoAcl(true);
    }
}

Add to hello-service-impl/src/main/java/cinnamon/lagom/impl/HelloServiceImpl.java:

package cinnamon.lagom.impl;

import akka.NotUsed;

import cinnamon.lagom.api.HelloService;
import com.lightbend.lagom.javadsl.api.ServiceCall;
import static java.util.concurrent.CompletableFuture.completedFuture;

public class HelloServiceImpl implements HelloService {

    @Override
    public ServiceCall<NotUsed, String> hello(String id) {
        return request -> {
            return completedFuture("Hello " + id);
        };
    }
}

Since circuit breakers are used for outgoing connections in Lagom, we need to add a second service that does an outgoing call to the first service. This Hello2Service is almost a copy of the HelloService.

Add to hello-service/src/main/java/cinnamon/lagom/api/Hello2Service.java:

package cinnamon.lagom.api;

import akka.NotUsed;

import com.lightbend.lagom.javadsl.api.*;
import static com.lightbend.lagom.javadsl.api.Service.*;

/**
 * The Hello2 service interface.
 * <p>
 * This describes everything that Lagom needs to know about how to serve and
 * consume the Hello2.
 */
public interface Hello2Service extends Service {
    /**
     * Example: curl http://localhost:9000/api/hello2/Alice
     */
    ServiceCall<NotUsed, String> hello2(String id);

    @Override
    default Descriptor descriptor() {
        return named("hello2").withCalls(
            pathCall("/api/hello2/:id", this::hello2)
        ).withAutoAcl(true);
    }
}

Add to hello-service-impl/src/main/java/cinnamon/lagom/impl/Hello2ServiceImpl.java:

package cinnamon.lagom.impl;

import akka.NotUsed;

import cinnamon.lagom.api.HelloService;
import cinnamon.lagom.api.Hello2Service;
import com.lightbend.lagom.javadsl.api.ServiceCall;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;

/**
 * Implementation of the Hello2Service.
 */
public class Hello2ServiceImpl implements Hello2Service {

    private final HelloService helloService;

    @Inject
    public Hello2ServiceImpl(HelloService helloService) {
        this.helloService = helloService;
    }

    @Override
    public ServiceCall<NotUsed, String> hello2(String id) {
        return msg -> {
                CompletionStage<String> response = helloService.hello(id).invoke(NotUsed.getInstance());
                return response.thenApply(answer -> "Hello service said: " + answer);
            };
    }
}

Now you need to register the two services in a module.

Add to hello-service-impl/src/main/java/cinnamon/lagom/impl/HelloModule.java:

package cinnamon.lagom.impl;

import cinnamon.lagom.api.HelloService;
import cinnamon.lagom.api.Hello2Service;
import com.google.inject.AbstractModule;
import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport;

public class HelloModule extends AbstractModule implements ServiceGuiceSupport {
  @Override
  protected void configure() {
      bindServices(serviceBinding(HelloService.class, HelloServiceImpl.class),
                   serviceBinding(Hello2Service.class, Hello2ServiceImpl.class));
  }
}

Finally, add to hello-service-impl/src/main/resources/application.conf:

play.modules.enabled += cinnamon.lagom.impl.HelloModule

lagom.circuit-breaker.default.max-failures = 10

lagom.spi.circuit-breaker-metrics-class = "cinnamon.lagom.CircuitBreakerInstrumentation"

cinnamon.application = "hello-lagom"

cinnamon.chmetrics.reporters += "console-reporter"

The application.conf file is where the actual wiring of telemetry takes place so let us dissect this further:

Setting Explanation
lagom.spi.circuit-breaker-metrics-class The class that should handle all circuit breaker related metrics. Lightbend Telemetry provides a default implementation. You can use your own implementation if you would like. For more information you please see the Lagom SPI implementation.
cinnamon.chmetrics.reporters Specifies the Coda Hale reporter you wish to use. For more information see Coda Hale. Note that there are other ways to send data, e.g. StatsD or OverOps.

A Lagom application normally consists of multiple projects, one for each microservice, and you need to make sure that there is an application.conf file for each project that you would like to instrument.

Running

When you have added the files above you simply use sbt to run the application:

> sbt runAll

The output should look something like this:

...
[info] Starting embedded Cassandra server
........
[info] Cassandra server running at 127.0.0.1:4000
[info] Service locator is running at http://localhost:8000
[info] Service gateway is running at http://localhost:9000
[info] c.l.c.c.CodaHaleMetrics - Reporter com.lightbend.cinnamon.chmetrics.reporter.provided.ConsoleReporter@15114edf started.
[info] c.l.c.c.CodaHaleMetrics - Reporter com.lightbend.cinnamon.chmetrics.reporter.provided.ConsoleReporter@728e96a3 started.
[info] Service hello-service-impl listening for HTTP on 0:0:0:0:0:0:0:0:25583
[info] (Service started, use Ctrl+D to stop and go back to the console...)
...

To try out the Hello2Service and see the metrics for the hello circuit breaker you can either point your browser to http://localhost:9000/api/hello2/World or simply run curl from the command line like this curl http://localhost:9000/api/hello2/World

The output should now look something like this:

...
6/15/17 10:07:54 AM ============================================================

-- Gauges ----------------------------------------------------------------------
metrics.lagom.circuit-breakers.hello.state
             value = 3

-- Histograms ------------------------------------------------------------------
metrics.lagom.circuit-breakers.hello.latency
             count = 1
               min = 575804000
               max = 575804000
              mean = 575804000.00
            stddev = 0.00
            median = 575804000.00
              75% <= 575804000.00
              95% <= 575804000.00
              98% <= 575804000.00
              99% <= 575804000.00
            99.9% <= 575804000.00
...