From: Michael Tremer Date: Tue, 21 Oct 2025 17:36:40 +0000 (+0000) Subject: source: Check if RRD files need migration X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=688e613e10b8f90aac67051fb07d578d55e66b03;p=telemetry.git source: Check if RRD files need migration Signed-off-by: Michael Tremer --- diff --git a/src/daemon/source.c b/src/daemon/source.c index 13f629a..21102f7 100644 --- a/src/daemon/source.c +++ b/src/daemon/source.c @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -572,6 +573,300 @@ ERROR: return r; } +static int td_source_check_step(td_source* self, const char* path, rrd_info_t* info) { + int step = 0; + + // Find the step + while (info) { + if (td_string_equals(info->key, "step")) { + switch (info->type) { + case RD_I_CNT: + step = info->value.u_cnt; + break; + + default: + DEBUG(self->ctx, "%s: Step is not an integer\n", path); + return -EINVAL; + } + } + + info = info->next; + } + + // Fail if we could not find the step + if (!step) { + ERROR(self->ctx, "Failed to determine step of %s\n", path); + return -EIO; + } + + // All okay if the step matches + if (step == USEC_TO_SEC(HEARTBEAT / 2)) + return 0; + + // The step does not match + DEBUG(self->ctx, "%s: Step does not match (%d, but should be %d)\n", + path, step, USEC_TO_SEC(HEARTBEAT / 2)); + + return 1; +} + +// Continues to read line as long as it matches s +static int consume(char** line, const char* s) { + char* p = *line; + + for (;;) { + // Successful if we have consumed all of s + if (!*s) + break; + + // It is bad if we have run out of input + if (!*p) + return -EBADMSG; + + // The strings don't seem to match + if (*p != *s) + return -EBADMSG; + + // Advance both pointers + p++; + s++; + } + + // Restore the line + *line = p; + + return 0; +} + +static int td_source_get_info(td_source* self, rrd_info_t* info, + const char* field, const char* key, rrd_info_type_t type, rrd_info_t** result) { + char* p = NULL; + int r; + + // Find the step + for (; info; info = info->next) { + // Skip anything that does not match by type + if (info->type != type) + continue; + + // Start at the beginning of the string + p = info->key; + + // Lines must start with "ds[" + r = consume(&p, "ds["); + if (r < 0) + continue; + + // After that, we must have the field name + r = consume(&p, field); + if (r < 0) + continue; + + // Then, we must have "]." + r = consume(&p, "]."); + if (r < 0) + continue; + + // Then we must have the key + r = consume(&p, key); + if (r < 0) + continue; + + // Skip if we didn't reach the end of the string + if (*p) + continue; + + // Match! + *result = info; + break; + } + + return 0; +} + +static int td_source_check_field_index(td_source* self, const char* path, + rrd_info_t* info, const char* field, unsigned long index) { + rrd_info_t* i = NULL; + int r; + + // Fetch the field + r = td_source_get_info(self, info, field, "index", RD_I_CNT, &i); + if (r < 0) + return r; + + // The field was not found + if (!i) { + DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field); + return 1; + } + + // All good, the index matches + if (i->value.u_cnt == index) + return 0; + + // The index does not match + DEBUG(self->ctx, "%s: Index of field %s does not match (is %d, but should be %d)\n", + path, field, i->value.u_cnt, index); + + return 1; +} + +static int td_source_check_field_type(td_source* self, const char* path, + rrd_info_t* info, const char* field, const char* type) { + rrd_info_t* i = NULL; + int r; + + // Fetch the field + r = td_source_get_info(self, info, field, "type", RD_I_STR, &i); + if (r < 0) + return r; + + // The field was not found + if (!i) { + DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field); + return 1; + } + + // All good, the type matches + if (td_string_equals(i->value.u_str, type)) + return 0; + + // The type does not match + DEBUG(self->ctx, "%s: Type of field %s does not match (is %s, but should be %s)\n", + path, field, i->value.u_str, type); + + return 1; +} + +/* + This is necessary as rrdtool stores the limits as double and we store integers +*/ +static int td_source_compare_limits(double current, int target) { + // -1 is for unlimited which is stored as NaN + if (isnan(current)) + return (target == -1); + + return fabs(current - target) < 1e-9; +} + +static int td_source_check_field_minimum(td_source* self, const char* path, + rrd_info_t* info, const char* field, int min) { + rrd_info_t* i = NULL; + int r; + + // Fetch the field + r = td_source_get_info(self, info, field, "min", RD_I_VAL, &i); + if (r < 0) + return r; + + // The field was not found + if (!i) { + DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field); + return 1; + } + + // All good, the value matches + if (td_source_compare_limits(i->value.u_val, min)) + return 0; + + // The minimum does not match + DEBUG(self->ctx, "%s: Minimum value of field %s does not match (is %lf, but should be %d)\n", + path, field, i->value.u_val, min); + + return 1; +} + +static int td_source_check_field_maximum(td_source* self, const char* path, + rrd_info_t* info, const char* field, int max) { + rrd_info_t* i = NULL; + int r; + + // Fetch the field + r = td_source_get_info(self, info, field, "max", RD_I_VAL, &i); + if (r < 0) + return r; + + // The field was not found + if (!i) { + DEBUG(self->ctx, "%s: Field %s does not seem to exist\n", path, field); + return 1; + } + + // All good, the value matches + if (td_source_compare_limits(i->value.u_val, max)) + return 0; + + // The maximum does not match + DEBUG(self->ctx, "%s: Maximum value of field %s does not match (is %lf, but should be %d)\n", + path, field, i->value.u_val, max); + + return 1; +} + +static int td_source_needs_migration(td_source* self, const char* path, rrd_info_t* info) { + unsigned long index = 0; + int r; + + // Check if the step matches + r = td_source_check_step(self, path, info); + if (r) + return r; + + // Check all data sources + for (const td_rrd_ds* ds = self->impl->rrd_dss; ds->field; ds++, index++) { + // Check if the field index matches + r = td_source_check_field_index(self, path, info, ds->field, index); + if (r) + return r; + + // Check if the field type matches + r = td_source_check_field_type(self, path, info, ds->field, ds->type); + if (r) + return r; + + // Check if the field minimum matches + r = td_source_check_field_minimum(self, path, info, ds->field, ds->min); + if (r) + return r; + + // Check if the field maximum matches + r = td_source_check_field_maximum(self, path, info, ds->field, ds->max); + if (r) + return r; + } + + // XXX Check RRAs + + return 0; +} + +static int td_source_migrate(td_source* self, const char* path) { + rrd_info_t* info = NULL; + int r; + + // Fetch information from the RRD file + info = rrd_info_r(path); + + // Does this RRD need migration? + r = td_source_needs_migration(self, path, info); + if (r < 0) { + ERROR(self->ctx, "Failed to check whether %s needs migration: %s\n", path, strerror(-r)); + goto ERROR; + + // Migrate + } else if (r) { + DEBUG(self->ctx, "%s needs migration!\n", path); + + // XXX TODO + } + +ERROR: + if (info) + rrd_info_free(info); + + return r; +} + /* Called to write all collected samples to disk */ @@ -601,6 +896,11 @@ int td_source_commit(td_source* self, } } + // Migrate the RRD file + r = td_source_migrate(self, path); + if (r < 0) + return r; + // Write the samples r = rrd_update_r(path, NULL, num_samples, samples); if (r < 0) {