mirror of
http://git.haproxy.org/git/haproxy.git/
synced 2025-02-20 04:37:04 +00:00
MEDIUM: stats: Add JSON output option to show (info|stat)
Add a json parameter to show (info|stat) which will output information in JSON format. A follow-up patch will add a JSON schema which describes the format of the JSON output of these commands. The JSON output is without any extra whitespace in order to reduce the volume of output. For human consumption passing the output through a pretty printer may be helpful. e.g.: $ echo "show info json" | socat /var/run/haproxy.stat stdio | \ python -m json.tool STAT_STARTED has bee added in order to track if show output has begun or not. This is used in order to allow the JSON output routines to only insert a "," between elements when needed. I would value any feedback on how this might be done better. Signed-off-by: Simon Horman <horms@verge.net.au>
This commit is contained in:
parent
2019f95997
commit
05ee213f8b
@ -1769,16 +1769,18 @@ show errors [<iid>|<proxy>] [request|response]
|
||||
show backend
|
||||
Dump the list of backends available in the running process
|
||||
|
||||
show info [typed]
|
||||
show info [typed|json]
|
||||
Dump info about haproxy status on current process. If "typed" is passed as an
|
||||
optional argument, field numbers, names and types are emitted as well so that
|
||||
external monitoring products can easily retrieve, possibly aggregate, then
|
||||
report information found in fields they don't know. Each field is dumped on
|
||||
its own line. By default, the format contains only two columns delimited by a
|
||||
colon (':'). The left one is the field name and the right one is the value.
|
||||
It is very important to note that in typed output format, the dump for a
|
||||
single object is contiguous so that there is no need for a consumer to store
|
||||
everything at once.
|
||||
its own line. If "json" is passed as an optional argument then
|
||||
information provided by "typed" output is provided in JSON format as a
|
||||
list of JSON objects. By default, the format contains only two columns
|
||||
delimited by a colon (':'). The left one is the field name and the right
|
||||
one is the value. It is very important to note that in typed output
|
||||
format, the dump for a single object is contiguous so that there is no
|
||||
need for a consumer to store everything at once.
|
||||
|
||||
When using the typed output format, each line is made of 4 columns delimited
|
||||
by colons (':'). The first column is a dot-delimited series of 3 elements. The
|
||||
@ -1855,6 +1857,16 @@ show info [typed]
|
||||
6.Uptime.2:MDP:str:0d 0h01m28s
|
||||
(...)
|
||||
|
||||
The format of JSON output is described in a schema which may be output
|
||||
using "show schema json" (to be implemented).
|
||||
|
||||
The JSON output contains no extra whitespace in order to reduce the
|
||||
volume of output. For human consumption passing the output through a
|
||||
pretty printer may be helpful. Example :
|
||||
|
||||
$ echo "show info json" | socat /var/run/haproxy.sock stdio | \
|
||||
python -m json.tool
|
||||
|
||||
show map [<map>]
|
||||
Dump info about map converters. Without argument, the list of all available
|
||||
maps is returned. If a <map> is specified, its contents are dumped. <map> is
|
||||
@ -1986,11 +1998,12 @@ show sess <id>
|
||||
The special id "all" dumps the states of all sessions, which must be avoided
|
||||
as much as possible as it is highly CPU intensive and can take a lot of time.
|
||||
|
||||
show stat [{<iid>|<proxy>} <type> <sid>] [typed]
|
||||
Dump statistics using the CSV format, or using the extended typed output
|
||||
format described in the section above if "typed" is passed after the other
|
||||
arguments. By passing <id>, <type> and <sid>, it is possible to dump only
|
||||
selected items :
|
||||
show stat [{<iid>|<proxy>} <type> <sid>] [typed|json]
|
||||
Dump statistics using the CSV format; using the extended typed output
|
||||
format described in the section above if "typed" is passed after the
|
||||
other arguments; or in JSON if "json" is passed after the other arguments
|
||||
. By passing <id>, <type> and <sid>, it is possible to dump only selected
|
||||
items :
|
||||
- <iid> is a proxy ID, -1 to dump everything. Alternatively, a proxy name
|
||||
<proxy> may be specified. In this case, this proxy's ID will be used as
|
||||
the ID selector.
|
||||
@ -2123,6 +2136,16 @@ show stat [{<iid>|<proxy>} <type> <sid>] [typed]
|
||||
B.3.0.6.slim.2:MGP:u32:1000
|
||||
(...)
|
||||
|
||||
The format of JSON output is described in a schema which may be output
|
||||
using "show schema json" (to be implemented).
|
||||
|
||||
The JSON output contains no extra whitespace in order to reduce the
|
||||
volume of output. For human consumption passing the output through a
|
||||
pretty printer may be helpful. Example :
|
||||
|
||||
$ echo "show stat json" | socat /var/run/haproxy.sock stdio | \
|
||||
python -m json.tool
|
||||
|
||||
show stat resolvers [<resolvers section id>]
|
||||
Dump statistics for the given resolvers section, or all resolvers sections
|
||||
if no section is supplied.
|
||||
|
@ -23,11 +23,13 @@
|
||||
/* Flags for applet.ctx.stats.flags */
|
||||
#define STAT_FMT_HTML 0x00000001 /* dump the stats in HTML format */
|
||||
#define STAT_FMT_TYPED 0x00000002 /* use the typed output format */
|
||||
#define STAT_FMT_JSON 0x00000004 /* dump the stats in JSON format */
|
||||
#define STAT_HIDE_DOWN 0x00000008 /* hide 'down' servers in the stats page */
|
||||
#define STAT_NO_REFRESH 0x00000010 /* do not automatically refresh the stats page */
|
||||
#define STAT_ADMIN 0x00000020 /* indicate a stats admin level */
|
||||
#define STAT_CHUNKED 0x00000040 /* use chunked encoding (HTTP/1.1) */
|
||||
#define STAT_BOUND 0x00800000 /* bound statistics to selected proxies/types/services */
|
||||
#define STAT_STARTED 0x01000000 /* some output has occurred */
|
||||
|
||||
#define STATS_TYPE_FE 0
|
||||
#define STATS_TYPE_BE 1
|
||||
@ -213,6 +215,9 @@ enum field_scope {
|
||||
FS_MASK = 0xFF000000,
|
||||
};
|
||||
|
||||
/* Please consider updating stats_dump_fields_*() and
|
||||
* stats_dump_.*_info_fields() when modifying struct field or related enums.
|
||||
*/
|
||||
struct field {
|
||||
uint32_t type;
|
||||
union {
|
||||
|
272
src/stats.c
272
src/stats.c
@ -229,6 +229,7 @@ static struct field stats[ST_F_TOTAL_FIELDS];
|
||||
* http_stats_io_handler()
|
||||
* -> stats_dump_stat_to_buffer() // same as above, but used for CSV or HTML
|
||||
* -> stats_dump_csv_header() // emits the CSV headers (same as above)
|
||||
* -> stats_dump_json_header() // emits the JSON headers (same as above)
|
||||
* -> stats_dump_html_head() // emits the HTML headers
|
||||
* -> stats_dump_html_info() // emits the equivalent of "show info" at the top
|
||||
* -> stats_dump_proxy_to_buffer() // same as above, valid for CSV and HTML
|
||||
@ -239,6 +240,7 @@ static struct field stats[ST_F_TOTAL_FIELDS];
|
||||
* -> stats_dump_be_stats()
|
||||
* -> stats_dump_html_px_end()
|
||||
* -> stats_dump_html_end() // emits HTML trailer
|
||||
* -> stats_dump_json_end() // emits JSON trailer
|
||||
*/
|
||||
|
||||
|
||||
@ -294,6 +296,58 @@ int stats_emit_typed_data_field(struct chunk *out, const struct field *f)
|
||||
}
|
||||
}
|
||||
|
||||
/* Limit JSON integer values to the range [-(2**53)+1, (2**53)-1] as per
|
||||
* the recommendation for interoperable integers in section 6 of RFC 7159.
|
||||
*/
|
||||
#define JSON_INT_MAX ((1ULL << 53) - 1)
|
||||
#define JSON_INT_MIN (0 - JSON_INT_MAX)
|
||||
|
||||
/* Emits a stats field value and its type in JSON.
|
||||
* Returns non-zero on success, 0 on error.
|
||||
*/
|
||||
int stats_emit_json_data_field(struct chunk *out, const struct field *f)
|
||||
{
|
||||
int old_len;
|
||||
char buf[20];
|
||||
const char *type, *value = buf, *quote = "";
|
||||
|
||||
switch (field_format(f, 0)) {
|
||||
case FF_EMPTY: return 1;
|
||||
case FF_S32: type = "\"s32\"";
|
||||
snprintf(buf, sizeof(buf), "%d", f->u.s32);
|
||||
break;
|
||||
case FF_U32: type = "\"u32\"";
|
||||
snprintf(buf, sizeof(buf), "%u", f->u.u32);
|
||||
break;
|
||||
case FF_S64: type = "\"s64\"";
|
||||
if (f->u.s64 < JSON_INT_MIN || f->u.s64 > JSON_INT_MAX)
|
||||
return 0;
|
||||
type = "\"s64\"";
|
||||
snprintf(buf, sizeof(buf), "%lld", (long long)f->u.s64);
|
||||
break;
|
||||
case FF_U64: if (f->u.u64 > JSON_INT_MAX)
|
||||
return 0;
|
||||
type = "\"u64\"";
|
||||
snprintf(buf, sizeof(buf), "%llu",
|
||||
(unsigned long long) f->u.u64);
|
||||
break;
|
||||
case FF_STR: type = "\"str\"";
|
||||
value = field_str(f, 0);
|
||||
quote = "\"";
|
||||
break;
|
||||
default: snprintf(buf, sizeof(buf), "%u", f->type);
|
||||
type = buf;
|
||||
value = "unknown";
|
||||
quote = "\"";
|
||||
break;
|
||||
}
|
||||
|
||||
old_len = out->len;
|
||||
chunk_appendf(out, ",\"value\":{\"type\":%s,\"value\":%s%s%s}",
|
||||
type, quote, value, quote);
|
||||
return !(old_len == out->len);
|
||||
}
|
||||
|
||||
/* Emits an encoding of the field type on 3 characters followed by a delimiter.
|
||||
* Returns non-zero on success, 0 if the buffer is full.
|
||||
*/
|
||||
@ -337,6 +391,55 @@ int stats_emit_field_tags(struct chunk *out, const struct field *f, char delim)
|
||||
return chunk_appendf(out, "%c%c%c%c", origin, nature, scope, delim);
|
||||
}
|
||||
|
||||
/* Emits an encoding of the field type as JSON.
|
||||
* Returns non-zero on success, 0 if the buffer is full.
|
||||
*/
|
||||
int stats_emit_json_field_tags(struct chunk *out, const struct field *f)
|
||||
{
|
||||
const char *origin, *nature, *scope;
|
||||
int old_len;
|
||||
|
||||
switch (field_origin(f, 0)) {
|
||||
case FO_METRIC: origin = "Metric"; break;
|
||||
case FO_STATUS: origin = "Status"; break;
|
||||
case FO_KEY: origin = "Key"; break;
|
||||
case FO_CONFIG: origin = "Config"; break;
|
||||
case FO_PRODUCT: origin = "Product"; break;
|
||||
default: origin = "Unknown"; break;
|
||||
}
|
||||
|
||||
switch (field_nature(f, 0)) {
|
||||
case FN_GAUGE: nature = "Gauge"; break;
|
||||
case FN_LIMIT: nature = "Limit"; break;
|
||||
case FN_MIN: nature = "Min"; break;
|
||||
case FN_MAX: nature = "Max"; break;
|
||||
case FN_RATE: nature = "Rate"; break;
|
||||
case FN_COUNTER: nature = "Counter"; break;
|
||||
case FN_DURATION: nature = "Duration"; break;
|
||||
case FN_AGE: nature = "Age"; break;
|
||||
case FN_TIME: nature = "Time"; break;
|
||||
case FN_NAME: nature = "Name"; break;
|
||||
case FN_OUTPUT: nature = "Output"; break;
|
||||
case FN_AVG: nature = "Avg"; break;
|
||||
default: nature = "Unknown"; break;
|
||||
}
|
||||
|
||||
switch (field_scope(f, 0)) {
|
||||
case FS_PROCESS: scope = "Process"; break;
|
||||
case FS_SERVICE: scope = "Service"; break;
|
||||
case FS_SYSTEM: scope = "System"; break;
|
||||
case FS_CLUSTER: scope = "Cluster"; break;
|
||||
default: scope = "Unknown"; break;
|
||||
}
|
||||
|
||||
old_len = out->len;
|
||||
chunk_appendf(out, "\"tags\":{"
|
||||
"\"origin\":\"%s\","
|
||||
"\"nature\":\"%s\","
|
||||
"\"scope\":\"%s\""
|
||||
"}", origin, nature, scope);
|
||||
return !(old_len == out->len);
|
||||
}
|
||||
|
||||
/* Dump all fields from <stats> into <out> using CSV format */
|
||||
static int stats_dump_fields_csv(struct chunk *out, const struct field *stats)
|
||||
@ -381,6 +484,123 @@ static int stats_dump_fields_typed(struct chunk *out, const struct field *stats)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Dump all fields from <stats> into <out> using the "show info json" format */
|
||||
static int stats_dump_json_info_fields(struct chunk *out,
|
||||
const struct field *info)
|
||||
{
|
||||
int field;
|
||||
int started = 0;
|
||||
|
||||
if (!chunk_strcat(out, "["))
|
||||
return 0;
|
||||
|
||||
for (field = 0; field < INF_TOTAL_FIELDS; field++) {
|
||||
int old_len;
|
||||
|
||||
if (!field_format(info, field))
|
||||
continue;
|
||||
|
||||
if (started && !chunk_strcat(out, ","))
|
||||
goto err;
|
||||
started = 1;
|
||||
|
||||
old_len = out->len;
|
||||
chunk_appendf(out,
|
||||
"{\"field\":{\"pos\":%d,\"name\":\"%s\"},"
|
||||
"\"processNum\":%u,",
|
||||
field, info_field_names[field],
|
||||
info[INF_PROCESS_NUM].u.u32);
|
||||
if (old_len == out->len)
|
||||
goto err;
|
||||
|
||||
if (!stats_emit_json_field_tags(out, &info[field]))
|
||||
goto err;
|
||||
|
||||
if (!stats_emit_json_data_field(out, &info[field]))
|
||||
goto err;
|
||||
|
||||
if (!chunk_strcat(out, "}"))
|
||||
goto err;
|
||||
}
|
||||
|
||||
if (!chunk_strcat(out, "]"))
|
||||
goto err;
|
||||
return 1;
|
||||
|
||||
err:
|
||||
chunk_reset(out);
|
||||
chunk_appendf(out, "{\"errorStr\":\"output buffer too short\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Dump all fields from <stats> into <out> using a typed "field:desc:type:value" format */
|
||||
static int stats_dump_fields_json(struct chunk *out, const struct field *stats,
|
||||
int first_stat)
|
||||
{
|
||||
int field;
|
||||
int started = 0;
|
||||
|
||||
if (!first_stat && !chunk_strcat(out, ","))
|
||||
return 0;
|
||||
if (!chunk_strcat(out, "["))
|
||||
return 0;
|
||||
|
||||
for (field = 0; field < ST_F_TOTAL_FIELDS; field++) {
|
||||
const char *obj_type;
|
||||
int old_len;
|
||||
|
||||
if (!stats[field].type)
|
||||
continue;
|
||||
|
||||
if (started && !chunk_strcat(out, ","))
|
||||
goto err;
|
||||
started = 1;
|
||||
|
||||
switch (stats[ST_F_TYPE].u.u32) {
|
||||
case STATS_TYPE_FE: obj_type = "Frontend"; break;
|
||||
case STATS_TYPE_BE: obj_type = "Backend"; break;
|
||||
case STATS_TYPE_SO: obj_type = "Listener"; break;
|
||||
case STATS_TYPE_SV: obj_type = "Server"; break;
|
||||
default: obj_type = "Unknown"; break;
|
||||
}
|
||||
|
||||
old_len = out->len;
|
||||
chunk_appendf(out,
|
||||
"{"
|
||||
"\"objType\":\"%s\","
|
||||
"\"proxyId\":%d,"
|
||||
"\"id\":%d,"
|
||||
"\"field\":{\"pos\":%d,\"name\":\"%s\"},"
|
||||
"\"processNum\":%u,",
|
||||
obj_type, stats[ST_F_IID].u.u32,
|
||||
stats[ST_F_SID].u.u32, field,
|
||||
stat_field_names[field], stats[ST_F_PID].u.u32);
|
||||
if (old_len == out->len)
|
||||
goto err;
|
||||
|
||||
if (!stats_emit_json_field_tags(out, &stats[field]))
|
||||
goto err;
|
||||
|
||||
if (!stats_emit_json_data_field(out, &stats[field]))
|
||||
goto err;
|
||||
|
||||
if (!chunk_strcat(out, "}"))
|
||||
goto err;
|
||||
}
|
||||
|
||||
if (!chunk_strcat(out, "]"))
|
||||
goto err;
|
||||
|
||||
return 1;
|
||||
|
||||
err:
|
||||
chunk_reset(out);
|
||||
if (!first_stat)
|
||||
chunk_strcat(out, ",");
|
||||
chunk_appendf(out, "{\"errorStr\":\"output buffer too short\"}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Dump all fields from <stats> into <out> using the HTML format. A column is
|
||||
* reserved for the checkbox is ST_SHOWADMIN is set in <flags>. Some extra info
|
||||
* are provided if ST_SHLGNDS is present in <flags>.
|
||||
@ -1022,15 +1242,26 @@ static int stats_dump_fields_html(struct chunk *out, const struct field *stats,
|
||||
|
||||
int stats_dump_one_line(const struct field *stats, unsigned int flags, struct proxy *px, struct appctx *appctx)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if ((px->cap & PR_CAP_BE) && px->srv && (appctx->ctx.stats.flags & STAT_ADMIN))
|
||||
flags |= ST_SHOWADMIN;
|
||||
|
||||
if (appctx->ctx.stats.flags & STAT_FMT_HTML)
|
||||
return stats_dump_fields_html(&trash, stats, flags);
|
||||
ret = stats_dump_fields_html(&trash, stats, flags);
|
||||
else if (appctx->ctx.stats.flags & STAT_FMT_TYPED)
|
||||
return stats_dump_fields_typed(&trash, stats);
|
||||
ret = stats_dump_fields_typed(&trash, stats);
|
||||
else if (appctx->ctx.stats.flags & STAT_FMT_JSON)
|
||||
ret = stats_dump_fields_json(&trash, stats,
|
||||
!(appctx->ctx.stats.flags &
|
||||
STAT_STARTED));
|
||||
else
|
||||
return stats_dump_fields_csv(&trash, stats);
|
||||
ret = stats_dump_fields_csv(&trash, stats);
|
||||
|
||||
if (ret)
|
||||
appctx->ctx.stats.flags |= STAT_STARTED;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Fill <stats> with the frontend statistics. <stats> is
|
||||
@ -2258,6 +2489,23 @@ static void stats_dump_html_end()
|
||||
chunk_appendf(&trash, "</body></html>\n");
|
||||
}
|
||||
|
||||
/* Dumps the stats JSON header to the trash buffer which. The caller is responsible
|
||||
* for clearing it if needed.
|
||||
*/
|
||||
static void stats_dump_json_header()
|
||||
{
|
||||
chunk_strcat(&trash, "[");
|
||||
}
|
||||
|
||||
|
||||
/* Dumps the JSON stats trailer block to the trash. The caller is responsible
|
||||
* for clearing the trash if needed.
|
||||
*/
|
||||
static void stats_dump_json_end()
|
||||
{
|
||||
chunk_strcat(&trash, "]");
|
||||
}
|
||||
|
||||
/* This function dumps statistics onto the stream interface's read buffer in
|
||||
* either CSV or HTML format. <uri> contains some HTML-specific parameters that
|
||||
* are ignored for CSV format (hence <uri> may be NULL there). It returns 0 if
|
||||
@ -2281,6 +2529,8 @@ static int stats_dump_stat_to_buffer(struct stream_interface *si, struct uri_aut
|
||||
case STAT_ST_HEAD:
|
||||
if (appctx->ctx.stats.flags & STAT_FMT_HTML)
|
||||
stats_dump_html_head(uri);
|
||||
else if (appctx->ctx.stats.flags & STAT_FMT_JSON)
|
||||
stats_dump_json_header(uri);
|
||||
else if (!(appctx->ctx.stats.flags & STAT_FMT_TYPED))
|
||||
stats_dump_csv_header();
|
||||
|
||||
@ -2329,8 +2579,11 @@ static int stats_dump_stat_to_buffer(struct stream_interface *si, struct uri_aut
|
||||
/* fall through */
|
||||
|
||||
case STAT_ST_END:
|
||||
if (appctx->ctx.stats.flags & STAT_FMT_HTML) {
|
||||
stats_dump_html_end();
|
||||
if (appctx->ctx.stats.flags & (STAT_FMT_HTML|STAT_FMT_JSON)) {
|
||||
if (appctx->ctx.stats.flags & STAT_FMT_HTML)
|
||||
stats_dump_html_end();
|
||||
else
|
||||
stats_dump_json_end();
|
||||
if (bi_putchk(rep, &trash) == -1) {
|
||||
si_applet_cant_put(si);
|
||||
return 0;
|
||||
@ -2757,6 +3010,7 @@ static int stats_send_http_redirect(struct stream_interface *si)
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/* This I/O handler runs as an applet embedded in a stream interface. It is
|
||||
* used to send HTTP stats over a TCP socket. The mechanism is very simple.
|
||||
* appctx->st0 contains the operation in progress (dump, done). The handler
|
||||
@ -3032,6 +3286,8 @@ static int stats_dump_info_to_buffer(struct stream_interface *si)
|
||||
|
||||
if (appctx->ctx.stats.flags & STAT_FMT_TYPED)
|
||||
stats_dump_typed_info_fields(&trash, info);
|
||||
else if (appctx->ctx.stats.flags & STAT_FMT_JSON)
|
||||
stats_dump_json_info_fields(&trash, info);
|
||||
else
|
||||
stats_dump_info_fields(&trash, info);
|
||||
|
||||
@ -3108,6 +3364,8 @@ static int cli_parse_show_info(char **args, struct appctx *appctx, void *private
|
||||
|
||||
if (strcmp(args[2], "typed") == 0)
|
||||
appctx->ctx.stats.flags |= STAT_FMT_TYPED;
|
||||
else if (strcmp(args[2], "json") == 0)
|
||||
appctx->ctx.stats.flags |= STAT_FMT_JSON;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -3138,9 +3396,13 @@ static int cli_parse_show_stat(char **args, struct appctx *appctx, void *private
|
||||
appctx->ctx.stats.sid = atoi(args[4]);
|
||||
if (strcmp(args[5], "typed") == 0)
|
||||
appctx->ctx.stats.flags |= STAT_FMT_TYPED;
|
||||
else if (strcmp(args[5], "json") == 0)
|
||||
appctx->ctx.stats.flags |= STAT_FMT_JSON;
|
||||
}
|
||||
else if (strcmp(args[2], "typed") == 0)
|
||||
appctx->ctx.stats.flags |= STAT_FMT_TYPED;
|
||||
else if (strcmp(args[2], "json") == 0)
|
||||
appctx->ctx.stats.flags |= STAT_FMT_JSON;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user