---
title: Instrumenting JDBC Operations in Spring Boot Applications with Datasource Micrometer
summary: This article introduces how to instrument JDBC operations in Spring Boot using Datasource Micrometer and visualize SQL logs, traces, and metrics in Grafana.
tags: ["OpenTelemetry", "Spring Boot", "Micrometer", "Java", "Logging", "Tracing", "Metrics", "Loki", "Grafana", "Tempo", "Prometheus"]
categories: ["Programming", "Java", "net", "ttddyy", "observation"]
date: 2026-01-26T05:30:03.578Z
updated: 2026-02-04T01:29:22.737Z
---

> [!NOTE]
> 2026-02-04 Added explanation of datasource-micrometer-opentelemetry.

[Datasource Micrometer](https://github.com/jdbc-observations/datasource-micrometer) ([documentation link](https://jdbc-observations.github.io/datasource-micrometer/docs/current/docs/html/)) is a JDBC proxy that records JDBC operations with Micrometer’s [Observation API](https://docs.micrometer.io/micrometer/reference/observation.html), enhancing observability. In addition to traces and metrics, it can capture SQL logs and slow‑query logs. Although it is now developed independently of Micrometer, it is still closely aligned with Micrometer.

Since the end of 2025, Datasource Micrometer can be selected directly from [Spring Initializr](https://start.spring.io/), making it easy to use. Datasource Micrometer 1.x works with Spring Boot 3.5, while Datasource Micrometer 2.x works with Spring Boot 4.  
![image](https://s3.ik.am/ikam/_/1769391518407_pasted-image.png)

For the sample application, we’ll create a simple Counter API. Generate a project skeleton with the following command:

```bash
curl -s https://start.spring.io/starter.tgz \
       -d artifactId=counter-api\
       -d name=counter-api \
       -d baseDir=counter-api \
       -d packageName=com.example \
       -d dependencies=web,jdbc,postgresql,actuator,configuration-processor,opentelemetry,datasource-micrometer,testcontainers \
       -d type=maven-project \
       -d applicationName=CounterApiApplication | tar -xzvf -
cd counter-api
```

The Datasource Micrometer dependency is already added. Compared with a project that does **not** include `datasource-micrometer` in `dependencies`, the following settings are added:

```diff
30a31
> 		<datasource-micrometer.version>2.1.0</datasource-micrometer.version>
48a50,57
> 		<dependency>
> 			<groupId>net.ttddyy.observation</groupId>
> 			<artifactId>datasource-micrometer-opentelemetry</artifactId>
> 		</dependency>
> 		<dependency>
> 			<groupId>net.ttddyy.observation</groupId>
> 			<artifactId>datasource-micrometer-spring-boot</artifactId>
> 		</dependency>
95a105,115
> 	<dependencyManagement>
> 		<dependencies>
> 			<dependency>
> 				<groupId>net.ttddyy.observation</groupId>
> 				<artifactId>datasource-micrometer-bom</artifactId>
> 				<version>${datasource-micrometer.version}</version>
> 				<type>pom</type>
> 				<scope>import</scope>
> 			</dependency>
> 		</dependencies>
> 	</dependencyManagement>
```

When `opentelemetry` is also listed in `dependencies`, `datasource-micrometer-opentelemetry` is added automatically.

Since we’re already here, let’s also add the [OpenTelemetry Logback Appender AutoConfiguration](https://github.com/making/otel-logs-autoconfigure) described in [this article](/entries/892/en).

```xml
<dependency>
    <groupId>am.ik.spring.opentelemetry</groupId>
    <artifactId>otel-logs-autoconfigure</artifactId>
    <version>0.5.0</version>
</dependency>
```

Implement a simple API.

```java
cat <<EOF > src/main/java/com/example/CounterController.java
package com.example;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CounterController {

	private final JdbcClient jdbcClient;

	private final Logger logger = LoggerFactory.getLogger(this.getClass());

	public CounterController(JdbcClient jdbcClient) {
		this.jdbcClient = jdbcClient;
	}

	@PostMapping(path = "/counter")
	@Transactional
	public CounterResponse increment(@RequestBody CounterRequest request) {
		CounterResponse counterResponse = this.jdbcClient.sql("""
				INSERT INTO counters (entry_id, counter)
				VALUES (?, 1)
				ON CONFLICT (entry_id)
				DO UPDATE
				  SET counter = counters.counter + 1
				RETURNING entry_id, counter
				""").param(request.entryId()).query(CounterResponse.class).single();
		logger.atInfo()
			.addKeyValue("entryId", counterResponse.entryId())
			.addKeyValue("counter", counterResponse.counter())
			.log("event=increment entryId={} counter={}", counterResponse.entryId(), counterResponse.counter());
		return counterResponse;
	}

	@GetMapping(path = "/counter")
	public List<CounterResponse> getAll() {
		return this.jdbcClient.sql("""
				SELECT entry_id, counter FROM counters ORDER BY counter DESC
				""").query(CounterResponse.class).list();
	}

	public record CounterRequest(int entryId) {
	}

	public record CounterResponse(int entryId, long counter) {
	}

}
EOF
```

Configure `application.properties`. Set up SQL logging and slow‑query logging for Datasource Micrometer.

```properties
cat <<EOF >> src/main/resources/application.properties
jdbc.datasource-proxy.json-format=true
jdbc.datasource-proxy.logging=slf4j
jdbc.datasource-proxy.multiline=false
jdbc.datasource-proxy.query.enable-logging=true
jdbc.datasource-proxy.slow-query.enable-logging=true
jdbc.datasource-proxy.slow-query.threshold=5
logging.level.net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener=debug
management.opentelemetry.instrumentation.logback-appender.capture-experimental-attributes=true
management.opentelemetry.instrumentation.logback-appender.capture-key-value-pair-attributes=true
management.otlp.metrics.export.base-time-unit=seconds
management.otlp.metrics.export.step=30s
management.tracing.sampling.probability=1.0
spring.sql.init.mode=always
EOF
```

Define a simple schema.

```sql
cat <<EOF > src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS counters
(
    entry_id BIGINT PRIMARY KEY,
    counter  BIGINT NOT NULL
);
EOF
```

Running the following command starts a PostgreSQL container via Testcontainers and a local development container for the [LGTM stack](https://hub.docker.com/r/grafana/otel-lgtm). (Check `src/test/java/com/example/TestcontainersConfiguration.java` for details.) DataSource configuration and the OTLP endpoint are set up automatically.

```bash
./mvnw spring-boot:test-run
```

When the application starts, logs similar to the following appear, showing the Grafana URL.

```
2026-01-26T13:08:41.512+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] tc.grafana/otel-lgtm:latest              : Creating container for image: grafana/otel-lgtm:latest
2026-01-26T13:08:41.579+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] tc.grafana/otel-lgtm:latest              : Container grafana/otel-lgtm:latest is starting: e0bb34b52593b45152a1236b6766c8ba2b1d3949b5ade2f4126dfbe8bf681790
2026-01-26T13:08:46.762+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] tc.grafana/otel-lgtm:latest              : Container grafana/otel-lgtm:latest started in PT5.249929S
2026-01-26T13:08:46.762+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] o.t.grafana.LgtmStackContainer           : Access to the Grafana dashboard: http://localhost:35507
2026-01-26T13:08:46.862+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] i.m.c.instrument.push.PushMeterRegistry  : Publishing metrics for OtlpMeterRegistry every 30s to http://localhost:35511/v1/metrics with resource attributes {service.name=counter-api}
```

Send a couple of requests.

```bash
curl -s http://localhost:8080/counter --json '{"entryId":100}'
curl -s http://localhost:8080/counter 
```

You’ll see SQL logs like the following.

```
2026-01-26T14:11:44.165+09:00 DEBUG 28654 --- [counter-api] [nio-8080-exec-1] [382b35f274086c23f3a45d4c94cfb785-6c507dd2b93e2c20] n.t.d.l.l.SLF4JQueryLoggingListener      : {"name":"test", "connection":4, "time":2, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["INSERT INTO counters (entry_id, counter)\nVALUES (?, 1)\nON CONFLICT (entry_id)\nDO UPDATE\n  SET counter = counters.counter + 1\nRETURNING entry_id, counter\n"], "params":[["100"]]}
2026-01-26T14:11:44.169+09:00  INFO 28654 --- [counter-api] [nio-8080-exec-1] [382b35f274086c23f3a45d4c94cfb785-25067aeeaad66229] com.example.CounterController            : event=increment entryId=100 counter=1
2026-01-26T14:11:45.923+09:00 DEBUG 28654 --- [counter-api] [nio-8080-exec-3] [1a250337750c5fbed850028f8fe5c636-72c7abbd0c14e831] n.t.d.l.l.SLF4JQueryLoggingListener      : {"name":"test", "connection":5, "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["SELECT entry_id, counter FROM counters ORDER BY counter DESC\n"], "params":[[]]}
```

Next, apply load with [`vegeta`](https://github.com/tsenart/vegeta).

```bash
for round in $(seq 20); do
  echo "=== Round $round/20 ==="
  for i in $(seq 3000); do
    id=$((RANDOM % 50 + 1))
    if [ $((RANDOM % 3)) -eq 0 ]; then
      echo '{"method":"GET","url":"http://localhost:8080/counter"}'
    else
      echo '{"method":"POST","url":"http://localhost:8080/counter","header":{"Content-Type":["application/json"]},"body":"'$(echo -n "{\"entryId\":$id}" | base64)'"}'
    fi
  done | vegeta attack -rate=100 -duration=30s -format=json | vegeta report
done
```

### Verify in Grafana

Open Grafana. The URL is printed in the logs.

![image](https://s3.ik.am/ikam/_/1769400574216_pasted-image.png)

#### Traces

From the left‑hand menu, select **"Traces"** under *Drilldown*.

![image](https://s3.ik.am/ikam/_/1769400767383_pasted-image.png)

Click the ◇ marker (Exemplar) on the **Span Rate** chart, then click **"View trace"**.

![image](https://s3.ik.am/ikam/_/1769400790970_pasted-image.png)

The Trace View for that request is displayed.

![image](https://s3.ik.am/ikam/_/1770167765909_pasted-image.png)

Click the **"connection"** span to see commit timings, counts, and durations.

![image](https://s3.ik.am/ikam/_/1770168097447_pasted-image.png)

Click the **"INSERT …"** span to view the exact SQL that was executed at that moment.

![image](https://s3.ik.am/ikam/_/1770168212581_pasted-image.png)

Because `datasource-micrometer-opentelemetry` is added, attribute names now follow the [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/).

Click the **"Log"** button.

![image](https://s3.ik.am/ikam/_/1770168428219_pasted-image.png)

You can see the logs associated with this trace.

![image](https://s3.ik.am/ikam/_/1770168478407_pasted-image.png)

#### Metrics

Next, select **"Metrics"** from the left‑hand *Drilldown* menu. Choose **"jdbc"** in **"Prefix filters"** to restrict the view to metrics sent by Datasource Micrometer.

![image](https://s3.ik.am/ikam/_/1769402849540_pasted-image.png)

Refer to [the documentation](https://jdbc-observations.github.io/datasource-micrometer/docs/current/docs/html/#observability-metrics) for the available metrics. You can obtain connection lifetimes, commit/rollback counts, query execution times, frequencies, and more.

#### Logs

Finally, select **"Logs"** from the left‑hand *Drilldown* menu.

![image](https://s3.ik.am/ikam/_/1769400620829_pasted-image.png)

Click **"Show logs"** for **"counter-api"**.

![image](https://s3.ik.am/ikam/_/1769400657018_pasted-image.png)

Click a specific log entry, then click the **"Trace"** button under **"Links"**.

![image](https://s3.ik.am/ikam/_/1769400710078_pasted-image.png)

You’ll be taken to the Trace View, where you can also inspect the related SQL statements.

![image](https://s3.ik.am/ikam/_/1769400730475_pasted-image.png)

---

We have shown how to instrument JDBC operations in a Spring Boot application using Datasource Micrometer. Since it can be added directly from Spring Initializr, integration is straightforward. Give it a try!
