1*d94ebd62SRobin Jarry#!/usr/bin/env python3 2*d94ebd62SRobin Jarry# SPDX-License-Identifier: BSD-3-Clause 3*d94ebd62SRobin Jarry# Copyright (c) 2023 Robin Jarry 4*d94ebd62SRobin Jarry 5*d94ebd62SRobin Jarryr''' 6*d94ebd62SRobin JarryDPDK telemetry exporter. 7*d94ebd62SRobin Jarry 8*d94ebd62SRobin JarryIt uses dynamically loaded endpoint exporters which are basic python files that 9*d94ebd62SRobin Jarrymust implement two functions: 10*d94ebd62SRobin Jarry 11*d94ebd62SRobin Jarry def info() -> dict[MetricName, MetricInfo]: 12*d94ebd62SRobin Jarry """ 13*d94ebd62SRobin Jarry Mapping of metric names to their description and type. 14*d94ebd62SRobin Jarry """ 15*d94ebd62SRobin Jarry 16*d94ebd62SRobin Jarry def metrics(sock: TelemetrySocket) -> list[MetricValue]: 17*d94ebd62SRobin Jarry """ 18*d94ebd62SRobin Jarry Request data from sock and return it as metric values. A metric value 19*d94ebd62SRobin Jarry is a 3-tuple: (name: str, value: any, labels: dict). Each name must be 20*d94ebd62SRobin Jarry present in info(). 21*d94ebd62SRobin Jarry """ 22*d94ebd62SRobin Jarry 23*d94ebd62SRobin JarryThe sock argument passed to metrics() has a single method: 24*d94ebd62SRobin Jarry 25*d94ebd62SRobin Jarry def cmd(self, uri, arg=None) -> dict | list: 26*d94ebd62SRobin Jarry """ 27*d94ebd62SRobin Jarry Request JSON data to the telemetry socket and parse it to python 28*d94ebd62SRobin Jarry values. 29*d94ebd62SRobin Jarry """ 30*d94ebd62SRobin Jarry 31*d94ebd62SRobin JarrySee existing endpoints for examples. 32*d94ebd62SRobin Jarry 33*d94ebd62SRobin JarryThe exporter supports multiple output formats: 34*d94ebd62SRobin Jarry 35*d94ebd62SRobin Jarryprometheus://ADDRESS:PORT 36*d94ebd62SRobin Jarryopenmetrics://ADDRESS:PORT 37*d94ebd62SRobin Jarry Expose the enabled endpoints via a local HTTP server listening on the 38*d94ebd62SRobin Jarry specified address and port. GET requests on that server are served with 39*d94ebd62SRobin Jarry text/plain responses in the prometheus/openmetrics format. 40*d94ebd62SRobin Jarry 41*d94ebd62SRobin Jarry More details: 42*d94ebd62SRobin Jarry https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format 43*d94ebd62SRobin Jarry 44*d94ebd62SRobin Jarrycarbon://ADDRESS:PORT 45*d94ebd62SRobin Jarrygraphite://ADDRESS:PORT 46*d94ebd62SRobin Jarry Export all enabled endpoints to the specified TCP ADDRESS:PORT in the pickle 47*d94ebd62SRobin Jarry carbon format. 48*d94ebd62SRobin Jarry 49*d94ebd62SRobin Jarry More details: 50*d94ebd62SRobin Jarry https://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-pickle-protocol 51*d94ebd62SRobin Jarry''' 52*d94ebd62SRobin Jarry 53*d94ebd62SRobin Jarryimport argparse 54*d94ebd62SRobin Jarryimport importlib.util 55*d94ebd62SRobin Jarryimport json 56*d94ebd62SRobin Jarryimport logging 57*d94ebd62SRobin Jarryimport os 58*d94ebd62SRobin Jarryimport pickle 59*d94ebd62SRobin Jarryimport re 60*d94ebd62SRobin Jarryimport socket 61*d94ebd62SRobin Jarryimport struct 62*d94ebd62SRobin Jarryimport sys 63*d94ebd62SRobin Jarryimport time 64*d94ebd62SRobin Jarryimport typing 65*d94ebd62SRobin Jarryfrom http import HTTPStatus, server 66*d94ebd62SRobin Jarryfrom urllib.parse import urlparse 67*d94ebd62SRobin Jarry 68*d94ebd62SRobin JarryLOG = logging.getLogger(__name__) 69*d94ebd62SRobin Jarry# Use local endpoints path only when running from source 70*d94ebd62SRobin JarryLOCAL = os.path.join(os.path.dirname(__file__), "telemetry-endpoints") 71*d94ebd62SRobin JarryDEFAULT_LOAD_PATHS = [] 72*d94ebd62SRobin Jarryif os.path.isdir(LOCAL): 73*d94ebd62SRobin Jarry DEFAULT_LOAD_PATHS.append(LOCAL) 74*d94ebd62SRobin JarryDEFAULT_LOAD_PATHS += [ 75*d94ebd62SRobin Jarry "/usr/local/share/dpdk/telemetry-endpoints", 76*d94ebd62SRobin Jarry "/usr/share/dpdk/telemetry-endpoints", 77*d94ebd62SRobin Jarry] 78*d94ebd62SRobin JarryDEFAULT_OUTPUT = "openmetrics://:9876" 79*d94ebd62SRobin Jarry 80*d94ebd62SRobin Jarry 81*d94ebd62SRobin Jarrydef main(): 82*d94ebd62SRobin Jarry logging.basicConfig( 83*d94ebd62SRobin Jarry stream=sys.stdout, 84*d94ebd62SRobin Jarry level=logging.INFO, 85*d94ebd62SRobin Jarry format="%(asctime)s %(levelname)s %(message)s", 86*d94ebd62SRobin Jarry datefmt="%Y-%m-%d %H:%M:%S", 87*d94ebd62SRobin Jarry ) 88*d94ebd62SRobin Jarry parser = argparse.ArgumentParser( 89*d94ebd62SRobin Jarry description=__doc__, 90*d94ebd62SRobin Jarry formatter_class=argparse.RawDescriptionHelpFormatter, 91*d94ebd62SRobin Jarry ) 92*d94ebd62SRobin Jarry parser.add_argument( 93*d94ebd62SRobin Jarry "-o", 94*d94ebd62SRobin Jarry "--output", 95*d94ebd62SRobin Jarry metavar="FORMAT://PARAMETERS", 96*d94ebd62SRobin Jarry default=urlparse(DEFAULT_OUTPUT), 97*d94ebd62SRobin Jarry type=urlparse, 98*d94ebd62SRobin Jarry help=f""" 99*d94ebd62SRobin Jarry Output format (default: "{DEFAULT_OUTPUT}"). Depending on the format, 100*d94ebd62SRobin Jarry URL elements have different meanings. By default, the exporter starts a 101*d94ebd62SRobin Jarry local HTTP server on port 9876 that serves requests in the 102*d94ebd62SRobin Jarry prometheus/openmetrics plain text format. 103*d94ebd62SRobin Jarry """, 104*d94ebd62SRobin Jarry ) 105*d94ebd62SRobin Jarry parser.add_argument( 106*d94ebd62SRobin Jarry "-p", 107*d94ebd62SRobin Jarry "--load-path", 108*d94ebd62SRobin Jarry dest="load_paths", 109*d94ebd62SRobin Jarry type=lambda v: v.split(os.pathsep), 110*d94ebd62SRobin Jarry default=DEFAULT_LOAD_PATHS, 111*d94ebd62SRobin Jarry help=f""" 112*d94ebd62SRobin Jarry The list of paths from which to disvover endpoints. 113*d94ebd62SRobin Jarry (default: "{os.pathsep.join(DEFAULT_LOAD_PATHS)}"). 114*d94ebd62SRobin Jarry """, 115*d94ebd62SRobin Jarry ) 116*d94ebd62SRobin Jarry parser.add_argument( 117*d94ebd62SRobin Jarry "-e", 118*d94ebd62SRobin Jarry "--endpoint", 119*d94ebd62SRobin Jarry dest="endpoints", 120*d94ebd62SRobin Jarry metavar="ENDPOINT", 121*d94ebd62SRobin Jarry action="append", 122*d94ebd62SRobin Jarry help=""" 123*d94ebd62SRobin Jarry Telemetry endpoint to export (by default, all discovered endpoints are 124*d94ebd62SRobin Jarry enabled). This option can be specified more than once. 125*d94ebd62SRobin Jarry """, 126*d94ebd62SRobin Jarry ) 127*d94ebd62SRobin Jarry parser.add_argument( 128*d94ebd62SRobin Jarry "-l", 129*d94ebd62SRobin Jarry "--list", 130*d94ebd62SRobin Jarry action="store_true", 131*d94ebd62SRobin Jarry help=""" 132*d94ebd62SRobin Jarry Only list detected endpoints and exit. 133*d94ebd62SRobin Jarry """, 134*d94ebd62SRobin Jarry ) 135*d94ebd62SRobin Jarry parser.add_argument( 136*d94ebd62SRobin Jarry "-s", 137*d94ebd62SRobin Jarry "--socket-path", 138*d94ebd62SRobin Jarry default="/run/dpdk/rte/dpdk_telemetry.v2", 139*d94ebd62SRobin Jarry help=""" 140*d94ebd62SRobin Jarry The DPDK telemetry socket path (default: "%(default)s"). 141*d94ebd62SRobin Jarry """, 142*d94ebd62SRobin Jarry ) 143*d94ebd62SRobin Jarry args = parser.parse_args() 144*d94ebd62SRobin Jarry output = OUTPUT_FORMATS.get(args.output.scheme) 145*d94ebd62SRobin Jarry if output is None: 146*d94ebd62SRobin Jarry parser.error(f"unsupported output format: {args.output.scheme}://") 147*d94ebd62SRobin Jarry 148*d94ebd62SRobin Jarry try: 149*d94ebd62SRobin Jarry endpoints = load_endpoints(args.load_paths, args.endpoints) 150*d94ebd62SRobin Jarry if args.list: 151*d94ebd62SRobin Jarry return 152*d94ebd62SRobin Jarry except Exception as e: 153*d94ebd62SRobin Jarry parser.error(str(e)) 154*d94ebd62SRobin Jarry 155*d94ebd62SRobin Jarry output(args, endpoints) 156*d94ebd62SRobin Jarry 157*d94ebd62SRobin Jarry 158*d94ebd62SRobin Jarryclass TelemetrySocket: 159*d94ebd62SRobin Jarry """ 160*d94ebd62SRobin Jarry Abstraction of the DPDK telemetry socket. 161*d94ebd62SRobin Jarry """ 162*d94ebd62SRobin Jarry 163*d94ebd62SRobin Jarry def __init__(self, path: str): 164*d94ebd62SRobin Jarry self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET) 165*d94ebd62SRobin Jarry self.sock.connect(path) 166*d94ebd62SRobin Jarry data = json.loads(self.sock.recv(1024).decode()) 167*d94ebd62SRobin Jarry self.max_output_len = data["max_output_len"] 168*d94ebd62SRobin Jarry 169*d94ebd62SRobin Jarry def cmd( 170*d94ebd62SRobin Jarry self, uri: str, arg: typing.Any = None 171*d94ebd62SRobin Jarry ) -> typing.Optional[typing.Union[dict, list]]: 172*d94ebd62SRobin Jarry """ 173*d94ebd62SRobin Jarry Request JSON data to the telemetry socket and parse it to python 174*d94ebd62SRobin Jarry values. 175*d94ebd62SRobin Jarry """ 176*d94ebd62SRobin Jarry if arg is not None: 177*d94ebd62SRobin Jarry u = f"{uri},{arg}" 178*d94ebd62SRobin Jarry else: 179*d94ebd62SRobin Jarry u = uri 180*d94ebd62SRobin Jarry self.sock.send(u.encode("utf-8")) 181*d94ebd62SRobin Jarry data = self.sock.recv(self.max_output_len) 182*d94ebd62SRobin Jarry return json.loads(data.decode("utf-8"))[uri] 183*d94ebd62SRobin Jarry 184*d94ebd62SRobin Jarry def __enter__(self): 185*d94ebd62SRobin Jarry return self 186*d94ebd62SRobin Jarry 187*d94ebd62SRobin Jarry def __exit__(self, *args, **kwargs): 188*d94ebd62SRobin Jarry self.sock.close() 189*d94ebd62SRobin Jarry 190*d94ebd62SRobin Jarry 191*d94ebd62SRobin JarryMetricDescription = str 192*d94ebd62SRobin JarryMetricType = str 193*d94ebd62SRobin JarryMetricName = str 194*d94ebd62SRobin JarryMetricLabels = typing.Dict[str, typing.Any] 195*d94ebd62SRobin JarryMetricInfo = typing.Tuple[MetricDescription, MetricType] 196*d94ebd62SRobin JarryMetricValue = typing.Tuple[MetricName, typing.Any, MetricLabels] 197*d94ebd62SRobin Jarry 198*d94ebd62SRobin Jarry 199*d94ebd62SRobin Jarryclass TelemetryEndpoint: 200*d94ebd62SRobin Jarry """ 201*d94ebd62SRobin Jarry Placeholder class only used for typing annotations. 202*d94ebd62SRobin Jarry """ 203*d94ebd62SRobin Jarry 204*d94ebd62SRobin Jarry @staticmethod 205*d94ebd62SRobin Jarry def info() -> typing.Dict[MetricName, MetricInfo]: 206*d94ebd62SRobin Jarry """ 207*d94ebd62SRobin Jarry Mapping of metric names to their description and type. 208*d94ebd62SRobin Jarry """ 209*d94ebd62SRobin Jarry raise NotImplementedError() 210*d94ebd62SRobin Jarry 211*d94ebd62SRobin Jarry @staticmethod 212*d94ebd62SRobin Jarry def metrics(sock: TelemetrySocket) -> typing.List[MetricValue]: 213*d94ebd62SRobin Jarry """ 214*d94ebd62SRobin Jarry Request data from sock and return it as metric values. Each metric 215*d94ebd62SRobin Jarry name must be present in info(). 216*d94ebd62SRobin Jarry """ 217*d94ebd62SRobin Jarry raise NotImplementedError() 218*d94ebd62SRobin Jarry 219*d94ebd62SRobin Jarry 220*d94ebd62SRobin Jarrydef load_endpoints( 221*d94ebd62SRobin Jarry paths: typing.List[str], names: typing.List[str] 222*d94ebd62SRobin Jarry) -> typing.List[TelemetryEndpoint]: 223*d94ebd62SRobin Jarry """ 224*d94ebd62SRobin Jarry Load selected telemetry endpoints from the specified paths. 225*d94ebd62SRobin Jarry """ 226*d94ebd62SRobin Jarry 227*d94ebd62SRobin Jarry endpoints = {} 228*d94ebd62SRobin Jarry dwb = sys.dont_write_bytecode 229*d94ebd62SRobin Jarry sys.dont_write_bytecode = True # never generate .pyc files for endpoints 230*d94ebd62SRobin Jarry 231*d94ebd62SRobin Jarry for p in paths: 232*d94ebd62SRobin Jarry if not os.path.isdir(p): 233*d94ebd62SRobin Jarry continue 234*d94ebd62SRobin Jarry for fname in os.listdir(p): 235*d94ebd62SRobin Jarry f = os.path.join(p, fname) 236*d94ebd62SRobin Jarry if os.path.isdir(f): 237*d94ebd62SRobin Jarry continue 238*d94ebd62SRobin Jarry try: 239*d94ebd62SRobin Jarry name, _ = os.path.splitext(fname) 240*d94ebd62SRobin Jarry if names is not None and name not in names: 241*d94ebd62SRobin Jarry # not selected by user 242*d94ebd62SRobin Jarry continue 243*d94ebd62SRobin Jarry if name in endpoints: 244*d94ebd62SRobin Jarry # endpoint with same name already loaded 245*d94ebd62SRobin Jarry continue 246*d94ebd62SRobin Jarry spec = importlib.util.spec_from_file_location(name, f) 247*d94ebd62SRobin Jarry module = importlib.util.module_from_spec(spec) 248*d94ebd62SRobin Jarry spec.loader.exec_module(module) 249*d94ebd62SRobin Jarry endpoints[name] = module 250*d94ebd62SRobin Jarry except Exception: 251*d94ebd62SRobin Jarry LOG.exception("parsing endpoint: %s", f) 252*d94ebd62SRobin Jarry 253*d94ebd62SRobin Jarry if not endpoints: 254*d94ebd62SRobin Jarry raise Exception("no telemetry endpoints detected/selected") 255*d94ebd62SRobin Jarry 256*d94ebd62SRobin Jarry sys.dont_write_bytecode = dwb 257*d94ebd62SRobin Jarry 258*d94ebd62SRobin Jarry modules = [] 259*d94ebd62SRobin Jarry info = {} 260*d94ebd62SRobin Jarry for name, module in sorted(endpoints.items()): 261*d94ebd62SRobin Jarry LOG.info("using endpoint: %s (from %s)", name, module.__file__) 262*d94ebd62SRobin Jarry try: 263*d94ebd62SRobin Jarry for metric, (description, type_) in module.info().items(): 264*d94ebd62SRobin Jarry info[(name, metric)] = (description, type_) 265*d94ebd62SRobin Jarry modules.append(module) 266*d94ebd62SRobin Jarry except Exception: 267*d94ebd62SRobin Jarry LOG.exception("getting endpoint info: %s", name) 268*d94ebd62SRobin Jarry return modules 269*d94ebd62SRobin Jarry 270*d94ebd62SRobin Jarry 271*d94ebd62SRobin Jarrydef serve_openmetrics( 272*d94ebd62SRobin Jarry args: argparse.Namespace, endpoints: typing.List[TelemetryEndpoint] 273*d94ebd62SRobin Jarry): 274*d94ebd62SRobin Jarry """ 275*d94ebd62SRobin Jarry Start an HTTP server and serve requests in the openmetrics/prometheus 276*d94ebd62SRobin Jarry format. 277*d94ebd62SRobin Jarry """ 278*d94ebd62SRobin Jarry listen = (args.output.hostname or "", int(args.output.port or 80)) 279*d94ebd62SRobin Jarry with server.HTTPServer(listen, OpenmetricsHandler) as httpd: 280*d94ebd62SRobin Jarry httpd.dpdk_socket_path = args.socket_path 281*d94ebd62SRobin Jarry httpd.telemetry_endpoints = endpoints 282*d94ebd62SRobin Jarry LOG.info("listening on port %s", httpd.server_port) 283*d94ebd62SRobin Jarry try: 284*d94ebd62SRobin Jarry httpd.serve_forever() 285*d94ebd62SRobin Jarry except KeyboardInterrupt: 286*d94ebd62SRobin Jarry LOG.info("shutting down") 287*d94ebd62SRobin Jarry 288*d94ebd62SRobin Jarry 289*d94ebd62SRobin Jarryclass OpenmetricsHandler(server.BaseHTTPRequestHandler): 290*d94ebd62SRobin Jarry """ 291*d94ebd62SRobin Jarry Basic HTTP handler that returns prometheus/openmetrics formatted responses. 292*d94ebd62SRobin Jarry """ 293*d94ebd62SRobin Jarry 294*d94ebd62SRobin Jarry CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8" 295*d94ebd62SRobin Jarry 296*d94ebd62SRobin Jarry def escape(self, value: typing.Any) -> str: 297*d94ebd62SRobin Jarry """ 298*d94ebd62SRobin Jarry Escape a metric label value. 299*d94ebd62SRobin Jarry """ 300*d94ebd62SRobin Jarry value = str(value) 301*d94ebd62SRobin Jarry value = value.replace('"', '\\"') 302*d94ebd62SRobin Jarry value = value.replace("\\", "\\\\") 303*d94ebd62SRobin Jarry return value.replace("\n", "\\n") 304*d94ebd62SRobin Jarry 305*d94ebd62SRobin Jarry def do_GET(self): 306*d94ebd62SRobin Jarry """ 307*d94ebd62SRobin Jarry Called upon GET requests. 308*d94ebd62SRobin Jarry """ 309*d94ebd62SRobin Jarry try: 310*d94ebd62SRobin Jarry lines = [] 311*d94ebd62SRobin Jarry metrics_names = set() 312*d94ebd62SRobin Jarry with TelemetrySocket(self.server.dpdk_socket_path) as sock: 313*d94ebd62SRobin Jarry for e in self.server.telemetry_endpoints: 314*d94ebd62SRobin Jarry info = e.info() 315*d94ebd62SRobin Jarry metrics_lines = [] 316*d94ebd62SRobin Jarry try: 317*d94ebd62SRobin Jarry metrics = e.metrics(sock) 318*d94ebd62SRobin Jarry except Exception: 319*d94ebd62SRobin Jarry LOG.exception("%s: metrics collection failed", e.__name__) 320*d94ebd62SRobin Jarry continue 321*d94ebd62SRobin Jarry for name, value, labels in metrics: 322*d94ebd62SRobin Jarry fullname = re.sub(r"\W", "_", f"dpdk_{e.__name__}_{name}") 323*d94ebd62SRobin Jarry labels = ", ".join( 324*d94ebd62SRobin Jarry f'{k}="{self.escape(v)}"' for k, v in labels.items() 325*d94ebd62SRobin Jarry ) 326*d94ebd62SRobin Jarry if labels: 327*d94ebd62SRobin Jarry labels = f"{{{labels}}}" 328*d94ebd62SRobin Jarry metrics_lines.append(f"{fullname}{labels} {value}") 329*d94ebd62SRobin Jarry if fullname not in metrics_names: 330*d94ebd62SRobin Jarry metrics_names.add(fullname) 331*d94ebd62SRobin Jarry desc, metric_type = info[name] 332*d94ebd62SRobin Jarry lines += [ 333*d94ebd62SRobin Jarry f"# HELP {fullname} {desc}", 334*d94ebd62SRobin Jarry f"# TYPE {fullname} {metric_type}", 335*d94ebd62SRobin Jarry ] 336*d94ebd62SRobin Jarry lines += metrics_lines 337*d94ebd62SRobin Jarry if not lines: 338*d94ebd62SRobin Jarry self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR) 339*d94ebd62SRobin Jarry LOG.error( 340*d94ebd62SRobin Jarry "%s %s: no metrics collected", 341*d94ebd62SRobin Jarry self.address_string(), 342*d94ebd62SRobin Jarry self.requestline, 343*d94ebd62SRobin Jarry ) 344*d94ebd62SRobin Jarry body = "\n".join(lines).encode("utf-8") + b"\n" 345*d94ebd62SRobin Jarry self.send_response(HTTPStatus.OK) 346*d94ebd62SRobin Jarry self.send_header("Content-Type", self.CONTENT_TYPE) 347*d94ebd62SRobin Jarry self.send_header("Content-Length", str(len(body))) 348*d94ebd62SRobin Jarry self.end_headers() 349*d94ebd62SRobin Jarry self.wfile.write(body) 350*d94ebd62SRobin Jarry LOG.info("%s %s", self.address_string(), self.requestline) 351*d94ebd62SRobin Jarry 352*d94ebd62SRobin Jarry except (FileNotFoundError, ConnectionRefusedError): 353*d94ebd62SRobin Jarry self.send_error(HTTPStatus.SERVICE_UNAVAILABLE) 354*d94ebd62SRobin Jarry LOG.exception( 355*d94ebd62SRobin Jarry "%s %s: telemetry socket not available", 356*d94ebd62SRobin Jarry self.address_string(), 357*d94ebd62SRobin Jarry self.requestline, 358*d94ebd62SRobin Jarry ) 359*d94ebd62SRobin Jarry except Exception: 360*d94ebd62SRobin Jarry self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR) 361*d94ebd62SRobin Jarry LOG.exception("%s %s", self.address_string(), self.requestline) 362*d94ebd62SRobin Jarry 363*d94ebd62SRobin Jarry def log_message(self, fmt, *args): 364*d94ebd62SRobin Jarry pass # disable built-in logger 365*d94ebd62SRobin Jarry 366*d94ebd62SRobin Jarry 367*d94ebd62SRobin Jarrydef export_carbon(args: argparse.Namespace, endpoints: typing.List[TelemetryEndpoint]): 368*d94ebd62SRobin Jarry """ 369*d94ebd62SRobin Jarry Collect all metrics and export them to a carbon server in the pickle format. 370*d94ebd62SRobin Jarry """ 371*d94ebd62SRobin Jarry addr = (args.output.hostname or "", int(args.output.port or 80)) 372*d94ebd62SRobin Jarry with TelemetrySocket(args.socket_path) as dpdk: 373*d94ebd62SRobin Jarry with socket.socket() as carbon: 374*d94ebd62SRobin Jarry carbon.connect(addr) 375*d94ebd62SRobin Jarry all_metrics = [] 376*d94ebd62SRobin Jarry for e in endpoints: 377*d94ebd62SRobin Jarry try: 378*d94ebd62SRobin Jarry metrics = e.metrics(dpdk) 379*d94ebd62SRobin Jarry except Exception: 380*d94ebd62SRobin Jarry LOG.exception("%s: metrics collection failed", e.__name__) 381*d94ebd62SRobin Jarry continue 382*d94ebd62SRobin Jarry for name, value, labels in metrics: 383*d94ebd62SRobin Jarry fullname = re.sub(r"\W", ".", f"dpdk.{e.__name__}.{name}") 384*d94ebd62SRobin Jarry for key, val in labels.items(): 385*d94ebd62SRobin Jarry val = str(val).replace(";", "") 386*d94ebd62SRobin Jarry fullname += f";{key}={val}" 387*d94ebd62SRobin Jarry all_metrics.append((fullname, (time.time(), value))) 388*d94ebd62SRobin Jarry if not all_metrics: 389*d94ebd62SRobin Jarry raise Exception("no metrics collected") 390*d94ebd62SRobin Jarry payload = pickle.dumps(all_metrics, protocol=2) 391*d94ebd62SRobin Jarry header = struct.pack("!L", len(payload)) 392*d94ebd62SRobin Jarry buf = header + payload 393*d94ebd62SRobin Jarry carbon.sendall(buf) 394*d94ebd62SRobin Jarry 395*d94ebd62SRobin Jarry 396*d94ebd62SRobin JarryOUTPUT_FORMATS = { 397*d94ebd62SRobin Jarry "openmetrics": serve_openmetrics, 398*d94ebd62SRobin Jarry "prometheus": serve_openmetrics, 399*d94ebd62SRobin Jarry "carbon": export_carbon, 400*d94ebd62SRobin Jarry "graphite": export_carbon, 401*d94ebd62SRobin Jarry} 402*d94ebd62SRobin Jarry 403*d94ebd62SRobin Jarry 404*d94ebd62SRobin Jarryif __name__ == "__main__": 405*d94ebd62SRobin Jarry main() 406