haproxy/src/stats-file.c
Amaury Denoyelle fbc3d46b9f BUILD: stats: remove non portable getline() usage
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.
2024-05-17 14:53:19 +02:00

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);
}