From 7eac6bf1821da96280545887ea4dd53191c7a0b9 Mon Sep 17 00:00:00 2001 From: Francesco Guardiani Date: Mon, 13 Nov 2023 12:25:17 +0100 Subject: [PATCH] Improve README (#122) * Improve README with detailed project setup. * Add Jackson * Add logging and tracing setup --- README.md | 362 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 317 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 9b3c4ff8..c8e3189f 100644 --- a/README.md +++ b/README.md @@ -16,94 +16,366 @@ Check [Restate GitHub](https://github.com/restatedev/) or the [docs](https://doc ## Using the SDK -To get started, check out a template project in https://github.com/restatedev/examples/tree/main/jvm, or look at the examples in the [examples](examples) directory. +### tl;dr Use project templates -The SDK is composed in modules that you can pick depending on the service you want to build: +To get started, check out a template project in https://github.com/restatedev/examples/tree/main/jvm. -- [sdk-java-blocking](sdk-java-blocking) contains the plain Java blocking interface. -- [sdk-kotlin](sdk-kotlin) contains the Kotlin coroutines based interface. -- [sdk-http-vertx](sdk-http-vertx) contains the HTTP server endpoint implementation, based on [Eclipse Vert.x](https://vertx.io). -- [sdk-lambda](sdk-lambda) contains the AWS Lambda endpoint implementation, based on the [official AWS SDK](https://docs.aws.amazon.com/lambda/latest/dg/lambda-java.html). +### Setup a project (Java) -You need to set up a gRPC code-generator to generate the required Protobuf/gRPC classes. +Scaffold a project using the build tool of your choice. For example, with Gradle (Kotlin script): -For example, given the following contract: +``` +gradle init --type java-application +``` -```protobuf -service Counter { - option (dev.restate.ext.service_type) = KEYED; +Add the dependency [sdk-java-blocking](sdk-java-blocking): + +``` +implementation("dev.restate.sdk:sdk-java-blocking:1.0-SNAPSHOT") +``` + +Now you need to configure the protobuf plugin to build your Protobuf contracts. For example, with Gradle (Kotlin script): + +```kts +import com.google.protobuf.gradle.id + +plugins { + // ... + id("com.google.protobuf") version "0.9.1" + // ... +} + +dependencies { + // ... + // You need the following dependencies to compile the generated code + implementation("com.google.protobuf:protobuf-java:3.24.3") + implementation("io.grpc:grpc-stub:1.58.0") + implementation("io.grpc:grpc-protobuf:1.58.0") + compileOnly("org.apache.tomcat:annotations-api:6.0.53") +} + +// Configure protoc plugin +protobuf { + protoc { artifact = "com.google.protobuf:protoc:3.24.3" } + + plugins { + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.58.0" } + } + + generateProtoTasks { + all().forEach { + it.plugins { + id("grpc") + } + } + } +} +``` + +For Maven you can use the [xolstice Protobuf plugin](https://www.xolstice.org/protobuf-maven-plugin/index.html). + +### Setup a project (Kotlin) + +Scaffold a project using the build tool of your choice. For example, with Gradle (Kotlin script): + +``` +gradle init --type kotlin-application +``` + +Add the dependency [`sdk-kotlin`](sdk-kotlin): + +``` +implementation("dev.restate.sdk:sdk-kotlin:1.0-SNAPSHOT") +``` + +Now you need to configure the protobuf plugin to build your Protobuf contracts. For example, with Gradle (Kotlin script): + +```kts +import com.google.protobuf.gradle.id + +plugins { + // ... + id("com.google.protobuf") version "0.9.1" + // ... +} - rpc Add (AddRequest) returns (google.protobuf.Empty); - rpc Get (GetRequest) returns (GetResponse); +dependencies { + // ... + // You need the following dependencies to compile the generated code + implementation("com.google.protobuf:protobuf-java:3.24.3") + implementation("com.google.protobuf:protobuf-kotlin:3.24.3") + implementation("io.grpc:grpc-stub:1.58.0") + implementation("io.grpc:grpc-protobuf:1.58.0") + implementation("io.grpc:grpc-kotlin-stub:1.4.0") { exclude("javax.annotation", "javax.annotation-api") } + compileOnly("org.apache.tomcat:annotations-api:6.0.53") } -message GetRequest { - string counter_name = 1 [(dev.restate.ext.field) = KEY]; +// Configure protoc plugin +protobuf { + protoc { artifact = "com.google.protobuf:protoc:3.24.3" } + + plugins { + id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.58.0" } + id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.0:jdk8@jar" } + } + + generateProtoTasks { + all().forEach { + // We need both java and kotlin codegen(s) because the kotlin protobuf/grpc codegen depends on the java ones + it.plugins { + id("grpc") + id("grpckt") + } + it.builtins { + java {} + id("kotlin") + } + } + } } +``` + +### Add the Protobuf contract -message GetResponse { - int64 value = 1; +Now you can add the Protobuf contract under `src/main/proto`. For example: + +```protobuf +syntax = "proto3"; +package greeter; + +option java_package = "my.greeter"; +option java_outer_classname = "GreeterProto"; + +import "dev/restate/ext.proto"; + +service Greeter { + option (dev.restate.ext.service_type) = KEYED; + + rpc Greet (GreetRequest) returns (GreetResponse); +} + +message GreetRequest { + string name = 1 [(dev.restate.ext.field) = KEY]; } -message AddRequest { - string counter_name = 1 [(dev.restate.ext.field) = KEY]; - int64 value = 2; +message GreetResponse { + string message = 1; } ``` -A Java implementation looks like the following: +By using the Gradle or Maven plugin, the code is automatically re-generated on every build. + +### Implement the service (Java) + +Implement the service in a new class, for example: ```java -public class Counter extends CounterGrpc.CounterImplBase implements RestateBlockingService { +public class Greeter extends GreeterGrpc.GreeterImplBase implements RestateBlockingService { - private static final StateKey TOTAL = StateKey.of("total", Long.class); + private static final StateKey COUNT = StateKey.of("total", Long.class); @Override - public void add(AddRequest request, StreamObserver responseObserver) { + public void greet(GreetRequest request, StreamObserver responseObserver) { RestateContext ctx = restateContext(); - long currentValue = ctx.get(TOTAL).orElse(0L); - long newValue = currentValue + request.getValue(); - ctx.set(TOTAL, newValue); + long count = ctx.get(COUNT).orElse(0L); + ctx.set(COUNT, count + 1); - responseObserver.onNext(Empty.getDefaultInstance()); + responseObserver.onNext( + GreetResponse.newBuilder() + .setMessage(String.format("Hello %s for the %d time!", request.getName(), count)) + .build()); responseObserver.onCompleted(); } +} +``` - @Override - public void get(GetRequest request, StreamObserver responseObserver) { - long currentValue = restateContext().get(TOTAL).orElse(0L); +To serialize the state, you need a state serializer/deserializer. +To use [Jackson Databind](https://github.com/FasterXML/jackson), add the dependency [`sdk-serde-jackson`](sdk-serde-jackson). For example, in Gradle: - responseObserver.onNext(GetResponse.newBuilder().setValue(currentValue).build()); - responseObserver.onCompleted(); +``` +implementation("dev.restate.sdk:sdk-serde-jackson:1.0-SNAPSHOT") +``` + +### Implement the service (Kotlin) + +Implement the service in a new class, for example: + +```kotlin +class Greeter : + // Use Dispatchers.Unconfined as the Executor/thread pool is managed by the SDK itself. + GreeterGrpcKt.GreeterCoroutineImplBase(Dispatchers.Unconfined), + RestateCoroutineService { + companion object { + private val COUNT = StateKey.of("total", Long::class.java) + } + + override suspend fun greet(request: GreetRequest): GreetResponse { + val ctx = restateContext() + val count = ctx.get(COUNT) ?: 0L + ctx.set(COUNT, count + 1) + return greetResponse { message = "Hello ${request.name} for the $count time!" } } } ``` -A kotlin implementation looks like the following: +To serialize the state, you need a state serializer/deserializer. +To use [Jackson Databind](https://github.com/FasterXML/jackson), add the dependency [`sdk-serde-jackson`](sdk-serde-jackson). For example, in Gradle: + +``` +implementation("dev.restate.sdk:sdk-serde-jackson:1.0-SNAPSHOT") +``` + +### Deploy the service (HTTP Server) + +To deploy the Restate service as HTTP server, add [`sdk-http-vertx`](sdk-http-vertx) to the dependencies. For example, in Gradle: + +``` +implementation("dev.restate.sdk:sdk-http-vertx:1.0-SNAPSHOT") +``` + +To deploy the service, add the following code to the `main`. For example in Java: + +```java +public static void main(String[] args) { + RestateHttpEndpointBuilder.builder() + .withService(new Greeter()) + .buildAndListen(); +} +``` + +In Kotlin: ```kotlin -class Counter(coroutineContext: CoroutineContext) : - CounterGrpcKt.CounterCoroutineImplBase(coroutineContext), RestateCoroutineService { +fun main() { + RestateHttpEndpointBuilder.builder() + .withService(Greeter()) + .buildAndListen() +} +``` - private val TOTAL = StateKey.of("total", Long::class.java) +Execute the project. For example, using Gradle: - override suspend fun add(request: AddRequest): Empty { - val currentValue = restateContext().get(TOTAL) ?: 0L - val newValue = currentValue + add +``` +gradle run +``` - restateContext().set(TOTAL, newValue) +### Deploy the service (AWS Lambda) - return currentValue to newValue - return Empty.getDefaultInstance() +To deploy the Restate service as Lambda, add [`sdk-lambda`](sdk-lambda) to the dependencies. For example, in Gradle: + +``` +implementation("dev.restate.sdk:sdk-lambda:1.0-SNAPSHOT") +``` + +Configure the build tool to generate Fat-JARs, which are required by AWS Lambda to correctly load the JAR. For example, using Gradle: + +``` +plugins { + // ... + // The shadow plugin generates a shadow JAR ready for AWS Lambda + id("com.github.johnrengelman.shadow").version("7.1.2") + // ... +} +``` + +To deploy the service, create the following class. For example, in Java: + +```java +public class LambdaFactory implements LambdaRestateServerFactory { + @Override + public LambdaRestateServer create() { + return LambdaRestateServer.builder().withService(new Greeter()).build(); } +} +``` - override suspend fun get(request: GetRequest): GetResponse { - return getResponse { value = getCounter() } +In Kotlin: + +```kotlin +class LambdaFactory : LambdaRestateServerFactory { + override fun create(): LambdaRestateServer { + return LambdaRestateServer.builder().withService(Greeter()).build() } } ``` +Add the file `dev.restate.sdk.lambda.LambdaRestateServerFactory` in the project resources in `resources/META-INF/services` containing the following content: + +``` +LambdaFactory +``` + +Now build the Fat-JAR. For example, using Gradle: + +``` +gradle shadowJar +``` + +You can now upload the generated Jar in AWS Lambda, and configure `dev.restate.sdk.lambda.LambdaHandler` as the Lambda class in the AWS UI. + +### Additional setup + +#### Logging + +The SDK uses log4j2 as logging facade. To enable logging, add the `log4j2` implementation to the dependencies: + +``` +implementation("org.apache.logging.log4j:log4j-core:2.20.0") +``` + +And configure the logging adding the file `resources/log4j2.properties`: + +``` +# Set to debug or trace if log4j initialization is failing +status = warn + +# Console appender configuration +appender.console.type = Console +appender.console.name = consoleLogger +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} [%tn] %-5p %c{1}:%L - %m%n + +# Restate logs to debug level +logger.app.name = dev.restate +logger.app.level = debug +logger.app.additivity = false +logger.app.appenderRef.console.ref = consoleLogger + +# Root logger +rootLogger.level = info +rootLogger.appenderRef.stdout.ref = consoleLogger +``` + +#### Tracing with OpenTelemetry + +The SDK can generate additional tracing information on top of what Restate already publishes. See https://docs.restate.dev/restate/tracing to configure Restate tracing. + +You can the additional SDK tracing information by configuring the `OpenTelemetry` in the `RestateHttpEndpointBuilder`/`LambdaRestateServer`. + +For example, to set up tracing using environment variables, add the following modules to your dependencies: + +``` +implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.31.0") +implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.31.0") +``` + +And then configure it in the Restate builder: + +```java +.withOpenTelemetry(AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk()) +``` + +By exporting the following environment variables the OpenTelemetry SDK will be automatically configured to push traces: + +```shell +export OTEL_SERVICE_NAME=my-service +export OTEL_TRACES_SAMPLER=always_on +export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:14250 +``` + +Please refer to the [Opentelemetry manual instrumentation documentation](https://opentelemetry.io/docs/instrumentation/java/manual/#manual-instrumentation-setup) and the [autoconfigure documentation](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md) for more info. + ## Contributing to the SDK Prerequisites: