summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--accel-pppd/extra/metrics.c335
1 files changed, 334 insertions, 1 deletions
diff --git a/accel-pppd/extra/metrics.c b/accel-pppd/extra/metrics.c
index c7a52ed8..15ad16da 100644
--- a/accel-pppd/extra/metrics.c
+++ b/accel-pppd/extra/metrics.c
@@ -2,10 +2,17 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
#include "triton.h"
#include "events.h"
#include "log.h"
+#include "list.h"
#include "memdebug.h"
@@ -14,9 +21,25 @@ enum metrics_format {
METRICS_FORMAT_JSON,
};
+#define METRICS_RECV_BUF_SIZE 2048
+
+struct metrics_client_t {
+ struct list_head entry;
+ struct triton_md_handler_t hnd;
+ struct sockaddr_in addr;
+ char *recv_buf;
+ int recv_pos;
+ unsigned int disconnect:1;
+};
+
static enum metrics_format conf_format = METRICS_FORMAT_PROMETHEUS;
static char *conf_address;
+static struct triton_context_t serv_ctx;
+static struct triton_md_handler_t serv_hnd;
+static LIST_HEAD(clients);
+static int serv_running;
+
static int parse_format(const char *opt, enum metrics_format *out)
{
if (!strcasecmp(opt, "prometheus")) {
@@ -30,11 +53,49 @@ static int parse_format(const char *opt, enum metrics_format *out)
return -1;
}
+/* TODO: IPv6 support. The listener, the ACL, and every sockaddr below are
+ * IPv4-only for now; bracketed-host syntax ("[::1]:8080"), AF_INET6 sockets,
+ * and IPv6 CIDRs in allowed_ips are intentionally left for a follow-up.
+ */
+static int parse_listen_address(const char *str, struct sockaddr_in *addr)
+{
+ char *buf, *colon;
+ int port;
+ int ret = -1;
+
+ buf = strdup(str);
+ if (!buf)
+ return -1;
+
+ colon = strrchr(buf, ':');
+ if (!colon)
+ goto out;
+
+ *colon = 0;
+ port = atoi(colon + 1);
+ if (port <= 0 || port > 65535)
+ goto out;
+
+ memset(addr, 0, sizeof(*addr));
+ addr->sin_family = AF_INET;
+ addr->sin_port = htons(port);
+ if (buf[0] == 0 || !strcmp(buf, "*") || !strcmp(buf, "0.0.0.0"))
+ addr->sin_addr.s_addr = htonl(INADDR_ANY);
+ else if (inet_pton(AF_INET, buf, &addr->sin_addr) != 1)
+ goto out;
+
+ ret = 0;
+out:
+ free(buf);
+ return ret;
+}
+
static int load_config(void)
{
const char *opt;
enum metrics_format fmt = METRICS_FORMAT_PROMETHEUS;
char *address = NULL;
+ struct sockaddr_in dummy;
opt = conf_get_opt("metrics", "format");
if (opt && parse_format(opt, &fmt) < 0) {
@@ -47,6 +108,10 @@ static int load_config(void)
log_emerg("metrics: 'address' option is required (host:port)\n");
return -1;
}
+ if (parse_listen_address(opt, &dummy) < 0) {
+ log_emerg("metrics: invalid address '%s', expected host:port\n", opt);
+ return -1;
+ }
address = _strdup(opt);
if (!address) {
log_emerg("metrics: out of memory while loading config\n");
@@ -61,12 +126,280 @@ static int load_config(void)
return 0;
}
+static const char *content_type(void)
+{
+ switch (conf_format) {
+ case METRICS_FORMAT_JSON:
+ return "application/json";
+ case METRICS_FORMAT_PROMETHEUS:
+ default:
+ return "text/plain; version=0.0.4; charset=utf-8";
+ }
+}
+
+static int write_all(int fd, const char *buf, int len)
+{
+ int n, total = 0;
+
+ while (total < len) {
+ n = write(fd, buf + total, len - total);
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ total += n;
+ }
+ return 0;
+}
+
+static void send_response(struct metrics_client_t *cln, int status, const char *reason,
+ const char *ctype, const char *body, int body_len)
+{
+ char header[256];
+ int hlen;
+
+ hlen = snprintf(header, sizeof(header),
+ "HTTP/1.1 %d %s\r\n"
+ "Server: accel-ppp\r\n"
+ "Content-Type: %s\r\n"
+ "Content-Length: %d\r\n"
+ "Connection: close\r\n"
+ "\r\n",
+ status, reason, ctype, body_len);
+ if (hlen <= 0 || hlen >= (int)sizeof(header))
+ return;
+
+ if (write_all(cln->hnd.fd, header, hlen) < 0)
+ return;
+ if (body_len > 0)
+ write_all(cln->hnd.fd, body, body_len);
+}
+
+static void send_simple(struct metrics_client_t *cln, int status, const char *reason)
+{
+ char body[128];
+ int len;
+
+ len = snprintf(body, sizeof(body), "%d %s\n", status, reason);
+ send_response(cln, status, reason, "text/plain; charset=utf-8", body, len);
+}
+
+static void serve_metrics(struct metrics_client_t *cln)
+{
+ const char *body = "";
+ send_response(cln, 200, "OK", content_type(), body, 0);
+}
+
+static void disconnect_client(struct metrics_client_t *cln)
+{
+ list_del(&cln->entry);
+ triton_md_unregister_handler(&cln->hnd, 1);
+ if (cln->recv_buf)
+ _free(cln->recv_buf);
+ _free(cln);
+}
+
+static void handle_request(struct metrics_client_t *cln)
+{
+ char *line_end, *space1, *space2;
+ char *method, *path;
+
+ line_end = strstr(cln->recv_buf, "\r\n");
+ if (!line_end)
+ return;
+ *line_end = 0;
+
+ method = cln->recv_buf;
+ space1 = strchr(method, ' ');
+ if (!space1) {
+ send_simple(cln, 400, "Bad Request");
+ return;
+ }
+ *space1 = 0;
+ path = space1 + 1;
+ space2 = strchr(path, ' ');
+ if (space2)
+ *space2 = 0;
+
+ if (strcmp(method, "GET")) {
+ send_simple(cln, 405, "Method Not Allowed");
+ return;
+ }
+
+ if (!strcmp(path, "/metrics"))
+ serve_metrics(cln);
+ else
+ send_simple(cln, 404, "Not Found");
+}
+
+static int cln_read(struct triton_md_handler_t *h)
+{
+ struct metrics_client_t *cln = container_of(h, typeof(*cln), hnd);
+ int n;
+
+ while (1) {
+ if (cln->recv_pos >= METRICS_RECV_BUF_SIZE - 1) {
+ send_simple(cln, 413, "Request Entity Too Large");
+ cln->disconnect = 1;
+ break;
+ }
+
+ n = read(h->fd, cln->recv_buf + cln->recv_pos,
+ METRICS_RECV_BUF_SIZE - 1 - cln->recv_pos);
+ if (n == 0) {
+ cln->disconnect = 1;
+ break;
+ }
+ if (n < 0) {
+ if (errno == EAGAIN)
+ return 0;
+ cln->disconnect = 1;
+ break;
+ }
+
+ cln->recv_pos += n;
+ cln->recv_buf[cln->recv_pos] = 0;
+
+ if (strstr(cln->recv_buf, "\r\n\r\n")) {
+ handle_request(cln);
+ cln->disconnect = 1;
+ break;
+ }
+ }
+
+ if (cln->disconnect) {
+ disconnect_client(cln);
+ return -1;
+ }
+ return 0;
+}
+
+static int serv_read(struct triton_md_handler_t *h)
+{
+ struct sockaddr_in addr;
+ socklen_t size = sizeof(addr);
+ int sock;
+ struct metrics_client_t *cln;
+
+ while (1) {
+ sock = accept(h->fd, (struct sockaddr *)&addr, &size);
+ if (sock < 0) {
+ if (errno == EAGAIN)
+ return 0;
+ log_error("metrics: accept failed: %s\n", strerror(errno));
+ continue;
+ }
+
+ if (fcntl(sock, F_SETFL, O_NONBLOCK)) {
+ log_error("metrics: failed to set nonblocking mode: %s\n", strerror(errno));
+ close(sock);
+ continue;
+ }
+
+ cln = _malloc(sizeof(*cln));
+ if (!cln) {
+ close(sock);
+ continue;
+ }
+ memset(cln, 0, sizeof(*cln));
+ cln->addr = addr;
+ cln->hnd.fd = sock;
+ cln->hnd.read = cln_read;
+ cln->recv_buf = _malloc(METRICS_RECV_BUF_SIZE);
+ if (!cln->recv_buf) {
+ close(sock);
+ _free(cln);
+ continue;
+ }
+
+ list_add_tail(&cln->entry, &clients);
+ triton_md_register_handler(&serv_ctx, &cln->hnd);
+ triton_md_enable_handler(&cln->hnd, MD_MODE_READ);
+ }
+
+ return 0;
+}
+
+static void serv_close(struct triton_context_t *ctx)
+{
+ struct metrics_client_t *cln;
+
+ while (!list_empty(&clients)) {
+ cln = list_entry(clients.next, typeof(*cln), entry);
+ disconnect_client(cln);
+ }
+
+ triton_md_unregister_handler(&serv_hnd, 1);
+ triton_context_unregister(ctx);
+}
+
+static struct triton_context_t serv_ctx = {
+ .close = serv_close,
+};
+
+static struct triton_md_handler_t serv_hnd = {
+ .read = serv_read,
+};
+
+static int start_server(void)
+{
+ struct sockaddr_in addr;
+ int reuse = 1;
+
+ if (parse_listen_address(conf_address, &addr) < 0) {
+ log_emerg("metrics: invalid listen address '%s'\n", conf_address);
+ return -1;
+ }
+
+ serv_hnd.fd = socket(PF_INET, SOCK_STREAM, 0);
+ if (serv_hnd.fd < 0) {
+ log_emerg("metrics: failed to create socket: %s\n", strerror(errno));
+ return -1;
+ }
+
+ fcntl(serv_hnd.fd, F_SETFD, fcntl(serv_hnd.fd, F_GETFD) | FD_CLOEXEC);
+ setsockopt(serv_hnd.fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
+
+ if (bind(serv_hnd.fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ log_emerg("metrics: failed to bind %s: %s\n", conf_address, strerror(errno));
+ goto err;
+ }
+
+ if (listen(serv_hnd.fd, 16) < 0) {
+ log_emerg("metrics: failed to listen on %s: %s\n", conf_address, strerror(errno));
+ goto err;
+ }
+
+ if (fcntl(serv_hnd.fd, F_SETFL, O_NONBLOCK)) {
+ log_emerg("metrics: failed to set nonblocking mode: %s\n", strerror(errno));
+ goto err;
+ }
+
+ triton_context_register(&serv_ctx, NULL);
+ triton_context_set_priority(&serv_ctx, 0);
+ triton_md_register_handler(&serv_ctx, &serv_hnd);
+ triton_md_enable_handler(&serv_hnd, MD_MODE_READ);
+ triton_context_wakeup(&serv_ctx);
+
+ serv_running = 1;
+ return 0;
+
+err:
+ close(serv_hnd.fd);
+ serv_hnd.fd = -1;
+ return -1;
+}
+
static void init(void)
{
if (load_config() < 0)
return;
- log_info2("metrics: module loaded, listen %s, format %s\n",
+ if (start_server() < 0)
+ return;
+
+ log_info2("metrics: listening on %s, format %s\n",
conf_address,
conf_format == METRICS_FORMAT_PROMETHEUS ? "prometheus" : "json");
}