MEDIUM: stats: add proxy name filtering on the statistic page

This patch adds a "scope" box in the statistics page in order to
display only proxies with a name that contains the requested value.
The scope filter is preserved across all clicks on the page.
This commit is contained in:
de Lafond Guillaume 2013-04-15 19:27:10 +02:00 committed by Willy Tarreau
parent d9bdccda55
commit 88c278fadf
6 changed files with 187 additions and 29 deletions

View File

@ -745,4 +745,7 @@ char *env_expand(char *in);
*/
#define fddebug(msg...) do { char *_m = NULL; memprintf(&_m, ##msg); if (_m) write(-1, _m, strlen(_m)); free(_m); } while (0)
/* same as strstr() but case-insensitive */
const char *strnistr(const char *str1, int len_str1, const char *str2, int len_str2);
#endif /* _COMMON_STANDARD_H */

View File

@ -54,6 +54,11 @@
#define STAT_CLI_O_SET 10 /* set entries in tables */
#define STAT_CLI_O_STAT 11 /* dump stats */
/* HTML form to limit output scope */
#define STAT_SCOPE_TXT_MAXLEN 20 /* max len for scope substring */
#define STAT_SCOPE_INPUT_NAME "scope" /* pattern form scope name <input> in html form */
#define STAT_SCOPE_PATTERN "?" STAT_SCOPE_INPUT_NAME "="
extern struct si_applet http_stats_applet;
void stats_io_handler(struct stream_interface *si);

View File

@ -124,6 +124,8 @@ struct stream_interface {
struct proxy *px;
struct server *sv;
void *l;
int scope_str; /* limit scope to a frontend/backend substring */
int scope_len; /* length of the string above in the buffer */
int px_st; /* STAT_PX_ST* */
unsigned int flags; /* STAT_* */
int iid, type, sid; /* proxy id, type and service id if bounding of stats is enabled */

View File

@ -2695,11 +2695,25 @@ static int stats_dump_be_stats(struct stream_interface *si, struct proxy *px, in
*/
static void stats_dump_html_px_hdr(struct stream_interface *si, struct proxy *px, struct uri_auth *uri)
{
char scope_txt[STAT_SCOPE_TXT_MAXLEN + sizeof STAT_SCOPE_PATTERN];
if (px->cap & PR_CAP_BE && px->srv && (si->applet.ctx.stats.flags & STAT_ADMIN)) {
/* A form to enable/disable this proxy servers */
/* scope_txt = search pattern + search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
scope_txt[0] = 0;
if (si->applet.ctx.stats.scope_len) {
strcpy(scope_txt, STAT_SCOPE_PATTERN);
memcpy(scope_txt + strlen(STAT_SCOPE_PATTERN), bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
scope_txt[strlen(STAT_SCOPE_PATTERN) + si->applet.ctx.stats.scope_len] = 0;
}
chunk_appendf(&trash,
"<form action=\"%s\" method=\"post\">",
uri->uri_prefix);
"<form action=\"%s%s%s%s\" method=\"post\">",
uri->uri_prefix,
(si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
scope_txt);
}
/* print a new table */
@ -2768,19 +2782,19 @@ static void stats_dump_html_px_end(struct stream_interface *si, struct proxy *px
if ((px->cap & PR_CAP_BE) && px->srv && (si->applet.ctx.stats.flags & STAT_ADMIN)) {
/* close the form used to enable/disable this proxy servers */
chunk_appendf(&trash,
"Choose the action to perform on the checked servers : "
"<select name=action>"
"<option value=\"\"></option>"
"<option value=\"disable\">Disable</option>"
"<option value=\"enable\">Enable</option>"
"<option value=\"stop\">Soft Stop</option>"
"<option value=\"start\">Soft Start</option>"
"<option value=\"shutdown\">Kill Sessions</option>"
"</select>"
"<input type=\"hidden\" name=\"b\" value=\"#%d\">"
"&nbsp;<input type=\"submit\" value=\"Apply\">"
"</form>",
px->uuid);
"Choose the action to perform on the checked servers : "
"<select name=action>"
"<option value=\"\"></option>"
"<option value=\"disable\">Disable</option>"
"<option value=\"enable\">Enable</option>"
"<option value=\"stop\">Soft Stop</option>"
"<option value=\"start\">Soft Start</option>"
"<option value=\"shutdown\">Kill Sessions</option>"
"</select>"
"<input type=\"hidden\" name=\"b\" value=\"#%d\">"
"&nbsp;<input type=\"submit\" value=\"Apply\">"
"</form>",
px->uuid);
}
chunk_appendf(&trash, "<p>\n");
@ -2829,6 +2843,13 @@ static int stats_dump_proxy_to_buffer(struct stream_interface *si, struct proxy
return 1;
}
/* if the user has requested a limited output and the proxy
* name does not match, skip it.
*/
if (si->applet.ctx.stats.scope_len &&
strnistr(px->id, strlen(px->id), bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len) == NULL)
return 1;
if ((si->applet.ctx.stats.flags & STAT_BOUND) &&
(si->applet.ctx.stats.iid != -1) &&
(px->uuid != si->applet.ctx.stats.iid))
@ -3092,6 +3113,7 @@ static void stats_dump_html_head(struct uri_auth *uri)
static void stats_dump_html_info(struct stream_interface *si, struct uri_auth *uri)
{
unsigned int up = (now.tv_sec - start_date.tv_sec);
char scope_txt[STAT_SCOPE_TXT_MAXLEN + sizeof STAT_SCOPE_PATTERN];
/* WARNING! this has to fit the first packet too.
* We are around 3.5 kB, add adding entries will
@ -3147,44 +3169,70 @@ static void stats_dump_html_info(struct stream_interface *si, struct uri_auth *u
run_queue_cur, nb_tasks_cur, idle_pct
);
/* scope_txt = search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
memcpy(scope_txt, bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
scope_txt[si->applet.ctx.stats.scope_len] = '\0';
chunk_appendf(&trash,
"<li><form method=GET ACTION='%s%s%s'>Scope : <input value='%s' name='" STAT_SCOPE_INPUT_NAME "' autofocus size=8 maxlength='%d'/></form>\n",
uri->uri_prefix,
(si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
(si->applet.ctx.stats.scope_len > 0) ? scope_txt : "",
STAT_SCOPE_TXT_MAXLEN);
/* scope_txt = search pattern + search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
scope_txt[0] = 0;
if (si->applet.ctx.stats.scope_len) {
strcpy(scope_txt, STAT_SCOPE_PATTERN);
memcpy(scope_txt + strlen(STAT_SCOPE_PATTERN), bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
scope_txt[strlen(STAT_SCOPE_PATTERN) + si->applet.ctx.stats.scope_len] = 0;
}
if (si->applet.ctx.stats.flags & STAT_HIDE_DOWN)
chunk_appendf(&trash,
"<li><a href=\"%s%s%s\">Show all servers</a><br>\n",
"<li><a href=\"%s%s%s%s\">Show all servers</a><br>\n",
uri->uri_prefix,
"",
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "");
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
scope_txt);
else
chunk_appendf(&trash,
"<li><a href=\"%s%s%s\">Hide 'DOWN' servers</a><br>\n",
"<li><a href=\"%s%s%s%s\">Hide 'DOWN' servers</a><br>\n",
uri->uri_prefix,
";up",
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "");
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
scope_txt);
if (uri->refresh > 0) {
if (si->applet.ctx.stats.flags & STAT_NO_REFRESH)
chunk_appendf(&trash,
"<li><a href=\"%s%s%s\">Enable refresh</a><br>\n",
"<li><a href=\"%s%s%s%s\">Enable refresh</a><br>\n",
uri->uri_prefix,
(si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
"");
"",
scope_txt);
else
chunk_appendf(&trash,
"<li><a href=\"%s%s%s\">Disable refresh</a><br>\n",
"<li><a href=\"%s%s%s%s\">Disable refresh</a><br>\n",
uri->uri_prefix,
(si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
";norefresh");
";norefresh",
scope_txt);
}
chunk_appendf(&trash,
"<li><a href=\"%s%s%s\">Refresh now</a><br>\n",
"<li><a href=\"%s%s%s%s\">Refresh now</a><br>\n",
uri->uri_prefix,
(si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "");
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
scope_txt);
chunk_appendf(&trash,
"<li><a href=\"%s;csv%s\">CSV export</a><br>\n",
"<li><a href=\"%s;csv%s%s\">CSV export</a><br>\n",
uri->uri_prefix,
(uri->refresh > 0) ? ";norefresh" : "");
(uri->refresh > 0) ? ";norefresh" : "",
scope_txt);
chunk_appendf(&trash,
"</ul></td>"

View File

@ -2971,6 +2971,8 @@ int http_handle_stats(struct session *s, struct channel *req)
/* Was the status page requested with a POST ? */
if (unlikely(txn->meth == HTTP_METH_POST)) {
char scope_txt[STAT_SCOPE_TXT_MAXLEN + sizeof STAT_SCOPE_PATTERN];
if (si->applet.ctx.stats.flags & STAT_ADMIN) {
if (msg->msg_state < HTTP_MSG_100_SENT) {
/* If we have HTTP/1.1 and Expect: 100-continue, then we must
@ -2993,6 +2995,14 @@ int http_handle_stats(struct session *s, struct channel *req)
}
else
si->applet.ctx.stats.st_code = STAT_STATUS_DENY;
/* scope_txt = search pattern + search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
scope_txt[0] = 0;
if (si->applet.ctx.stats.scope_len) {
strcpy(scope_txt, STAT_SCOPE_PATTERN);
memcpy(scope_txt + strlen(STAT_SCOPE_PATTERN), bo_ptr(req->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
scope_txt[strlen(STAT_SCOPE_PATTERN) + si->applet.ctx.stats.scope_len] = 0;
}
/* We don't want to land on the posted stats page because a refresh will
* repost the data. We don't want this to happen on accident so we redirect
@ -3003,14 +3013,17 @@ int http_handle_stats(struct session *s, struct channel *req)
"Cache-Control: no-cache\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n"
"Location: %s;st=%s\r\n"
"Location: %s;st=%s%s%s%s\r\n"
"\r\n",
uri->uri_prefix,
((si->applet.ctx.stats.st_code > STAT_STATUS_INIT) &&
(si->applet.ctx.stats.st_code < STAT_STATUS_SIZE) &&
stat_status_codes[si->applet.ctx.stats.st_code]) ?
stat_status_codes[si->applet.ctx.stats.st_code] :
stat_status_codes[STAT_STATUS_UNKN]);
stat_status_codes[STAT_STATUS_UNKN],
(si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
(si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
scope_txt);
s->txn.status = 303;
s->logs.tv_request = now;
@ -7827,6 +7840,44 @@ int stats_check_uri(struct stream_interface *si, struct http_txn *txn, struct pr
}
h++;
}
si->applet.ctx.stats.scope_str = 0;
si->applet.ctx.stats.scope_len = 0;
h = uri + uri_auth->uri_len;
while (h <= uri + msg->sl.rq.u_l - 8) {
if (memcmp(h, STAT_SCOPE_INPUT_NAME "=", strlen(STAT_SCOPE_INPUT_NAME) + 1) == 0) {
int itx = 0;
const char *h2;
char scope_txt[STAT_SCOPE_TXT_MAXLEN + 1];
const char *err;
h += strlen(STAT_SCOPE_INPUT_NAME) + 1;
h2 = h;
si->applet.ctx.stats.scope_str = h2 - msg->chn->buf->p;
while (*h != ';' && *h != '\0' && *h != '&' && *h != ' ' && *h != '\n') {
itx++;
h++;
}
if (itx > STAT_SCOPE_TXT_MAXLEN)
itx = STAT_SCOPE_TXT_MAXLEN;
si->applet.ctx.stats.scope_len = itx;
/* scope_txt = search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
memcpy(scope_txt, h2, itx);
scope_txt[itx] = '\0';
err = invalid_char(scope_txt);
if (err) {
/* bad char in search text => clear scope */
si->applet.ctx.stats.scope_str = 0;
si->applet.ctx.stats.scope_len = 0;
}
break;
}
h++;
}
return 1;
}

View File

@ -2081,6 +2081,55 @@ char *env_expand(char *in)
return out;
}
/* same as strstr() but case-insensitive and with limit length */
const char *strnistr(const char *str1, int len_str1, const char *str2, int len_str2)
{
char *pptr, *sptr, *start;
uint slen, plen;
uint tmp1, tmp2;
if (str1 == NULL || len_str1 == 0) // search pattern into an empty string => search is not found
return NULL;
if (str2 == NULL || len_str2 == 0) // pattern is empty => every str1 match
return str1;
if (len_str1 < len_str2) // pattern is longer than string => search is not found
return NULL;
for (tmp1 = 0, start = (char *)str1, pptr = (char *)str2, slen = len_str1, plen = len_str2; slen >= plen; start++, slen--) {
while (toupper(*start) != toupper(*str2)) {
start++;
slen--;
tmp1++;
if (tmp1 >= len_str1)
return NULL;
/* if pattern longer than string */
if (slen < plen)
return NULL;
}
sptr = start;
pptr = (char *)str2;
tmp2 = 0;
while (toupper(*sptr) == toupper(*pptr)) {
sptr++;
pptr++;
tmp2++;
if (*pptr == '\0' || tmp2 == len_str2) /* end of pattern found */
return start;
if (*sptr == '\0' || tmp2 == len_str1) /* end of string found and the pattern is not fully found */
return NULL;
}
}
return NULL;
}
/*
* Local variables:
* c-indent-level: 8