Skip to content

Commit ccc66d9

Browse files
committed
Add JDBI 3 plugin
1 parent 144104d commit ccc66d9

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

instrumentation/jdbi3/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# brave-instrumentation-jdbi3
2+
3+
This includes a JDBI 3 plugin will report to Zipkin
4+
how long each statement takes, along with relevant tags like the query.
5+
6+
To use it, call the `installPlugin` on the `Jdbi` instance you want do instrument,
7+
with a suitably configured `Jdbi3BRavePlugin`.
8+
9+
By default, bind variable values are not included in the traces, for security
10+
reasons. If you want to include them, pass `true` as `includeBindVariables` in
11+
the `Jdbi3BravePlugin` constructor.

instrumentation/jdbi3/pom.xml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
4+
Copyright The OpenZipkin Authors
5+
SPDX-License-Identifier: Apache-2.0
6+
7+
-->
8+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
9+
<parent>
10+
<groupId>io.zipkin.brave</groupId>
11+
<artifactId>brave-instrumentation-parent</artifactId>
12+
<version>6.2.1-SNAPSHOT</version>
13+
</parent>
14+
<modelVersion>4.0.0</modelVersion>
15+
16+
<artifactId>brave-instrumentation-jdbi3</artifactId>
17+
<name>Brave Instrumentation: JDBI3</name>
18+
19+
<properties>
20+
<!-- Matches Export-Package in bnd.bnd -->
21+
<module.name>brave.jdbi3</module.name>
22+
23+
<main.basedir>${project.basedir}/../..</main.basedir>
24+
25+
<jdbi3.version>3.49.1</jdbi3.version>
26+
</properties>
27+
28+
<dependencies>
29+
<dependency>
30+
<groupId>org.jdbi</groupId>
31+
<artifactId>jdbi3-core</artifactId>
32+
<version>${jdbi3.version}</version>
33+
</dependency>
34+
<dependency>
35+
<groupId>${project.groupId}</groupId>
36+
<artifactId>brave-tests</artifactId>
37+
<version>${project.version}</version>
38+
<scope>test</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>com.h2database</groupId>
42+
<artifactId>h2</artifactId>
43+
<version>2.3.232</version>
44+
<scope>test</scope>
45+
</dependency>
46+
</dependencies>
47+
48+
<build>
49+
<plugins>
50+
<plugin>
51+
<artifactId>maven-invoker-plugin</artifactId>
52+
</plugin>
53+
<plugin>
54+
<groupId>de.qaware.maven</groupId>
55+
<artifactId>go-offline-maven-plugin</artifactId>
56+
<executions>
57+
<execution>
58+
<phase>package</phase>
59+
<goals>
60+
<goal>resolve-dependencies</goal>
61+
</goals>
62+
</execution>
63+
</executions>
64+
<configuration>
65+
<!-- Add dependencies specific to invoker tests so that they cache on go-offline -->
66+
<dynamicDependencies>
67+
<DynamicDependency>
68+
<groupId>org.jdbi</groupId>
69+
<artifactId>jdbi-3</artifactId>
70+
<version>${jdbi3.version}</version>
71+
<repositoryType>MAIN</repositoryType>
72+
<type>jar</type>
73+
</DynamicDependency>
74+
</dynamicDependencies>
75+
</configuration>
76+
</plugin>
77+
</plugins>
78+
</build>
79+
</project>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright The OpenZipkin Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package brave.jdbi3;
6+
7+
import java.time.Instant;
8+
9+
import org.jdbi.v3.core.Jdbi;
10+
import org.jdbi.v3.core.spi.JdbiPlugin;
11+
import org.jdbi.v3.core.statement.SqlStatements;
12+
import org.jdbi.v3.core.statement.StatementContext;
13+
import org.jdbi.v3.core.statement.StatementContextListener;
14+
15+
import brave.Span;
16+
import brave.Tracing;
17+
18+
/**
19+
* Instrumentation for JDBI 3.
20+
21+
* JDBI plugin that sends a trace span for every SQL command executed by Jdbi.
22+
* Install by calling {@code jdbi.installPlugin(new Jdbi3BravePlugin(tracing));}
23+
*/
24+
public class Jdbi3BravePlugin extends JdbiPlugin.Singleton {
25+
private final Tracing tracing;
26+
private final boolean includeBindVariables;
27+
28+
public Jdbi3BravePlugin(Tracing tracing) {
29+
this(tracing, false);
30+
}
31+
32+
public Jdbi3BravePlugin(Tracing tracing, boolean includeBindVariables) {
33+
this.tracing = tracing;
34+
this.includeBindVariables = includeBindVariables;
35+
}
36+
37+
@Override
38+
public void customizeJdbi(Jdbi jdbi) {
39+
if (tracing == null) {
40+
return;
41+
}
42+
jdbi.getConfig(SqlStatements.class)
43+
.addContextListener(new StatementContextListener() {
44+
@Override
45+
public void contextCreated(final StatementContext ctx) {
46+
String spanName = "jdbi." + ctx.describeJdbiStatementType();
47+
Span span = tracing.tracer().nextSpan().name(spanName);
48+
49+
Instant start = ctx.getExecutionMoment();
50+
if (start != null) {
51+
span.start(start.toEpochMilli() * 1000);
52+
} else {
53+
span.start();
54+
}
55+
56+
ctx.setTraceId(span.context().traceIdString());
57+
58+
ctx.addCleanable(() -> {
59+
final SqlStatements stmtConfig = ctx.getConfig(SqlStatements.class);
60+
61+
span.kind(Span.Kind.CLIENT);
62+
63+
final String renderedSql = ctx.getRenderedSql();
64+
if (renderedSql != null) {
65+
String truncated = renderedSql.substring(
66+
0,
67+
Math.min(renderedSql.length(), stmtConfig.getJfrSqlMaxLength())
68+
);
69+
span.tag("sql.query", truncated);
70+
}
71+
72+
if (includeBindVariables) {
73+
String bindVariables = ctx.getBinding()
74+
.describe(stmtConfig.getJfrParamMaxLength());
75+
span.tag("sql.binding", bindVariables);
76+
}
77+
78+
span.tag("sql.rows", Long.toString(ctx.getMappedRows()));
79+
80+
if (ctx.getCompletionMoment() == null) {
81+
span.tag("error", "true");
82+
}
83+
84+
if (ctx.getCompletionMoment() != null) {
85+
span.finish(ctx.getCompletionMoment().toEpochMilli() * 1000);
86+
} else if (ctx.getExceptionMoment() != null) {
87+
span.finish(ctx.getExceptionMoment().toEpochMilli() * 1000);
88+
} else {
89+
span.finish();
90+
}
91+
});
92+
}
93+
});
94+
}
95+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright The OpenZipkin Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package brave.jdbi3;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
9+
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
13+
import org.jdbi.v3.core.Jdbi;
14+
import org.jdbi.v3.core.JdbiException;
15+
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.Test;
17+
18+
import brave.Span;
19+
import brave.Tracing;
20+
import brave.handler.MutableSpan;
21+
import brave.handler.SpanHandler;
22+
import brave.propagation.TraceContext;
23+
24+
class Jdbi3BravePluginIT {
25+
ListSpanHandler spanHandler = new ListSpanHandler();
26+
27+
Tracing tracing = Tracing.newBuilder()
28+
.addSpanHandler(spanHandler)
29+
.build();
30+
31+
@BeforeEach
32+
void clearSpans() {
33+
spanHandler.clear();
34+
}
35+
36+
@Test
37+
public void shouldSendSpans() {
38+
buildJdbi().withHandle(h -> {
39+
h.execute("CREATE TABLE test (id INT PRIMARY KEY, name VARCHAR(255))");
40+
h.execute("INSERT INTO test (id, name) VALUES (1, 'test')");
41+
return h.createQuery("SELECT * FROM test where name = :name")
42+
.bind("name", "test")
43+
.mapToMap()
44+
.list();
45+
});
46+
47+
assertThat(spanHandler.getSpans())
48+
.anySatisfy(span -> {
49+
assertThat(span.kind()).isEqualTo(Span.Kind.CLIENT);
50+
assertThat(span.name()).isEqualTo("jdbi.Query");
51+
assertThat(span.tags()).containsEntry("sql.query", "SELECT * FROM test where name = :name")
52+
.containsEntry("sql.rows", "1")
53+
.containsEntry("sql.binding", "{named:{name:test}}");
54+
});
55+
}
56+
57+
@Test
58+
public void shouldSendSpansForFailedStatements() {
59+
assertThatThrownBy(() -> buildJdbi().useHandle(h -> {
60+
h.execute("INSERT INTO nonexistant (id, name) VALUES (1, 'test')");
61+
}))
62+
.isInstanceOf(JdbiException.class)
63+
.hasMessageContaining("Table \"NONEXISTANT\" not found");
64+
65+
assertThat(spanHandler.getSpans())
66+
.anySatisfy(span -> {
67+
assertThat(span.name()).isEqualTo("jdbi.Update");
68+
assertThat(span.error()).isNull();
69+
assertThat(span.tags()).containsKey("error");
70+
});
71+
}
72+
73+
private Jdbi buildJdbi() {
74+
Jdbi jdbi = Jdbi.create("jdbc:h2:mem:");
75+
jdbi.installPlugin(new Jdbi3BravePlugin(tracing, true));
76+
return jdbi;
77+
}
78+
79+
private static class ListSpanHandler extends SpanHandler {
80+
private final List<MutableSpan> spans = new ArrayList<>();
81+
82+
@Override
83+
public boolean end(TraceContext context, MutableSpan span, Cause cause) {
84+
spans.add(span);
85+
return super.end(context, span, cause);
86+
}
87+
88+
public List<MutableSpan> getSpans() {
89+
return spans;
90+
}
91+
92+
public void clear() {
93+
spans.clear();
94+
}
95+
}
96+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright The OpenZipkin Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package brave.jdbi3;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
import java.sql.SQLException;
10+
11+
import org.jdbi.v3.core.Jdbi;
12+
import org.jdbi.v3.core.statement.Cleanable;
13+
import org.jdbi.v3.core.statement.SqlStatements;
14+
import org.jdbi.v3.core.statement.StatementContext;
15+
import org.jdbi.v3.core.statement.StatementContextListener;
16+
import org.junit.jupiter.api.Test;
17+
import org.mockito.ArgumentCaptor;
18+
import org.mockito.Mockito;
19+
20+
import brave.Span;
21+
import brave.Tracer;
22+
import brave.Tracing;
23+
import brave.propagation.TraceContext;
24+
25+
class Jdbi3BravePluginTest {
26+
@Test
27+
public void shouldSendSpans() throws SQLException {
28+
Span span = Mockito.mock(Span.class);
29+
Tracer tracer = Mockito.mock(Tracer.class);
30+
Mockito.when(tracer.nextSpan()).thenReturn(span);
31+
Mockito.when(span.name(Mockito.anyString())).thenReturn(span);
32+
TraceContext traceContext = Mockito.mock(TraceContext.class);
33+
Mockito.when(span.context()).thenReturn(traceContext);
34+
Tracing tracing = Mockito.mock(Tracing.class);
35+
Mockito.when(tracing.tracer()).thenReturn(tracer);
36+
Jdbi jdbi = Mockito.mock(Jdbi.class);
37+
SqlStatements sqlStatements = Mockito.mock(SqlStatements.class);
38+
Mockito.when(sqlStatements.addContextListener(Mockito.any())).thenReturn(sqlStatements);
39+
Mockito.when(sqlStatements.getJfrSqlMaxLength()).thenReturn(100);
40+
Mockito.when(jdbi.getConfig(SqlStatements.class)).thenReturn(sqlStatements);
41+
42+
Jdbi3BravePlugin plugin = new Jdbi3BravePlugin(tracing);
43+
44+
plugin.customizeJdbi(jdbi);
45+
46+
ArgumentCaptor<StatementContextListener> statementContextListenerArgumentCaptor =
47+
ArgumentCaptor.forClass(StatementContextListener.class);
48+
Mockito.verify(sqlStatements, Mockito.times(1))
49+
.addContextListener(statementContextListenerArgumentCaptor.capture());
50+
51+
StatementContext statementContext = Mockito.mock(StatementContext.class);
52+
Mockito.when(statementContext.describeJdbiStatementType()).thenReturn("statement");
53+
Mockito.when(statementContext.getRenderedSql()).thenReturn("SELECT * FROM table");
54+
Mockito.when(statementContext.getConfig(SqlStatements.class)).thenReturn(sqlStatements);
55+
Mockito.when(statementContext.getMappedRows()).thenReturn(42L);
56+
57+
statementContextListenerArgumentCaptor.getValue().contextCreated(statementContext);
58+
59+
ArgumentCaptor<Cleanable> cleanableArgumentCaptor = ArgumentCaptor.forClass(Cleanable.class);
60+
Mockito.verify(statementContext, Mockito.times(1)).addCleanable(cleanableArgumentCaptor.capture());
61+
62+
cleanableArgumentCaptor.getValue().close();
63+
64+
Mockito.verify(span, Mockito.times(1)).start();
65+
Mockito.verify(span, Mockito.times(1)).name("jdbi.statement");
66+
Mockito.verify(span, Mockito.times(1)).kind(Span.Kind.CLIENT);
67+
Mockito.verify(span, Mockito.times(1)).tag("sql.query", "SELECT * FROM table");
68+
Mockito.verify(span, Mockito.times(1)).tag("sql.rows", "42");
69+
Mockito.verify(span, Mockito.times(1)).finish();
70+
}
71+
}

instrumentation/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<module>jaxrs2</module>
3535
<module>jersey-server</module>
3636
<module>jersey-server-jakarta</module>
37+
<module>jdbi3</module>
3738
<module>jms</module>
3839
<module>jms-jakarta</module>
3940
<module>jakarta-jms</module>

0 commit comments

Comments
 (0)