Tiny Server for Microservices

GitLab Docs Pipeline Coverage
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:

<dependency>
    <groupId>net.morimekta.tiny.server</groupId>
    <artifactId>tiny-server</artifactId>
    <version>0.5.0</version>
</dependency>

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

implementation 'net.morimekta.tiny.server:tiny-server:0.5.0'

What does it provide

Default admin servlets:

  • /healthy: Health check.
  • /ready: Ready check.
  • /drain: Drain / stop service.

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.

Now the server is active. At some point, either /drain is called, or a signal do stop the process is caught, triggering the shutdownHook, each starts the following:

  • 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 app should exit here.

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.addReadyCheck("my-service", new MyServiceReadyCheck(myService));
        context.addHealthCheck("grpc-server", new GrpcHealthCheck(server));
    }

    @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())
    }

    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.server.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>net.morimekta.tiny.server:*</artifact>
        <includes>
            <include>**</include>
        </includes>
    </filter>
</filters>