2323from .validation import _is_valid_legacy_metric_name
2424
2525__all__ = (
26- 'CONTENT_TYPE_LATEST ' ,
26+ 'CONTENT_TYPE_PLAIN ' ,
2727 'delete_from_gateway' ,
2828 'generate_latest' ,
2929 'instance_ip_grouping_key' ,
3737 'write_to_textfile' ,
3838)
3939
40- CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
40+ CONTENT_TYPE_PLAIN = 'text/plain; version=0.0.4; charset=utf-8'
4141"""Content type of the latest text format"""
4242
4343
@@ -245,29 +245,38 @@ class TmpServer(ThreadingWSGIServer):
245245start_http_server = start_wsgi_server
246246
247247
248- def generate_latest (registry : CollectorRegistry = REGISTRY ) -> bytes :
249- """Returns the metrics from the registry in latest text format as a string."""
248+ def generate_latest (registry : CollectorRegistry = REGISTRY , escaping : str = openmetrics .UNDERSCORES ) -> bytes :
249+ """
250+ Generates the exposition format using the basic Prometheus text format.
251+
252+ Params:
253+ registry: CollectorRegistry to export data from.
254+ escaping: Escaping scheme used for metric and label names.
255+
256+ Returns: UTF-8 encoded string containing the metrics in text format.
257+ """
250258
251259 def sample_line (samples ):
252260 if samples .labels :
253261 labelstr = '{0}' .format (',' .join (
262+ # Label values always support UTF-8
254263 ['{}="{}"' .format (
255- openmetrics .escape_label_name (k ), openmetrics ._escape (v ))
264+ openmetrics .escape_label_name (k , escaping ), openmetrics ._escape (v , openmetrics . ALLOWUTF8 , False ))
256265 for k , v in sorted (samples .labels .items ())]))
257266 else :
258267 labelstr = ''
259268 timestamp = ''
260269 if samples .timestamp is not None :
261270 # Convert to milliseconds.
262271 timestamp = f' { int (float (samples .timestamp ) * 1000 ):d} '
263- if _is_valid_legacy_metric_name (samples .name ):
272+ if escaping != openmetrics . ALLOWUTF8 or _is_valid_legacy_metric_name (samples .name ):
264273 if labelstr :
265274 labelstr = '{{{0}}}' .format (labelstr )
266- return f'{ samples .name } { labelstr } { floatToGoString (samples .value )} { timestamp } \n '
275+ return f'{ openmetrics . escape_metric_name ( samples .name , escaping ) } { labelstr } { floatToGoString (samples .value )} { timestamp } \n '
267276 maybe_comma = ''
268277 if labelstr :
269278 maybe_comma = ','
270- return f'{{{ openmetrics .escape_metric_name (samples .name )} { maybe_comma } { labelstr } }} { floatToGoString (samples .value )} { timestamp } \n '
279+ return f'{{{ openmetrics .escape_metric_name (samples .name , escaping )} { maybe_comma } { labelstr } }} { floatToGoString (samples .value )} { timestamp } \n '
271280
272281 output = []
273282 for metric in registry .collect ():
@@ -290,8 +299,8 @@ def sample_line(samples):
290299 mtype = 'untyped'
291300
292301 output .append ('# HELP {} {}\n ' .format (
293- openmetrics .escape_metric_name (mname ), metric .documentation .replace ('\\ ' , r'\\' ).replace ('\n ' , r'\n' )))
294- output .append (f'# TYPE { openmetrics .escape_metric_name (mname )} { mtype } \n ' )
302+ openmetrics .escape_metric_name (mname , escaping ), metric .documentation .replace ('\\ ' , r'\\' ).replace ('\n ' , r'\n' )))
303+ output .append (f'# TYPE { openmetrics .escape_metric_name (mname , escaping )} { mtype } \n ' )
295304
296305 om_samples : Dict [str , List [str ]] = {}
297306 for s in metric .samples :
@@ -307,21 +316,69 @@ def sample_line(samples):
307316 raise
308317
309318 for suffix , lines in sorted (om_samples .items ()):
310- output .append ('# HELP {} {}\n ' .format (openmetrics .escape_metric_name (metric .name + suffix ),
319+ output .append ('# HELP {} {}\n ' .format (openmetrics .escape_metric_name (metric .name + suffix , escaping ),
311320 metric .documentation .replace ('\\ ' , r'\\' ).replace ('\n ' , r'\n' )))
312- output .append (f'# TYPE { openmetrics .escape_metric_name (metric .name + suffix )} gauge\n ' )
321+ output .append (f'# TYPE { openmetrics .escape_metric_name (metric .name + suffix , escaping )} gauge\n ' )
313322 output .extend (lines )
314323 return '' .join (output ).encode ('utf-8' )
315324
316325
317326def choose_encoder (accept_header : str ) -> Tuple [Callable [[CollectorRegistry ], bytes ], str ]:
327+ # Python client library accepts a much narrower range of content-types than
328+ # Prometheus does -- UTF-8 is only supported on OpenMetrics v1.0.0.
318329 accept_header = accept_header or ''
330+ escaping = openmetrics .UNDERSCORES
319331 for accepted in accept_header .split (',' ):
320332 if accepted .split (';' )[0 ].strip () == 'application/openmetrics-text' :
321- return (openmetrics .generate_latest ,
322- openmetrics .CONTENT_TYPE_LATEST )
323- return generate_latest , CONTENT_TYPE_LATEST
324-
333+ toks = accepted .split (';' )
334+ version = _get_version (toks )
335+ escaping = _get_escaping (toks )
336+ # Only return an escaping header if we have a good version and
337+ # mimetype.
338+ if version == '1.0.0' :
339+ return (openmetrics .generate_latest_fn (escaping ),
340+ openmetrics .CONTENT_TYPE_LATEST + '; escaping=' + str (escaping ))
341+ return generate_latest , CONTENT_TYPE_PLAIN
342+
343+
344+ def _get_version (accept_header : List [str ]) -> str :
345+ """Return the version tag from the Accept header.
346+
347+ If no escaping scheme is specified, returns empty string."""
348+
349+ for tok in accept_header :
350+ if '=' not in tok :
351+ continue
352+ key , value = tok .strip ().split ('=' , 1 )
353+ if key == 'version' :
354+ return value
355+ return ""
356+
357+
358+ def _get_escaping (accept_header : List [str ]) -> str :
359+ """Return the escaping scheme from the Accept header.
360+
361+ If no escaping scheme is specified or the scheme is not one of the allowed
362+ strings, defaults to UNDERSCORES."""
363+
364+ for tok in accept_header :
365+ if '=' not in tok :
366+ continue
367+ key , value = tok .strip ().split ('=' , 1 )
368+ if key != 'escaping' :
369+ continue
370+ if value == openmetrics .ALLOWUTF8 :
371+ return openmetrics .ALLOWUTF8
372+ elif value == openmetrics .UNDERSCORES :
373+ return openmetrics .UNDERSCORES
374+ elif value == openmetrics .DOTS :
375+ return openmetrics .DOTS
376+ elif value == openmetrics .VALUES :
377+ return openmetrics .VALUES
378+ else :
379+ return openmetrics .UNDERSCORES
380+ return openmetrics .UNDERSCORES
381+
325382
326383def gzip_accepted (accept_encoding_header : str ) -> bool :
327384 accept_encoding_header = accept_encoding_header or ''
@@ -369,15 +426,15 @@ def factory(cls, registry: CollectorRegistry) -> type:
369426 return MyMetricsHandler
370427
371428
372- def write_to_textfile (path : str , registry : CollectorRegistry ) -> None :
429+ def write_to_textfile (path : str , registry : CollectorRegistry , escaping : str = openmetrics . ALLOWUTF8 ) -> None :
373430 """Write metrics to the given path.
374431
375432 This is intended for use with the Node exporter textfile collector.
376433 The path must end in .prom for the textfile collector to process it."""
377434 tmppath = f'{ path } .{ os .getpid ()} .{ threading .current_thread ().ident } '
378435 try :
379436 with open (tmppath , 'wb' ) as f :
380- f .write (generate_latest (registry ))
437+ f .write (generate_latest (registry , escaping ))
381438
382439 # rename(2) is atomic but fails on Windows if the destination file exists
383440 if os .name == 'nt' :
@@ -645,7 +702,7 @@ def _use_gateway(
645702
646703 handler (
647704 url = url , method = method , timeout = timeout ,
648- headers = [('Content-Type' , CONTENT_TYPE_LATEST )], data = data ,
705+ headers = [('Content-Type' , CONTENT_TYPE_PLAIN )], data = data ,
649706 )()
650707
651708
0 commit comments