-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(druid): optimize dataSourceName to resolve metrics high cardinality #19108
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,54 +12,90 @@ | |
| import io.opentelemetry.api.metrics.ObservableLongMeasurement; | ||
| import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbConnectionPoolMetrics; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
| final class ConnectionPoolMetrics { | ||
|
|
||
| private static final String INSTRUMENTATION_NAME = "io.opentelemetry.alibaba-druid-1.0"; | ||
|
|
||
| private static final Map<DruidDataSourceMBean, BatchCallback> dataSourceMetrics = | ||
| private static final Map<DruidDataSourceMBean, DataSourceMetricsRegistration> dataSourceMetrics = | ||
| new ConcurrentHashMap<>(); | ||
| private static final Set<String> activeDataSourceNames = ConcurrentHashMap.newKeySet(); | ||
|
|
||
| static void registerMetrics( | ||
| OpenTelemetry openTelemetry, DruidDataSourceMBean dataSource, String dataSourceName) { | ||
| dataSourceMetrics.computeIfAbsent( | ||
| dataSource, | ||
| ds -> { | ||
| DbConnectionPoolMetrics metrics = | ||
| DbConnectionPoolMetrics.create(openTelemetry, INSTRUMENTATION_NAME, dataSourceName); | ||
|
|
||
| ObservableLongMeasurement connections = metrics.connections(); | ||
| ObservableLongMeasurement minIdleConnections = metrics.minIdleConnections(); | ||
| ObservableLongMeasurement maxIdleConnections = metrics.maxIdleConnections(); | ||
| ObservableLongMeasurement maxConnections = metrics.maxConnections(); | ||
| ObservableLongMeasurement pendingRequestsForConnection = | ||
| metrics.pendingRequestsForConnection(); | ||
|
|
||
| Attributes attributes = metrics.getAttributes(); | ||
| Attributes usedConnectionsAttributes = metrics.getUsedConnectionsAttributes(); | ||
| Attributes idleConnectionsAttributes = metrics.getIdleConnectionsAttributes(); | ||
|
|
||
| return metrics.batchCallback( | ||
| () -> { | ||
| connections.record(ds.getActiveCount(), usedConnectionsAttributes); | ||
| connections.record(ds.getPoolingCount(), idleConnectionsAttributes); | ||
| pendingRequestsForConnection.record(ds.getWaitThreadCount(), attributes); | ||
| minIdleConnections.record(ds.getMinIdle(), attributes); | ||
| maxIdleConnections.record(ds.getMaxIdle(), attributes); | ||
| maxConnections.record(ds.getMaxActive(), attributes); | ||
| }, | ||
| connections, | ||
| pendingRequestsForConnection, | ||
| minIdleConnections, | ||
| maxIdleConnections, | ||
| maxConnections); | ||
| unused -> { | ||
| String rewrittenDataSourceName = reserveDataSourceName(dataSourceName); | ||
| return new DataSourceMetricsRegistration( | ||
| createCallback(openTelemetry, dataSource, rewrittenDataSourceName), | ||
| rewrittenDataSourceName); | ||
| }); | ||
| } | ||
|
|
||
| private static String reserveDataSourceName(String dataSourceName) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the idea here is that when there are multiple data sources with the same name we'll append a suffix. Since we are not doing this for other connection pool instrumentations it is questionable whether we should be doing this here. If we decide to do this then we should apply the same strategy across the connection pool instrumentations. |
||
| if (activeDataSourceNames.add(dataSourceName)) { | ||
| return dataSourceName; | ||
| } | ||
| for (int count = 2; ; count++) { | ||
| String candidate = dataSourceName + "-" + count; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pending the discussion in #19160 (comment), I think we should avoid process-local |
||
| if (activeDataSourceNames.add(candidate)) { | ||
| return candidate; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static BatchCallback createCallback( | ||
| OpenTelemetry openTelemetry, DruidDataSourceMBean dataSource, String dataSourceName) { | ||
| DbConnectionPoolMetrics metrics = | ||
| DbConnectionPoolMetrics.create(openTelemetry, INSTRUMENTATION_NAME, dataSourceName); | ||
|
|
||
| ObservableLongMeasurement connections = metrics.connections(); | ||
| ObservableLongMeasurement minIdleConnections = metrics.minIdleConnections(); | ||
| ObservableLongMeasurement maxIdleConnections = metrics.maxIdleConnections(); | ||
| ObservableLongMeasurement maxConnections = metrics.maxConnections(); | ||
| ObservableLongMeasurement pendingRequestsForConnection = metrics.pendingRequestsForConnection(); | ||
|
|
||
| Attributes attributes = metrics.getAttributes(); | ||
| Attributes usedConnectionsAttributes = metrics.getUsedConnectionsAttributes(); | ||
| Attributes idleConnectionsAttributes = metrics.getIdleConnectionsAttributes(); | ||
|
|
||
| return metrics.batchCallback( | ||
| () -> { | ||
| connections.record(dataSource.getActiveCount(), usedConnectionsAttributes); | ||
| connections.record(dataSource.getPoolingCount(), idleConnectionsAttributes); | ||
| pendingRequestsForConnection.record(dataSource.getWaitThreadCount(), attributes); | ||
| minIdleConnections.record(dataSource.getMinIdle(), attributes); | ||
| maxIdleConnections.record(dataSource.getMaxIdle(), attributes); | ||
| maxConnections.record(dataSource.getMaxActive(), attributes); | ||
| }, | ||
| connections, | ||
| pendingRequestsForConnection, | ||
| minIdleConnections, | ||
| maxIdleConnections, | ||
| maxConnections); | ||
| } | ||
|
|
||
| static void unregisterMetrics(DruidDataSourceMBean dataSource) { | ||
| BatchCallback callback = dataSourceMetrics.remove(dataSource); | ||
| if (callback != null) { | ||
| DataSourceMetricsRegistration registration = dataSourceMetrics.remove(dataSource); | ||
| if (registration != null) { | ||
| registration.close(); | ||
| } | ||
| } | ||
|
|
||
| private static final class DataSourceMetricsRegistration { | ||
| private final BatchCallback callback; | ||
| private final String dataSourceName; | ||
|
|
||
| private DataSourceMetricsRegistration(BatchCallback callback, String dataSourceName) { | ||
| this.callback = callback; | ||
| this.dataSourceName = dataSourceName; | ||
| } | ||
|
|
||
| private void close() { | ||
| activeDataSourceNames.remove(dataSourceName); | ||
| callback.close(); | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that we usually just omit name components that are not present instead of using placeholders like
unknown.Noticed that spec https://opentelemetry.io/docs/specs/semconv/registry/attributes/db/#db-client-connection-pool-name provides some guidance on how to create a name when it is not available. Idk whether this is a viable strategy since these properties might not be available either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In typical business scenarios, when configuring a data source using DruidDataSource, the setName or setObjectName methods of DruidDataSource are not called by default. Similarly, in Spring projects, the name property is generally not configured via application.yml. The current implementation will definitely result in a high cardinality issue, and we can simply omit these fields as you mentioned, since neither the type nor the passed name attribute in objectName may be obtainable.
I’m wondering if we should skip the unavailable attribute as long as either one of the two can be retrieved. If neither attribute can be obtained, we need a fallback strategy with a default value such as unknown, default, or something component-related like druid.
@laurit @trask Could you two please review this and share any better suggestions?