stats: Add optional labels to prometheus metrics
[vpp.git] / src / vpp / app / vpp_prometheus_export.c
1 /*
2  *------------------------------------------------------------------
3  * vpp_get_stats.c
4  *
5  * Copyright (c) 2018 Cisco and/or its affiliates.
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at:
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  *------------------------------------------------------------------
18  */
19
20 #include <arpa/inet.h>
21 #include <sys/epoll.h>
22 #include <stdbool.h>
23 #include <stdint.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <stdio.h>
27 #include <errno.h>
28 #include <unistd.h>
29 #include <netdb.h>
30 #include <sys/socket.h>
31 #include <vpp-api/client/stat_client.h>
32 #include <vlib/vlib.h>
33 #include <ctype.h>
34
35 /* https://github.com/prometheus/prometheus/wiki/Default-port-allocations */
36 #define SERVER_PORT 9482
37
38 #define MAX_TOKENS 10
39
40 static char *
41 prom_string (char *s)
42 {
43   char *p = s;
44   while (*p)
45     {
46       if (!isalnum (*p))
47         *p = '_';
48       p++;
49     }
50   return s;
51 }
52
53 static void
54 print_metric_v1 (FILE *stream, stat_segment_data_t *res)
55 {
56   int j, k;
57
58   switch (res->type)
59     {
60     case STAT_DIR_TYPE_COUNTER_VECTOR_SIMPLE:
61       fformat (stream, "# TYPE %s counter\n", prom_string (res->name));
62       for (k = 0; k < vec_len (res->simple_counter_vec); k++)
63         for (j = 0; j < vec_len (res->simple_counter_vec[k]); j++)
64           fformat (stream, "%s{thread=\"%d\",interface=\"%d\"} %lld\n",
65                    prom_string (res->name), k, j,
66                    res->simple_counter_vec[k][j]);
67       break;
68
69     case STAT_DIR_TYPE_COUNTER_VECTOR_COMBINED:
70       fformat (stream, "# TYPE %s_packets counter\n", prom_string (res->name));
71       fformat (stream, "# TYPE %s_bytes counter\n", prom_string (res->name));
72       for (k = 0; k < vec_len (res->simple_counter_vec); k++)
73         for (j = 0; j < vec_len (res->combined_counter_vec[k]); j++)
74           {
75             fformat (stream,
76                      "%s_packets{thread=\"%d\",interface=\"%d\"} %lld\n",
77                      prom_string (res->name), k, j,
78                      res->combined_counter_vec[k][j].packets);
79             fformat (stream, "%s_bytes{thread=\"%d\",interface=\"%d\"} %lld\n",
80                      prom_string (res->name), k, j,
81                      res->combined_counter_vec[k][j].bytes);
82           }
83       break;
84     case STAT_DIR_TYPE_SCALAR_INDEX:
85       fformat (stream, "# TYPE %s counter\n", prom_string (res->name));
86       fformat (stream, "%s %.2f\n", prom_string (res->name),
87                res->scalar_value);
88       break;
89
90     case STAT_DIR_TYPE_NAME_VECTOR:
91       fformat (stream, "# TYPE %s_info gauge\n", prom_string (res->name));
92       for (k = 0; k < vec_len (res->name_vector); k++)
93         if (res->name_vector[k])
94           fformat (stream, "%s_info{index=\"%d\",name=\"%s\"} 1\n",
95                    prom_string (res->name), k, res->name_vector[k]);
96       break;
97
98     case STAT_DIR_TYPE_EMPTY:
99       break;
100
101     default:
102       fformat (stderr, "Unknown value %d\n", res->type);
103       ;
104     }
105 }
106
107 static void
108 sanitize (char *str, int len)
109 {
110   for (int i = 0; i < len; i++)
111     {
112       if (!isalnum (str[i]))
113         str[i] = '_';
114     }
115 }
116
117 static int
118 tokenize (const char *name, char **tokens, int *lengths, int max_tokens)
119 {
120   char *p = (char *) name;
121   char *savep = p;
122
123   int i = 0;
124   while (*p && i < max_tokens - 1)
125     {
126       if (*p == '/')
127         {
128           tokens[i] = (char *) savep;
129           lengths[i] = (int) (p - savep);
130           i++;
131           p++;
132           savep = p;
133         }
134       else
135         {
136           p++;
137         }
138     }
139   tokens[i] = (char *) savep;
140   lengths[i] = (int) (p - savep);
141
142   i++;
143   return i;
144 }
145
146 static void
147 print_metric_v2 (FILE *stream, stat_segment_data_t *res)
148 {
149   int num_tokens = 0;
150   char *tokens[MAX_TOKENS];
151   int lengths[MAX_TOKENS];
152   int j, k;
153
154   num_tokens = tokenize (res->name, tokens, lengths, MAX_TOKENS);
155   switch (res->type)
156     {
157     case STAT_DIR_TYPE_COUNTER_VECTOR_SIMPLE:
158       if (res->simple_counter_vec == 0)
159         return;
160       for (k = 0; k < vec_len (res->simple_counter_vec); k++)
161         for (j = 0; j < vec_len (res->simple_counter_vec[k]); j++)
162           {
163             if ((num_tokens == 4) &&
164                 (!strncmp (tokens[1], "nodes", lengths[1]) ||
165                  !strncmp (tokens[1], "interfaces", lengths[1])))
166               {
167                 sanitize (tokens[1], lengths[1]);
168                 sanitize (tokens[3], lengths[3]);
169                 fformat (
170                   stream,
171                   "%.*s_%.*s{%.*s=\"%.*s\",index=\"%d\",thread=\"%d\"} %lu\n",
172                   lengths[1], tokens[1], lengths[3], tokens[3], lengths[1] - 1,
173                   tokens[1], lengths[2], tokens[2], j, k,
174                   res->simple_counter_vec[k][j]);
175               }
176             else if ((num_tokens == 3) &&
177                      !strncmp (tokens[1], "sys", lengths[1]))
178               {
179                 sanitize (tokens[1], lengths[1]);
180                 fformat (stream, "%.*s_%.*s{index=\"%d\",thread=\"%d\"} %lu\n",
181                          lengths[1], tokens[1], lengths[2], tokens[2], j, k,
182                          res->simple_counter_vec[k][j]);
183               }
184             else if (!strncmp (tokens[1], "mem", lengths[1]))
185               {
186                 if (num_tokens == 3)
187                   {
188                     fformat (
189                       stream,
190                       "%.*s{heap=\"%.*s\",index=\"%d\",thread=\"%d\"} %lu\n",
191                       lengths[1], tokens[1], lengths[2], tokens[2], j, k,
192                       res->simple_counter_vec[k][j]);
193                   }
194                 else if (num_tokens == 4)
195                   {
196                     fformat (stream,
197                              "%.*s_%.*s{heap=\"%.*s\",index=\"%d\",thread=\"%"
198                              "d\"} %lu\n",
199                              lengths[1], tokens[1], lengths[3], tokens[3],
200                              lengths[2], tokens[2], j, k,
201                              res->simple_counter_vec[k][j]);
202                   }
203                 else
204                   {
205                     print_metric_v1 (stream, res);
206                   }
207               }
208             else if (!strncmp (tokens[1], "err", lengths[1]))
209               {
210                 // NOTE: the error is in token3, but it may contain '/'.
211                 // Considering this is the last token, it is safe to print
212                 // token3 until the end of res->name
213                 fformat (stream,
214                          "%.*s{node=\"%.*s\",error=\"%s\",index=\"%d\",thread="
215                          "\"%d\"} %lu\n",
216                          lengths[1], tokens[1], lengths[2], tokens[2],
217                          tokens[3], j, k, res->simple_counter_vec[k][j]);
218               }
219             else
220               {
221                 print_metric_v1 (stream, res);
222               }
223           }
224       break;
225
226     case STAT_DIR_TYPE_COUNTER_VECTOR_COMBINED:
227       if (res->combined_counter_vec == 0)
228         return;
229       for (k = 0; k < vec_len (res->combined_counter_vec); k++)
230         for (j = 0; j < vec_len (res->combined_counter_vec[k]); j++)
231           {
232             if ((num_tokens == 4) &&
233                 !strncmp (tokens[1], "interfaces", lengths[1]))
234               {
235                 sanitize (tokens[1], lengths[1]);
236                 sanitize (tokens[3], lengths[3]);
237                 fformat (stream,
238                          "%.*s_%.*s_packets{interface=\"%.*s\",index=\"%d\","
239                          "thread=\"%d\"} %lu\n",
240                          lengths[1], tokens[1], lengths[3], tokens[3],
241                          lengths[2], tokens[2], j, k,
242                          res->combined_counter_vec[k][j].packets);
243                 fformat (stream,
244                          "%.*s_%.*s_bytes{interface=\"%.*s\",index=\"%d\","
245                          "thread=\"%d\"} %lu\n",
246                          lengths[1], tokens[1], lengths[3], tokens[3],
247                          lengths[2], tokens[2], j, k,
248                          res->combined_counter_vec[k][j].bytes);
249               }
250             else
251               {
252                 print_metric_v1 (stream, res);
253               }
254           }
255       break;
256
257     case STAT_DIR_TYPE_SCALAR_INDEX:
258       if ((num_tokens == 4) &&
259           !strncmp (tokens[1], "buffer-pools", lengths[1]))
260         {
261           sanitize (tokens[1], lengths[1]);
262           sanitize (tokens[3], lengths[3]);
263           fformat (stream, "%.*s_%.*s{pool=\"%.*s\"} %.2f\n", lengths[1],
264                    tokens[1], lengths[3], tokens[3], lengths[2], tokens[2],
265                    res->scalar_value);
266         }
267       else if ((num_tokens == 3) && !strncmp (tokens[1], "sys", lengths[1]))
268         {
269           sanitize (tokens[1], lengths[1]);
270           sanitize (tokens[2], lengths[2]);
271           fformat (stream, "%.*s_%.*s %.2f\n", lengths[1], tokens[1],
272                    lengths[2], tokens[2], res->scalar_value);
273           if (!strncmp (tokens[2], "boottime", lengths[2]))
274             {
275               struct timeval tv;
276               gettimeofday (&tv, NULL);
277               fformat (stream, "sys_uptime %.2f\n",
278                        tv.tv_sec - res->scalar_value);
279             }
280         }
281       else
282         {
283           print_metric_v1 (stream, res);
284         }
285       break;
286
287     default:;
288       fformat (stderr, "Unhandled type %d name %s\n", res->type, res->name);
289     }
290 }
291
292 static void
293 dump_metrics (FILE *stream, u8 **patterns, u8 v2)
294 {
295   stat_segment_data_t *res;
296   int i;
297   static u32 *stats = 0;
298
299 retry:
300   res = stat_segment_dump (stats);
301   if (res == 0)
302     { /* Memory layout has changed */
303       if (stats)
304         vec_free (stats);
305       stats = stat_segment_ls (patterns);
306       goto retry;
307     }
308
309   for (i = 0; i < vec_len (res); i++)
310     {
311       if (v2)
312         print_metric_v2 (stream, &res[i]);
313       else
314         print_metric_v1 (stream, &res[i]);
315     }
316   stat_segment_data_free (res);
317 }
318
319
320 #define ROOTPAGE  "<html><head><title>Metrics exporter</title></head><body><ul><li><a href=\"/metrics\">metrics</a></li></ul></body></html>"
321 #define NOT_FOUND_ERROR "<html><head><title>Document not found</title></head><body><h1>404 - Document not found</h1></body></html>"
322
323 static void
324 http_handler (FILE *stream, u8 **patterns, u8 v2)
325 {
326   char status[80] = { 0 };
327   if (fgets (status, sizeof (status) - 1, stream) == 0)
328     {
329       fprintf (stderr, "fgets error: %s %s\n", status, strerror (errno));
330       return;
331     }
332   char *saveptr;
333   char *method = strtok_r (status, " \t\r\n", &saveptr);
334   if (method == 0 || strncmp (method, "GET", 4) != 0)
335     {
336       fputs ("HTTP/1.0 405 Method Not Allowed\r\n", stream);
337       return;
338     }
339   char *request_uri = strtok_r (NULL, " \t", &saveptr);
340   char *protocol = strtok_r (NULL, " \t\r\n", &saveptr);
341   if (protocol == 0 || strncmp (protocol, "HTTP/1.", 7) != 0)
342     {
343       fputs ("HTTP/1.0 400 Bad Request\r\n", stream);
344       return;
345     }
346   /* Read the other headers */
347   for (;;)
348     {
349       char header[1024];
350       if (fgets (header, sizeof (header) - 1, stream) == 0)
351         {
352           fprintf (stderr, "fgets error: %s\n", strerror (errno));
353           return;
354         }
355       if (header[0] == '\n' || header[1] == '\n')
356         {
357           break;
358         }
359     }
360   if (strcmp (request_uri, "/") == 0)
361     {
362       fprintf (stream, "HTTP/1.0 200 OK\r\nContent-Length: %lu\r\n\r\n",
363                (unsigned long) strlen (ROOTPAGE));
364       fputs (ROOTPAGE, stream);
365       return;
366     }
367   if (strcmp (request_uri, "/metrics") != 0)
368     {
369       fprintf (stream,
370                "HTTP/1.0 404 Not Found\r\nContent-Length: %lu\r\n\r\n",
371                (unsigned long) strlen (NOT_FOUND_ERROR));
372       fputs (NOT_FOUND_ERROR, stream);
373       return;
374     }
375   fputs ("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n", stream);
376   dump_metrics (stream, patterns, v2);
377 }
378
379 static int
380 start_listen (u16 port)
381 {
382   struct sockaddr_in6 serveraddr;
383   int addrlen = sizeof (serveraddr);
384   int enable = 1;
385
386   int listenfd = socket (AF_INET6, SOCK_STREAM, 0);
387   if (listenfd == -1)
388     {
389       perror ("Failed opening socket");
390       return -1;
391     }
392
393   int rv =
394     setsockopt (listenfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof (int));
395   if (rv < 0)
396     {
397       perror ("Failed setsockopt");
398       close (listenfd);
399       return -1;
400     }
401
402   clib_memset (&serveraddr, 0, sizeof (serveraddr));
403   serveraddr.sin6_family = AF_INET6;
404   serveraddr.sin6_port = htons (port);
405   serveraddr.sin6_addr = in6addr_any;
406
407   if (bind (listenfd, (struct sockaddr *) &serveraddr, addrlen) < 0)
408     {
409       fprintf (stderr, "bind() error %s\n", strerror (errno));
410       close (listenfd);
411       return -1;
412     }
413   if (listen (listenfd, 1000000) != 0)
414     {
415       fprintf (stderr, "listen() error for %s\n", strerror (errno));
416       close (listenfd);
417       return -1;
418     }
419   return listenfd;
420 }
421
422 /* Socket epoll, linux-specific */
423 union my_sockaddr
424 {
425   struct sockaddr_storage storage;
426   struct sockaddr addr;
427   struct sockaddr_in sin_addr;
428   struct sockaddr_in6 sin6_addr;
429 };
430
431
432
433 int
434 main (int argc, char **argv)
435 {
436   unformat_input_t _argv, *a = &_argv;
437   u8 *stat_segment_name, *pattern = 0, **patterns = 0;
438   u16 port = SERVER_PORT;
439   char *usage =
440     "%s: usage [socket-name <name>] [port <0 - 65535>] [v2] <patterns> ...\n";
441   int rv;
442   u8 v2 = 0;
443
444   /* Allocating 256MB heap */
445   clib_mem_init (0, 256 << 20);
446
447   unformat_init_command_line (a, argv);
448
449   stat_segment_name = (u8 *) STAT_SEGMENT_SOCKET_FILE;
450
451   while (unformat_check_input (a) != UNFORMAT_END_OF_INPUT)
452     {
453       if (unformat (a, "socket-name %s", &stat_segment_name))
454         ;
455       if (unformat (a, "v2"))
456         v2 = 1;
457       else if (unformat (a, "port %d", &port))
458         ;
459       else if (unformat (a, "%s", &pattern))
460         {
461           vec_add1 (patterns, pattern);
462         }
463       else
464         {
465           fformat (stderr, usage, argv[0]);
466           exit (1);
467         }
468     }
469
470   if (vec_len (patterns) == 0)
471     {
472       fformat (stderr, usage, argv[0]);
473       exit (1);
474     }
475
476   rv = stat_segment_connect ((char *) stat_segment_name);
477   if (rv)
478     {
479       fformat (stderr, "Couldn't connect to vpp, does %s exist?\n",
480                stat_segment_name);
481       exit (1);
482     }
483
484   int fd = start_listen (port);
485   if (fd < 0)
486     {
487       exit (1);
488     }
489   for (;;)
490     {
491       int conn_sock = accept (fd, NULL, NULL);
492       if (conn_sock < 0)
493         {
494           fprintf (stderr, "Accept failed: %s", strerror (errno));
495           continue;
496         }
497       else
498         {
499           struct sockaddr_in6 clientaddr = { 0 };
500           char address[INET6_ADDRSTRLEN];
501           socklen_t addrlen;
502           getpeername (conn_sock, (struct sockaddr *) &clientaddr, &addrlen);
503           if (inet_ntop
504               (AF_INET6, &clientaddr.sin6_addr, address, sizeof (address)))
505             {
506               fprintf (stderr, "Client address is [%s]:%d\n", address,
507                        ntohs (clientaddr.sin6_port));
508             }
509         }
510
511       FILE *stream = fdopen (conn_sock, "r+");
512       if (stream == NULL)
513         {
514           fprintf (stderr, "fdopen error: %s\n", strerror (errno));
515           close (conn_sock);
516           continue;
517         }
518       /* Single reader at the moment */
519       http_handler (stream, patterns, v2);
520       fclose (stream);
521     }
522
523   stat_segment_disconnect ();
524   close (fd);
525
526   exit (0);
527 }
528
529 /*
530  * fd.io coding-style-patch-verification: ON
531  *
532  * Local Variables:
533  * eval: (c-set-style "gnu")
534  * End:
535  */