Programming

basic programming

Creating Java 21 Runtime on top of an UBI Base Image

Just recently had a request to create a custom image based on UBI but extendable, means able to be installed custom package for other functionality.

For this sample, im using UBI9 latest version which is 9.4. We can find our Dockerfile below,

FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4

LABEL BASE_IMAGE="registry.access.redhat.com/ubi9/ubi-minimal:9.4"
LABEL JAVA_VERSION="21"

ENV LANGUAGE='en_US:en'
ENV TZ='Asia/Jakarta'

RUN microdnf install -y --nodocs java-21-openjdk-headless  \
    && microdnf clean all  \
    && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security

WORKDIR /work/

COPY --chown=185 target/quarkus-app/lib/ /work/lib/
COPY --chown=185 target/quarkus-app/*.jar /work/application.jar
COPY --chown=185 target/quarkus-app/app/ /work/app/
COPY --chown=185 target/quarkus-app/quarkus/ /work/quarkus/

ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:TieredStopAtLevel=1 -noverify -XX:+UseShenandoahGC -XX:+AlwaysPreTouch -XX:+UseNUMA -Xlog:gc*,safepoint=debug:file=/tmp/gc.log.%p:time,uptime:filecount=5,filesize=50M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/"

EXPOSE 8080
USER 185

CMD java $JAVA_OPTS -jar application.jar

Based on above script, we are using UBI9 and installing some packages using microdnf with a custom JAVA_OPTS variables.

Code can be found on this URL,

https://github.com/edwin/quarkus-and-java21

Compress and Deleting AMQ Broker Logs Regularly

AMQ Broker, or its Open Source product which is ActiveMQ Artemis, can generate a log to a specific file and can be use for auditing purpose. However sometimes we ned to maintain some log retentions by archieving and deleting logs after some period of time to prevent logs filling up our storage.

We can achieve that by using log4j2 configuration which is comes out of the box in AMQ instance. For example, this configuration below would create a new log file every 5 minutes, compress old logs, and deleting logs that is older than 10 minutes.

# Log file appender
appender.log_file.type = RollingFile
appender.log_file.name = log_file
appender.log_file.fileName = ${sys:artemis.instance}/log/artemis.log
appender.log_file.filePattern = ${sys:artemis.instance}/log/artemis.log.%d{yyyyMMdd.HHmm}.gz
appender.log_file.layout.type = PatternLayout
appender.log_file.layout.pattern = %d %-5level [%logger] %msg%n
appender.log_file.policies.type = Policies
appender.log_file.policies.cron.type = CronTriggeringPolicy
appender.log_file.policies.cron.schedule = 0 */5 * ? * *
appender.log_file.policies.cron.evaluateOnStartup = true
appender.log_file.strategy.type = DefaultRolloverStrategy
appender.log_file.strategy.action.type = Delete
appender.log_file.strategy.action.basePath = ${sys:artemis.instance}/log/
appender.log_file.strategy.action.maxDepth = 1
appender.log_file.strategy.action.condition.type = IfFileName
appender.log_file.strategy.action.condition.glob = artemis.log.*.gz
appender.log_file.strategy.action.ifAny.type = IfAny
appender.log_file.strategy.action.ifAny.ifLastModified.type = IfLastModified
appender.log_file.strategy.action.ifAny.ifLastModified.age = PT10M

We can change 10minutes period into something else by changing the value of “age” following ISO-8601 duration format,

https://logging.apache.org/log4j/2.x/javadoc/log4j-core/org/apache/logging/log4j/core/appender/rolling/action/Duration.html#parse(java.lang.CharSequence)

While log compressing, can be modified by changing the “cron schedule” value,

https://logging.apache.org/log4j/2.12.x/log4j-core/apidocs/org/apache/logging/log4j/core/util/CronExpression.html

Make Spring Boot Starting Time Faster

When working in a cloud environment, startup time is something that is important especially when you application is relying in HPA (Horizontal Pod Autoscale). This means that an application that can start faster is better since they can handle traffic sooner compared when having a slower startup time.

Lets take my sample Spring Boot and Camel project,

https://github.com/edwin/spring-boot-camel-json-and-wsdl

in a normal condition, this application can take almost 10second to starting-up

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.5)

2024-06-22T20:24:06.196+07:00  INFO 12328 --- [           main] com.edw.Main                             : Starting Main using Java 17.0.6 with PID 12328 (spring-boot-camel-json-and-wsdl-1.0.jar started by edwin in /tmp/spring-boot-camel-json-and-wsdl)
2024-06-22T20:24:06.198+07:00  INFO 12328 --- [           main] com.edw.Main                             : No active profile set, falling back to 1 default profile: "default"
.......
2024-06-22T20:24:14.718+07:00  INFO 12328 --- [           main] o.a.c.impl.engine.AbstractCamelContext   : Apache Camel 4.4.0.redhat-00019 (camel-testing) started in 1s623ms (build:0ms init:0ms start:1s623ms)
2024-06-22T20:24:14.726+07:00  INFO 12328 --- [           main] com.edw.Main                             : Started Main in 9.116 seconds (process running for 9.692)

We can see from above logs that it needs 9second to starting. But some might not see this as ideal, and start looking for some areas of improvements. And we can start by creating our Spring Boot as Lazy-Loading using below application.properties configuration

spring.main.lazy-initialization=true

Adding below Java Variables also help to increase the speed of starting up

 -XX:TieredStopAtLevel=1 -noverify

Which finally gives us this Java command

$ java -jar  -XX:TieredStopAtLevel=1 -noverify .\target\spring-boot-camel-json-and-wsdl-1.0.jar

and we can see from below logs, starting time is literally reduced into 5seconds, which is good enough.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.5)

2024-06-22T20:31:54.155+07:00  INFO 2512 --- [           main] com.edw.Main                             : Starting Main using Java 17.0.6 with PID 12328 (spring-boot-camel-json-and-wsdl-1.0.jar started by edwin in /tmp/spring-boot-camel-json-and-wsdl)
2024-06-22T20:31:54.182+07:00  INFO 2512 --- [           main] com.edw.Main                             : No active profile set, falling back to 1 default profile: "default"
........
2024-06-22T20:31:59.231+07:00  INFO 2512 --- [           main] o.a.c.impl.engine.AbstractCamelContext   : Apache Camel 4.4.0.redhat-00019 (camel-testing) started in 931ms (build:0ms init:0ms start:931ms)
2024-06-22T20:31:59.359+07:00  INFO 2512 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2024-06-22T20:31:59.375+07:00  INFO 2512 --- [           main] com.edw.Main                             : Started Main in 5.759 seconds (process running for 6.201)

Bringing Back the Old IntelliJ Theme Back

The latest version of IntelliJ having a very strong VSCode ambience in it, which is perhaps good for some people but perhaps not my cup of tea.

Bringing the old theme back is pretty much straight forward, we can do it by pressing Shift twice and write “switch to classic UI” there, bringing the old legacy theme back.

Creating a Logback with Masked Pattern to Hide Sensitive Data

I had one case where i need to send HTTP logs from Apache HTTP Client into console, the thing is this approach has a security impact since everything will be sent to console including credentials and authorization headers. This is one reason why we comes up with an approach to hide sensitive values based on specific pattern from Logback console logs.

Lets start with a simple Java project,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>logback-with-masked-credentials</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.4</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.wiremock</groupId>
            <artifactId>wiremock</artifactId>
            <version>3.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

And create a Java class for doing a simple HTTP outbound request to a simulator,

package com.edw;


import com.github.tomakehurst.wiremock.WireMockServer;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;

@SpringBootTest
public class MainTest {

    private WireMockServer wireMockServer = new WireMockServer();
    private CloseableHttpClient httpClient = HttpClients.createDefault();

    private Logger logger = LoggerFactory.getLogger(MainTest.class);

    @Test
    @DisplayName("01. Testing Hello World Page")
    public void indexTest() throws IOException {
        wireMockServer.start();

        configureFor("localhost", 8080);
        stubFor(
                get(
                    urlEqualTo("/"))
                        .willReturn(
                                aResponse()
                                        .withBody("Hello World")
                        ));

        HttpGet request = new HttpGet("http://localhost:8080/");
        request.addHeader("Accept-Encoding", "text/plain");
        request.addHeader("Accept-Charset", "utf-8");

        // authentication header --- this is a sample of sensitive value that we are going to masked
        request.addHeader("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiS.jwt-sample.cpQcLckrTofjLZtCFFcMfThBNWD");

        CloseableHttpResponse httpResponse = httpClient.execute(request);
        String stringResponse = convertResponseToString(httpResponse);

        verify(getRequestedFor(urlEqualTo("/")));
        Assertions.assertEquals("Hello World", stringResponse);

        wireMockServer.stop();
    }

    private String convertResponseToString(CloseableHttpResponse response) throws IOException {
        InputStream responseStream = response.getEntity().getContent();
        Scanner scanner = new Scanner(responseStream, "UTF-8");
        String stringResponse = scanner.useDelimiter("\\Z").next();
        scanner.close();

        logger.debug("response is {}", stringResponse);

        return stringResponse;
    }

}

With below logback.xml configuration,

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <logger name="com.edw" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
    </logger>

    <logger name="org.apache.hc.client5.http.wire" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
    </logger>

    <root level="WARN">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

This will generate this log files, and as we can see theres an Authorization header printed there

16:41:35.164 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "GET / HTTP/1.1[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Accept-Encoding: text/plain[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Accept-Charset: utf-8[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiS.jwt-sample.cpQcLckrTofjLZtCFFcMfThBNWD[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Host: localhost:8080[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Connection: keep-alive[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "User-Agent: Apache-HttpClient/5.1.4 (Java/17.0.6)[\r][\n]"
16:41:35.165 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "[\r][\n]"
16:41:35.175 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "HTTP/1.1 200 OK[\r][\n]"
16:41:35.176 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "Matched-Stub-Id: 584bed8e-a120-46c4-9139-ce3837b47ef6[\r][\n]"
16:41:35.176 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "Transfer-Encoding: chunked[\r][\n]"
16:41:35.176 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "[\r][\n]"
16:41:35.176 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "b[\r][\n]"
16:41:35.176 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "Hello World[\r][\n]"
16:41:35.179 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "0[\r][\n]"
16:41:35.179 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "[\r][\n]"

For hiding Authorization header, we can leverage Logback.xml replace function and use something like this.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <appender name="CUSTOM-HTTP-CONSOLE"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){"(Authorization.*$)", "Authorization: xxxxx"}%n</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <logger name="com.edw" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

Which will eventually generates logs like this,

08:56:23.046 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "GET / HTTP/1.1[\r][\n]"
08:56:23.046 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Accept-Encoding: text/plain[\r][\n]"
08:56:23.047 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Accept-Charset: utf-8[\r][\n]"
08:56:23.047 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Authorization: xxxxx"
08:56:23.047 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Host: localhost:8080[\r][\n]"
08:56:23.047 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "Connection: keep-alive[\r][\n]"
08:56:23.047 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "User-Agent: Apache-HttpClient/5.1.4 (Java/17.0.6)[\r][\n]"
08:56:23.047 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 >> "[\r][\n]"
08:56:23.064 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "HTTP/1.1 200 OK[\r][\n]"
08:56:23.064 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "Matched-Stub-Id: eccc2faf-5b55-4850-981e-a0ef26219c87[\r][\n]"
08:56:23.064 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "Transfer-Encoding: chunked[\r][\n]"
08:56:23.064 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "[\r][\n]"
08:56:23.069 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "b[\r][\n]"
08:56:23.069 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "Hello World[\r][\n]"
08:56:23.069 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "0[\r][\n]"
08:56:23.069 [main] DEBUG org.apache.hc.client5.http.wire - http-outgoing-1 << "[\r][\n]"

Code for this article can be found here,

https://github.com/edwin/logback-with-masked-credentials