The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Creating a Quarkus extension for AWS CloudWatch

Creating a Quarkus extension for AWS CloudWatch

We recently had the situation that we wanted to log our Quarkus application logs to AWS CloudWatch. Basically it takes some time but is not a big deal. Adding a CloudWatch dependency, creating a Log Handler and push the logs to CloudWatch via the provided AWS CloudWatch API. But what if you want to share it with others? Of course you could put it on GitHub as part of your project so others can copy and paste it, but that’s not the most elegant way to share code with others.

That’s why we implemented a Quarkus extension so others can use it more easily and don’t need to reinvent the wheel or need to copy paste things around. How do I do that? Here the Quarkiverse Hub comes in. Quarkiverse is a GitHub organization where developers can host and share their extension with others. There are several benefits you get for free when hosting the extension in Quarkiverse instead of using the good old way of doing all the things on your own. By using Quarkiverse you don’t need to build the artifact, release it with the Sonatype Nexus Manager (or something similar) and distribute it on Maven Central and other repositories. Quarkiverse comes with all these things so you can focus on implementing the extension itself. The following post describes what needs to be done to initialize, implement and share a CloudWatch Quarkus extension.

If you want to use Quarkiverse to publish your extension on the Hub (which is what we recommend) and make use of all the advantages you get by using it, you simply need to open a new extension proposal issue in the quarkusio/quarkus GitHub organization. By doing this, most of the requirements are fulfilled already as there will be a template generated for you and you just have to implement your extension code. In case you are using an existing project as template, there are some requirements you need to take care of. In order to automate extension releases and publication of docs, there are some rules that need to be followed for projects under the Quarkiverse organization:

  • The extension repository should be named quarkus-<project>

  • A Quarkiverse extension MUST belong to the io.quarkiverse.<project> groupId

  • The root pom.xml MUST inherit from io.quarkiverse:quarkiverse-parent

  • A Quarkiverse extension contains the following folders and files:

    • deployment

    • runtime

    • integration-test

    • docs

    • pom.xml

    • LICENCIA

    • README

This article covers the runtime and deployment content only as the other things are optional, generated by the project template already or even important, but not the most important and pragmatic thing when you want to learn how a Quarkus extension can be created. Let’s start with the deployment section. It contains classes needed for the initialization of the Quarkus extension. Without this initialization class, your extension won’t be identified when starting your Quarkus application.

class LoggingCloudwatchProcessor {

    @BuildStep
    FeatureBuildItem feature() {
        return new FeatureBuildItem("logging-cloudwatch");
    }

    @BuildStep
    @Record(ExecutionTime.RUNTIME_INIT)
    LogHandlerBuildItem addCloudwatchLogHandler(final LoggingCloudWatchConfig config,
            final LoggingCloudWatchHandlerValueFactory cloudWatchHandlerValueFactory) {
        return new LogHandlerBuildItem(cloudWatchHandlerValueFactory.create(config));
    }
}

In the snippet above you can see a feature() method which is annotated with @BuildStep and returns a new FeatureBuildItem. It exposes the name of the feature (logging-cloudwatch) displayed in the log during application bootstrap. The second method addCloudWatchHandler() initializes the extensions runtime configurations provided by the LoggingCloudWatchConfig and LoggingCloudWatchHandlerValueFactory class. Luckily there is a LogHandlerBuildItem provided, so we can overwrite the existing log handler by adding our own implementation. There are lots of other BuildItems provided so it’s definitely worth it, taking a look at it if you want to create your own extension. The parameter of this method is a config class which will be described in the following snippet.

@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log.cloudwatch")
public class LoggingCloudWatchConfig {

    @ConfigItem(defaultValue = "true")
    boolean enabled;

    @ConfigItem
    public String region;

    // ...
}

The LoggingCloudWatchConfig is building the bridge between the extension itself and the Quarkus application which uses the extension. It’s combining the application.properties entries in the Quarkus application with our extension. That means with this class you can define the properties available in the application.properties file and make the extension configurable from the outside. The @ConfigRoot defines the prefix of the property in the application.properties and the @ConfigItems is the postfix. One application.properties entry we are accepting with this class is log.cloudwatch.enabled for example.

Besides the LoggingCloudWatchConfig there is another parameter of the addCloudwatchLogHandler() method. It’s the corresponding factory class.

@Recorder
public class LoggingCloudWatchHandlerValueFactory {

    public RuntimeValue<Optional<Handler>> create(final LoggingCloudWatchConfig config) {
        if (!config.enabled) {
            return new RuntimeValue<>(Optional.empty());
        }

        AWSLogsClientBuilder clientBuilder = AWSLogsClientBuilder.standard();
        clientBuilder.setCredentials(new CloudWatchCredentialsProvider(config));

        // …

        AWSLogs awsLogs = clientBuilder.build();

        // …

        LoggingCloudWatchHandler handler = new LoggingCloudWatchHandler(awsLogs, config.logGroup.get(),
                config.logStreamName.get(), token);
        // …

        return new RuntimeValue<>(Optional.of(handler));
    }
}

The LoggingCloudWatchHandlerValueFactory is the glue between the actually business logic of the extension: dealing with application logs and putting these logs to AWS and the configurations of the application.properties file mentioned before. As you can see in the create() method, configuration entries are checked and used for initializing the CloudWatch connection.

Now that we have made the extension configurable for the extension users by adding application.properties entries, exposing the extension name and providing the configurations to the handler class which creates the AWS CloudWatch objects needed to put log messages in AWS CloudWatch, we only need to add one missing piece. The Log Handler itself. In the snippet above, in the LoggingCloudWatchHandlerValueFactory we created it already and returned it as a RuntimeValue which we are using in the LoggingCloudwatchProcessor class. That’s the call chain needed to overwrite the existing default log handler.

class LoggingCloudWatchHandler extends Handler {

    private AWSLogs awsLogs;
    private String logStreamName;
    private String logGroupName;
    private String sequenceToken;

    // ...

    LoggingCloudWatchHandler(AWSLogs awsLogs, String logGroup, String logStreamName, String token) {
        this.logGroupName = logGroup;
        this.awsLogs = awsLogs;
        this.logStreamName = logStreamName;
        this.sequenceToken = token;
    }

    @Override
    public void publish(LogRecord record) {

        // ...

        InputLogEvent logEvent = new InputLogEvent()
                .withMessage(body)
                .withTimestamp(System.currentTimeMillis());
        awsLogs.putLogEvents(request);
    }
}

This log handler is a java.util.LogHandler which takes the LogRecord object as a parameter of the publish method which will be called when writing a log in an application. For example like log.info(“I Love Open Source!”);. If configured correctly, this log handler will be called when writing logs. As we want to put the log messages in AWS CloudWatch, we need to add the logic for doing it. Therefore we create an InputLogEvent and call putLogEvents() which puts the log message to CloudWatch. That’s basically it.

The snippets in this article are a bit shortened, but basically that’s what the extension contains.

Let’s sum it up: There is a processor class which initializes the extension, a configuration class which is needed to make the extension configurable, a value factory class which takes these configurations and creates a AWS CloudWatch connection as well as a custom LogHandler class which pushes each log message to CloudWatch.

After doing all these things, the only thing missing is releasing a version of the extension. This can be done by opening a Pull Request which updates the current-version and next-version entry of the project.yml file in the .github folder. After merging this Pull Request, some GitHub Actions will be triggered which will bring your new version to Maven Central and finally others can use your extension as well :-)

Summary

As you can see, creating, implementing and sharing Quarkus extensions with others is actually very easy. So if you have an idea of an extension which could be useful for the community, feel free to pitch your idea by creating a new extension proposal issue on the quarkusio/quarkus GitHub Issues section :-)

In case you have questions, suggestions or something else, please feel free to contact me on Twitter.

Best regards, Bennet