6363"""
6464
6565from collections import deque
66+ from enum import Enum
6667from itertools import chain
6768from json import dumps
6869from logging import getLogger
7677 GaugeMetricFamily ,
7778 HistogramMetricFamily ,
7879 InfoMetricFamily ,
80+ UnknownMetricFamily ,
7981)
8082from prometheus_client .core import Metric as PrometheusMetric
8183
116118_TARGET_INFO_DESCRIPTION = "Target metadata"
117119
118120
121+ class TranslationStrategy (Enum ):
122+ """Controls how OpenTelemetry metric names are translated to Prometheus conventions."""
123+
124+ UNDERSCORE_ESCAPING_WITH_SUFFIXES = "underscore_escaping_with_suffixes"
125+ UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES = (
126+ "underscore_escaping_without_suffixes"
127+ )
128+ NO_UTF8_ESCAPING_WITH_SUFFIXES = "no_utf8_escaping_with_suffixes"
129+ NO_TRANSLATION = "no_translation"
130+
131+
119132def _convert_buckets (
120133 bucket_counts : Sequence [int ], explicit_bounds : Sequence [float ]
121134) -> Sequence [Tuple [str , int ]]:
@@ -135,7 +148,10 @@ class PrometheusMetricReader(MetricReader):
135148 """Prometheus metric exporter for OpenTelemetry."""
136149
137150 def __init__ (
138- self , disable_target_info : bool = False , prefix : str = ""
151+ self ,
152+ disable_target_info : bool = False ,
153+ prefix : str = "" ,
154+ translation_strategy : TranslationStrategy = TranslationStrategy .UNDERSCORE_ESCAPING_WITH_SUFFIXES ,
139155 ) -> None :
140156 super ().__init__ (
141157 preferred_temporality = {
@@ -149,7 +165,9 @@ def __init__(
149165 otel_component_type = OtelComponentTypeValues .PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER ,
150166 )
151167 self ._collector = _CustomCollector (
152- disable_target_info = disable_target_info , prefix = prefix
168+ disable_target_info = disable_target_info ,
169+ prefix = prefix ,
170+ translation_strategy = translation_strategy ,
153171 )
154172 REGISTRY .register (self ._collector )
155173 self ._collector ._callback = self .collect
@@ -176,12 +194,18 @@ class _CustomCollector:
176194 https://github.com/prometheus/client_python#custom-collectors
177195 """
178196
179- def __init__ (self , disable_target_info : bool = False , prefix : str = "" ):
197+ def __init__ (
198+ self ,
199+ disable_target_info : bool = False ,
200+ prefix : str = "" ,
201+ translation_strategy : TranslationStrategy = TranslationStrategy .UNDERSCORE_ESCAPING_WITH_SUFFIXES ,
202+ ):
180203 self ._callback = None
181204 self ._metrics_datas : Deque [MetricsData ] = deque ()
182205 self ._disable_target_info = disable_target_info
183206 self ._target_info = None
184207 self ._prefix = prefix
208+ self ._translation_strategy = translation_strategy
185209
186210 def add_metrics_data (self , metrics_data : MetricsData ) -> None :
187211 """Add metrics to Prometheus data"""
@@ -220,7 +244,7 @@ def collect(self) -> Iterable[PrometheusMetric]:
220244 if metric_family_id_metric_family :
221245 yield from metric_family_id_metric_family .values ()
222246
223- # pylint: disable=too-many-locals,too-many-branches
247+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
224248 def _translate_to_prometheus (
225249 self ,
226250 metrics_data : MetricsData ,
@@ -233,16 +257,30 @@ def _translate_to_prometheus(
233257 for metric in scope_metrics .metrics :
234258 metrics .append (metric )
235259
260+ _add_suffixes = self ._translation_strategy in (
261+ TranslationStrategy .UNDERSCORE_ESCAPING_WITH_SUFFIXES ,
262+ TranslationStrategy .NO_UTF8_ESCAPING_WITH_SUFFIXES ,
263+ )
264+ _escape_names = self ._translation_strategy in (
265+ TranslationStrategy .UNDERSCORE_ESCAPING_WITH_SUFFIXES ,
266+ TranslationStrategy .UNDERSCORE_ESCAPING_WITHOUT_SUFFIXES ,
267+ )
268+
236269 for metric in metrics :
237270 label_values_data_points = []
238271 values = []
239272
240273 metric_name = metric .name
241- if self ._prefix :
274+ if (
275+ self ._translation_strategy
276+ != TranslationStrategy .NO_TRANSLATION
277+ and self ._prefix
278+ ):
242279 metric_name = self ._prefix + "_" + metric_name
243- metric_name = sanitize_full_name (metric_name )
280+ if _escape_names :
281+ metric_name = sanitize_full_name (metric_name )
244282 metric_description = metric .description or ""
245- metric_unit = map_unit (metric .unit )
283+ metric_unit = map_unit (metric .unit ) if _add_suffixes else ""
246284
247285 # First pass: collect all unique label keys across all data points
248286 all_label_keys_set = set ()
@@ -306,17 +344,25 @@ def _translate_to_prometheus(
306344 isinstance (metric .data , Sum )
307345 and not should_convert_sum_to_gauge
308346 ):
347+ family_kwargs = {}
348+ if _add_suffixes :
349+ family_class = CounterMetricFamily
350+ family_kwargs ["unit" ] = metric_unit
351+ else :
352+ # The CounterMetricFamily always adds the "_total" suffix to
353+ # metric names. To avoid adding this suffix for Sums, we must
354+ # use the untyped (unknown) metric family.
355+ family_class = UnknownMetricFamily
309356 metric_family_id = "|" .join (
310- [per_metric_family_id , CounterMetricFamily .__name__ ]
357+ [per_metric_family_id , family_class .__name__ ]
311358 )
312-
313359 if metric_family_id not in metric_family_id_metric_family :
314360 metric_family_id_metric_family [metric_family_id ] = (
315- CounterMetricFamily (
361+ family_class (
316362 name = metric_name ,
317363 documentation = metric_description ,
318364 labels = all_label_keys ,
319- unit = metric_unit ,
365+ ** family_kwargs ,
320366 )
321367 )
322368 for label_values , value in zip (
0 commit comments