Tiny Server for Microservices

GitLab Docs Pipeline Coverage License
Tiny Server Application wrapper for non-WEB microservices. It is designed to be absolutely minimal but with all the essential features of a service running on Kubernetes.

See morimekta.net/utils for procedures on releases. It is managed in the same group as the utilities, even though it is a more specialized utility. See here for a small example server using gRPC.

Getting Started

To add to maven: Add this line to pom.xmlunder dependencies:

<dependencies>
    <dependency>
        <groupId>net.morimekta.tiny</groupId>
        <artifactId>tiny-http</artifactId>
        <version>0.7.1</version>
    </dependency>
    <dependency>
        <groupId>net.morimekta.tiny</groupId>
        <artifactId>tiny-logback</artifactId>
        <version>0.7.1</version>
    </dependency>
    <dependency>
        <groupId>net.morimekta.tiny</groupId>
        <artifactId>tiny-server</artifactId>
        <version>0.7.1</version>
    </dependency>
</dependencies>

To add to gradle: Add this line to the dependencies group in build.gradle:

implementation 'net.morimekta.tiny.server:tiny-http:0.7.1'
implementation 'net.morimekta.tiny.server:tiny-logback:0.7.1'
implementation 'net.morimekta.tiny.server:tiny-server:0.7.1'

What does it provide

Default admin servlets:

  • /healthy: Health check.
  • /ready: Ready check.
  • /drain: Drain / stop service, but not kill it. Will call the beforeStop() method, but not stop the adming HTTP server, and not the afterStop() method. The latter one will be called when the service is stopped by the normal shutdown hook. See shutdown sequence for details.

The life-cycle of the service is as follows:

  • initialize() is called.
  • Command line arguments are parsed, and value setters called.
  • Admin HTTP server is started with a default /ready check with failing status ("Server is starting.").
  • onStart() is called.
  • The default ready check is removed, so only user-specific ready checks remain.

Now the server is active. At some point, a signal do stop the process is caught, triggering the shutdownHook. Alternatively a /drain call may run the first half of this.

  • This is only done once, either from shutdownHook, or from /drain
  • A default /ready check with failing status is added ("Server is stopping.")
  • beforeStop() is called.
  • The HTTP server is stopped. Now calls to /ready and /health will both fail with connection refused.
  • afterStop() is called. The /drain hook will not do this.
  • The app should exit here. The /drain hook will not do this.

Starting a Simple Server

Notes:

Example code for java:

package net.morimekta.test;

import net.morimekta.config.ConfigSupplier;
import net.morimekta.terminal.args.ArgParser;
import net.morimekta.terminal.args.Option;
import net.morimekta.tiny.server.TinyApplication;
import net.morimekta.tiny.server.TinyApplicationContext;
import io.grpc.Server;
import io.grpc.ServerBuilder;

import java.util.concurrent.atomic.AtomicInteger;

import static net.morimekta.terminal.args.ValueParser.i32;
import static net.morimekta.terminal.args.ValueParser.path;

public class MyApplication extends TinyApplication {
    private final ConfigSupplier<MyConfig> config = ConfigSupplier.yamlConfig(MyConfig.class);

    // My gRPC server.
    private Server server;

    @Override
    protected void initialize(ArgParser.Builder argParser, TinyApplicationContext.Builder context) {
        argParser.add(Option.optionLong("--config", "The config", path(config::loadUnchecked))
                            .required());
        context.addHttpHandler("/metrics", new HTTPMetricHandler(CollectorRegistry.defaultRegistry));
    }

    @Override
    protected void onStart(TinyApplicationContext context) {
        var myService = new MyServiceImpl();
        // start my gRPC service.
        server = ServerBuilder
                .forPort(config.get().grpcPort)
                .addService(myService)
                .build();
        server.start();

        context.addHealthCheck("grpc-server", new GrpcHealthCheck(server))
               .addReadyCheck("my-service", new MyServiceReadyCheck(myService));
    }

    @Override
    protected void afterStop(TinyApplicationContext context) {
        server.stop();
    }

    public static void main(String[] args) {
        TinyApplication.start(new MyApplication(), args);
    }
}

Example code for kotlin:

package net.morimekta.test

import io.grpc.Server
import io.grpc.ServerBuilder
import net.morimekta.config.ConfigSupplier
import net.morimekta.terminal.args.ArgParser
import net.morimekta.terminal.args.Option.optionLong
import net.morimekta.terminal.args.ValueParser.path
import net.morimekta.tiny.server.TinyApplication
import net.morimekta.tiny.server.TinyApplicationContext

class MyApplication : TinyApplication() {
    val config = ConfigSupplier.yamlConfig(MyConfig::class.java) {
        it.registerModule(KotlinModule.Builder().build())
    }

    lateinit var server: Server

    override fun initialize(argParser: ArgParser.Builder, context: TinyApplicationContext.Builder) {
        argParser.add(optionLong("--config", "The application config.", path { config.load(it) }).required())
        context.addHttpHandler("/metrics", HTTPMetricHandler(CollectorRegistry.defaultRegistry))
    }

    override fun onStart(context: TinyApplicationContext) {
        val myService = MyServiceImpl()
        // start my gRPC service.
        server = ServerBuilder
                .forPort(config.get().grpcPort)
                .addService(/* my service */)
                .build()
        server.start()

        context.addReadyCheck("my-service", MyServiceReadyCheck(myService))
        context.addHealthCheck("grpc-server", GrpcHealthCheck(server))
    }

    override fun afterStop(context: TinyApplicationContext) {
        server.stop()
    }
}

fun main(args: Array<String>) {
    TinyApplication.start(MyApplication(), args)
}

Using Json Logging Layout

You can use the Json logging layout in order to get the full power of stackdriver or elasticsearch with kibana et al. Example logback.xml file.

<configuration>
    <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
    <appender name="JSON-OUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="net.morimekta.tiny.logback.JsonLayout">
                <zoneId>UTC</zoneId>
                <includeMdc>true</includeMdc>
                <stackTraceFormat>full</stackTraceFormat>
                <stackTraceIncludeShort>true</stackTraceIncludeShort>
                <stackTraceFilter>
                    com.sun.net.httpserver,
                    java.lang.reflect,
                    java.util.ArrayList.forEach,
                    java.util.concurrent,
                    java.util.stream,
                    jdk.httpserver,
                    jdk.internal.reflect,
                    sun.net.httpserver,
                </stackTraceFilter>
            </layout>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="JSON-OUT"/>
    </root>
</configuration>

Note that if you use the maven-shade-plugin with the minimize option, you need to make sure the JsonLayout class is not excluded, as it is not referenced anywhere in the code. You need to add a filter for this in the <configuration> block, and you will most likely need a similar rule for ch.qos.logback:* for ch/qos/logback/** and META-INF/services/**):

<filters>
    <filter>
        <artifact>org.qos.logback:*</artifact>
        <includes>
            <include>**</include>
        </includes>
    </filter>
    <filter>
        <artifact>net.morimekta.tiny:tiny-logback</artifact>
        <includes>
            <include>**</include>
        </includes>
    </filter>
</filters>