Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prometheus_client/exposition.py: 21%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

267 statements  

1import base64 

2from contextlib import closing 

3import gzip 

4from http.server import BaseHTTPRequestHandler 

5import os 

6import socket 

7from socketserver import ThreadingMixIn 

8import ssl 

9import sys 

10import threading 

11from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union 

12from urllib.error import HTTPError 

13from urllib.parse import parse_qs, quote_plus, urlparse 

14from urllib.request import ( 

15 BaseHandler, build_opener, HTTPHandler, HTTPRedirectHandler, HTTPSHandler, 

16 Request, 

17) 

18from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer 

19 

20from .openmetrics import exposition as openmetrics 

21from .registry import CollectorRegistry, REGISTRY 

22from .utils import floatToGoString 

23from .validation import _is_valid_legacy_metric_name 

24 

25__all__ = ( 

26 'CONTENT_TYPE_LATEST', 

27 'delete_from_gateway', 

28 'generate_latest', 

29 'instance_ip_grouping_key', 

30 'make_asgi_app', 

31 'make_wsgi_app', 

32 'MetricsHandler', 

33 'push_to_gateway', 

34 'pushadd_to_gateway', 

35 'start_http_server', 

36 'start_wsgi_server', 

37 'write_to_textfile', 

38) 

39 

40CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8' 

41"""Content type of the latest text format""" 

42 

43 

44class _PrometheusRedirectHandler(HTTPRedirectHandler): 

45 """ 

46 Allow additional methods (e.g. PUT) and data forwarding in redirects. 

47 

48 Use of this class constitute a user's explicit agreement to the 

49 redirect responses the Prometheus client will receive when using it. 

50 You should only use this class if you control or otherwise trust the 

51 redirect behavior involved and are certain it is safe to full transfer 

52 the original request (method and data) to the redirected URL. For 

53 example, if you know there is a cosmetic URL redirect in front of a 

54 local deployment of a Prometheus server, and all redirects are safe, 

55 this is the class to use to handle redirects in that case. 

56 

57 The standard HTTPRedirectHandler does not forward request data nor 

58 does it allow redirected PUT requests (which Prometheus uses for some 

59 operations, for example `push_to_gateway`) because these cannot 

60 generically guarantee no violations of HTTP RFC 2616 requirements for 

61 the user to explicitly confirm redirects that could have unexpected 

62 side effects (such as rendering a PUT request non-idempotent or 

63 creating multiple resources not named in the original request). 

64 """ 

65 

66 def redirect_request(self, req, fp, code, msg, headers, newurl): 

67 """ 

68 Apply redirect logic to a request. 

69 

70 See parent HTTPRedirectHandler.redirect_request for parameter info. 

71 

72 If the redirect is disallowed, this raises the corresponding HTTP error. 

73 If the redirect can't be determined, return None to allow other handlers 

74 to try. If the redirect is allowed, return the new request. 

75 

76 This method specialized for the case when (a) the user knows that the 

77 redirect will not cause unacceptable side effects for any request method, 

78 and (b) the user knows that any request data should be passed through to 

79 the redirect. If either condition is not met, this should not be used. 

80 """ 

81 # note that requests being provided by a handler will use get_method to 

82 # indicate the method, by monkeypatching this, instead of setting the 

83 # Request object's method attribute. 

84 m = getattr(req, "method", req.get_method()) 

85 if not (code in (301, 302, 303, 307) and m in ("GET", "HEAD") 

86 or code in (301, 302, 303) and m in ("POST", "PUT")): 

87 raise HTTPError(req.full_url, code, msg, headers, fp) 

88 new_request = Request( 

89 newurl.replace(' ', '%20'), # space escaping in new url if needed. 

90 headers=req.headers, 

91 origin_req_host=req.origin_req_host, 

92 unverifiable=True, 

93 data=req.data, 

94 ) 

95 new_request.method = m 

96 return new_request 

97 

98 

99def _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression): 

100 """Bake output for metrics output.""" 

101 # Choose the correct plain text format of the output. 

102 encoder, content_type = choose_encoder(accept_header) 

103 if 'name[]' in params: 

104 registry = registry.restricted_registry(params['name[]']) 

105 output = encoder(registry) 

106 headers = [('Content-Type', content_type)] 

107 # If gzip encoding required, gzip the output. 

108 if not disable_compression and gzip_accepted(accept_encoding_header): 

109 output = gzip.compress(output) 

110 headers.append(('Content-Encoding', 'gzip')) 

111 return '200 OK', headers, output 

112 

113 

114def make_wsgi_app(registry: CollectorRegistry = REGISTRY, disable_compression: bool = False) -> Callable: 

115 """Create a WSGI app which serves the metrics from a registry.""" 

116 

117 def prometheus_app(environ, start_response): 

118 # Prepare parameters 

119 accept_header = environ.get('HTTP_ACCEPT') 

120 accept_encoding_header = environ.get('HTTP_ACCEPT_ENCODING') 

121 params = parse_qs(environ.get('QUERY_STRING', '')) 

122 method = environ['REQUEST_METHOD'] 

123 

124 if method == 'OPTIONS': 

125 status = '200 OK' 

126 headers = [('Allow', 'OPTIONS,GET')] 

127 output = b'' 

128 elif method != 'GET': 

129 status = '405 Method Not Allowed' 

130 headers = [('Allow', 'OPTIONS,GET')] 

131 output = '# HTTP {}: {}; use OPTIONS or GET\n'.format(status, method).encode() 

132 elif environ['PATH_INFO'] == '/favicon.ico': 

133 # Serve empty response for browsers 

134 status = '200 OK' 

135 headers = [] 

136 output = b'' 

137 else: 

138 # Note: For backwards compatibility, the URI path for GET is not 

139 # constrained to the documented /metrics, but any path is allowed. 

140 # Bake output 

141 status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression) 

142 # Return output 

143 start_response(status, headers) 

144 return [output] 

145 

146 return prometheus_app 

147 

148 

149class _SilentHandler(WSGIRequestHandler): 

150 """WSGI handler that does not log requests.""" 

151 

152 def log_message(self, format, *args): 

153 """Log nothing.""" 

154 

155 

156class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): 

157 """Thread per request HTTP server.""" 

158 # Make worker threads "fire and forget". Beginning with Python 3.7 this 

159 # prevents a memory leak because ``ThreadingMixIn`` starts to gather all 

160 # non-daemon threads in a list in order to join on them at server close. 

161 daemon_threads = True 

162 

163 

164def _get_best_family(address, port): 

165 """Automatically select address family depending on address""" 

166 # HTTPServer defaults to AF_INET, which will not start properly if 

167 # binding an ipv6 address is requested. 

168 # This function is based on what upstream python did for http.server 

169 # in https://github.com/python/cpython/pull/11767 

170 infos = socket.getaddrinfo(address, port, type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE) 

171 family, _, _, _, sockaddr = next(iter(infos)) 

172 return family, sockaddr[0] 

173 

174 

175def _get_ssl_ctx( 

176 certfile: str, 

177 keyfile: str, 

178 protocol: int, 

179 cafile: Optional[str] = None, 

180 capath: Optional[str] = None, 

181 client_auth_required: bool = False, 

182) -> ssl.SSLContext: 

183 """Load context supports SSL.""" 

184 ssl_cxt = ssl.SSLContext(protocol=protocol) 

185 

186 if cafile is not None or capath is not None: 

187 try: 

188 ssl_cxt.load_verify_locations(cafile, capath) 

189 except IOError as exc: 

190 exc_type = type(exc) 

191 msg = str(exc) 

192 raise exc_type(f"Cannot load CA certificate chain from file " 

193 f"{cafile!r} or directory {capath!r}: {msg}") 

194 else: 

195 try: 

196 ssl_cxt.load_default_certs(purpose=ssl.Purpose.CLIENT_AUTH) 

197 except IOError as exc: 

198 exc_type = type(exc) 

199 msg = str(exc) 

200 raise exc_type(f"Cannot load default CA certificate chain: {msg}") 

201 

202 if client_auth_required: 

203 ssl_cxt.verify_mode = ssl.CERT_REQUIRED 

204 

205 try: 

206 ssl_cxt.load_cert_chain(certfile=certfile, keyfile=keyfile) 

207 except IOError as exc: 

208 exc_type = type(exc) 

209 msg = str(exc) 

210 raise exc_type(f"Cannot load server certificate file {certfile!r} or " 

211 f"its private key file {keyfile!r}: {msg}") 

212 

213 return ssl_cxt 

214 

215 

216def start_wsgi_server( 

217 port: int, 

218 addr: str = '0.0.0.0', 

219 registry: CollectorRegistry = REGISTRY, 

220 certfile: Optional[str] = None, 

221 keyfile: Optional[str] = None, 

222 client_cafile: Optional[str] = None, 

223 client_capath: Optional[str] = None, 

224 protocol: int = ssl.PROTOCOL_TLS_SERVER, 

225 client_auth_required: bool = False, 

226) -> Tuple[WSGIServer, threading.Thread]: 

227 """Starts a WSGI server for prometheus metrics as a daemon thread.""" 

228 

229 class TmpServer(ThreadingWSGIServer): 

230 """Copy of ThreadingWSGIServer to update address_family locally""" 

231 

232 TmpServer.address_family, addr = _get_best_family(addr, port) 

233 app = make_wsgi_app(registry) 

234 httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler) 

235 if certfile and keyfile: 

236 context = _get_ssl_ctx(certfile, keyfile, protocol, client_cafile, client_capath, client_auth_required) 

237 httpd.socket = context.wrap_socket(httpd.socket, server_side=True) 

238 t = threading.Thread(target=httpd.serve_forever) 

239 t.daemon = True 

240 t.start() 

241 

242 return httpd, t 

243 

244 

245start_http_server = start_wsgi_server 

246 

247 

248def generate_latest(registry: CollectorRegistry = REGISTRY) -> bytes: 

249 """Returns the metrics from the registry in latest text format as a string.""" 

250 

251 def sample_line(samples): 

252 if samples.labels: 

253 labelstr = '{0}'.format(','.join( 

254 ['{}="{}"'.format( 

255 openmetrics.escape_label_name(k), openmetrics._escape(v)) 

256 for k, v in sorted(samples.labels.items())])) 

257 else: 

258 labelstr = '' 

259 timestamp = '' 

260 if samples.timestamp is not None: 

261 # Convert to milliseconds. 

262 timestamp = f' {int(float(samples.timestamp) * 1000):d}' 

263 if _is_valid_legacy_metric_name(samples.name): 

264 if labelstr: 

265 labelstr = '{{{0}}}'.format(labelstr) 

266 return f'{samples.name}{labelstr} {floatToGoString(samples.value)}{timestamp}\n' 

267 maybe_comma = '' 

268 if labelstr: 

269 maybe_comma = ',' 

270 return f'{{{openmetrics.escape_metric_name(samples.name)}{maybe_comma}{labelstr}}} {floatToGoString(samples.value)}{timestamp}\n' 

271 

272 output = [] 

273 for metric in registry.collect(): 

274 try: 

275 mname = metric.name 

276 mtype = metric.type 

277 # Munging from OpenMetrics into Prometheus format. 

278 if mtype == 'counter': 

279 mname = mname + '_total' 

280 elif mtype == 'info': 

281 mname = mname + '_info' 

282 mtype = 'gauge' 

283 elif mtype == 'stateset': 

284 mtype = 'gauge' 

285 elif mtype == 'gaugehistogram': 

286 # A gauge histogram is really a gauge, 

287 # but this captures the structure better. 

288 mtype = 'histogram' 

289 elif mtype == 'unknown': 

290 mtype = 'untyped' 

291 

292 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') 

295 

296 om_samples: Dict[str, List[str]] = {} 

297 for s in metric.samples: 

298 for suffix in ['_created', '_gsum', '_gcount']: 

299 if s.name == metric.name + suffix: 

300 # OpenMetrics specific sample, put in a gauge at the end. 

301 om_samples.setdefault(suffix, []).append(sample_line(s)) 

302 break 

303 else: 

304 output.append(sample_line(s)) 

305 except Exception as exception: 

306 exception.args = (exception.args or ('',)) + (metric,) 

307 raise 

308 

309 for suffix, lines in sorted(om_samples.items()): 

310 output.append('# HELP {} {}\n'.format(openmetrics.escape_metric_name(metric.name + suffix), 

311 metric.documentation.replace('\\', r'\\').replace('\n', r'\n'))) 

312 output.append(f'# TYPE {openmetrics.escape_metric_name(metric.name + suffix)} gauge\n') 

313 output.extend(lines) 

314 return ''.join(output).encode('utf-8') 

315 

316 

317def choose_encoder(accept_header: str) -> Tuple[Callable[[CollectorRegistry], bytes], str]: 

318 accept_header = accept_header or '' 

319 for accepted in accept_header.split(','): 

320 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 

325 

326def gzip_accepted(accept_encoding_header: str) -> bool: 

327 accept_encoding_header = accept_encoding_header or '' 

328 for accepted in accept_encoding_header.split(','): 

329 if accepted.split(';')[0].strip().lower() == 'gzip': 

330 return True 

331 return False 

332 

333 

334class MetricsHandler(BaseHTTPRequestHandler): 

335 """HTTP handler that gives metrics from ``REGISTRY``.""" 

336 registry: CollectorRegistry = REGISTRY 

337 

338 def do_GET(self) -> None: 

339 # Prepare parameters 

340 registry = self.registry 

341 accept_header = self.headers.get('Accept') 

342 accept_encoding_header = self.headers.get('Accept-Encoding') 

343 params = parse_qs(urlparse(self.path).query) 

344 # Bake output 

345 status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, False) 

346 # Return output 

347 self.send_response(int(status.split(' ')[0])) 

348 for header in headers: 

349 self.send_header(*header) 

350 self.end_headers() 

351 self.wfile.write(output) 

352 

353 def log_message(self, format: str, *args: Any) -> None: 

354 """Log nothing.""" 

355 

356 @classmethod 

357 def factory(cls, registry: CollectorRegistry) -> type: 

358 """Returns a dynamic MetricsHandler class tied 

359 to the passed registry. 

360 """ 

361 # This implementation relies on MetricsHandler.registry 

362 # (defined above and defaulted to REGISTRY). 

363 

364 # As we have unicode_literals, we need to create a str() 

365 # object for type(). 

366 cls_name = str(cls.__name__) 

367 MyMetricsHandler = type(cls_name, (cls, object), 

368 {"registry": registry}) 

369 return MyMetricsHandler 

370 

371 

372def write_to_textfile(path: str, registry: CollectorRegistry) -> None: 

373 """Write metrics to the given path. 

374 

375 This is intended for use with the Node exporter textfile collector. 

376 The path must end in .prom for the textfile collector to process it.""" 

377 tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}' 

378 try: 

379 with open(tmppath, 'wb') as f: 

380 f.write(generate_latest(registry)) 

381 

382 # rename(2) is atomic but fails on Windows if the destination file exists 

383 if os.name == 'nt': 

384 os.replace(tmppath, path) 

385 else: 

386 os.rename(tmppath, path) 

387 except Exception: 

388 if os.path.exists(tmppath): 

389 os.remove(tmppath) 

390 raise 

391 

392 

393def _make_handler( 

394 url: str, 

395 method: str, 

396 timeout: Optional[float], 

397 headers: Sequence[Tuple[str, str]], 

398 data: bytes, 

399 base_handler: Union[BaseHandler, type], 

400) -> Callable[[], None]: 

401 def handle() -> None: 

402 request = Request(url, data=data) 

403 request.get_method = lambda: method # type: ignore 

404 for k, v in headers: 

405 request.add_header(k, v) 

406 resp = build_opener(base_handler).open(request, timeout=timeout) 

407 if resp.code >= 400: 

408 raise OSError(f"error talking to pushgateway: {resp.code} {resp.msg}") 

409 

410 return handle 

411 

412 

413def default_handler( 

414 url: str, 

415 method: str, 

416 timeout: Optional[float], 

417 headers: List[Tuple[str, str]], 

418 data: bytes, 

419) -> Callable[[], None]: 

420 """Default handler that implements HTTP/HTTPS connections. 

421 

422 Used by the push_to_gateway functions. Can be re-used by other handlers.""" 

423 

424 return _make_handler(url, method, timeout, headers, data, HTTPHandler) 

425 

426 

427def passthrough_redirect_handler( 

428 url: str, 

429 method: str, 

430 timeout: Optional[float], 

431 headers: List[Tuple[str, str]], 

432 data: bytes, 

433) -> Callable[[], None]: 

434 """ 

435 Handler that automatically trusts redirect responses for all HTTP methods. 

436 

437 Augments standard HTTPRedirectHandler capability by permitting PUT requests, 

438 preserving the method upon redirect, and passing through all headers and 

439 data from the original request. Only use this handler if you control or 

440 trust the source of redirect responses you encounter when making requests 

441 via the Prometheus client. This handler will simply repeat the identical 

442 request, including same method and data, to the new redirect URL.""" 

443 

444 return _make_handler(url, method, timeout, headers, data, _PrometheusRedirectHandler) 

445 

446 

447def basic_auth_handler( 

448 url: str, 

449 method: str, 

450 timeout: Optional[float], 

451 headers: List[Tuple[str, str]], 

452 data: bytes, 

453 username: Optional[str] = None, 

454 password: Optional[str] = None, 

455) -> Callable[[], None]: 

456 """Handler that implements HTTP/HTTPS connections with Basic Auth. 

457 

458 Sets auth headers using supplied 'username' and 'password', if set. 

459 Used by the push_to_gateway functions. Can be re-used by other handlers.""" 

460 

461 def handle(): 

462 """Handler that implements HTTP Basic Auth. 

463 """ 

464 if username is not None and password is not None: 

465 auth_value = f'{username}:{password}'.encode() 

466 auth_token = base64.b64encode(auth_value) 

467 auth_header = b'Basic ' + auth_token 

468 headers.append(('Authorization', auth_header)) 

469 default_handler(url, method, timeout, headers, data)() 

470 

471 return handle 

472 

473 

474def tls_auth_handler( 

475 url: str, 

476 method: str, 

477 timeout: Optional[float], 

478 headers: List[Tuple[str, str]], 

479 data: bytes, 

480 certfile: str, 

481 keyfile: str, 

482 cafile: Optional[str] = None, 

483 protocol: int = ssl.PROTOCOL_TLS_CLIENT, 

484 insecure_skip_verify: bool = False, 

485) -> Callable[[], None]: 

486 """Handler that implements an HTTPS connection with TLS Auth. 

487 

488 The default protocol (ssl.PROTOCOL_TLS_CLIENT) will also enable 

489 ssl.CERT_REQUIRED and SSLContext.check_hostname by default. This can be 

490 disabled by setting insecure_skip_verify to True. 

491 

492 Both this handler and the TLS feature on pushgateay are experimental.""" 

493 context = ssl.SSLContext(protocol=protocol) 

494 if cafile is not None: 

495 context.load_verify_locations(cafile) 

496 else: 

497 context.load_default_certs() 

498 

499 if insecure_skip_verify: 

500 context.check_hostname = False 

501 context.verify_mode = ssl.CERT_NONE 

502 

503 context.load_cert_chain(certfile=certfile, keyfile=keyfile) 

504 handler = HTTPSHandler(context=context) 

505 return _make_handler(url, method, timeout, headers, data, handler) 

506 

507 

508def push_to_gateway( 

509 gateway: str, 

510 job: str, 

511 registry: CollectorRegistry, 

512 grouping_key: Optional[Dict[str, Any]] = None, 

513 timeout: Optional[float] = 30, 

514 handler: Callable = default_handler, 

515) -> None: 

516 """Push metrics to the given pushgateway. 

517 

518 `gateway` the url for your push gateway. Either of the form 

519 'http://pushgateway.local', or 'pushgateway.local'. 

520 Scheme defaults to 'http' if none is provided 

521 `job` is the job label to be attached to all pushed metrics 

522 `registry` is an instance of CollectorRegistry 

523 `grouping_key` please see the pushgateway documentation for details. 

524 Defaults to None 

525 `timeout` is how long push will attempt to connect before giving up. 

526 Defaults to 30s, can be set to None for no timeout. 

527 `handler` is an optional function which can be provided to perform 

528 requests to the 'gateway'. 

529 Defaults to None, in which case an http or https request 

530 will be carried out by a default handler. 

531 If not None, the argument must be a function which accepts 

532 the following arguments: 

533 url, method, timeout, headers, and content 

534 May be used to implement additional functionality not 

535 supported by the built-in default handler (such as SSL 

536 client certicates, and HTTP authentication mechanisms). 

537 'url' is the URL for the request, the 'gateway' argument 

538 described earlier will form the basis of this URL. 

539 'method' is the HTTP method which should be used when 

540 carrying out the request. 

541 'timeout' requests not successfully completed after this 

542 many seconds should be aborted. If timeout is None, then 

543 the handler should not set a timeout. 

544 'headers' is a list of ("header-name","header-value") tuples 

545 which must be passed to the pushgateway in the form of HTTP 

546 request headers. 

547 The function should raise an exception (e.g. IOError) on 

548 failure. 

549 'content' is the data which should be used to form the HTTP 

550 Message Body. 

551 

552 This overwrites all metrics with the same job and grouping_key. 

553 This uses the PUT HTTP method.""" 

554 _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler) 

555 

556 

557def pushadd_to_gateway( 

558 gateway: str, 

559 job: str, 

560 registry: Optional[CollectorRegistry], 

561 grouping_key: Optional[Dict[str, Any]] = None, 

562 timeout: Optional[float] = 30, 

563 handler: Callable = default_handler, 

564) -> None: 

565 """PushAdd metrics to the given pushgateway. 

566 

567 `gateway` the url for your push gateway. Either of the form 

568 'http://pushgateway.local', or 'pushgateway.local'. 

569 Scheme defaults to 'http' if none is provided 

570 `job` is the job label to be attached to all pushed metrics 

571 `registry` is an instance of CollectorRegistry 

572 `grouping_key` please see the pushgateway documentation for details. 

573 Defaults to None 

574 `timeout` is how long push will attempt to connect before giving up. 

575 Defaults to 30s, can be set to None for no timeout. 

576 `handler` is an optional function which can be provided to perform 

577 requests to the 'gateway'. 

578 Defaults to None, in which case an http or https request 

579 will be carried out by a default handler. 

580 See the 'prometheus_client.push_to_gateway' documentation 

581 for implementation requirements. 

582 

583 This replaces metrics with the same name, job and grouping_key. 

584 This uses the POST HTTP method.""" 

585 _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler) 

586 

587 

588def delete_from_gateway( 

589 gateway: str, 

590 job: str, 

591 grouping_key: Optional[Dict[str, Any]] = None, 

592 timeout: Optional[float] = 30, 

593 handler: Callable = default_handler, 

594) -> None: 

595 """Delete metrics from the given pushgateway. 

596 

597 `gateway` the url for your push gateway. Either of the form 

598 'http://pushgateway.local', or 'pushgateway.local'. 

599 Scheme defaults to 'http' if none is provided 

600 `job` is the job label to be attached to all pushed metrics 

601 `grouping_key` please see the pushgateway documentation for details. 

602 Defaults to None 

603 `timeout` is how long delete will attempt to connect before giving up. 

604 Defaults to 30s, can be set to None for no timeout. 

605 `handler` is an optional function which can be provided to perform 

606 requests to the 'gateway'. 

607 Defaults to None, in which case an http or https request 

608 will be carried out by a default handler. 

609 See the 'prometheus_client.push_to_gateway' documentation 

610 for implementation requirements. 

611 

612 This deletes metrics with the given job and grouping_key. 

613 This uses the DELETE HTTP method.""" 

614 _use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler) 

615 

616 

617def _use_gateway( 

618 method: str, 

619 gateway: str, 

620 job: str, 

621 registry: Optional[CollectorRegistry], 

622 grouping_key: Optional[Dict[str, Any]], 

623 timeout: Optional[float], 

624 handler: Callable, 

625) -> None: 

626 gateway_url = urlparse(gateway) 

627 # See https://bugs.python.org/issue27657 for details on urlparse in py>=3.7.6. 

628 if not gateway_url.scheme or gateway_url.scheme not in ['http', 'https']: 

629 gateway = f'http://{gateway}' 

630 

631 gateway = gateway.rstrip('/') 

632 url = '{}/metrics/{}/{}'.format(gateway, *_escape_grouping_key("job", job)) 

633 

634 data = b'' 

635 if method != 'DELETE': 

636 if registry is None: 

637 registry = REGISTRY 

638 data = generate_latest(registry) 

639 

640 if grouping_key is None: 

641 grouping_key = {} 

642 url += ''.join( 

643 '/{}/{}'.format(*_escape_grouping_key(str(k), str(v))) 

644 for k, v in sorted(grouping_key.items())) 

645 

646 handler( 

647 url=url, method=method, timeout=timeout, 

648 headers=[('Content-Type', CONTENT_TYPE_LATEST)], data=data, 

649 )() 

650 

651 

652def _escape_grouping_key(k, v): 

653 if v == "": 

654 # Per https://github.com/prometheus/pushgateway/pull/346. 

655 return k + "@base64", "=" 

656 elif '/' in v: 

657 # Added in Pushgateway 0.9.0. 

658 return k + "@base64", base64.urlsafe_b64encode(v.encode("utf-8")).decode("utf-8") 

659 else: 

660 return k, quote_plus(v) 

661 

662 

663def instance_ip_grouping_key() -> Dict[str, Any]: 

664 """Grouping key with instance set to the IP Address of this host.""" 

665 with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s: 

666 if sys.platform == 'darwin': 

667 # This check is done this way only on MacOS devices 

668 # it is done this way because the localhost method does 

669 # not work. 

670 # This method was adapted from this StackOverflow answer: 

671 # https://stackoverflow.com/a/28950776 

672 s.connect(('10.255.255.255', 1)) 

673 else: 

674 s.connect(('localhost', 0)) 

675 

676 return {'instance': s.getsockname()[0]} 

677 

678 

679from .asgi import make_asgi_app # noqa