Observability
foundations-jdbc provides lightweight observability hooks: query listeners, named operations, timeouts, and interpolated SQL for debugging. Zero overhead when no listener is configured.
Query Listeners
A QueryListener receives callbacks before and after every query. Implement the interface to add logging, metrics, or alerting:
- Kotlin
- Java
- Scala
object logger : QueryListener {
override fun beforeQuery(sql: String, name: String?) {
println("Executing: $sql")
}
override fun afterQuery(event: QueryEvent) {
println("${event.name()} completed in ${event.elapsed().toMillis()}ms")
}
override fun failedQuery(event: QueryEvent) {
System.err.println("${event.name()} failed after ${event.elapsed().toMillis()}ms: ${event.error()?.message}")
}
}
QueryListener logger = new QueryListener() {
@Override
public void beforeQuery(String sql, String name) {
System.out.println("Executing: " + sql);
}
@Override
public void afterQuery(QueryEvent event) {
System.out.println(event.name()
+ " completed in " + event.elapsed().toMillis() + "ms");
}
@Override
public void failedQuery(QueryEvent event) {
System.err.println(event.name()
+ " failed after " + event.elapsed().toMillis() + "ms: "
+ event.error().getMessage());
}
};
object logger extends QueryListener:
override def beforeQuery(sql: String, name: String): Unit =
println(s"Executing: $sql")
override def afterQuery(event: QueryEvent): Unit =
println(s"${event.name()} completed in ${event.elapsed().toMillis}ms")
override def failedQuery(event: QueryEvent): Unit =
System.err.println(s"${event.name()} failed after ${event.elapsed().toMillis}ms: ${event.error().getMessage}")
Attaching to a Strategy
Attach a listener to a Strategy so all operations through that transactor are observed:
- Kotlin
- Java
- Scala
val strategy: Strategy =
Transactor.defaultStrategy()
.replaceListener(logger)
Transactor.Strategy strategy =
Transactor.defaultStrategy()
.replaceListener(logger);
val strategy: Transactor.Strategy =
Transactor.defaultStrategy()
.replaceListener(logger)
Per-Operation Listeners
You can also attach a listener to a specific operation:
operation.withListener(myListener).transact(tx);
Named Operations
Give operations a name with .named(). The name appears as a SQL comment prefix (/* name */) visible in pg_stat_activity, slow query logs, and listener callbacks:
- Kotlin
- Java
- Scala
val users: Operation<List<String>> =
sql { "SELECT name FROM users" }
.query(RowCodec.of(PgTypes.text).all())
.named("load-users")
.timeout(Duration.ofSeconds(5))
Operation<List<String>> users =
Fragment.of("SELECT name FROM users")
.query(RowCodec.of(PgTypes.text).all())
.named("load-users")
.timeout(Duration.ofSeconds(5));
val users: Operation[List[String]] =
sql"SELECT name FROM users"
.queryAll(PgTypes.text)
.named("load-users")
.timeout(Duration.ofSeconds(5))
For composite operations (e.g. a.combine(b).named("dashboard")), each leaf query gets a unique suffix: dashboard#1, dashboard#2, etc. Single queries get no suffix.
Query Timeouts
.timeout() transfers a timeout to the database via setQueryTimeout():
Fragment.of("SELECT * FROM large_table")
.query(codec.all())
.timeout(Duration.ofSeconds(10))
.transact(tx);
Interpolated SQL
QueryEvent.interpolatedSql() inlines parameter values into the SQL string for debugging. String values are quoted, numeric values are bare, and nulls render as NULL:
- Kotlin
- Java
- Scala
object debugListener : QueryListener {
override fun beforeQuery(sql: String, name: String?) {}
override fun afterQuery(event: QueryEvent) {
println(event.interpolatedSql())
}
override fun failedQuery(event: QueryEvent) {
System.err.println("Failed: ${event.interpolatedSql()}")
}
}
QueryListener debugListener = new QueryListener() {
@Override
public void beforeQuery(String sql, String name) {}
@Override
public void afterQuery(QueryEvent event) {
// SQL with values inlined for debugging:
// SELECT * FROM users WHERE id = 42
System.out.println(event.interpolatedSql());
}
@Override
public void failedQuery(QueryEvent event) {
System.err.println("Failed: "
+ event.interpolatedSql());
}
};
object debugListener extends QueryListener:
override def beforeQuery(sql: String, name: String): Unit = ()
override def afterQuery(event: QueryEvent): Unit =
println(event.interpolatedSql())
override def failedQuery(event: QueryEvent): Unit =
System.err.println(s"Failed: ${event.interpolatedSql()}")
For strategy merging and per-transaction overrides, see Transactors — Strategies.
Patterns
Slow Query Detection
- Kotlin
- Java
- Scala
fun slowQueryDetector(threshold: Duration): QueryListener =
object : QueryListener {
override fun beforeQuery(sql: String, name: String?) {}
override fun afterQuery(event: QueryEvent) {
if (event.elapsed() > threshold) {
System.err.println("SLOW QUERY [${event.elapsed().toMillis()}ms]: ${event.interpolatedSql()}")
}
}
override fun failedQuery(event: QueryEvent) {}
}
QueryListener slowQueryDetector(Duration threshold) {
return new QueryListener() {
@Override
public void beforeQuery(String sql, String name) {}
@Override
public void afterQuery(QueryEvent event) {
if (event.elapsed().compareTo(threshold) > 0) {
System.err.println("SLOW QUERY ["
+ event.elapsed().toMillis() + "ms]: "
+ event.interpolatedSql());
}
}
@Override
public void failedQuery(QueryEvent event) {}
};
}
def slowQueryDetector(threshold: Duration): QueryListener =
new QueryListener:
override def beforeQuery(sql: String, name: String): Unit = ()
override def afterQuery(event: QueryEvent): Unit =
if event.elapsed().compareTo(threshold) > 0 then
System.err.println(s"SLOW QUERY [${event.elapsed().toMillis}ms]: ${event.interpolatedSql()}")
override def failedQuery(event: QueryEvent): Unit = ()
Micrometer Integration
- Kotlin
- Java
- Scala
fun metricsListener(/* registry: MeterRegistry */): QueryListener =
object : QueryListener {
override fun beforeQuery(sql: String, name: String?) {}
override fun afterQuery(event: QueryEvent) {
// registry.timer("db.query",
// "name", event.name(),
// "outcome", "success"
// ).record(event.elapsed())
}
override fun failedQuery(event: QueryEvent) {
// registry.timer("db.query",
// "name", event.name(),
// "outcome", "failure"
// ).record(event.elapsed())
}
}
QueryListener metricsListener(/* MeterRegistry registry */) {
return new QueryListener() {
@Override
public void beforeQuery(String sql, String name) {}
@Override
public void afterQuery(QueryEvent event) {
// registry.timer("db.query",
// "name", event.name(),
// "outcome", "success"
// ).record(event.elapsed());
}
@Override
public void failedQuery(QueryEvent event) {
// registry.timer("db.query",
// "name", event.name(),
// "outcome", "failure"
// ).record(event.elapsed());
}
};
}
def metricsListener(/* registry: MeterRegistry */): QueryListener =
new QueryListener:
override def beforeQuery(sql: String, name: String): Unit = ()
override def afterQuery(event: QueryEvent): Unit = ()
// registry.timer("db.query",
// "name", event.name(),
// "outcome", "success"
// ).record(event.elapsed())
override def failedQuery(event: QueryEvent): Unit = ()
// registry.timer("db.query",
// "name", event.name(),
// "outcome", "failure"
// ).record(event.elapsed())