Skip to content

Add JDBI 3 plugin #1460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

reftel
Copy link
Contributor

@reftel reftel commented Apr 10, 2025

Adds a plugin for JDBI 3 (https://jdbi.org/ ) that sends spans via Brave for all SQL commands run. Tag names are chosen to match the old P6Spy instrumentation from the 5.x series.
Including both a Mocktio test and an integration test that uses H2. Not sure both are needed, as they test pretty much the same thing.

@reftel reftel force-pushed the feature/jdbi_instrumentation branch from f756d98 to 969af12 Compare April 11, 2025 06:56
@reftel reftel force-pushed the feature/jdbi_instrumentation branch from 969af12 to 646c901 Compare April 28, 2025 06:26
@reftel
Copy link
Contributor Author

reftel commented Apr 28, 2025

Rebased, bumped parent version, bumped jdbi version.

@reftel reftel force-pushed the feature/jdbi_instrumentation branch from 646c901 to ccc66d9 Compare April 29, 2025 06:11
@reta reta requested a review from codefromthecrypt April 29, 2025 12:29
@reta
Copy link
Contributor

reta commented Apr 29, 2025

@codefromthecrypt any concerns from you? thanks!

Copy link
Member

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the work, but I think we need to study a bit more other modules (or ask your favorite coding assistant to). Please use same conventions in coding and tests for the more recent similar code. you've made a good start though!

.addContextListener(new StatementContextListener() {
@Override
public void contextCreated(final StatementContext ctx) {
String spanName = "jdbi." + ctx.describeJdbiStatementType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm try to match the data policy with mysql. I don't think we want to put jdbi in the span name.. https://github.com/openzipkin/brave/blob/master/instrumentation/mysql8/src/main/java/brave/mysql8/TracingQueryInterceptor.java

this(tracing, false);
}

public Jdbi3BravePlugin(Tracing tracing, boolean includeBindVariables) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you look elsewhere you'll see types that have builders. Typically we don't add public ctors with options as it is constant maintenance. maybe find one to copy/paste find/replace

import brave.handler.SpanHandler;
import brave.propagation.TraceContext;

class Jdbi3BravePluginIT {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try for naming conventions and other conventions in the project. IT prefix e.g. https://github.com/openzipkin/brave/blob/master/instrumentation/mysql8/src/test/java/brave/mysql8/ITTracingQueryInterceptor.java and also IT implies real DB. This might be a unit test? ack jdbc:h2:mem is grey area.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It´s the use of an actual (though in-process and in-memory) database, along with using the third-party libraries as-is (as opposed to stubbing them, or parts of them, out) that made me call it an IT, but if you disagree, I´ll gladly change it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah well you can probably use the mysql container we use in the other but agree it is a minor point. Just make sure the tests are similar as possible. Cheers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I´ve changed the tests over to more-or-less a copy of the tests from the mysql8 instrumentation. I´ll make a stab at getting the tests run with both H2 and MySQL, as a way to show that the instrumentation is not tied to any particular database.

Copy link
Contributor Author

@reftel reftel May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the test abstract, with concrete implementations that test against H2 in-memory, MySQL configured as in the mysql8 tests, Postgres via the Zonky.io extension, and Sqlite via the xerial.org library.
In Junit 5.13, there will be a cleaner way of doing this, but it´s not released yet.

@reftel reftel force-pushed the feature/jdbi_instrumentation branch from ccc66d9 to 17321f1 Compare April 30, 2025 06:59
@reftel
Copy link
Contributor Author

reftel commented Apr 30, 2025

I like the work, but I think we need to study a bit more other modules (or ask your favorite coding assistant to). Please use same conventions in coding and tests for the more recent similar code. you've made a good start though!

Thank you for your comments! I believe I´ve addressed them all in 17321f1

@reftel reftel force-pushed the feature/jdbi_instrumentation branch from 17321f1 to ae0492c Compare May 2, 2025 08:21
Copy link
Member

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last feedback!

Unless there is a main code difference, please test only one DB as an IT. We should save test time for things that improve coverage.

Finally, it is easy to miss, but brave-bom needs an update both version and also to include this module https://github.com/openzipkin/brave/blob/master/brave-bom/pom.xml#L12

@reftel reftel force-pushed the feature/jdbi_instrumentation branch from ae0492c to f5fd953 Compare May 2, 2025 11:17
@reftel
Copy link
Contributor Author

reftel commented May 2, 2025

Thank you for your patience and prompt reviews! I hope all should be in order now.

Unless there is a main code difference, please test only one DB as an IT. We should save test time for things that improve coverage.

OK, dropped everything but the MySQL one.

Finally, it is easy to miss, but brave-bom needs an update both version and also to include this module https://github.com/openzipkin/brave/blob/master/brave-bom/pom.xml#L12

Done.

Copy link
Member

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to click approve, but really there are a lot of little things to clean up. clean up what you can. If you are out of time, then let's leave this PR open until it can be.

private boolean includeBindVariables = false;

public Builder(Tracing tracing) {
this.tracing = tracing;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.tracing = tracing;
if (tracing == null) throw new NullPointerException("tracing == null");
this.tracing = tracing;

return null;
}

public static class Builder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static class Builder {
public static final class Builder {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure the builder is still needed now that includeBindVariables is not there any longer, though.

}
URI uri;
try {
uri = new URI(connection.getMetaData().getURL().replaceAll("^jdbc:", ""));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normally I would challenge you to make this similar to mysql code which is more efficient than regex. However, you've had a lot of back and forth so we can leave that for someone else to refine. Generally it is better to look at how we do things as often there is some performance mania going on, though not always.

URI url = URI.create(connection.getMetaData().getURL().substring(5)); // strip "jdbc:"
String remoteServiceName = connection.getProperties().getProperty("zipkinServiceName");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added a simple cache, since a listener usually only ever sees one URL, and the remote service name for a given URL will always be the same.

* JDBI plugin that sends a trace span for every SQL command executed by Jdbi.
* Install by calling {@code jdbi.installPlugin(new Jdbi3BravePlugin(tracing));}
*/
public class Jdbi3BravePlugin extends JdbiPlugin.Singleton {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class Jdbi3BravePlugin extends JdbiPlugin.Singleton {
public final class Jdbi3BravePlugin extends JdbiPlugin.Singleton {

return this;
}

public Jdbi3BravePlugin build() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always return the most narrow type you can that works. Either plugin or plugin.singleton depending on what you think here. This is likely a place where since mysql plugin was made so long ago we didn't make do this.

Suggested change
public Jdbi3BravePlugin build() {
public JdbiPlugin.Singleton build() {


assertThat(spans)
.extracting(MutableSpan::name)
.containsExactly("Query");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you've misunderstood me. We shouldn't model differently than mysql. Please match your tests to what is here, as the span name shouldn't be "Query" that's not a SQL operator

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That´s the reason I had the jdbi. prefix originally - the name is the Jdbi description of the statement type, not the first word of the statement. Extracting the first word of the statement seems very brittle (e.g. it fails when using comment prefixes, which is generally is good practice), and since the full SQL is in the sql.query tag, it is also redundant. Using the Jdbi description of the type of statement (e.g. Query, Update, or Call) seems to me to be a better way.
I can change it if you insist, but I do think it would result in a worse implementation.


@Override
public void customizeJdbi(Jdbi jdbi) {
if (tracing == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove tracing == null here to reduce branching, as it is a bug to have null here and we can guard against it.

* Instrumentation for JDBI 3.
* <p>
* JDBI plugin that sends a trace span for every SQL command executed by Jdbi.
* Install by calling {@code jdbi.installPlugin(new Jdbi3BravePlugin(tracing));}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this drifted, also check your README

/**
* Instrumentation for JDBI 3.
* <p>
* JDBI plugin that sends a trace span for every SQL command executed by Jdbi.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds redundant. A lot of code here has had a lot of review, so you can avoid back and forth by copying it first, then revising. For example, this says what (a couple times) not why. e.g. if you look at the reference I mentioned, the javadoc says this A MySQL query interceptor that will report to Zipkin how long each query takes So the why is about the query. Also, we don't need to get specific to the version as that's already in the package name.

https://github.com/openzipkin/brave/blob/master/instrumentation/mysql8/src/main/java/brave/mysql8/TracingQueryInterceptor.java#L23-L31


<main.basedir>${project.basedir}/../..</main.basedir>

<embedded-postgres.version>2.1.0</embedded-postgres.version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a number of unused versions here. Also make sure you aren't declaring a version already declared elsewhere. I think you might be on mysql-connector-java.version. If that version isn't in the top-level pom due to lack of re-use, I would extract the one from mysql8 up to the top-level pom, then use the same variable name here.

When we don't take care to match other instrumentation, it leaves little bombs for maintainers later. For example, @reta and I have been dealing with versions for many years on code that sometimes was only polished once by the initial author (the original PR). This is why it is really ideal to look at things mentioned for review and look closely, as others are likely going to maintain it for you, and this is a perpetual task. Since it is, going the same way as other code lightens the load.

return;
}
jdbi.getConfig(SqlStatements.class)
.addContextListener(new StatementContextListener() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one last thing is that this code would be easier to read and more conventional extracted to a package-private type (TracingStatementContextListener) and then used here. Another way to reduce ceremony is to switch the whole type to TracingStatementContextListener similar to this https://github.com/openzipkin/brave/blob/master/instrumentation/okhttp3/src/main/java/brave/okhttp3/TracingCallFactory.java (keeping the builder if you decide to keep the toggle).

This allows people to configure JDBI however they want including via a plugin and keeps the complexity of this code to a minimal. In other words, the actual code is the context listener, plugin is just one way to configure it. By limiting to plugin we add more harder to test code here, to save someone from doing jdbi.getConfig(SqlStatements.class) .addContextListener(our_listener) despite that being how all the other instrumentation work (direct integration where possible)

Making this comment as on the mind, but like I said you can feel free to stop refactoring if you have run out of time. If I were being strict like years ago I would have insisted on it.

@reftel reftel force-pushed the feature/jdbi_instrumentation branch from f5fd953 to 65f563f Compare May 5, 2025 07:05
@reftel reftel force-pushed the feature/jdbi_instrumentation branch from 65f563f to 2ecc9d4 Compare May 6, 2025 06:38
@reftel reftel force-pushed the feature/jdbi_instrumentation branch from 2ecc9d4 to f803bb1 Compare May 8, 2025 06:17
@reftel reftel force-pushed the feature/jdbi_instrumentation branch from f803bb1 to 9f9842b Compare May 8, 2025 11:47
@reftel reftel force-pushed the feature/jdbi_instrumentation branch from 9f9842b to cb6d68f Compare May 8, 2025 12:00
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RemoteServiceNameResolver {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor, I think we shouldn't expose this class publicly:

Suggested change
public class RemoteServiceNameResolver {
final class RemoteServiceNameResolver {

});
}

String getRemoteServiceName(StatementContext ctx) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thank you

@reta
Copy link
Contributor

reta commented May 8, 2025

@codefromthecrypt mind taking another look? thank you!

Copy link
Member

@codefromthecrypt codefromthecrypt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure I'll do a small amount of polishing post merge. good work!

@codefromthecrypt codefromthecrypt merged commit 0ad3f6d into openzipkin:master May 27, 2025
4 checks passed
codefromthecrypt added a commit that referenced this pull request May 28, 2025
This converts jdbi3 instrumentation to be more like MongoDB in
implementation: configured by instance, not static like mysql connection
driver. It also pares back the data policy to be the same as MySQL: this
allows us a base case to move forward with. Finally, it switches
implementation to a StatementLogger, so that exceptions are visible.

Infrastructure wise, this re-uses the more recent style from
instrumentation here, including docker for the IT.

This is a follow-up to #1460 before we release a version, so API breaks
are not an issue, yet.

Thanks to @reftel for the initial work on this, and @reta on the review
of that. I did my best in RATIONALE.md here to explain why the changes
are important.

---------

Signed-off-by: Adrian Cole <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants