Overview
This blog shows you how to observe Red Hat Quarkus applications with Azure Application Insights using OpenTelemetry. The application is a "to do list" with a JavaScript front end and a REST endpoint. Azure Database for PostgreSQL Flexible Server provides the persistence layer for the app. The app utilizes OpenTelemetry to instrument, generate, collect, and export telemetry data for observability. The blog guides you to test your app locally, deploy it to Azure Container Apps and observe its telemetry data with Azure Application Insights.
Prerequisites
- An Azure subscription. If you don't have an Azure subscription, create a free account before you begin.
- Prepare a local machine with Unix-like operating system installed - for example, Ubuntu, macOS, or Windows Subsystem for Linux.
- Install a Java SE implementation version 17 - for example, Microsoft build of OpenJDK.
- Install Maven, version 3.9.8 or higher.
- Install Docker for your OS.
- Install the Azure CLI to run Azure CLI commands.
- Sign in to the Azure CLI by using the az login command. To finish the authentication process, follow the steps displayed in your terminal. For other sign-in options, see Sign into Azure with Azure CLI.
- When you're prompted, install the Azure CLI extension on first use. For more information about extensions, see Use and manage extensions with the Azure CLI.
- Run az version to find the version and dependent libraries that are installed. To upgrade to the latest version, run az upgrade. This blog requires at least version 2.65.0 of Azure CLI.
Prepare the Quarkus app
Run the following commands to get the sample app app-insights-quarkus from GitHub:
git clone https://github.com/Azure-Samples/quarkus-azure
cd quarkus-azure
git checkout 2024-11-27
cd app-insights-quarkus
Here's the file structure of the application, with important files and directories:
├── src/main/
│ ├── java/io/quarkus/sample/
│ │ └── TodoResource.java
│ └── resources/
│ ├── META-INF/resources/
│ ├── application.properties
├── pom.xml
The directory src/main/resources/META-INF/resources contains the front-end code for the application. It's a Vue.js front end where you can view, add, update, and delete todo items.
The src/main/java/io/quarkus/sample/TodoResource.java file implements the REST resource for the application. It uses the Jakarta REST API to expose the REST endpoints for the front end. The invocation of the REST endpoints is automatically instrumented by OpenTelemetry tracing. Besides, each REST endpoint uses the org.jboss.logging.Logger to log messages, which are collected by OpenTelemetry logging. For example, the GET method for the /api endpoint that returns all todo items is shown in the following code snippet:
package io.quarkus.sample;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.util.List;
import org.jboss.logging.Logger;
@Path("/api")
public class TodoResource {
private static final Logger LOG = Logger.getLogger(TodoResource.class);
@Inject
TodoRepository todoRepository;
@GET
public List<Todo> getAll() {
List<Todo> todos = todoRepository.findAll();
LOG.info("Found " + todos.size() + " todos");
return todos;
}
}
The pom.xml file contains the project configuration, including the dependencies for the Quarkus application. The application uses the following extensions to support OpenTelemetry:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-jdbc</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-logging</artifactId>
</dependency>
The src/main/resources/application.properties file contains the configuration for the Quarkus application. The configuration includes database connection properties for production, such as the JDBC URL and username. The configuration also includes the OpenTelemetry properties, such as enabling OpenTelemetry including logs and JDBC instrumentation at build time, using logging as exporter in development mode, and specifying the endpoint for the OpenTelemetry Protocol (OTLP) exporter in production mode. The following example shows the configuration for the OpenTelemetry:
quarkus.otel.enabled=true
quarkus.otel.logs.enabled=true
quarkus.datasource.jdbc.telemetry=true
%dev.quarkus.otel.logs.exporter=logging
%dev.quarkus.otel.traces.exporter=logging
%prod.quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT}
Run the Quarkus app locally
Quarkus supports the automatic provisioning of unconfigured services in development mode. For more information, see Dev Services Overview in the Quarkus documentation.
Now, run the following command to enter Quarkus dev mode, which automatically provisions a PostgreSQL database as a Docker container for the app:
mvn clean package quarkus:dev
The output should look like the following example:
2025-03-17 11:14:32,880 INFO [io.qua.dat.dep.dev.DevServicesDatasourceProcessor] (build-26) Dev Services for default datasource (postgresql) started - container ID is 56acc7e1cb46
2025-03-17 11:14:32,884 INFO [io.qua.hib.orm.dep.dev.HibernateOrmDevServicesProcessor] (build-4) Setting quarkus.hibernate-orm.database.generation=drop-and-create to initialize Dev Services managed database
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2025-03-17 11:14:36,202 INFO [io.ope.exp.log.LoggingSpanExporter] (JPA Startup Thread) 'quarkus' : 80437b598962f82bffd0735bbf00e9f1 aa86f0553056a8c9 CLIENT [tracer: io.opentelemetry.jdbc:2.8.0-alpha] AttributesMap{data={db.name=quarkus, server.port=59406, server.address=localhost, db.connection_string=postgresql://localhost:59406, db.statement=set client_min_messages = WARNING, db.system=postgresql}, capacity=128, totalAddedValues=6}
1970-01-01T00:00:00Z INFO ''quarkus' : 80437b598962f82bffd0735bbf00e9f1 aa86f0553056a8c9 CLIENT [tracer: io.opentelemetry.jdbc:2.8.0-alpha] AttributesMap{data={db.name=quarkus, server.port=59406, server.address=localhost, db.connection_string=postgresql://localhost:59406, db.statement=set client_min_messages = WARNING, db.system=postgresql}, capacity=128, totalAddedValues=6}' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.lineno=-1, log.logger.namespace="org.jboss.logmanager.Logger", thread.id=122, thread.name="JPA Startup Thread"}
2025-03-17 11:14:36,236 INFO [io.ope.exp.log.LoggingSpanExporter] (JPA Startup Thread) 'DROP table quarkus' : 6b732661c29a9f0966403d49db9e4cff d86f29284f0d8eac CLIENT [tracer: io.opentelemetry.jdbc:2.8.0-alpha] AttributesMap{data={db.operation=DROP table, db.name=quarkus, server.port=59406, server.address=localhost, db.connection_string=postgresql://localhost:59406, db.statement=drop table if exists Todo cascade, db.system=postgresql}, capacity=128, totalAddedValues=7}
1970-01-01T00:00:00Z INFO ''DROP table quarkus' : 6b732661c29a9f0966403d49db9e4cff d86f29284f0d8eac CLIENT [tracer: io.opentelemetry.jdbc:2.8.0-alpha] AttributesMap{data={db.operation=DROP table, db.name=quarkus, server.port=59406, server.address=localhost, db.connection_string=postgresql://localhost:59406, db.statement=drop table if exists Todo cascade, db.system=postgresql}, capacity=128, totalAddedValues=7}' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.lineno=-1, log.logger.namespace="org.jboss.logmanager.Logger", thread.id=122, thread.name="JPA Startup Thread"}
2025-03-17 11:14:36,259 INFO [io.ope.exp.log.LoggingSpanExporter] (JPA Startup Thread) 'CREATE table quarkus' : 54df3edf9f523a71bc85d0106a57016c bb43aa63ec3526ed CLIENT [tracer: io.opentelemetry.jdbc:2.8.0-alpha] AttributesMap{data={db.operation=CREATE table, db.name=quarkus, server.port=59406, server.address=localhost, db.connection_string=postgresql://localhost:59406, db.statement=create table Todo (completed boolean not null, ordering integer, id bigint generated by default as identity, title varchar(?) unique, url varchar(?), primary key (id)), db.system=postgresql}, capacity=128, totalAddedValues=7}
1970-01-01T00:00:00Z INFO ''CREATE table quarkus' : 54df3edf9f523a71bc85d0106a57016c bb43aa63ec3526ed CLIENT [tracer: io.opentelemetry.jdbc:2.8.0-alpha] AttributesMap{data={db.operation=CREATE table, db.name=quarkus, server.port=59406, server.address=localhost, db.connection_string=postgresql://localhost:59406, db.statement=create table Todo (completed boolean not null, ordering integer, id bigint generated by default as identity, title varchar(?) unique, url varchar(?), primary key (id)), db.system=postgresql}, capacity=128, totalAddedValues=7}' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.lineno=-1, log.logger.namespace="org.jboss.logmanager.Logger", thread.id=122, thread.name="JPA Startup Thread"}
2025-03-17 11:14:36,438 INFO [io.quarkus] (Quarkus Main Thread) quarkus-todo-demo-app-insights 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.16.3) started in 7.409s. Listening on: http://localhost:8080
1970-01-01T00:00:00Z INFO 'quarkus-todo-demo-app-insights 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.16.3) started in 7.409s. Listening on: http://localhost:8080' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.function="printStartupTime", code.lineno=109, code.namespace="io.quarkus.bootstrap.runner.Timing", log.logger.namespace="org.jboss.logging.Logger", thread.id=112, thread.name="Quarkus Main Thread"}
2025-03-17 11:14:36,441 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
1970-01-01T00:00:00Z INFO 'Profile dev activated. Live Coding activated.' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.function="printStartupTime", code.lineno=113, code.namespace="io.quarkus.bootstrap.runner.Timing", log.logger.namespace="org.jboss.logging.Logger", thread.id=112, thread.name="Quarkus Main Thread"}
2025-03-17 11:14:36,443 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [agroal, cdi, hibernate-orm, hibernate-validator, jdbc-postgresql, narayana-jta, opentelemetry, rest, rest-jackson, smallrye-context-propagation, vertx]
1970-01-01T00:00:00Z INFO 'Installed features: [agroal, cdi, hibernate-orm, hibernate-validator, jdbc-postgresql, narayana-jta, opentelemetry, rest, rest-jackson, smallrye-context-propagation, vertx]' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.function="printStartupTime", code.lineno=115, code.namespace="io.quarkus.bootstrap.runner.Timing", log.logger.namespace="org.jboss.logging.Logger", thread.id=112, thread.name="Quarkus Main Thread"}
--
Tests paused
Press [e] to edit command line args (currently ''), [r] to resume testing, [o] Toggle test output, [:] for the terminal, [h] for more options>
The output shows that the Quarkus app is running in development mode. The app is listening on http://localhost:8080. The PostgreSQL database is automatically provisioned as a Docker container for the app. The OpenTelemetry instrumentation for Quarkus and JDBC is enabled, and the telemetry data is exported to the console.
Access the application GUI at http://localhost:8080. You should see a similar Todo app with an empty todo list, as shown in the following screenshot:
Switch back to the terminal where Quarkus dev mode is running, you should see more OpenTelemetry data exported to the console. For example, the following output shows the OpenTelemetry logging and tracing data for the GET method for the /api endpoint:
2025-03-17 11:15:13,785 INFO [io.qua.sam.TodoResource] (executor-thread-1) Found 0 todos
1970-01-01T00:00:00Z INFO 'Found 0 todos' : 7cf260232ff81caf90abc354357c16ab c48a4a02e74e1901 [scopeInfo: io.quarkus.opentelemetry:] {code.function="getAll", code.lineno=25, code.namespace="io.quarkus.sample.TodoResource", log.logger.namespace="org.jboss.logging.Logger", parentId="c48a4a02e74e1901", thread.id=116, thread.name="executor-thread-1"}
2025-03-17 11:15:13,802 INFO [io.ope.exp.log.LoggingSpanExporter] (vert.x-eventloop-thread-1) 'GET /api' : 7cf260232ff81caf90abc354357c16ab c48a4a02e74e1901 SERVER [tracer: io.quarkus.opentelemetry:] AttributesMap{data={http.response.status_code=200, url.scheme=http, server.port=8080, server.address=localhost, client.address=0:0:0:0:0:0:0:1, user_agent.original=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0, url.path=/api/, code.namespace=io.quarkus.sample.TodoResource, http.request.method=GET, code.function=getAll, http.response.body.size=2, http.route=/api}, capacity=128, totalAddedValues=12}
1970-01-01T00:00:00Z INFO ''GET /api' : 7cf260232ff81caf90abc354357c16ab c48a4a02e74e1901 SERVER [tracer: io.quarkus.opentelemetry:] AttributesMap{data={http.response.status_code=200, url.scheme=http, server.port=8080, server.address=localhost, client.address=0:0:0:0:0:0:0:1, user_agent.original=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0, url.path=/api/, code.namespace=io.quarkus.sample.TodoResource, http.request.method=GET, code.function=getAll, http.response.body.size=2, http.route=/api}, capacity=128, totalAddedValues=12}' : 00000000000000000000000000000000 0000000000000000 [scopeInfo: io.quarkus.opentelemetry:] {code.function="export", code.lineno=65, code.namespace="io.opentelemetry.exporter.logging.LoggingSpanExporter", log.logger.namespace="org.jboss.logmanager.Logger", thread.id=126, thread.name="vert.x-eventloop-thread-1"}
Then return to the web browser and interact with the Todo app, try to add a new todo item by typing in the text box and pressing ENTER, selecting the checkbox to mark the todo item as completed, or selecting Clear completed to remove all completed todo items. You can also delete a todo item by selecting the x icon when you hover over it. The app should work as expected.
Finally, switch back to the terminal and press q to exit Quarkus dev mode.
Create the Azure resources
The steps in this section show you how to create the following Azure resources to run the Quarkus sample app on Azure:
- Azure Database for PostgreSQL Flexible Server
- Azure Container Registry
- Azure Container Apps environment
- Azure Application Insights
First, define the following variables in your bash shell by replacing the placeholders with your own values. They will be used throughout the example:
UNIQUE_VALUE=<your unique value>
LOCATION=eastus2
RESOURCE_GROUP_NAME=${UNIQUE_VALUE}rg
DB_SERVER_NAME=${UNIQUE_VALUE}db
DB_NAME=demodb
REGISTRY_NAME=${UNIQUE_VALUE}reg
ACA_ENV=${UNIQUE_VALUE}env
APP_INSIGHTS=${UNIQUE_VALUE}appinsights
ACA_NAME=${UNIQUE_VALUE}aca
Next, create the resource group to host Azure resources:
az group create \
--name $RESOURCE_GROUP_NAME \
--location $LOCATION
Then, create the Azure resources in the resource group by following the steps below.
Create an Azure Database for PostgreSQL flexible server instance:
az postgres flexible-server create \
--name $DB_SERVER_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--database-name $DB_NAME \
--public-access None \
--sku-name Standard_B1ms \
--tier Burstable \
--active-directory-auth Enabled
Create the Azure Container Registry and get the login server:
az acr create \
--resource-group $RESOURCE_GROUP_NAME \
--location ${LOCATION} \
--name $REGISTRY_NAME \
--sku Basic
LOGIN_SERVER=$(az acr show \
--name $REGISTRY_NAME \
--query 'loginServer' \
--output tsv)
Create the Azure Container Apps environment:
az containerapp env create \
--resource-group $RESOURCE_GROUP_NAME \
--location $LOCATION \
--name $ACA_ENV
Create an Azure Application Insights instance:
logAnalyticsWorkspace=$(az monitor log-analytics workspace list \
-g $RESOURCE_GROUP_NAME \
--query "[0].name" -o tsv | tr -d '\r')
az monitor app-insights component create \
--resource-group $RESOURCE_GROUP_NAME \
--location $LOCATION \
--app $APP_INSIGHTS \
--workspace $logAnalyticsWorkspace
Use the created Application Insights instance as the destination service to enable the managed OpenTelemetry agent for the Azure Container Apps environment:
appInsightsConn=$(az monitor app-insights component show \
--app $APP_INSIGHTS \
-g $RESOURCE_GROUP_NAME \
--query 'connectionString' -o tsv | tr -d '\r')
az containerapp env telemetry app-insights set \
--name $ACA_ENV \
--resource-group $RESOURCE_GROUP_NAME \
--connection-string $appInsightsConn \
--enable-open-telemetry-logs true \
--enable-open-telemetry-traces true
When you deploy the Quarkus app to the Azure Container Apps environment later in this blog, the OpenTelemetry data is automatically collected by the managed OpenTelemetry agent and exported to the Application Insights instance.
Deploy the Quarkus app to Azure Container Apps
You have set up all the necessary Azure resources to run the Quarkus app on Azure Container Apps. In this section, you containerize the Quarkus app and deploy it to Azure Container Apps.
First, use the following command to build the application. This command uses the Jib extension to build the container image. Quarkus instrumentation works both in JVM and native modes. In this blog, you build the container image for JVM mode, to work with Microsoft Entra ID authentication for Azure Database for PostgreSQL flexible server.
TODO_QUARKUS_IMAGE_NAME=todo-quarkus-app-insights
TODO_QUARKUS_IMAGE_TAG=${LOGIN_SERVER}/${TODO_QUARKUS_IMAGE_NAME}:1.0
mvn clean package -Dquarkus.container-image.build=true -Dquarkus.container-image.image=${TODO_QUARKUS_IMAGE_TAG}
Next, sign in to the Azure Container Registry and push the Docker image to the registry:
az acr login --name $REGISTRY_NAME
docker push $TODO_QUARKUS_IMAGE_TAG
Then, use the following command to create a Container Apps instance to run the app after pulling the image from the Container Registry:
az containerapp create \
--resource-group $RESOURCE_GROUP_NAME \
--name $ACA_NAME \
--image $TODO_QUARKUS_IMAGE_TAG \
--environment $ACA_ENV \
--registry-server $LOGIN_SERVER \
--registry-identity system \
--target-port 8080 \
--ingress 'external' \
--min-replicas 1
Finally, connect the Azure Database for PostgreSQL Flexible Server instance to the container app using Service Connector:
# Install the Service Connector passwordless extension
az extension add --name serviceconnector-passwordless --upgrade --allow-preview true
az containerapp connection create postgres-flexible \
--resource-group $RESOURCE_GROUP_NAME \
--name $ACA_NAME \
--target-resource-group $RESOURCE_GROUP_NAME \
--server $DB_SERVER_NAME \
--database $DB_NAME \
--system-identity \
--container $ACA_NAME \
--yes
Wait for a while until the application is deployed, started and running. Then get the application URL and open it in a browser:
QUARKUS_URL=https://$(az containerapp show \
--resource-group $RESOURCE_GROUP_NAME \
--name $ACA_NAME \
--query properties.configuration.ingress.fqdn -o tsv)
echo $QUARKUS_URL
You should see the similar Todo app when you ran the app locally before. Interact with the app by adding, completing and removing todo items, which generates telemetry data and sends it to Azure Application Insights.
Observe the Quarkus app with Azure Application Insights
Open the Azure Portal and navigate to the Azure Monitor Application Insights resource you created earlier. You can monitor the application with different views backed by the telemetry data sent from the application. For example:
- Investigate > Application map: Shows the application components and their dependencies.
- Investigate > Failures: Shows the failures and exceptions in the application.
- Investigate > Performance: Shows the performance of the application.
- Monitoring > Logs: Shows the logs and traces of the application.
You may notice that metrics are not observed in the Application Insights in this blog, that's because the Application Insights endpoint used in the managed OpenTelemetry agent doesn't accept metrics yet, which is listed as a known limitation. This is also the reason why Quarkus metrics is not enabled in the configuration file with quarkus.otel.metrics.enabled=true above. Alternatively, you can consider using Quarkus Opentelemetry Exporter for Microsoft Azure in your Quarkus apps to export the telemetry data directly to Azure Application Insights.
Clean up resources
To avoid Azure charges, you should clean up unneeded resources. When the resources are no longer needed, use the az group delete command to remove the resource group and all Azure resources within it:
az group delete \
--name $RESOURCE_GROUP_NAME \
--yes \
--no-wait
Next steps
In this blog, you observe the Quarkus app with Azure Application Insights using OpenTelemetry. To learn more, explore the following resources:
- OpenTelemetry on Azure
- Collect and read OpenTelemetry data in Azure Container Apps (preview)
- Application Insights overview
- Using OpenTelemetry
- Deploy a Java application with Quarkus on an Azure Container Apps
- Secure Quarkus applications with Microsoft Entra ID using OpenID Connect
- Deploy a Java application with Quarkus on an Azure Kubernetes Service cluster
- Deploy serverless Java apps with Quarkus on Azure Functions
- Jakarta EE on Azure