Add executor thread pool metrics instrumentation#19277
Conversation
Pull request dashboard statusStatus last refreshed: 2026-07-24 05:19:52 UTC.
This automated status or its linked feedback items may be incorrect. If something looks wrong, please report it with the result you expected. |
There was a problem hiding this comment.
Pull request overview
Adds automatic executor metrics for JDK thread pools, including reusable registration for future framework-specific instrumentation.
Changes:
- Adds executor metric instruments and weak-reference registrations.
- Instruments
ThreadPoolExecutorand JDK 21ThreadPerTaskExecutorlifecycles. - Adds name-normalization configuration, documentation, and tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
instrumentation/executors/README.md |
Documents name normalization. |
instrumentation/executors/metadata.yaml |
Defines the new configuration. |
instrumentation/executors/jdk21-testing/.../ThreadPerTaskExecutorMetricsTest.java |
Tests JDK 21 executor metrics. |
instrumentation/executors/javaagent/.../ThreadPoolExecutorMetricsTest.java |
Tests thread-pool metrics and lifecycle behavior. |
instrumentation/executors/javaagent/.../ThreadPoolExecutorMetricsInstrumentation.java |
Instruments thread-pool lifecycle methods. |
instrumentation/executors/javaagent/.../ThreadPerTaskExecutorMetricsInstrumentation.java |
Instruments JDK 21 per-task executors. |
instrumentation/executors/javaagent/.../ExecutorsInstrumentationModule.java |
Registers the new instrumentations. |
instrumentation/executors/javaagent/build.gradle.kts |
Adds trailing-normalization tests. |
instrumentation/executors/bootstrap/.../ThreadPoolExecutorMetrics.java |
Implements registration and metric callbacks. |
instrumentation/executors/bootstrap/.../JvmExecutorMetrics.java |
Defines executor metric instruments. |
instrumentation/executors/bootstrap/build.gradle.kts |
Adds required bootstrap dependency. |
| implements EarlyInstrumentationModule { | ||
|
|
||
| public ExecutorsMetricsInstrumentationModule() { | ||
| super("executors", "executors-metrics"); |
There was a problem hiding this comment.
The code that uses these names to check if the module is enabled/disabed as part of AgentDistributionConfig.isInstrumentationEnabled uses the first record it finds, so If someone has set otel.instrumentation.executors.enabled=true it will also enable this module by default, and ignore the defaultEnabled value, even if otel.instrumentation.executors-metrics.enabled is not set
There was a problem hiding this comment.
Initially, I planned to follow the implementation approach used in the Kafka instrumentation.
Reference code link: KafkaMetricsInstrumentationModule
For example, I intended to declare it via the super constructor call: super("executors-metrics", "executors").
However, the check-javaagent-suppression-keys.sh validation failed during execution.
After investigation, extra handling logic needs to be added into this shell script.
Nevertheless, there is an existing TODO comment inside the script, leaving me uncertain about the optimal modification strategy.
Two options are available: either extract the relevant logic into a standalone module, or directly edit the check-javaagent-suppression-keys.sh script to make the test pass. I would like to discuss this matter with the team to reach a consensus.
| private static final String INSTRUMENTATION_NAME = "io.opentelemetry.executors"; | ||
| private static final String UNKNOWN = "unknown"; | ||
| private static final String THREAD_NAME_NORMALIZATION = | ||
| DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "executors") |
There was a problem hiding this comment.
perhaps this should be consistent with the other one?
| DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "executors") | |
| DeclarativeConfigUtil.getInstrumentationConfig(GlobalOpenTelemetry.get(), "executors-metrics") |
There was a problem hiding this comment.
This issue has been considered beforehand. If we rename executors to executors-metrics, do we also need to change io.opentelemetry.executors to io.opentelemetry.executors-metrics?
I referenced the existing Kafka implementation cases, so for now we keep our logic consistent with Kafka's implementation.
| registrations.computeIfAbsent(executor, ignored -> new Registration(threads)); | ||
| } | ||
|
|
||
| public static void reregister( |
There was a problem hiding this comment.
this looks like it is only used in tests right now. I'm guessing maybe its part of the rest of the work mentioned in the PR description? as of now ownerName is only ever mutated by reregister, so it will always be "unknown"
There was a problem hiding this comment.
Currently this field is hardcoded to unknown. When other components adopt the JDK-native ThreadPoolExecutor and ThreadPerTaskExecutor thread pools (which we predefine) in the future, we can reset this field to mark the corresponding component being used.
Reference examples:
I have finished implementing this part. I will submit a new PR only after the semantic specifications for thread pool metrics are finalized and the current PR gets merged.
Relates to #11149
Motivation
Executor metrics provide visibility into utilization, saturation, queue pressure, completed work, and rejected work. This change adds an initial implementation to the existing Java executors instrumentation and makes the proposed metric model concrete for semantic-convention discussion.
Design and lifecycle
ThreadPoolExecutorinstances are pre-registered at construction time, but no metrics are created and the configuredThreadFactoryis not invoked then. Metrics are registered only after the first worker thread actually starts, using that worker's real thread name to derivejvm.executor.name.When
setThreadFactory()is called, the existing metric identity remains active until a worker created by the new factory starts. At that point, the registration is replaced using the new worker thread name.Generic JDK instrumentation uses
jvm.executor.owner.name=unknown. Framework-specific instrumentation can reusereregister(...)to provide an owner name and a component-specificallortrailingname-normalization mode. This works both before the first worker starts and after metrics are already registered.Rejected tasks are recorded after metrics have been registered. Metrics are unregistered only after an executor has actually entered the shutdown state. The registration cache and callbacks use weak references, so metrics do not retain an unclosed executor; callbacks close themselves after the observed executor is collected.
Metrics emitted by
ThreadPoolExecutorjvm.executor.thread.count{thread}jvm.executor.name,jvm.executor.owner.name,jvm.executor.type,jvm.executor.statejvm.executor.thread.core{thread}jvm.executor.name,jvm.executor.owner.name,jvm.executor.typejvm.executor.thread.max{thread}jvm.executor.name,jvm.executor.owner.name,jvm.executor.typejvm.executor.queue.size{task}jvm.executor.name,jvm.executor.owner.name,jvm.executor.typejvm.executor.queue.remaining{task}jvm.executor.name,jvm.executor.owner.name,jvm.executor.typejvm.executor.task.completed{task}jvm.executor.name,jvm.executor.owner.name,jvm.executor.typejvm.executor.task.rejected{task}jvm.executor.name,jvm.executor.owner.name,jvm.executor.typeJDK 21
ThreadPerTaskExecutoris also covered. It currently emits onlyjvm.executor.thread.countwithjvm.executor.state=active.jvm.executor.nameis derived from the first actual worker thread name.otel.instrumentation.executors.name-normalization=allis the default and replaces every digit group with*;trailingreplaces only the final digit group. For example,tomcat-8080-exec-1becomestomcat-*-exec-*withallandtomcat-8080-exec-*withtrailing. Empty thread names useunknown.Scope and follow-up
This is an initial implementation intended to support discussion of experimental executor metric semantic conventions.
ScheduledThreadPoolExecutorandjvm.executor.queue.wait.durationare intentionally out of scope. Once the metric and attribute semantics are agreed, the reusable registration path can evolve into an incubating shared helper for framework-specific instrumentations.