Sunday, March 17, 2024

All about Dropwizard Metrics

Dropwizard stands out as a favored Java framework for developing rest-based applications. Ensuring the observability of an application stands as a critical aspect for maintaining service continuity and stability. This directly impacts the end-user experience and, in many instances, revenue generation. The Metrics library simplifies the process within a Dropwizard application, enabling the collection of various system metrics alongside custom metrics. Additionally, it facilitates the authoring of health checks for the application.

To integrate metrics into a Dropwizard application, adding the following dependency is essential:

<dependency>
    <groupId>io.dropwizard.metrics</groupId>
    <artifactId>metrics-core</artifactId>
    <version>3.1.2</version>
</dependency>

Metrics Overview:

Metrics necessitate a metric registry to function effectively, which can accommodate numerous metrics.

private final MetricRegistry metrics = new MetricRegistry();

In a Dropwizard application, access to the metric registry and health check registry is available in the initialize method:

public void initialize(final Bootstrap<AppConfiguration> bootstrap) {
    metrics = bootstrap.getMetricRegistry();
    healthCheckRegistry = bootstrap.getHealthCheckRegistry();
}

Metrics are broadly categorized into:

  1. Meters:
    Meters gauge rates of events and track mean rates. They also monitor 1, 5, and 15-minute moving averages.
    private final Meter requests = metrics.meter("requests");
    public void handleRequest(Request request, Response response) {
        requests.mark();
        // ...
    }
    
  2. Gauges:
    Gauges provide a snapshot of a particular value at a given time. For instance, the number of pending jobs.
    final Queue<String> queue = new LinkedList<String>();
    final Gauge<Integer> pendingJobs = new Gauge<Integer>() {
        public Integer getValue() {
            return queue.size();
        }
    };
    
  3. Counters:
    Counters are a type of gauge that can be incremented or decremented, offering more efficiency than gauges.
    private final Counter pendingJobs = metrics.counter(name(QueueManager.class, "pending-jobs"));
    public void addJob(Job job) {
        pendingJobs.inc();
        queue.offer(job);
    }
    public Job takeJob() {
        pendingJobs.dec();
        return queue.take();
    }
    
  4. Histograms:
    Histograms track various statistical aspects of a data stream, including minimum, maximum, mean, median, and various percentiles.
    private final Histogram stockPrice = metrics.histogram(name(StockPriceHandler.class, "stock-price"));
    public void updateStockPrice(Request request, Response response) {
        stockPrice.update(request.getStockPrice());
    }
    
  5. Timers:
    Timers measure both the rate at which a piece of code is called and the duration of its execution.
    private final Timer responses = metrics.timer(name(RequestHandler.class, "responses"));
    public String handleRequest(Request request, Response response) {
        try(final Timer.Context context = responses.time()) {
            return "OK";
        } 
    }
    
    Alternatively, the @Timed annotation can be used:
    @GET
    @Path("sayhello")
    @Timed(name = "my-timed-metric")
    public Response sayHello(@QueryParam("q") @DefaultValue("%") String queryString) {
        return Response.ok().entity("Hello " + queryString).build(); 
    }
    
  6. Healthchecks:
    Health checks determine whether a service is healthy or not.
    public class DatabaseHealthCheck extends HealthCheck {
        public final static String NAME = "database-health";
        private final HibernateBundle<ApiAppConfiguration> hibernate;
    
        public DatabaseHealthCheck(HibernateBundle<AppConfiguration> hibernate) {
            this.hibernate = hibernate;
        }
        @Override
        protected Result check() throws Exception {
            if (hibernate.getSessionFactory().isClosed()) {
                return Result.unhealthy("Database session factory is closed");
            }
            return Result.healthy();
        }
    }
    
    Registration of the health check in the health check registry is necessary:
    environment.healthChecks().register(DatabaseHealthCheck.NAME, new DatabaseHealthCheck(hibernate));
    

These metrics play a crucial role in monitoring and maintaining the health and performance of applications.

Reporters play a crucial role in outputting metrics gathered by Dropwizard. There are several out-of-the-box popular reporters available, including console, CSV, SLF4J, Graphite, and Prometheus.

Here is an example of a console reporter:

ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();
reporter.start(10, TimeUnit.SECONDS);

Additionally, we can create our custom reporter to integrate with our monitoring system:

package example.monitoring;

import com.codahale.metrics.*;

import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import java.util.Map;

public class MyCustomReporter extends ScheduledReporter {

    public MyCustomReporter(MetricRegistry registry, TimeUnit rateUnit, TimeUnit durationUnit) {
        super(registry, "custom-metric-reporter", MetricFilter.ALL, rateUnit, durationUnit);
    }

    // This method will be called periodically by the ScheduledReporter
    @Override
    public void report(SortedMap<String, Gauge> gauges,
                       SortedMap<String, Counter> counters,
                       SortedMap<String, Histogram> histograms,
                       SortedMap<String, Meter> meters,
                       SortedMap<String, Timer> timers) {
        // Implement reporting logic here
        // Example: Print out all metric names and their values
        System.out.println("MyCustomReporter in action !!!");
        for (Map.Entry<String, Gauge> entry : gauges.entrySet()) {
            System.out.println("Gauge: " + entry.getKey() + ", Value: " + entry.getValue().getValue());
        }
        for (Map.Entry<String, Counter> entry : counters.entrySet()) {
            System.out.println("Counter: " + entry.getKey() + ", Count: " + entry.getValue().getCount());
        }
        // Similarly, report for histograms, meters, and timers
    }
    // Factory method to create an instance of CustomMetricReporter
    public static MyCustomReporter forRegistry(MetricRegistry registry) {
        return new MyCustomReporter(registry, TimeUnit.SECONDS, TimeUnit.SECONDS);
    }
}

MyCustomReporter myCustomReporter = MyCustomReporter.forRegistry(metrics);
myCustomReporter.start(10, TimeUnit.SECONDS); // Report every 10 seconds

I hope this information is useful. Please let me know if you have any thoughts or questions about it.

Reference: Dropwizard Metrics Documentation

Saturday, December 2, 2023

Java 17 Pattern Matching in Switch and Yield

 In Java switch statements are used instead of multiple if-else statements. Something like below

switch (day) {
case "Monday" -> System.out.println("Today is an monday");
case "Tuesday" -> System.out.println("Today is an tuesday");
case "Wednesday" -> System.out.println("Today is an wednesday");
case "Thursday" -> System.out.println("Today is an thursday");
case "Friday" -> System.out.println("Today is an friday");
case "Saturday" -> System.out.println("Today is an saturday");
case "Sunday" -> System.out.println("Today is an sunday");
default -> System.out.println("Invalid date");
}

There are multiple advantages of this we dont' need to write multiple nested if-else block

if ("Monday".equals(day)) {
System.out.println("Today is an monday");
}
else if ("Tuesday".equals(day)) {
System.out.println("Today is an tuesday");
}
......
else {
System.out.println("Invalid date");
}

Switch statements are there in java for a long back. over time it has evolved. Earlier Switch expressions are supposed to be numbers or enums. Case statements used to start with : and break need to be added to stop the overflow

switch (day) {
case "Monday": System.out.println("Today is an monday"); break;
case "Tuesday": System.out.println("Today is an Tuesday"); break;
.....
}

In Java 17 switch is enhanced. Before Java 17 switch started supporting string as expression type. In 17 the case key word can be followed with -> so that we dont need break any more.

case "Saturday" -> System.out.println("Today is an saturday");

we can have multiple levels in case.

case "Saturday", "Sunday" -> System.out.println("Its weekend");

We can return value from the case using yield

int dayOfTheWeek = switch (day) {
case "Monday" -> {
yield 1;
}
case "Tuesday" -> {
yield 2;
}
default -> {
yield -1;
}
};

In case expression we can try an instance of the operator or null check also

Object x = 0;
switch (x) {
case null -> System.out.println("x is null");
case Integer i -> System.out.println("x is an integer");
default -> System.out.println("x is of type "+x.getClass().getName());
}

These are still preview features to try this we need to enable it in our java env

java --enable-preview --source 17 <filename.java>

Please let me know your thoughts about this topic.

Reference

https://docs.oracle.com/en/java/javase/17/language/pattern-matching-switch-expressions-and-statements.html#GUID-E69EEA63-E204-41B4-AA7F-D58B26A3B232

Sunday, September 4, 2022

OCI function in golang connecting to autonomous database over TLS

 In this post we will discuss how to connect oracle cloud infrastructure function implemented in golang connect to the autonomous database. We will configure the adb on TLS, so that we don't need the client credential wallet for connection. If we need to connect over mtls still this post will give some idea but we need to stage the client credential in the function runtime container, this I may discuss in a different post. 

1. Configure ADB for TLS

We need to enable TLS connection in the autonomous database detail page. (More details).  

a) In adb detail page under the Network section enable the access control list, here I configured the allow-only ip addresses of a particular vcn should able to connect the adb.

 b) Set Mutual TLS authentication to not required, so that TLS connection will be allowed. 

2. Create a oracle function appliaction in the same VCN, which we configured  in above step (1. a), if we wish to run the function in private subnet then few more things we need to take care, details can be found here

3. Go lang function which will connect to ADB. 

we will use godror library to connect to ADB. A standalone golang program connecting ADB will be found here, the same concept we will use to write our function code. 

/**
* @Author Pallab (pallab.rath@gmail.com)
*/

package main

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"

fdk "github.com/fnproject/fdk-go"
_ "github.com/godror/godror"
)

func main() {
fdk.Handle(fdk.HandlerFunc(myHandler))
}

func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
db_user := "<db-user>"
db_pwd := "<db-pwd>"
db_host := "<db-host>"
db_port := "<db-port>>"
db_srvc := "<db-service>"

db_details := fmt.Sprintf(`user="%s" password="%s" connectString="tcps://%s:%s/%s"`, db_user, db_pwd,
db_host, db_port, db_srvc)
db, err := sql.Open("godror", db_details)
if err != nil {
fmt.Println(err)
return
}
defer db.Close()
rows, err := db.Query("select sysdate from dual")
if err != nil {
fmt.Println("Error running query")
fmt.Println(err)
return
}
defer rows.Close()
var resData string
for rows.Next() {
rows.Scan(&resData)
}
json.NewEncoder(out).Encode(&resData)
}

In this program, we need to provide db user, pwd, host, port, and service name. we can find the host port and service name from tls connect string. The tls connection string can be found on the Database Connection page.

4. Building and publishing the above function.

To build this function we need godror dependency, and this requires gcc compile to be present in build time env. So I created my custom build environment where gcc is installed, instead of the default fnproject/go:1.15-dev build image.

a) Create the build image docker definition (gofn_build.Dockerfile)

FROM oraclelinux:8
RUN yum -y install golang && yum -y clean all && rm -rf /var/cache

Then build this image using the below command

$ docker build -f gofn_build.Dockerfile -t gofn_build .

b) Create the runtime image docker definition (gofn_runtime.Dockerfile), in the runtime container godror need oracle instant client.

FROM oraclelinux:8
WORKDIR /function
RUN curl https://download.oracle.com/otn_software/linux/instantclient/217000/oracle-instantclient-basiclite-21.7.0.0.0-1.el8.x86_64.rpm --output oracle-instantclient-basiclite-21.7.0.0.0-1.el8.x86_64.rpm
RUN yum -y install oracle-instantclient-basiclite-21.7.0.0.0-1.el8.x86_64.rpm && yum -y clean all && rm -rf /var/cache
RUN rm -f oracle-instantclient-basiclite-21.7.0.0.0-1.el8.x86_64.rpm

Then build this image using the below comman

docker build -f gofn_build.Dockerfile -t gofn_runtime

c) Create the custom Dockerfile with below content, where we use build go function in the build conatiner we just created in above steps and create a image of our runtime image and the executable function.

FROM gofn_build as build-stage
WORKDIR /function
WORKDIR /go/src/func/
ENV GO111MODULE=on
COPY . .
RUN cd /go/src/func/ && go build -o func
FROM gofn_runtime
WORKDIR /function
COPY --from=build-stage /go/src/func/func /function/
ENTRYPOINT ["./func"]

d) Finally change the func.yaml with below content

schema_version: 20180708
name: function-adb
version: 0.0.26
runtime: docker

$ fn deploy -verbose <ap-name>

Then fn deploy should do the job for us.

$ fn invoke <ap-name>
"2022-09-04T17:56:09Z"