Skip to content

[improve][build] Upgrade OpenTelemetry to 1.65.0, instrumentation to 2.30.0, semconv to 1.43.0 - #26334

Merged
nodece merged 1 commit into
masterfrom
lh-upgrade-opentelemetry-1.65.0
Aug 16, 2026
Merged

nodece merged 1 commit into
masterfrom
lh-upgrade-opentelemetry-1.65.0

Conversation

@lhotari

@lhotari lhotari commented Aug 14, 2026

Copy link
Copy Markdown
Member

Motivation

Keep the OpenTelemetry stack current. Pulsar is several releases behind on every
OpenTelemetry artifact it consumes, which means it misses upstream bug fixes in the
Prometheus and OTLP exporters and drifts further from the semantic-convention baseline.

OpenTelemetry 1.65.0 also stops publishing opentelemetry-exporter-zipkin and removes a
batch of APIs that were deprecated in earlier releases, so staying on 1.62.0 only makes the
eventual upgrade larger.

Modifications

Version catalog (gradle/libs.versions.toml):

Artifact From To
io.opentelemetry:opentelemetry-bom (and -bom-alpha) 1.62.0 1.65.0
io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom (and -bom-alpha) 2.28.1 2.30.0
io.opentelemetry.semconv:opentelemetry-semconv 1.41.1 1.43.0
io.opentelemetry.contrib:opentelemetry-gcp-resources 1.57.0-alpha 1.59.0-alpha

opentelemetry-java-contrib 1.59.0 is the release that targets
opentelemetry-java-instrumentation 2.30.0, so the four stay aligned.
opentelemetry-gcp-resources is a version-catalog entry only: it resolves on no module's
classpath and is in no LICENSE file, but it is not inert -- pulsar-dependencies turns every
catalog alias into a constraint, so it appears in the published BOM's <dependencyManagement>.

Instrumentation 2.30.0 is built against SDK 1.64.0 while Pulsar pins core at 1.65.0. That is a
forward pin (Gradle resolves 1.64.0 -> 1.65.0, no downgrade, no conflict), and it was checked
for link-safety rather than assumed: all 796 io.opentelemetry.* class/method/field references
in the four shipped instrumentation jars resolve against core 1.65.0 + semconv 1.43.0 with zero
missing classes and zero missing members.

Binary LICENSE files for the server and shell distributions were updated to match the bundled
jars. This includes the transitive Prometheus client bump (io.prometheus:prometheus-metrics-*
1.5.1 -> 1.8.0) that opentelemetry-exporter-prometheus brings in. No jar was added to or
removed from either distribution -- every change is a version rename.

No source changes were needed. Details below.

Deprecated code review

The build runs with -Xlint:deprecation (pulsar.java-conventions.gradle.kts). After the
upgrade, ./gradlew sanityCheck compiles every module's main and test sources with zero
OpenTelemetry deprecation warnings -- while still reporting unrelated Netty and Pulsar
deprecations, so the lint is doing its job.

Individually checked against the release notes:

  • opentelemetry-exporter-zipkin unpublished (1.65.0) -- Pulsar never depended on it.
  • PrometheusMetricReader deprecated constructors dropped (1.64.0) -- Pulsar uses
    PrometheusHttpServer, not those constructors.
  • InstrumentationConfigUtil.peerServiceMapping removed (1.64.0), ExtendedAttributes
    removed (1.63.0)
    , ViewConfig/ViewConfigCustomizer removed (1.63.0),
    otel.experimental.config.file removed (1.63.0), deprecated OTLP SPI property names
    removed (1.63.0)
    -- none are referenced anywhere in the repo.
  • Declarative-config experimental types moved to an internal package (1.64.0) -- Pulsar
    does not use declarative config.
  • RuntimeTelemetryBuilder -- its public API is byte-for-byte the same in 2.28.1 and
    2.30.0, so OpenTelemetryService's use of
    io.opentelemetry.instrumentation.runtimetelemetry.internal.{Internal,Experimental} to
    disable JFR and enable experimental JMX metrics is still the only supported way to do that.
  • semconv -- the only constant Pulsar references is ServiceAttributes (SERVICE_NAME,
    SERVICE_VERSION), which is stable and unchanged in 1.43.0.

User-visible change: Prometheus series identity

Some label values change, so Prometheus assigns new series identities across the upgrade and
rate()/increase() see a one-time discontinuity at restart. All of these affect only
deployments that enable the OpenTelemetry Prometheus exporter (otel.sdk.disabled defaults to
true), and none warrants a code change -- changing what Pulsar emits to avoid them would itself
be a break.

1. otel_scope_version on the jvm.* runtime metrics: 2.28.1-alpha -> 2.30.0-alpha.
RuntimeTelemetryBuilder.getMeter() sets the meter's instrumentation version from the version
file embedded in the opentelemetry-runtime-telemetry jar, and the Prometheus exporter emits
otel_scope_name/otel_scope_version by default. This is routine for any instrumentation bump.
It is limited to the jvm.* series: the broker and worker meters
(PulsarBrokerOpenTelemetry, PulsarWorkerOpenTelemetry) are built with getMeter(name) and
carry no version at all, and while the client meter does set one
(InstrumentProvider.java:46, PulsarVersion.getVersion()), that value is a Pulsar version and
is unaffected by this dependency bump.

2. Colliding label names now merge instead of dropping. When two OpenTelemetry attribute
keys normalize to the same Prometheus label name, 1.62.0 kept a single value; 1.65.0 sorts the
original keys and joins their values with ; ("Merge colliding Prometheus label values" in the
1.65.0 notes). None of Pulsar's own 126 metric names or 61 attribute keys collide -- the
differential below would have shown it -- so this is only reachable via user-supplied colliding
keys, e.g. OTEL_RESOURCE_ATTRIBUTES=foo.bar=a,foo-bar=b now yielding foo_bar="a;b".

3. process_command_args label format -- described next.

User-visible change: process_command_args label format

OpenTelemetry 1.64.0 fixed serialization of array-valued resource and scope attributes in
the Prometheus exporter (open-telemetry/opentelemetry-java#8497). Otel2PrometheusConverter
now routes them through toLabelValue() -> toJsonStr(); before, the resource-attribute path
used a plain Object.toString(). Array-valued labels therefore render as JSON:

# before (1.62.0)
process_command_args="[/usr/bin/java, -Xmx1g, -jar, pulsar.jar]"
# after  (1.65.0)
process_command_args="[\"/usr/bin/java\",\"-Xmx1g\",\"-jar\",\"pulsar.jar\"]"

On a default deployment the only such label is process_command_args, emitted by
ProcessResourceProvider (SPI-registered in opentelemetry-resources; its
emitCommandAttributes() gate returns true unless otel.instrumentation.common.v3-preview is
set, and Pulsar sets no otel.instrumentation.* property). Because OpenTelemetryService
sets setAllowedResourceAttributesFilter(s -> true), that label is copied onto every
exported series -- so Prometheus assigns new series identities to all Pulsar OpenTelemetry
metrics across the upgrade, and rate()/increase() see a one-time discontinuity at restart.
This makes per-metric labels consistent with target_info, which already used the JSON form.

This affects only deployments that enable the OpenTelemetry Prometheus exporter
(otel.sdk.disabled defaults to true), and hosts where the JVM cannot resolve process
arguments emit process_command_line (a plain string) instead and are unaffected.

There is nothing to fix in Pulsar -- changing what Pulsar emits would itself be a break -- so
this is documented rather than worked around. Whether Pulsar should keep copying
process.command_args onto every series at all is a separate question (upstream warns the
attribute may carry sensitive information, and it is a sizeable constant payload per series);
that would be a narrowing of setAllowedResourceAttributesFilter in its own PR.

Also undocumented upstream and worth knowing: setting
otel.instrumentation.common.v3-preview=true now silently drops
process.command_line/process.command_args unless
otel.instrumentation.resources.experimental.process-command-attributes.enabled=true is also
set. No default deployment is affected.

Metric compatibility

1.64.0/1.65.0 rewrote the Prometheus naming path: Otel2PrometheusConverter gained a
TranslationStrategy and replaced its use of the Prometheus client's PrometheusNaming
helpers with hand-rolled normalization, and reserved-suffix stripping moved from the
Prometheus client into OpenTelemetry (Prometheus client 1.6.0 moved suffix handling from
metric-creation time to scrape time). Since Pulsar exposes these metrics to users, the
emitted names are backward-compatibility surface.

To confirm nothing changed, every one of Pulsar's 126 OpenTelemetry metric names was rendered
through the Prometheus exporter on both 1.62.0 and 1.65.0 -- crossed with all 26 unit strings
Pulsar passes to setUnit() and all four instrument kinds, carrying all 61 Pulsar attribute
keys as labels plus resource attributes with setAllowedResourceAttributesFilter(s -> true)
as OpenTelemetryService does. After normalizing the telemetry_sdk_version label and the
process_command_args format change described above, the two 95,006-line exposition dumps are
byte-identical: metric names, label names, units, types and _total handling are unchanged,
and the array-attribute rendering is the only difference in the entire scrape. (The harness
uses its own instrumentation scope, so it deliberately does not cover the otel_scope_version
change on the jvm.* metrics noted above; that one was verified from the jars' embedded version
files and RuntimeTelemetryBuilder.getMeter().)

The default strategy is UNDERSCORE_ESCAPING_WITH_SUFFIXES, which is the pre-1.64.0 behavior,
and PrometheusMetricReaderProvider does not wire setTranslationStrategy to any autoconfigure
property, so it cannot be flipped by configuration either.

Two upstream changes touch code paths Pulsar uses but are inert here:

  • The 1.65.0 fix for PrometheusHttpServer.toBuilder() dropping the default handler.
    OpenTelemetryService calls toBuilder() in its addMetricReaderCustomizer, but
    setDefaultHandler is only ever called inside the exporter module's own tests --
    PrometheusMetricReaderProvider never sets one -- so nothing was being dropped.
  • The autoconfigure properties OpenTelemetryService supplies (otel.sdk.disabled,
    otel.java.metrics.cardinality.limit, otel.java.exporter.memory_mode,
    otel.exporter.prometheus.host) all still exist verbatim at v1.65.0. A renamed property
    would have failed silently, so these were checked against the tagged source rather than
    inferred from the changelog.

Verifying this change

  • Make sure that the change passes the CI checks.

This change is already covered by existing tests, such as OpenTelemetryServiceTest,
pulsar-broker's org.apache.pulsar.broker.stats.OpenTelemetry* suites,
PrometheusMetricsTest, the pulsar-client impl.metrics / impl.tracing tests, and the
CI - Integration - Metrics suite (OpenTelemetrySanityTest), which scrapes the
OpenTelemetry Prometheus endpoint of a real broker. All green locally alongside sanityCheck,
quickCheck and checkBinaryLicense, and green in full CI on the fork.

Does this pull request potentially affect one of the following parts:

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

Dependencies: the four OpenTelemetry version-catalog entries above, plus the transitive
io.prometheus:prometheus-metrics-* 1.5.1 -> 1.8.0 bump they pull in
(opentelemetry-exporter-prometheus declares 1.5.1 at 1.62.0-alpha and 1.8.0 at 1.65.0-alpha).
The module set is unchanged -- six Prometheus jars in, six out -- so the LICENSE delta is
version-only. This is deliberately not pinned in the version catalog: the platform is
consumed as an enforcedPlatform, so a catalog entry would override the exporter's own request
and could silently force a stale Prometheus client onto a newer exporter.

The metrics: no metric name, label name, unit or type changes. What changes is label
values -- otel_scope_version on the jvm.* runtime metrics, the process_command_args
format, and merging of colliding label names -- all described under "User-visible change" above.
Together they shift Prometheus series identity on deployments that enable the OpenTelemetry
Prometheus exporter.

Documentation

  • doc-required
  • doc-not-needed
  • doc
  • doc-complete

Matching PR in forked repository

PR in forked repository: lhotari#256 -- all 43 checks green
(one OneWayReplicatorTest.testMultipleVersionSchemas failure on the first run was an unrelated
pre-existing teardown race -- waitReplicatorStopped() waits for the replicator while the
delete-guard reads the topic's replication clusters -- and passed on rerun; that test file
contains no OpenTelemetry references).

…2.30.0, semconv to 1.43.0

- opentelemetry-java: 1.62.0 -> 1.65.0
- opentelemetry-java-instrumentation: 2.28.1 -> 2.30.0
- opentelemetry-semconv: 1.41.1 -> 1.43.0
- opentelemetry-gcp-resources (contrib): 1.57.0-alpha -> 1.59.0-alpha

Also updates the binary LICENSE files for the server and shell distributions,
including the transitive Prometheus client bump (1.5.1 -> 1.8.0) pulled in by
opentelemetry-exporter-prometheus.

Assisted-by: Claude Code (claude-opus-5)
@lhotari lhotari added type/enhancement The enhancements for the existing features or docs. e.g. reduce memory usage of the delayed messages area/build labels Aug 14, 2026
@lhotari
lhotari requested review from merlimat and nodece August 14, 2026 22:04
@nodece
nodece merged commit 3b176c3 into master Aug 16, 2026
43 checks passed
@lhotari lhotari added this to the 5.0.0-M2 milestone Sep 12, 2026
@lhotari
lhotari deleted the lh-upgrade-opentelemetry-1.65.0 branch October 1, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build type/enhancement The enhancements for the existing features or docs. e.g. reduce memory usage of the delayed messages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants