Skip to content
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

Implement HikariCP connection pool metrics #6003

Merged
merged 4 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ These are the supported libraries and frameworks:
| [Guava ListenableFuture](https://guava.dev/releases/snapshot/api/docs/com/google/common/util/concurrent/ListenableFuture.html) | 10.0+ |
| [GWT](http://www.gwtproject.org/) | 2.0+ |
| [Hibernate](https://github.com/hibernate/hibernate-orm) | 3.3+ |
| [HikariCP](https://github.com/brettwooldridge/HikariCP) | 3.0+ |
| [HttpURLConnection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html) | Java 8+ |
| [Hystrix](https://github.com/Netflix/Hystrix) | 1.4+ |
| [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.metrics.db;

import static io.opentelemetry.api.common.AttributeKey.stringKey;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.MeterBuilder;
import io.opentelemetry.api.metrics.ObservableLongUpDownCounter;
import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties;
import java.util.function.LongSupplier;

/**
* A helper class that models the <a
* href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/database-metrics.md#connection-pools">database
* client connection pool metrics semantic conventions</a>.
*/
public final class DbConnectionPoolMetrics {

static final AttributeKey<String> POOL_NAME = stringKey("pool.name");
static final AttributeKey<String> CONNECTION_STATE = stringKey("state");

static final String STATE_IDLE = "idle";
static final String STATE_USED = "used";

public static DbConnectionPoolMetrics create(
OpenTelemetry openTelemetry, String instrumentationName, String poolName) {

MeterBuilder meterBuilder = openTelemetry.getMeterProvider().meterBuilder(instrumentationName);
String version = EmbeddedInstrumentationProperties.findVersion(instrumentationName);
if (version != null) {
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
meterBuilder.setInstrumentationVersion(version);
}
return new DbConnectionPoolMetrics(meterBuilder.build(), Attributes.of(POOL_NAME, poolName));
}

private final Meter meter;
private final Attributes attributes;
private final Attributes usedConnectionsAttributes;
private final Attributes idleConnectionsAttributes;

DbConnectionPoolMetrics(Meter meter, Attributes attributes) {
this.meter = meter;
this.attributes = attributes;
usedConnectionsAttributes = attributes.toBuilder().put(CONNECTION_STATE, STATE_USED).build();
idleConnectionsAttributes = attributes.toBuilder().put(CONNECTION_STATE, STATE_IDLE).build();
}

public ObservableLongUpDownCounter usedConnections(LongSupplier usedConnectionsGetter) {
return meter
.upDownCounterBuilder("db.client.connections.usage")
.setUnit("connections")
.setDescription(
"The number of connections that are currently in state described by the state attribute.")
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
.buildWithCallback(
measurement ->
measurement.record(usedConnectionsGetter.getAsLong(), usedConnectionsAttributes));
}

public ObservableLongUpDownCounter idleConnections(LongSupplier idleConnectionsGetter) {
return meter
.upDownCounterBuilder("db.client.connections.usage")
.setUnit("connections")
.setDescription(
"The number of connections that are currently in state described by the state attribute.")
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
.buildWithCallback(
measurement ->
measurement.record(idleConnectionsGetter.getAsLong(), idleConnectionsAttributes));
}

public ObservableLongUpDownCounter minIdleConnections(LongSupplier minIdleConnectionsGetter) {
return meter
.upDownCounterBuilder("db.client.connections.idle.min")
.setUnit("connections")
.setDescription("The minimum number of idle open connections allowed.")
.buildWithCallback(
measurement -> measurement.record(minIdleConnectionsGetter.getAsLong(), attributes));
}

public ObservableLongUpDownCounter maxIdleConnections(LongSupplier maxIdleConnectionsGetter) {
return meter
.upDownCounterBuilder("db.client.connections.idle.max")
.setUnit("connections")
.setDescription("The maximum number of idle open connections allowed.")
.buildWithCallback(
measurement -> measurement.record(maxIdleConnectionsGetter.getAsLong(), attributes));
}

public ObservableLongUpDownCounter maxConnections(LongSupplier maxConnectionsGetter) {
return meter
.upDownCounterBuilder("db.client.connections.max")
.setUnit("connections")
.setDescription("The maximum number of open connections allowed.")
.buildWithCallback(
measurement -> measurement.record(maxConnectionsGetter.getAsLong(), attributes));
}

public ObservableLongUpDownCounter pendingRequestsForConnection(
LongSupplier pendingRequestsGetter) {
return meter
.upDownCounterBuilder("db.client.connections.pending_requests")
.setUnit("requests")
.setDescription(
"The number of pending requests for an open connection, cumulative for the entire pool.")
.buildWithCallback(
measurement -> measurement.record(pendingRequestsGetter.getAsLong(), attributes));
}

// TODO: should be a BoundLongCounter
public LongCounter connectionTimeouts() {
return meter
.counterBuilder("db.client.connections.timeouts")
.setUnit("timeouts")
.setDescription(
"The number of connection timeouts that have occurred trying to obtain a connection from the pool.")
.build();
}

// TODO: should be a BoundDoubleHistogram
public DoubleHistogram connectionCreateTime() {
return meter
.histogramBuilder("db.client.connections.create_time")
.setUnit("ms")
.setDescription("The time it took to create a new connection.")
.build();
}

// TODO: should be a BoundDoubleHistogram
public DoubleHistogram connectionWaitTime() {
return meter
.histogramBuilder("db.client.connections.wait_time")
.setUnit("ms")
.setDescription("The time it took to obtain an open connection from the pool.")
.build();
}

// TODO: should be a BoundDoubleHistogram
public DoubleHistogram connectionUseTime() {
return meter
.histogramBuilder("db.client.connections.use_time")
.setUnit("ms")
.setDescription("The time between borrowing a connection and returning it to the pool.")
.build();
}

// TODO: should be removed once bound instruments are back
public Attributes getAttributes() {
return attributes;
}
}
19 changes: 19 additions & 0 deletions instrumentation/hikaricp-3.0/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("com.zaxxer")
module.set("HikariCP")
versions.set("[3.0.0,)")
// muzzle does not detect PoolStats method references used - some of these methods were introduced in 3.0 and we can't assertInverse

// 4.0.0 uses a broken slf4j version: the "${slf4j.version}" placeholder is taken literally
skip("4.0.0")
}
}

dependencies {
library("com.zaxxer:HikariCP:3.0.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.hikaricp;

import static java.util.Collections.singletonList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class HikariCpInstrumentationModule extends InstrumentationModule {

public HikariCpInstrumentationModule() {
super("hikaricp", "hikaricp-3.0");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HikariPoolInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.hikaricp;

import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.zaxxer.hikari.metrics.MetricsTrackerFactory;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class HikariPoolInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.zaxxer.hikari.pool.HikariPool");
}

@Override
public void transform(TypeTransformer transformer) {
// this method is always called in the HikariPool constructor, even if the user does not
// configure anything
Comment on lines +28 to +29
Copy link
Member

Choose a reason for hiding this comment

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

👍

transformer.applyAdviceToMethod(
named("setMetricsTrackerFactory")
.and(takesArguments(1))
.and(takesArgument(0, named("com.zaxxer.hikari.metrics.MetricsTrackerFactory"))),
this.getClass().getName() + "$SetMetricsTrackerFactoryAdvice");
}

@SuppressWarnings("unused")
public static class SetMetricsTrackerFactoryAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(value = 0, readOnly = false) MetricsTrackerFactory metricsTrackerFactory) {

if (!(metricsTrackerFactory instanceof OpenTelemetryMetricsTrackerFactory)) {
metricsTrackerFactory = new OpenTelemetryMetricsTrackerFactory(metricsTrackerFactory);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.hikaricp;

import com.zaxxer.hikari.metrics.IMetricsTracker;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice - let's go ahead and add library instrumentation

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mind if I do that in a separate PR? This one is not that small already, and it establishes some common code parts that might be used in other connection pill instrumentations.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if the library instrumentation was contributed to Hikari iteself and lived alongside the dropwizard, micrometer, and prometheus implementations?

Copy link
Member Author

Choose a reason for hiding this comment

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

That could also work, probably.
The semantic conventions are not stable though, they could change in the future -- I wonder what kind of strategy we have for updating 3rd party libs when the telemetry changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Splitting library out in separate PR is fine too - that being said "establishes some common code parts that might be used in other connection pill instrumentations" makes it sound like other instrumentation might come before a split, which I wouldn't recommend ;)

The semantic conventions are not stable though, they could change in the future -- I wonder what kind of strategy we have for updating 3rd party libs when the telemetry changes.

Most stable libraries will be reluctant if not forbid having a dependency on an alpha module, our -semconv module. I would probably keep upstream instrumentation off the table until the semconv is stable and part of our non-alpha artifact.

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.ObservableLongUpDownCounter;
import java.util.List;
import java.util.concurrent.TimeUnit;

final class OpenTelemetryMetricsTracker implements IMetricsTracker {

private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1);

private final IMetricsTracker userMetricsTracker;

private final List<ObservableLongUpDownCounter> observableInstruments;
private final LongCounter timeouts;
private final DoubleHistogram createTime;
private final DoubleHistogram waitTime;
private final DoubleHistogram useTime;
private final Attributes attributes;

OpenTelemetryMetricsTracker(
IMetricsTracker userMetricsTracker,
List<ObservableLongUpDownCounter> observableInstruments,
LongCounter timeouts,
DoubleHistogram createTime,
DoubleHistogram waitTime,
DoubleHistogram useTime,
Attributes attributes) {
this.userMetricsTracker = userMetricsTracker;
this.observableInstruments = observableInstruments;
this.timeouts = timeouts;
this.createTime = createTime;
this.waitTime = waitTime;
this.useTime = useTime;
this.attributes = attributes;
}
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved

@Override
public void recordConnectionCreatedMillis(long connectionCreatedMillis) {
createTime.record((double) connectionCreatedMillis, attributes);
userMetricsTracker.recordConnectionCreatedMillis(connectionCreatedMillis);
}

@Override
public void recordConnectionAcquiredNanos(long elapsedAcquiredNanos) {
double millis = elapsedAcquiredNanos / NANOS_PER_MS;
mateuszrzeszutek marked this conversation as resolved.
Show resolved Hide resolved
waitTime.record(millis, attributes);
userMetricsTracker.recordConnectionAcquiredNanos(elapsedAcquiredNanos);
}

@Override
public void recordConnectionUsageMillis(long elapsedBorrowedMillis) {
useTime.record((double) elapsedBorrowedMillis, attributes);
userMetricsTracker.recordConnectionUsageMillis(elapsedBorrowedMillis);
}

@Override
public void recordConnectionTimeout() {
timeouts.add(1, attributes);
userMetricsTracker.recordConnectionTimeout();
}

@Override
public void close() {
for (ObservableLongUpDownCounter observable : observableInstruments) {
observable.close();
}
userMetricsTracker.close();
}
}
Loading