23
23
from .validation import _is_valid_legacy_metric_name
24
24
25
25
__all__ = (
26
- 'CONTENT_TYPE_LATEST ' ,
26
+ 'CONTENT_TYPE_PLAIN ' ,
27
27
'delete_from_gateway' ,
28
28
'generate_latest' ,
29
29
'instance_ip_grouping_key' ,
37
37
'write_to_textfile' ,
38
38
)
39
39
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'
41
41
"""Content type of the latest text format"""
42
42
43
43
@@ -245,29 +245,38 @@ class TmpServer(ThreadingWSGIServer):
245
245
start_http_server = start_wsgi_server
246
246
247
247
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
+ """
250
258
251
259
def sample_line (samples ):
252
260
if samples .labels :
253
261
labelstr = '{0}' .format (',' .join (
262
+ # Label values always support UTF-8
254
263
['{}="{}"' .format (
255
- openmetrics .escape_label_name (k ), openmetrics ._escape (v ))
264
+ openmetrics .escape_label_name (k , escaping ), openmetrics ._escape (v , openmetrics . ALLOWUTF8 , False ))
256
265
for k , v in sorted (samples .labels .items ())]))
257
266
else :
258
267
labelstr = ''
259
268
timestamp = ''
260
269
if samples .timestamp is not None :
261
270
# Convert to milliseconds.
262
271
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 ):
264
273
if labelstr :
265
274
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 '
267
276
maybe_comma = ''
268
277
if labelstr :
269
278
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 '
271
280
272
281
output = []
273
282
for metric in registry .collect ():
@@ -290,8 +299,8 @@ def sample_line(samples):
290
299
mtype = 'untyped'
291
300
292
301
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 ' )
295
304
296
305
om_samples : Dict [str , List [str ]] = {}
297
306
for s in metric .samples :
@@ -307,21 +316,69 @@ def sample_line(samples):
307
316
raise
308
317
309
318
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 ),
311
320
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 ' )
313
322
output .extend (lines )
314
323
return '' .join (output ).encode ('utf-8' )
315
324
316
325
317
326
def 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.
318
329
accept_header = accept_header or ''
330
+ escaping = openmetrics .UNDERSCORES
319
331
for accepted in accept_header .split (',' ):
320
332
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
+
325
382
326
383
def gzip_accepted (accept_encoding_header : str ) -> bool :
327
384
accept_encoding_header = accept_encoding_header or ''
@@ -369,15 +426,15 @@ def factory(cls, registry: CollectorRegistry) -> type:
369
426
return MyMetricsHandler
370
427
371
428
372
- def write_to_textfile (path : str , registry : CollectorRegistry ) -> None :
429
+ def write_to_textfile (path : str , registry : CollectorRegistry , escaping : str = openmetrics . ALLOWUTF8 ) -> None :
373
430
"""Write metrics to the given path.
374
431
375
432
This is intended for use with the Node exporter textfile collector.
376
433
The path must end in .prom for the textfile collector to process it."""
377
434
tmppath = f'{ path } .{ os .getpid ()} .{ threading .current_thread ().ident } '
378
435
try :
379
436
with open (tmppath , 'wb' ) as f :
380
- f .write (generate_latest (registry ))
437
+ f .write (generate_latest (registry , escaping ))
381
438
382
439
# rename(2) is atomic but fails on Windows if the destination file exists
383
440
if os .name == 'nt' :
@@ -645,7 +702,7 @@ def _use_gateway(
645
702
646
703
handler (
647
704
url = url , method = method , timeout = timeout ,
648
- headers = [('Content-Type' , CONTENT_TYPE_LATEST )], data = data ,
705
+ headers = [('Content-Type' , CONTENT_TYPE_PLAIN )], data = data ,
649
706
)()
650
707
651
708
0 commit comments