xref: /dpdk/usertools/dpdk-telemetry-exporter.py (revision d94ebd627a86dfbc0ccf8d7d3802196c55d826cf)
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