blob: 68ecfdc8b0881e8a58783652fa44587c85e9a1a6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include "config.h"
#include "http.h"
#include "server_private.h"
#include "qpid/dispatch/amqp.h"
#include "qpid/dispatch/atomic.h"
#include "qpid/dispatch/protocol_adaptor.h"
#include "qpid/dispatch/threading.h"
#include "qpid/dispatch/timer.h"
#include <proton/connection_driver.h>
#include <proton/object.h>
#include <ctype.h>
#include <inttypes.h>
#include <libwebsockets.h>
#if LWS_LIBRARY_VERSION_MAJOR > 3 || (LWS_LIBRARY_VERSION_MAJOR == 3 && LWS_LIBRARY_VERSION_MINOR >= 2)
#define QD_HAVE_MODERN_LIBWEBSOCKETS 1
#endif
static const char *CIPHER_LIST = "ALL:aNULL:!eNULL:@STRENGTH"; /* Default */
static const char *IGNORED = "ignore-this-log-message";
/* Log for LWS messages. For dispatch server messages use qd_http_server_t::log */
static qd_log_source_t* http_log;
static qd_log_level_t qd_level(int lll) {
switch (lll) {
case LLL_ERR: return QD_LOG_ERROR;
case LLL_WARN: return QD_LOG_WARNING;
/* LWS is noisy compared to dispatch on the informative levels, downgrade */
case LLL_NOTICE: return QD_LOG_DEBUG;
default: return QD_LOG_TRACE; /* Everything else to trace */
}
}
static void logger(int lll, const char *line) {
if (strstr(line, IGNORED)) return;
size_t len = strlen(line);
while (len > 1 && isspace(line[len-1])) { /* Strip trailing newline */
--len;
}
qd_log(http_log, qd_level(lll), "%.*s", (int)len, line);
}
static void log_init() {
http_log = qd_log_source("HTTP");
int levels = 0;
for (int i = 0; i < LLL_COUNT; ++i) {
int lll = 1<<i;
levels |= qd_log_enabled(http_log, qd_level(lll)) ? lll : 0;
}
lws_set_log_level(levels, logger);
}
/* Intermediate write buffer: LWS needs extra header space on write. */
typedef struct buffer_t {
char *start;
size_t size, cap;
} buffer_t;
/* Ensure size bytes in buffer, make buf empty if alloc fails */
static void buffer_set_size(buffer_t *buf, size_t size) {
if (size > buf->cap) {
buf->cap = (size > buf->cap * 2) ? size : buf->cap * 2;
buf->start = realloc(buf->start, buf->cap);
}
if (buf->start) {
buf->size = size;
} else {
buf->size = buf->cap = 0;
}
}
/* AMQPWS connection: set as lws user data and qd_conn->context */
typedef struct connection_t {
pn_connection_driver_t driver;
qd_connection_t* qd_conn;
buffer_t wbuf; /* LWS requires allocated header space at start of buffer */
struct lws *wsi;
} connection_t;
typedef struct stats_request_state_t {
bool callback_completed;
bool wsi_deleted;
qdr_global_stats_t stats;
qd_http_server_t *server;
struct lws *wsi;
} stats_request_state_t;
typedef struct stats_t {
size_t current;
bool headers_sent;
stats_request_state_t *context;
} stats_t;
/* Navigating from WSI pointer to qd objects */
static qd_http_server_t *wsi_server(struct lws *wsi);
static qd_lws_listener_t *wsi_listener(struct lws *wsi);
static qd_log_source_t *wsi_log(struct lws *wsi);
/* Declare LWS callbacks and protocol list */
inline static void finalize_http(struct lws_vhost *vh, void *arg);
static int callback_http(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
static int callback_amqpws(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
static int callback_metrics(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
static int callback_healthz(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len);
static struct lws_protocols protocols[] = {
/* HTTP only protocol comes first */
{
"http-only",
callback_http,
0,
},
/* "amqp" is the official oasis AMQP over WebSocket protocol name */
{
"amqp",
callback_amqpws,
sizeof(connection_t),
},
/* "binary" is an alias for "amqp", for compatibility with clients designed
* to work with a WebSocket proxy
*/
{
"binary",
callback_amqpws,
sizeof(connection_t),
},
{
"http",
callback_metrics,
sizeof(stats_t),
},
{
"healthz",
callback_healthz,
sizeof(stats_t),
},
{ NULL, NULL, 0, 0 } /* terminator */
};
static inline int unexpected_close(struct lws *wsi, const char *msg) {
lws_close_reason(wsi, LWS_CLOSE_STATUS_UNEXPECTED_CONDITION,
(unsigned char*)msg, strlen(msg));
char peer[64];
lws_get_peer_simple(wsi, peer, sizeof(peer));
qd_log(wsi_log(wsi), QD_LOG_ERROR, "Error on HTTP connection from %s: %s", peer, msg);
return -1;
}
static int handle_events(connection_t* c) {
if (!c->qd_conn) {
return unexpected_close(c->wsi, "not-established");
}
pn_event_t *e;
while ((e = pn_connection_driver_next_event(&c->driver))) {
if (c->qd_conn && e) {
if (!qd_connection_handle(c->qd_conn, e)) {
c->qd_conn = 0; // connection closed
}
}
}
if (pn_connection_driver_write_buffer(&c->driver).size) {
lws_callback_on_writable(c->wsi);
}
if (pn_connection_driver_write_closed(&c->driver)) {
lws_close_reason(c->wsi, LWS_CLOSE_STATUS_NORMAL, NULL, 0);
return -1;
}
return 0;
}
/* The server has a bounded, thread-safe queue for external work */
typedef struct work_t {
enum { W_NONE, W_LISTEN, W_CLOSE, W_WAKE, W_STOP, W_HANDLE_STATS } type;
void *value;
} work_t;
#define WORK_MAX 8 /* Just decouple threads, not a big buffer */
typedef struct work_queue_t {
sys_mutex_t *lock;
sys_cond_t *cond;
work_t work[WORK_MAX];
size_t head, len; /* Ring buffer */
} work_queue_t;
/* HTTP Server runs in a single thread, communication from other threads via work_queue */
struct qd_http_server_t {
qd_server_t *server;
qdr_core_t *core;
sys_thread_t *thread;
work_queue_t work;
qd_log_source_t *log;
struct lws_context *context;
qd_timestamp_t now; /* Cache current time in thread_run */
qd_timestamp_t next_tick; /* Next requested tick service */
};
static void work_queue_destroy(work_queue_t *wq) {
if (wq->lock) sys_mutex_free(wq->lock);
if (wq->cond) sys_cond_free(wq->cond);
}
static void work_queue_init(work_queue_t *wq) {
wq->lock = sys_mutex();
wq->cond = sys_cond();
}
/* Block till there is space */
static void work_push(qd_http_server_t *hs, work_t w) {
work_queue_t *wq = &hs->work;
sys_mutex_lock(wq->lock);
while (wq->len == WORK_MAX) {
lws_cancel_service(hs->context); /* Wake up the run thread to clear space */
sys_cond_wait(wq->cond, wq->lock);
}
wq->work[(wq->head + wq->len) % WORK_MAX] = w;
++wq->len;
sys_mutex_unlock(wq->lock);
lws_cancel_service(hs->context); /* Wake up the run thread to handle my work */
}
/* Non-blocking, return { W_NONE, NULL } if empty */
static work_t work_pop(qd_http_server_t *hs) {
work_t w = { W_NONE, NULL };
work_queue_t *wq = &hs->work;
sys_mutex_lock(wq->lock);
if (wq->len > 0) {
w = wq->work[wq->head];
wq->head = (wq->head + 1) % WORK_MAX;
--wq->len;
sys_cond_signal(wq->cond);
}
sys_mutex_unlock(wq->lock);
return w;
}
/* Each qd_lws_listener_t is associated with an lws_vhost */
struct qd_lws_listener_t {
qd_listener_t *listener;
qd_http_server_t *server;
struct lws_vhost *vhost;
struct lws_http_mount mount;
struct lws_http_mount metrics;
struct lws_http_mount healthz;
};
void qd_lws_listener_free(qd_lws_listener_t *hl) {
if (!hl) return;
if (hl->listener) {
hl->listener->http = NULL;
qd_listener_decref(hl->listener);
}
free(hl);
}
static qd_lws_listener_t *qd_lws_listener(qd_http_server_t *hs, qd_listener_t *li) {
qd_lws_listener_t *hl = calloc(1, sizeof(*hl));
if (hl) {
hl->server = hs;
hl->listener = li;
li->http = hl;
sys_atomic_inc(&li->ref_count); /* Keep it around till qd_http_server_free() */
} else {
qd_log(hs->log, QD_LOG_CRITICAL, "No memory for HTTP listen on %s",
li->config.host_port);
}
return hl;
}
/* Linked list: first entry on each line should point to next, last line should be the
* octet-stream default.
*/
static const struct lws_protocol_vhost_options mime_types[] = {
{ &mime_types[1], NULL, ".json", "application/json" },
{ &mime_types[2], NULL, ".woff2", "font/woff2" },
{ NULL, NULL, "*", "application/octet-stream" }
};
static int is_ipv6_address(qd_http_server_t *hs, const char* host, const char* port)
{
int result = 0;
struct addrinfo *addr;
struct addrinfo hints = {0, AF_UNSPEC, SOCK_STREAM};
int code = getaddrinfo(host, port, &hints, &addr);
if (code) {
qd_log(hs->log, QD_LOG_ERROR, "getaddrinfo(%s, %s) failed with %s", host, port, gai_strerror(code));
} else {
result = addr->ai_family == AF_INET6;
freeaddrinfo(addr);
}
return result;
}
static void listener_start(qd_lws_listener_t *hl, qd_http_server_t *hs) {
log_init(); /* Update log flags at each listener */
qd_server_config_t *config = &hl->listener->config;
int port = qd_port_int(config->port);
if (port < 0) {
qd_log(hs->log, QD_LOG_ERROR, "HTTP listener %s has invalid port %s",
config->host_port, config->port);
goto error;
}
struct lws_http_mount *m = &hl->mount;
m->mountpoint = "/"; /* URL mount point */
m->mountpoint_len = strlen(m->mountpoint); /* length of the mountpoint */
m->origin = (config->http_root_dir && *config->http_root_dir) ? /* File system root */
config->http_root_dir : QPID_CONSOLE_STAND_ALONE_INSTALL_DIR;
m->def = "index.html"; /* Default file name */
m->origin_protocol = LWSMPRO_FILE; /* mount type is a directory in a filesystem */
m->extra_mimetypes = mime_types;
struct lws_http_mount *tail = m;
if (config->metrics) {
struct lws_http_mount *metrics = &hl->metrics;
tail->mount_next = metrics;
tail = metrics;
metrics->mountpoint = "/metrics";
metrics->mountpoint_len = strlen(metrics->mountpoint);
metrics->origin_protocol = LWSMPRO_CALLBACK;
metrics->protocol = "http";
metrics->origin = IGNORED;
}
if (config->healthz) {
struct lws_http_mount *healthz = &hl->healthz;
tail->mount_next = healthz;
healthz->mountpoint = "/healthz";
healthz->mountpoint_len = strlen(healthz->mountpoint);
healthz->origin_protocol = LWSMPRO_CALLBACK;
healthz->protocol = "healthz";
healthz->origin = IGNORED;
}
struct lws_context_creation_info info = {0};
info.mounts = m;
info.port = port;
info.protocols = protocols;
info.keepalive_timeout = 1;
info.ssl_cipher_list = CIPHER_LIST;
info.options |= LWS_SERVER_OPTION_VALIDATE_UTF8;
if (!is_ipv6_address(hs, config->host, config->port)) {
qd_log(hs->log, QD_LOG_NOTICE, "Disabling ipv6 on %s", config->host_port);
info.options |= LWS_SERVER_OPTION_DISABLE_IPV6;
}
if (config->ssl_profile) {
info.ssl_cert_filepath = config->ssl_certificate_file;
info.ssl_private_key_filepath = config->ssl_private_key_file;
info.ssl_private_key_password = config->ssl_password;
info.ssl_ca_filepath = config->ssl_trusted_certificate_db;
info.ssl_cipher_list = config->ssl_ciphers;
info.options |=
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT |
#ifdef QD_HAVE_MODERN_LIBWEBSOCKETS
(config->ssl_required ? 0 : LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT | LWS_SERVER_OPTION_ALLOW_HTTP_ON_HTTPS_LISTENER) |
#else
(config->ssl_required ? 0 : LWS_SERVER_OPTION_ALLOW_NON_SSL_ON_SSL_PORT) |
#endif
((config->requireAuthentication && info.ssl_ca_filepath) ? LWS_SERVER_OPTION_REQUIRE_VALID_OPENSSL_CLIENT_CERT : 0);
}
info.vhost_name = hl->listener->config.host_port;
#ifdef QD_HAVE_MODERN_LIBWEBSOCKETS
info.finalize = finalize_http;
info.finalize_arg = hl;
#endif
hl->vhost = lws_create_vhost(hs->context, &info);
if (!hl->vhost) {
qd_log(hs->log, QD_LOG_NOTICE, "Error listening for HTTP on %s", config->host_port);
goto error;
}
/* Store hl pointer in vhost */
void *vp = lws_protocol_vh_priv_zalloc(hl->vhost, &protocols[0], sizeof(hl));
memcpy(vp, &hl, sizeof(hl));
if (port == 0) {
// If a 0 (zero) is specified for a port, get the actual listening port from the listener.
const int resolved_port = lws_get_vhost_port(hl->vhost);
assert(resolved_port != -1); // already checked the vhost is successfully started
if (config->name)
qd_log(hs->log, QD_LOG_NOTICE, "Listening for HTTP on %s:%d (%s)", config->host, resolved_port, config->name);
else
qd_log(hs->log, QD_LOG_NOTICE, "Listening for HTTP on %s:%d", config->host, resolved_port);
} else {
qd_log(hs->log, QD_LOG_NOTICE, "Listening for HTTP on %s", config->host_port);
}
return;
error:
if (hl->listener->exit_on_error) {
qd_log(hs->log, QD_LOG_CRITICAL, "Shutting down, required listener failed %s",
config->host_port);
exit(1);
}
qd_lws_listener_free(hl);
}
static void listener_close(qd_lws_listener_t *hl, qd_http_server_t *hs) {
qd_server_config_t *config = &hl->listener->config;
qd_log(hs->log, QD_LOG_NOTICE, "Stopped listening for HTTP on %s", config->host_port);
lws_vhost_destroy(hl->vhost);
}
/*
* LWS callback for un-promoted HTTP connections.
* Note main HTTP file serving is handled by the "mount" struct below.
*/
static int callback_http(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
/*
* Modern LibWebSockets uses the .finalize callback on lws_context for cleanup
*/
#ifndef QD_HAVE_MODERN_LIBWEBSOCKETS
switch (reason) {
case LWS_CALLBACK_PROTOCOL_DESTROY:
finalize_http(NULL, wsi_listener(wsi));
break;
default:
break;
}
#endif
/* Do default HTTP handling for all the cases we don't care about. */
return lws_callback_http_dummy(wsi, reason, user, in, len);
}
inline static void finalize_http(struct lws_vhost *vh /*unused*/, void *arg) {
qd_lws_listener_t *listener = (qd_lws_listener_t*) arg;
qd_lws_listener_free(listener);
}
/* Wake up a connection managed by the http server thread */
static void connection_wake(qd_connection_t *qd_conn)
{
connection_t *c = qd_conn->context;
if (c && qd_conn->listener->http) {
qd_http_server_t *hs = qd_conn->listener->http->server;
work_t w = { W_WAKE, c };
work_push(hs, w);
}
}
/**
* Called on router worker thread
*/
static void handle_stats_results(void *context)
{
stats_request_state_t* state = (stats_request_state_t*) context;
if (state->wsi_deleted) {
free(state);
} else {
qd_http_server_t *hs = state->server;
if (hs) {
work_t w = { W_HANDLE_STATS, state };
work_push(hs, w);
}
}
}
/**
* Called on http thread
*/
static void handle_stats_result_HT(stats_request_state_t* state)
{
if (state->wsi_deleted) {
free(state);
} else {
state->callback_completed = true;
lws_callback_on_writable(state->wsi);
}
}
typedef int (*int_metric) (qdr_global_stats_t *stats);
typedef struct metric_definition {
const char* name;
const char* type;
int_metric value;
} metric_definition;
typedef struct allocator_metric_definition {
const char* name;
qd_alloc_stats_t *(*fn)(void);
} allocator_metric_definition;
static int stats_get_connections(qdr_global_stats_t *stats) { return stats->connections; }
static int stats_get_links(qdr_global_stats_t *stats) { return stats->links; }
static int stats_get_addrs(qdr_global_stats_t *stats) { return stats->addrs; }
static int stats_get_routers(qdr_global_stats_t *stats) { return stats->routers; }
static int stats_get_link_routes(qdr_global_stats_t *stats) { return stats->link_routes; }
static int stats_get_auto_links(qdr_global_stats_t *stats) { return stats->auto_links; }
static int stats_get_presettled_deliveries(qdr_global_stats_t *stats) { return stats->presettled_deliveries; }
static int stats_get_dropped_presettled_deliveries(qdr_global_stats_t *stats) { return stats->dropped_presettled_deliveries; }
static int stats_get_accepted_deliveries(qdr_global_stats_t *stats) { return stats->accepted_deliveries; }
static int stats_get_released_deliveries(qdr_global_stats_t *stats) { return stats->released_deliveries; }
static int stats_get_rejected_deliveries(qdr_global_stats_t *stats) { return stats->rejected_deliveries; }
static int stats_get_modified_deliveries(qdr_global_stats_t *stats) { return stats->modified_deliveries; }
static int stats_get_deliveries_ingress(qdr_global_stats_t *stats) { return stats->deliveries_ingress; }
static int stats_get_deliveries_egress(qdr_global_stats_t *stats) { return stats->deliveries_egress; }
static int stats_get_deliveries_transit(qdr_global_stats_t *stats) { return stats->deliveries_transit; }
static int stats_get_deliveries_ingress_route_container(qdr_global_stats_t *stats) { return stats->deliveries_ingress_route_container; }
static int stats_get_deliveries_egress_route_container(qdr_global_stats_t *stats) { return stats->deliveries_egress_route_container; }
static int stats_get_deliveries_delayed_1sec(qdr_global_stats_t *stats) { return stats->deliveries_delayed_1sec; }
static int stats_get_deliveries_delayed_10sec(qdr_global_stats_t *stats) { return stats->deliveries_delayed_10sec; }
static int stats_get_deliveries_stuck(qdr_global_stats_t *stats) { return stats->deliveries_stuck; }
static int stats_get_links_blocked(qdr_global_stats_t *stats) { return stats->links_blocked; }
static int stats_get_deliveries_redirected_to_fallback(qdr_global_stats_t *stats) { return stats->deliveries_redirected_to_fallback; }
qd_alloc_stats_t *alloc_stats_qd_bitmask_t(void);
qd_alloc_stats_t *alloc_stats_qd_buffer_t(void);
qd_alloc_stats_t *alloc_stats_qd_composed_field_t(void);
qd_alloc_stats_t *alloc_stats_qd_composite_t(void);
qd_alloc_stats_t *alloc_stats_qd_connection_t(void);
qd_alloc_stats_t *alloc_stats_qd_hash_handle_t(void);
qd_alloc_stats_t *alloc_stats_qd_hash_item_t(void);
qd_alloc_stats_t *alloc_stats_qd_iterator_t(void);
qd_alloc_stats_t *alloc_stats_qd_link_ref_t(void);
qd_alloc_stats_t *alloc_stats_qd_link_t(void);
qd_alloc_stats_t *alloc_stats_qd_listener_t(void);
qd_alloc_stats_t *alloc_stats_qd_log_entry_t(void);
qd_alloc_stats_t *alloc_stats_qd_management_context_t(void);
qd_alloc_stats_t *alloc_stats_qd_message_content_t(void);
qd_alloc_stats_t *alloc_stats_qd_message_t(void);
qd_alloc_stats_t *alloc_stats_qd_node_t(void);
qd_alloc_stats_t *alloc_stats_qd_parse_node_t(void);
qd_alloc_stats_t *alloc_stats_qd_parsed_field_t(void);
qd_alloc_stats_t *alloc_stats_qd_timer_t(void);
qd_alloc_stats_t *alloc_stats_qdr_action_t(void);
qd_alloc_stats_t *alloc_stats_qdr_address_config_t(void);
qd_alloc_stats_t *alloc_stats_qdr_address_t(void);
qd_alloc_stats_t *alloc_stats_qdr_connection_info_t(void);
qd_alloc_stats_t *alloc_stats_qdr_connection_t(void);
qd_alloc_stats_t *alloc_stats_qdr_connection_work_t(void);
qd_alloc_stats_t *alloc_stats_qdr_core_timer_t(void);
qd_alloc_stats_t *alloc_stats_qdr_delivery_cleanup_t(void);
qd_alloc_stats_t *alloc_stats_qdr_delivery_ref_t(void);
qd_alloc_stats_t *alloc_stats_qdr_delivery_t(void);
qd_alloc_stats_t *alloc_stats_qdr_field_t(void);
qd_alloc_stats_t *alloc_stats_qdr_general_work_t(void);
qd_alloc_stats_t *alloc_stats_qdr_link_ref_t(void);
qd_alloc_stats_t *alloc_stats_qdr_link_t(void);
qd_alloc_stats_t *alloc_stats_qdr_link_work_t(void);
qd_alloc_stats_t *alloc_stats_qdr_query_t(void);
qd_alloc_stats_t *alloc_stats_qdr_terminus_t(void);
static struct metric_definition metrics[] = {
{"qdr_connections_total", "gauge", stats_get_connections},
{"qdr_links_total", "gauge", stats_get_links},
{"qdr_addresses_total", "gauge", stats_get_addrs},
{"qdr_routers_total", "gauge", stats_get_routers},
{"qdr_link_routes_total", "gauge", stats_get_link_routes},
{"qdr_auto_links_total", "gauge", stats_get_auto_links},
{"qdr_presettled_deliveries_total", "counter", stats_get_presettled_deliveries},
{"qdr_dropped_presettled_deliveries_total", "counter", stats_get_dropped_presettled_deliveries},
{"qdr_accepted_deliveries_total", "counter", stats_get_accepted_deliveries},
{"qdr_released_deliveries_total", "counter", stats_get_released_deliveries},
{"qdr_rejected_deliveries_total", "counter", stats_get_rejected_deliveries},
{"qdr_modified_deliveries_total", "counter", stats_get_modified_deliveries},
{"qdr_deliveries_ingress_total", "counter", stats_get_deliveries_ingress},
{"qdr_deliveries_egress_total", "counter", stats_get_deliveries_egress},
{"qdr_deliveries_transit_total", "counter", stats_get_deliveries_transit},
{"qdr_deliveries_ingress_route_container_total", "counter", stats_get_deliveries_ingress_route_container},
{"qdr_deliveries_egress_route_container_total", "counter", stats_get_deliveries_egress_route_container},
{"qdr_deliveries_delayed_1sec_total", "counter", stats_get_deliveries_delayed_1sec},
{"qdr_deliveries_delayed_10sec_total", "counter", stats_get_deliveries_delayed_10sec},
{"qdr_deliveries_stuck_total", "gauge", stats_get_deliveries_stuck},
{"qdr_links_blocked_total", "gauge", stats_get_links_blocked},
{"qdr_deliveries_redirected_to_fallback_total", "counter", stats_get_deliveries_redirected_to_fallback}
};
static size_t metrics_length = sizeof(metrics)/sizeof(metrics[0]);
static struct allocator_metric_definition allocator_metrics[] = {
{"qdr_allocator_qd_bitmask_t", alloc_stats_qd_bitmask_t},
{"qdr_allocator_qd_buffer_t", alloc_stats_qd_buffer_t},
{"qdr_allocator_qd_composed_field_t", alloc_stats_qd_composed_field_t},
{"qdr_allocator_qd_composite_t", alloc_stats_qd_composite_t},
{"qdr_allocator_qd_connection_t", alloc_stats_qd_connection_t},
{"qdr_allocator_qd_hash_handle_t", alloc_stats_qd_hash_handle_t},
{"qdr_allocator_qd_hash_item_t", alloc_stats_qd_hash_item_t},
{"qdr_allocator_qd_iterator_t", alloc_stats_qd_iterator_t},
{"qdr_allocator_qd_link_ref_t", alloc_stats_qd_link_ref_t},
{"qdr_allocator_qd_link_t", alloc_stats_qd_link_t},
{"qdr_allocator_qd_listener_t", alloc_stats_qd_listener_t},
{"qdr_allocator_qd_log_entry_t", alloc_stats_qd_log_entry_t},
{"qdr_allocator_qd_management_context_t", alloc_stats_qd_management_context_t},
{"qdr_allocator_qd_message_content_t", alloc_stats_qd_message_content_t},
{"qdr_allocator_qd_message_t", alloc_stats_qd_message_t},
{"qdr_allocator_qd_node_t", alloc_stats_qd_node_t},
{"qdr_allocator_qd_parse_node_t", alloc_stats_qd_parse_node_t},
{"qdr_allocator_qd_parsed_field_t", alloc_stats_qd_parsed_field_t},
{"qdr_allocator_qd_timer_t", alloc_stats_qd_timer_t},
{"qdr_allocator_qdr_action_t", alloc_stats_qdr_action_t},
{"qdr_allocator_qdr_address_config_t", alloc_stats_qdr_address_config_t},
{"qdr_allocator_qdr_address_t", alloc_stats_qdr_address_t},
{"qdr_allocator_qdr_connection_info_t", alloc_stats_qdr_connection_info_t},
{"qdr_allocator_qdr_connection_t", alloc_stats_qdr_connection_t},
{"qdr_allocator_qdr_connection_work_t", alloc_stats_qdr_connection_work_t},
{"qdr_allocator_qdr_core_timer_t", alloc_stats_qdr_core_timer_t},
{"qdr_allocator_qdr_delivery_cleanup_t", alloc_stats_qdr_delivery_cleanup_t},
{"qdr_allocator_qdr_delivery_ref_t", alloc_stats_qdr_delivery_ref_t},
{"qdr_allocator_qdr_delivery_t", alloc_stats_qdr_delivery_t},
{"qdr_allocator_qdr_field_t", alloc_stats_qdr_field_t},
{"qdr_allocator_qdr_general_work_t", alloc_stats_qdr_general_work_t},
{"qdr_allocator_qdr_link_ref_t", alloc_stats_qdr_link_ref_t},
{"qdr_allocator_qdr_link_t", alloc_stats_qdr_link_t},
{"qdr_allocator_qdr_link_work_t", alloc_stats_qdr_link_work_t},
{"qdr_allocator_qdr_query_t", alloc_stats_qdr_query_t},
{"qdr_allocator_qdr_terminus_t", alloc_stats_qdr_terminus_t}
};
static size_t allocator_metrics_length = sizeof(allocator_metrics)/sizeof(allocator_metrics[0]);
#define ALLOC_DATA(S, F) ((allocator_field) {#F, (S!=NULL? S->F: 0)})
typedef struct allocator_field {
const char* name;
uint64_t value;
} allocator_field;
static bool write_stats(uint8_t **position, const uint8_t * const end, const char* name, const char* type, int value)
{
//11 chars + type + 2*name + 20 chars for int
// average metric name size is 30 bytes
// average metric type size is 8 bytes
// current number of metrics is 22
// total metric buffer size = 22 * (11 + 8 + 2*30 + 20) = 2178
size_t length = 11 + strlen(type) + strlen(name)*2 + 20;
if (end - *position >= length) {
*position += lws_snprintf((char*) *position, end - *position, "# TYPE %s %s\n", name, type);
*position += lws_snprintf((char*) *position, end - *position, "%s %i\n", name, value);
return true;
} else {
return false;
}
}
static bool write_allocator_stats(uint8_t **position, const uint8_t * const end, const char* name, allocator_field field)
{
// 30 chars (static) + 2*name + 2*field.name + 20 for int
// average allocator metric name size is 54 bytes (name:field.name)
// current number of metrics is 180
// total allocator buffer size = 180 * (30 + 2*54 + 20) = 28440
size_t length = 30 + strlen(name)*2 + strlen(field.name)*2 + 20;
if (end - *position >= length) {
*position += lws_snprintf((char*) *position, end - *position, "# TYPE %s:%s_bytes gauge\n", name, field.name);
*position += lws_snprintf((char*) *position, end - *position, "%s:%s_bytes %"PRIu64"\n", name, field.name, field.value);
return true;
} else {
return false;
}
}
static bool write_metric(uint8_t **position, const uint8_t * const end, metric_definition* definition, qdr_global_stats_t* stats)
{
return write_stats(position, end, definition->name, definition->type, definition->value(stats));
}
static bool write_allocator_metric(uint8_t **position, const uint8_t * const end, allocator_metric_definition* definition)
{
qd_alloc_stats_t *allocator_stats = definition->fn();
if (!write_allocator_stats(position, end, definition->name, ALLOC_DATA(allocator_stats, total_alloc_from_heap))) return false;
if (!write_allocator_stats(position, end, definition->name, ALLOC_DATA(allocator_stats, total_free_to_heap))) return false;
if (!write_allocator_stats(position, end, definition->name, ALLOC_DATA(allocator_stats, held_by_threads))) return false;
if (!write_allocator_stats(position, end, definition->name, ALLOC_DATA(allocator_stats, batches_rebalanced_to_threads))) return false;
if (!write_allocator_stats(position, end, definition->name, ALLOC_DATA(allocator_stats, batches_rebalanced_to_global))) return false;
return true;
}
static int add_header_by_name(struct lws *wsi, const char* name, const char* value, uint8_t** position, uint8_t* end)
{
return lws_add_http_header_by_name(wsi, (unsigned char*) name, (unsigned char*) value, strlen(value), position, end);
}
static int callback_metrics(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
qd_http_server_t *hs = wsi_server(wsi);
stats_t *stats = (stats_t*) user;
// rationale for buffer size is explained at write_stats and write_allocator_stats
uint8_t buffer[LWS_PRE + 30618];
uint8_t *start = &buffer[LWS_PRE], *position = start, *end = &buffer[sizeof(buffer) - LWS_PRE - 1];
switch (reason) {
case LWS_CALLBACK_HTTP: {
stats->context = NEW(stats_request_state_t);
ZERO(stats->context);
stats->context->wsi = wsi;
stats->context->server = hs;
//request stats from core thread
qdr_request_global_stats(hs->core, &stats->context->stats, handle_stats_results, (void*) stats->context);
return 0;
}
case LWS_CALLBACK_HTTP_WRITEABLE: {
//encode stats into buffer
if (!stats->headers_sent) {
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &position, end)
|| add_header_by_name(wsi, "content-type:", "text/plain", &position, end)
|| add_header_by_name(wsi, "connection:", "close", &position, end))
return 1;
if (lws_finalize_http_header(wsi, &position, end))
return 1;
stats->headers_sent = true;
}
while (stats->current < metrics_length) {
if (write_metric(&position, end, &metrics[stats->current], &stats->context->stats)) {
stats->current++;
qd_log(hs->log, QD_LOG_DEBUG, "wrote metric %lu of %lu", stats->current, metrics_length);
} else {
qd_log(hs->log, QD_LOG_WARNING, "insufficient space in buffer");
break;
}
}
int alloc_cur = 0;
while (alloc_cur < allocator_metrics_length) {
if (write_allocator_metric(&position, end, &allocator_metrics[alloc_cur])) {
qd_log(hs->log, QD_LOG_DEBUG, "wrote allocator metric %i of %lu", alloc_cur, allocator_metrics_length);
alloc_cur++;
} else {
qd_log(hs->log, QD_LOG_WARNING, "insufficient space in buffer");
break;
}
}
int n = (stats->current < metrics_length) || (alloc_cur < allocator_metrics_length) ? LWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL;
//write buffer
size_t available = position - start;
if (lws_write(wsi, (unsigned char*) start, available, n) != available)
return 1;
if (n == LWS_WRITE_HTTP_FINAL) {
if (lws_http_transaction_completed(wsi)) return -1;
} else {
lws_callback_on_writable(wsi);
}
return 0;
}
case LWS_CALLBACK_CLOSED: {
stats->context->wsi_deleted = true;
if (stats->context->callback_completed) {
free(stats->context);
}
}
default:
return 0;
}
}
static int callback_healthz(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
qd_http_server_t *hs = wsi_server(wsi);
stats_t *stats = (stats_t*) user;
uint8_t buffer[LWS_PRE + 2048];
uint8_t *start = &buffer[LWS_PRE], *position = start, *end = &buffer[sizeof(buffer) - LWS_PRE - 1];
switch (reason) {
case LWS_CALLBACK_HTTP: {
stats->context = NEW(stats_request_state_t);
ZERO(stats->context);
stats->context->wsi = wsi;
stats->context->server = hs;
//make dummy request for stats (pass in null ptr); this still exercises the
//path through core thread and back through callback on io thread which is
//a reasonable initial liveness check
qdr_request_global_stats(hs->core, 0, handle_stats_results, (void*) stats->context);
return 0;
}
case LWS_CALLBACK_HTTP_WRITEABLE: {
//encode stats into buffer
if (!stats->headers_sent) {
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &position, end)
|| add_header_by_name(wsi, "content-type:", "text/plain", &position, end)
|| lws_add_http_header_content_length(wsi, 3, &position, end))
return 1;
if (lws_finalize_http_header(wsi, &position, end))
return 1;
stats->headers_sent = true;
}
position += lws_snprintf((char*) position, end - position, "OK\n");
int n = LWS_WRITE_HTTP_FINAL;
//write buffer
size_t available = position - start;
if (lws_write(wsi, (unsigned char*) start, available, n) != available)
return 1;
else if (lws_http_transaction_completed(wsi))
return -1;
else return 0;
}
case LWS_CALLBACK_CLOSED: {
stats->context->wsi_deleted = true;
if (stats->context->callback_completed) {
free(stats->context);
}
}
default:
return 0;
}
}
/* Callbacks for promoted AMQP over WS connections. */
static int callback_amqpws(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
qd_http_server_t *hs = wsi_server(wsi);
connection_t *c = (connection_t*)user;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED: {
/* Upgrade accepted HTTP connection to AMQPWS */
memset(c, 0, sizeof(*c));
c->wsi = wsi;
qd_lws_listener_t *hl = wsi_listener(wsi);
if (hl == NULL || !hl->listener->config.websockets) {
return unexpected_close(c->wsi, "cannot-upgrade");
}
c->qd_conn = qd_server_connection(hs->server, &hl->listener->config);
if (c->qd_conn == NULL) {
return unexpected_close(c->wsi, "out-of-memory");
}
c->qd_conn->context = c;
c->qd_conn->wake = connection_wake;
c->qd_conn->listener = hl->listener;
lws_get_peer_simple(wsi, c->qd_conn->rhost, sizeof(c->qd_conn->rhost));
int err = pn_connection_driver_init(&c->driver, c->qd_conn->pn_conn, NULL);
if (err) {
return unexpected_close(c->wsi, pn_code(err));
}
strncpy(c->qd_conn->rhost_port, c->qd_conn->rhost, sizeof(c->qd_conn->rhost_port));
qd_log(hs->log, QD_LOG_DEBUG,
"[%"PRIu64"] upgraded HTTP connection from %s to AMQPWS",
qd_connection_connection_id(c->qd_conn), qd_connection_name(c->qd_conn));
return handle_events(c);
}
case LWS_CALLBACK_SERVER_WRITEABLE: {
if (handle_events(c)) return -1;
pn_bytes_t dbuf = pn_connection_driver_write_buffer(&c->driver);
if (dbuf.size) {
/* lws_write() demands LWS_PRE bytes of free space before the data,
* so we must copy from the driver's buffer to larger temporary wbuf
*/
buffer_set_size(&c->wbuf, LWS_PRE + dbuf.size);
if (c->wbuf.start == NULL) {
return unexpected_close(c->wsi, "out-of-memory");
}
unsigned char* buf = (unsigned char*)c->wbuf.start + LWS_PRE;
memcpy(buf, dbuf.start, dbuf.size);
ssize_t wrote = lws_write(wsi, buf, dbuf.size, LWS_WRITE_BINARY);
if (wrote < 0) {
pn_connection_driver_write_close(&c->driver);
return unexpected_close(c->wsi, "write-error");
} else {
pn_connection_driver_write_done(&c->driver, wrote);
}
}
return handle_events(c);
}
case LWS_CALLBACK_RECEIVE: {
while (len > 0) {
if (handle_events(c)) return -1;
pn_rwbytes_t dbuf = pn_connection_driver_read_buffer(&c->driver);
if (dbuf.size == 0) {
return unexpected_close(c->wsi, "unexpected-data");
}
size_t copy = (len < dbuf.size) ? len : dbuf.size;
memcpy(dbuf.start, in, copy);
pn_connection_driver_read_done(&c->driver, copy);
len -= copy;
in = (char*)in + copy;
}
return handle_events(c);
}
case LWS_CALLBACK_USER: {
pn_timestamp_t next_tick = pn_transport_tick(c->driver.transport, hs->now);
if (next_tick && next_tick > hs->now && next_tick < hs->next_tick) {
hs->next_tick = next_tick;
}
return handle_events(c);
}
case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: {
pn_connection_driver_read_close(&c->driver);
return handle_events(c);
}
case LWS_CALLBACK_CLOSED: {
if (c->driver.transport) {
pn_connection_driver_close(&c->driver);
handle_events(c);
}
pn_connection_driver_destroy(&c->driver);
free(c->wbuf.start);
return -1;
}
default:
return 0;
}
}
#define DEFAULT_TICK 1000
static void* http_thread_run(void* v) {
qd_http_server_t *hs = v;
qd_log(hs->log, QD_LOG_INFO, "HTTP server thread running");
int result = 0;
while(result >= 0) {
/* Send a USER event to run transport ticks, may decrease hs->next_tick. */
hs->now = qd_timer_now();
hs->next_tick = hs->now + DEFAULT_TICK;
lws_callback_all_protocol(hs->context, &protocols[1], LWS_CALLBACK_USER);
lws_callback_all_protocol(hs->context, &protocols[2], LWS_CALLBACK_USER);
pn_millis_t timeout = (hs->next_tick > hs->now) ? hs->next_tick - hs->now : 1;
result = lws_service(hs->context, timeout);
/* Process any work items on the queue */
for (work_t w = work_pop(hs); w.type != W_NONE; w = work_pop(hs)) {
switch (w.type) {
case W_NONE:
break;
case W_STOP:
result = -1;
break;
case W_LISTEN:
listener_start((qd_lws_listener_t*)w.value, hs);
break;
case W_CLOSE:
listener_close((qd_lws_listener_t*)w.value, hs);
break;
case W_HANDLE_STATS:
handle_stats_result_HT((stats_request_state_t*) w.value);
break;
case W_WAKE: {
connection_t *c = w.value;
pn_collector_put(c->driver.collector, PN_OBJECT, c->driver.connection,
PN_CONNECTION_WAKE);
handle_events(c);
break;
}
}
}
}
qd_log(hs->log, QD_LOG_INFO, "HTTP server thread exit");
return NULL;
}
void qd_http_server_stop(qd_http_server_t *hs) {
if (!hs) return;
if (hs->thread) {
/* Thread safe, stop via work queue then clean up */
work_t work = { W_STOP, NULL };
work_push(hs, work);
sys_thread_join(hs->thread);
sys_thread_free(hs->thread);
hs->thread = NULL;
}
}
void qd_http_server_free(qd_http_server_t *hs) {
if (!hs) return;
qd_http_server_stop(hs);
work_queue_destroy(&hs->work);
if (hs->context) lws_context_destroy(hs->context);
free(hs);
}
qd_http_server_t *qd_http_server(qd_server_t *s, qd_log_source_t *log) {
log_init();
qd_http_server_t *hs = calloc(1, sizeof(*hs));
if (hs) {
work_queue_init(&hs->work);
struct lws_context_creation_info info = {0};
info.gid = info.uid = -1;
info.user = hs;
info.server_string = QD_CONNECTION_PROPERTY_PRODUCT_VALUE;
info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
LWS_SERVER_OPTION_SKIP_SERVER_CANONICAL_NAME |
LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.max_http_header_pool = 32;
info.timeout_secs = 1;
hs->context = lws_create_context(&info);
hs->server = s;
hs->log = log; /* For messages from this file */
hs->core = 0; // not yet available
if (!hs->context) {
qd_log(hs->log, QD_LOG_CRITICAL, "No memory starting HTTP server");
qd_http_server_free(hs);
hs = NULL;
}
}
return hs;
}
/* Thread safe calls that put items on work queue */
qd_lws_listener_t *qd_http_server_listen(qd_http_server_t *hs, qd_listener_t *li)
{
hs->core = qd_dispatch_router_core(qd_server_dispatch(hs->server));
sys_mutex_lock(hs->work.lock);
if (!hs->thread) {
hs->thread = sys_thread(http_thread_run, hs);
}
bool ok = hs->thread;
sys_mutex_unlock(hs->work.lock);
if (!ok) return NULL;
qd_lws_listener_t *hl = qd_lws_listener(hs, li);
if (hl) {
work_t w = { W_LISTEN, hl };
work_push(hs, w);
}
return hl;
}
void qd_lws_listener_close(qd_lws_listener_t *hl)
{
work_t w = { W_CLOSE, hl };
work_push(hl->server, w);
}
static qd_http_server_t *wsi_server(struct lws *wsi) {
return (qd_http_server_t*)lws_context_user(lws_get_context(wsi));
}
static qd_lws_listener_t *wsi_listener(struct lws *wsi) {
qd_lws_listener_t *hl = NULL;
struct lws_vhost *vhost = lws_get_vhost(wsi);
if (vhost) { /* Get qd_lws_listener from vhost data */
void *vp = lws_protocol_vh_priv_get(vhost, &protocols[0]);
memcpy(&hl, vp, sizeof(hl));
}
return hl;
}
static qd_log_source_t *wsi_log(struct lws *wsi) {
return wsi_server(wsi)->log;
}