micrometer-metrics/micrometer

[jOOQ] MetricsDSLContext calls time() twice on fetchValue(SelectField) path, causing tag loss

Open

#6659 opened on Aug 23, 2025

View on GitHub
 (4 comments) (3 reactions) (0 assignees)Java (4,220 stars) (935 forks)batch import
bughelp wantedinstrumentationmodule: micrometer-core

Description

Describe the bug

When using MetricsDSLContext with jOOQ 3.20.x, calling DSLContext#fetchValue(SelectField) leads to double instrumentation: time() is invoked twice along the select(...) path. This causes tag state to be reset/overwritten and results in missing or unexpected tags on the recorded jooq.query timer. This occurs even if Micrometer is compiled against jOOQ 3.14.x, because at runtime jOOQ 3.20.x introduces a new overload that changes the call path.

Environment

- Micrometer version: 1.15.3
- jOOQ version: 3.20.6
- Java version: 21

To Reproduce

Minimal JUnit test that demonstrates the issue: How to reproduce the bug:

@Test
void jooqMethodTest() throws SQLException {
    try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:test")) {
        Configuration config = new DefaultConfiguration().set(conn).set(SQLDialect.H2);
        MeterRegistry registry = new SimpleMeterRegistry();

        MetricsDSLContext jooq =
            MetricsDSLContext.withMetrics(DSL.using(config), registry, Tags.empty());

        Integer result = jooq.tag("name", "checkAuthorExists")
                             .fetchValue(DSL.inline(123));

        assertThat(registry.get("jooq.query")
                .tag("name", "checkAuthorExists")
                .tag("type", "read")
                .timer()
                .count()).isEqualTo(1);
    }
}

In practice, the assertion fails because the timer is not recorded with the expected tags (or the meter cannot be found under that tag set).

Expected behavior

A single jooq.query timer is recorded once with the expected tags, e.g. name=checkAuthorExists, type=read.

Additional context

Call path (showing the double time()):

DefaultDSLContext.fetchValue(DSL.inline(123)) -> fetchValue(SelectField<T> field)
  -> DefaultDSLContext.fetchValue(select(field))
      -> MetricsDSLContext.select(SelectField<T1> field1)                 // time() #1
          -> DefaultDSLContext.select(SelectField<T1> field1)
              -> MetricsDSLContext.select(SelectFieldOrAsterisk... fields) // time() 

Root cause (fetchValue() in jOOQ 3.20.x):

@Override
public <T> T fetchValue(SelectField<T> field) {
    return field instanceof TableField
         ? fetchValue((TableField<?, T>) field)
         : field instanceof Table<?>
         ? fetchValue(select(field).from((Table<?>) field))
         : fetchValue(select(field));
}

Because MetricsDSLContext instruments select(...), the new delegation path results in double instrumentation and tag loss.

So, Please add a compatibility note to the official Micrometer documentation for for users running jOOQ newer(i.e., latest versions), informing users that certain newly added overloads (e.g., DefaultDSLContext#fetchValue(SelectField)) explaining that certain newly added overloads (e.g., DefaultDSLContext#fetchValue(SelectField)) may internally delegate to select(...), which can cause double instrumentation and tag loss with MetricsDSLContext unless Micrometer provides corresponding overrides.

Contributor guide