Skip to content

Prometheus map_unit incorrectly normalizes percent ratio units such as %/s #5875

Open

Description

@Quantum-Enigma-dot

Describe your environment

  • Repository: open-telemetry/opentelemetry-python
  • Branch: main
  • Tested commit: 3462000
  • Operating system: Windows 11
  • Python version: 3.10.9
  • uv version: 0.13.5
  • Package: opentelemetry-exporter-prometheus

My local main branch matched upstream/main, and the working tree was clean when I reproduced the issue.

What happened?

The Prometheus exporter's map_unit function does not correctly normalize compound ratio units when the numerator is the percent symbol.

The following standalone and compound units are normalized correctly:

  • map_unit("%") returns "percent"
  • map_unit("m/s") returns "meters_per_second"
  • map_unit("By/s") returns "bytes_per_second"

However:

  • map_unit("%/s") returns "__per_second"
  • map_unit("%/min") returns "__per_min"
  • map_unit("%/h") returns "__per_hour"

The percent unit is lost because the ratio-unit path sanitizes "%" into "_" before checking _UNIT_MAPPINGS, even though _UNIT_MAPPINGS contains the mapping "%": "percent".

In an end-to-end Prometheus export, a counter named "cpu.usage.rate" with unit "%/s" is exported as:

cpu_usage_rate___per_second_total

The generated name is syntactically valid for Prometheus, but it is semantically incorrect because the "percent" unit is lost during normalization.

Steps to Reproduce

From the repository root, run:

uv run python -c "from opentelemetry.exporter.prometheus._mapping import map_unit; print('percent:', map_unit('%')); print('meters per second:', map_unit('m/s')); print('percent per second:', map_unit('%/s'))"

The output is:

percent: percent
meters per second: meters_per_second
percent per second: __per_second

A minimal failing assertion is:

from opentelemetry.exporter.prometheus._mapping import map_unit

assert map_unit("%/s") == "percent_per_second"

The assertion currently fails because the actual value is "__per_second".

The same issue occurs with other percent ratio units:

assert map_unit("%/min") == "percent_per_min"
assert map_unit("%/h") == "percent_per_hour"

The current results are "__per_min" and "__per_hour", respectively.

Expected Result

Percent ratio units should preserve the existing percent mapping and normalize both parts of the compound unit.

Expected results:

  • map_unit("%/s") returns "percent_per_second"
  • map_unit("%/min") returns "percent_per_min"
  • map_unit("%/h") returns "percent_per_hour"

For a counter named "cpu.usage.rate" with unit "%/s", the normalized Prometheus metric name should contain the suffix "percent_per_second", for example:

cpu_usage_rate_percent_per_second_total

Actual Result

The percent numerator is replaced with an underscore before the unit mapping is applied.

Actual results:

  • map_unit("%/s") returns "__per_second"
  • map_unit("%/min") returns "__per_min"
  • map_unit("%/h") returns "__per_hour"

For a counter named "cpu.usage.rate" with unit "%/s", the end-to-end Prometheus output contains:

HELP cpu_usage_rate___per_second_total Test metric description

TYPE cpu_usage_rate___per_second_total counter

cpu_usage_rate___per_second_total{otel_scope_name="test_meter",otel_scope_schema_url="",otel_scope_version=""} 42.0

The metric name is syntactically valid, but the normalized unit is semantically incorrect because "percent" has been lost.

Additional context

The issue appears to be in:

exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/_mapping.py

_UNIT_MAPPINGS already contains:

"%": "percent"

The standalone "%" case works because the direct mapping lookup happens before sanitization.

In the ratio-unit path, the numerator is sanitized first. Sanitizing "%" produces "_", so the subsequent lookup no longer finds the "%" mapping. The final value becomes "__per_second".

I ran the full Prometheus exporter test suite:

uv run pytest exporter/opentelemetry-exporter-prometheus/tests/

Result:

41 passed

The current tests cover standalone "%" and ratio units such as "m/s", but they do not appear to cover "%" as the numerator of a ratio unit.

I searched open and closed issues and pull requests using terms including map_unit, %/s, percent_per_second, __per_second, _UNIT_MAPPINGS, and ratio unit. I could not find existing work covering this behavior.

Issue #5766 and PR #5798 relate to annotation stripping in compound units, but they do not appear to address percent-symbol normalization in the ratio-unit path.

I would like to add regression coverage and work on a focused fix if the expected normalization is confirmed.

Would you like to implement a fix?

Yes

Tip

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it. Learn more here.

Activity

  1. Quantum-Enigma-dot commented on Nov 1, 2026

    @Quantum-Enigma-dot
    Author

    I reproduced this behavior on the latest upstream main commit and would like to work on the fix. If percent_per_second is the expected normalization, could this issue please be assigned to me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions