mirror of
https://git.haproxy.org/git/haproxy.git/
synced 2025-08-05 22:56:57 +02:00
getline() was used to read stats-file. However, this function is not portable and may cause build issue on some systems. Replace it by standard fgets(). No need to backport.
427 lines
9.7 KiB
C
427 lines
9.7 KiB
C
#include <haproxy/stats-file.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <import/ebmbtree.h>
|
|
#include <import/ebsttree.h>
|
|
#include <import/ist.h>
|
|
#include <haproxy/api.h>
|
|
#include <haproxy/buf.h>
|
|
#include <haproxy/chunk.h>
|
|
#include <haproxy/clock.h>
|
|
#include <haproxy/errors.h>
|
|
#include <haproxy/global.h>
|
|
#include <haproxy/guid.h>
|
|
#include <haproxy/intops.h>
|
|
#include <haproxy/list.h>
|
|
#include <haproxy/listener-t.h>
|
|
#include <haproxy/obj_type.h>
|
|
#include <haproxy/proxy-t.h>
|
|
#include <haproxy/server-t.h>
|
|
#include <haproxy/stats.h>
|
|
#include <haproxy/time.h>
|
|
|
|
/* Dump all fields from <stats> into <out> for stats-file. */
|
|
int stats_dump_fields_file(struct buffer *out,
|
|
const struct field *line, size_t stats_count,
|
|
struct show_stat_ctx *ctx)
|
|
{
|
|
struct guid_node *guid;
|
|
struct listener *l;
|
|
int i;
|
|
|
|
switch (ctx->px_st) {
|
|
case STAT_PX_ST_FE:
|
|
case STAT_PX_ST_BE:
|
|
guid = &__objt_proxy(ctx->obj1)->guid;
|
|
break;
|
|
|
|
case STAT_PX_ST_LI:
|
|
l = LIST_ELEM(ctx->obj2, struct listener *, by_fe);
|
|
guid = &l->guid;
|
|
break;
|
|
|
|
case STAT_PX_ST_SV:
|
|
guid = &__objt_server(ctx->obj2)->guid;
|
|
break;
|
|
|
|
default:
|
|
ABORT_NOW();
|
|
return 1;
|
|
}
|
|
|
|
/* Skip objects without GUID. */
|
|
if (!guid->node.key)
|
|
return 1;
|
|
|
|
chunk_appendf(out, "%s,", (char *)guid->node.key);
|
|
|
|
for (i = 0; i < stats_count; ++i) {
|
|
/* Empty field for stats-file is used to skip its output,
|
|
* including any separator.
|
|
*/
|
|
if (field_format(line, i) == FF_EMPTY)
|
|
continue;
|
|
|
|
if (!stats_emit_raw_data_field(out, &line[i]))
|
|
return 0;
|
|
if (!chunk_strcat(out, ","))
|
|
return 0;
|
|
}
|
|
|
|
chunk_strcat(out, "\n");
|
|
return 1;
|
|
}
|
|
|
|
void stats_dump_file_header(int type, struct buffer *out)
|
|
{
|
|
const struct stat_col *col;
|
|
int i;
|
|
|
|
/* Caller must specified ither FE or BE. */
|
|
BUG_ON(!(type & ((1 << STATS_TYPE_FE) | (1 << STATS_TYPE_BE))));
|
|
|
|
if (type & (1 << STATS_TYPE_FE)) {
|
|
chunk_strcat(out, "#fe guid,");
|
|
for (i = 0; i < ST_I_PX_MAX; ++i) {
|
|
col = &stat_cols_px[i];
|
|
if (stcol_is_generic(col) &&
|
|
col->cap & (STATS_PX_CAP_FE|STATS_PX_CAP_LI)) {
|
|
chunk_appendf(out, "%s,", col->name);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
chunk_appendf(out, "#be guid,");
|
|
for (i = 0; i < ST_I_PX_MAX; ++i) {
|
|
col = &stat_cols_px[i];
|
|
if (stcol_is_generic(col) &&
|
|
col->cap & (STATS_PX_CAP_BE|STATS_PX_CAP_SRV)) {
|
|
chunk_appendf(out, "%s,", col->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
chunk_strcat(out, "\n");
|
|
}
|
|
|
|
/* Parse an identified header line <header> starting with '#' character.
|
|
*
|
|
* If the section is recognized, <domain> will point to the current stats-file
|
|
* scope. <cols> will be filled as a matrix to identify each stat_col position
|
|
* using <st_tree> as prefilled proxy stats columns. If stats-file section is
|
|
* unknown, only <domain> will be set to STFILE_DOMAIN_UNSET.
|
|
*
|
|
* Returns 0 on success. On fatal error, non-zero is returned and parsing should
|
|
* be interrupted.
|
|
*/
|
|
static int parse_header_line(struct ist header, struct eb_root *st_tree,
|
|
enum stfile_domain *domain,
|
|
const struct stat_col *cols[])
|
|
{
|
|
enum stfile_domain dom = STFILE_DOMAIN_UNSET;
|
|
struct ist token;
|
|
char last;
|
|
int i;
|
|
|
|
header = iststrip(header);
|
|
last = istptr(header)[istlen(header) - 1];
|
|
token = istsplit(&header, ' ');
|
|
|
|
/* A header line is considered valid if:
|
|
* - a space delimiter is found and first token is several chars
|
|
* - last line character must be a comma separator
|
|
*/
|
|
if (!istlen(header) || istlen(token) == 1 || last != ',')
|
|
goto err;
|
|
|
|
if (isteq(token, ist("#fe")))
|
|
dom = STFILE_DOMAIN_PX_FE;
|
|
else if (isteq(token, ist("#be")))
|
|
dom = STFILE_DOMAIN_PX_BE;
|
|
|
|
/* Remove 'guid' field. */
|
|
token = istsplit(&header, ',');
|
|
if (!isteq(token, ist("guid"))) {
|
|
/* Fatal error if FE/BE domain without guid token. */
|
|
if (dom == STFILE_DOMAIN_PX_FE || dom == STFILE_DOMAIN_PX_BE)
|
|
goto err;
|
|
}
|
|
|
|
/* Unknown domain. Following lines should be ignored until next header. */
|
|
if (dom == STFILE_DOMAIN_UNSET)
|
|
return 0;
|
|
|
|
/* Generate matrix of stats column into cols[]. */
|
|
memset(cols, 0, sizeof(void *) * STAT_FILE_MAX_COL_COUNT);
|
|
|
|
i = 0;
|
|
while (istlen(header) && i < STAT_FILE_MAX_COL_COUNT) {
|
|
struct stcol_node *col_node;
|
|
const struct stat_col *col;
|
|
struct ebmb_node *node;
|
|
|
|
/* Lookup column by its name into <st_tree>. */
|
|
token = istsplit(&header, ',');
|
|
node = ebst_lookup(st_tree, ist0(token));
|
|
if (!node) {
|
|
++i;
|
|
continue;
|
|
}
|
|
|
|
col_node = ebmb_entry(node, struct stcol_node, name);
|
|
col = col_node->col;
|
|
|
|
/* Ignore column if its cap is not valid with current stats-file section. */
|
|
if ((dom == STFILE_DOMAIN_PX_FE &&
|
|
!(col->cap & (STATS_PX_CAP_FE|STATS_PX_CAP_LI))) ||
|
|
(dom == STFILE_DOMAIN_PX_BE &&
|
|
!(col->cap & (STATS_PX_CAP_BE|STATS_PX_CAP_SRV)))) {
|
|
++i;
|
|
continue;
|
|
}
|
|
|
|
cols[i] = col;
|
|
++i;
|
|
}
|
|
|
|
*domain = dom;
|
|
return 0;
|
|
|
|
err:
|
|
*domain = STFILE_DOMAIN_UNSET;
|
|
return 1;
|
|
}
|
|
|
|
/* Preload an individual counter instance stored at <counter> with <token>
|
|
* value> for the <col> stat column.
|
|
*
|
|
* Returns 0 on success else non-zero if counter was not updated.
|
|
*/
|
|
static int load_ctr(const struct stat_col *col, const struct ist token,
|
|
void* counter)
|
|
{
|
|
const enum field_nature fn = stcol_nature(col);
|
|
const enum field_format ff = stcol_format(col);
|
|
const char *ptr = istptr(token);
|
|
struct field value;
|
|
|
|
switch (ff) {
|
|
case FF_U64:
|
|
value.u.u64 = read_uint64(&ptr, istend(token));
|
|
break;
|
|
|
|
case FF_S32:
|
|
case FF_U32:
|
|
value.u.u32 = read_uint(&ptr, istend(token));
|
|
break;
|
|
|
|
default:
|
|
/* Unsupported field nature. */
|
|
return 1;
|
|
}
|
|
|
|
/* Do not load value if non numeric characters present. */
|
|
if (ptr != istend(token))
|
|
return 1;
|
|
|
|
if (fn == FN_COUNTER && ff == FF_U64) {
|
|
*(uint64_t *)counter = value.u.u64;
|
|
}
|
|
else if (fn == FN_RATE && ff == FF_U32) {
|
|
preload_freq_ctr(counter, value.u.u32);
|
|
}
|
|
else if (fn == FN_AGE && (ff == FF_U32 || ff == FF_S32)) {
|
|
*(uint32_t *)counter = ns_to_sec(now_ns) - value.u.u32;
|
|
}
|
|
else {
|
|
/* Unsupported field format/nature combination. */
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Parse a non header stats-file line <line>. Specify current parsing <domain>
|
|
* and <cols> stats column matrix derived from the last header line.
|
|
*
|
|
* Returns 0 on success else non-zero.
|
|
*/
|
|
static int parse_stat_line(struct ist line,
|
|
enum stfile_domain domain,
|
|
const struct stat_col *cols[])
|
|
{
|
|
struct guid_node *node;
|
|
struct listener *li;
|
|
struct server *srv;
|
|
struct proxy *px;
|
|
struct ist token;
|
|
char *base_off;
|
|
char *guid;
|
|
int i, off;
|
|
|
|
token = istsplit(&line, ',');
|
|
guid = ist0(token);
|
|
if (!guid_is_valid_fmt(guid, NULL))
|
|
goto err;
|
|
|
|
node = guid_lookup(guid);
|
|
if (!node) {
|
|
/* Silently ignored unknown GUID. */
|
|
return 0;
|
|
}
|
|
|
|
switch (obj_type(node->obj_type)) {
|
|
case OBJ_TYPE_PROXY:
|
|
px = __objt_proxy(node->obj_type);
|
|
|
|
if (domain == STFILE_DOMAIN_PX_FE) {
|
|
if (!(px->cap & PR_CAP_FE))
|
|
goto err;
|
|
base_off = (char *)&px->fe_counters;
|
|
off = 0;
|
|
}
|
|
else if (domain == STFILE_DOMAIN_PX_BE) {
|
|
if (!(px->cap & PR_CAP_BE))
|
|
goto err;
|
|
base_off = (char *)&px->be_counters;
|
|
off = 1;
|
|
}
|
|
else {
|
|
goto err;
|
|
}
|
|
|
|
break;
|
|
|
|
case OBJ_TYPE_LISTENER:
|
|
if (domain != STFILE_DOMAIN_PX_FE)
|
|
goto err;
|
|
|
|
li = __objt_listener(node->obj_type);
|
|
/* Listeners counters are not allocated if 'option socket-stats' unset. */
|
|
if (!li->counters)
|
|
return 0;
|
|
|
|
base_off = (char *)li->counters;
|
|
off = 0;
|
|
break;
|
|
|
|
case OBJ_TYPE_SERVER:
|
|
if (domain != STFILE_DOMAIN_PX_BE)
|
|
goto err;
|
|
|
|
srv = __objt_server(node->obj_type);
|
|
base_off = (char *)&srv->counters;
|
|
off = 1;
|
|
break;
|
|
|
|
default:
|
|
goto err;
|
|
}
|
|
|
|
i = 0;
|
|
while (istlen(line) && i < STAT_FILE_MAX_COL_COUNT) {
|
|
const struct stat_col *col = cols[i++];
|
|
|
|
token = istsplit(&line, ',');
|
|
if (!istlen(token))
|
|
continue;
|
|
|
|
if (!col)
|
|
continue;
|
|
|
|
load_ctr(col, token, base_off + col->metric.offset[off]);
|
|
}
|
|
|
|
return 0;
|
|
|
|
err:
|
|
return 1;
|
|
}
|
|
|
|
/* Parse a stats-file and preload haproxy internal counters. */
|
|
void apply_stats_file(void)
|
|
{
|
|
const struct stat_col *cols[STAT_FILE_MAX_COL_COUNT];
|
|
struct eb_root st_tree = EB_ROOT;
|
|
enum stfile_domain domain;
|
|
int valid_format = 0;
|
|
FILE *file;
|
|
struct ist istline;
|
|
char *line = NULL;
|
|
int linenum;
|
|
|
|
if (!global.stats_file)
|
|
return;
|
|
|
|
file = fopen(global.stats_file, "r");
|
|
if (!file) {
|
|
ha_warning("config: Can't load stats file: cannot open file.\n");
|
|
return;
|
|
}
|
|
|
|
/* Generate stat columns map indexed by name. */
|
|
if (generate_stat_tree(&st_tree, stat_cols_px)) {
|
|
ha_warning("config: Can't load stats file: not enough memory.\n");
|
|
goto out;
|
|
}
|
|
|
|
line = malloc(sizeof(char) * LINESIZE);
|
|
if (!line) {
|
|
ha_warning("config: Can't load stats file: line alloc error.\n");
|
|
goto out;
|
|
}
|
|
|
|
linenum = 0;
|
|
domain = STFILE_DOMAIN_UNSET;
|
|
while (1) {
|
|
if (!fgets(line, LINESIZE, file))
|
|
break;
|
|
|
|
++linenum;
|
|
istline = iststrip(ist(line));
|
|
if (!istlen(istline))
|
|
continue;
|
|
|
|
if (*istptr(istline) == '#') {
|
|
if (parse_header_line(istline, &st_tree, &domain, cols)) {
|
|
if (!valid_format) {
|
|
ha_warning("config: Invalid stats-file format.\n");
|
|
break;
|
|
}
|
|
|
|
ha_warning("config: Ignored stats-file header line '%d'.\n", linenum);
|
|
}
|
|
|
|
valid_format = 1;
|
|
}
|
|
else if (domain != STFILE_DOMAIN_UNSET) {
|
|
if (parse_stat_line(istline, domain, cols))
|
|
ha_warning("config: Ignored stats-file line %d.\n", linenum);
|
|
}
|
|
else {
|
|
/* Stop parsing if first line is not a valid header.
|
|
* Allows to immediately stop reading garbage file.
|
|
*/
|
|
if (!valid_format) {
|
|
ha_warning("config: Invalid stats-file format.\n");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
out:
|
|
while (!eb_is_empty(&st_tree)) {
|
|
struct ebmb_node *node = ebmb_first(&st_tree);
|
|
struct stcol_node *snode = ebmb_entry(node, struct stcol_node, name);
|
|
|
|
ebmb_delete(node);
|
|
ha_free(&snode);
|
|
}
|
|
|
|
ha_free(&line);
|
|
fclose(file);
|
|
}
|