From cf2b5c709e875eea2870fcd4e7a546fc2121f390 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 16 Feb 2026 17:58:25 -0600 Subject: [PATCH 01/14] processor_cumulative_to_delta: add cumulative->delta processor for metrics - Introduce new processor_cumulative_to_delta plugin. - Convert cumulative monotonic sums to delta values. - Convert cumulative histograms to delta values. - Add reset handling with drop_on_reset behavior. - Add first-sample behavior controls via initial_value (auto|keep|drop) and compatibility option drop_first. - Drop out-of-order samples per timeseries to avoid invalid deltas. - Keep non-monotonic sums unchanged (passthrough). - Add state tracking per timeseries and bounded cleanup/eviction logic. - Add internal and runtime tests for counters/histograms, reset behavior, first-sample modes, out-of-order samples, multi-series, and non-monotonic-sum passthrough. Config options: - initial_value (string, default: unset) First-point behavior for new series: auto, keep, or drop. If unset, drop_first compatibility mode is used. - drop_first (bool, default: true) Compatibility option used only when initial_value=unset. If enabled, first sample is dropped. Signed-off-by: Eduardo Silva --- plugins/CMakeLists.txt | 1 + .../CMakeLists.txt | 5 + .../cumulative_to_delta.c | 280 +++ .../cumulative_to_delta.h | 45 + .../cumulative_to_delta_core.c | 1512 +++++++++++++++++ 5 files changed, 1843 insertions(+) create mode 100644 plugins/processor_cumulative_to_delta/CMakeLists.txt create mode 100644 plugins/processor_cumulative_to_delta/cumulative_to_delta.c create mode 100644 plugins/processor_cumulative_to_delta/cumulative_to_delta.h create mode 100644 plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index baaa2a7ffbb..4ea284d25d2 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -344,6 +344,7 @@ REGISTER_IN_PLUGIN("in_random") # PROCESSORS # ========== REGISTER_PROCESSOR_PLUGIN("processor_content_modifier") +REGISTER_PROCESSOR_PLUGIN("processor_cumulative_to_delta") REGISTER_PROCESSOR_PLUGIN("processor_labels") REGISTER_PROCESSOR_PLUGIN("processor_metrics_selector") REGISTER_PROCESSOR_PLUGIN("processor_opentelemetry_envelope") diff --git a/plugins/processor_cumulative_to_delta/CMakeLists.txt b/plugins/processor_cumulative_to_delta/CMakeLists.txt new file mode 100644 index 00000000000..f7b1818fe4f --- /dev/null +++ b/plugins/processor_cumulative_to_delta/CMakeLists.txt @@ -0,0 +1,5 @@ +set(src + cumulative_to_delta.c + cumulative_to_delta_core.c) + +FLB_PLUGIN(processor_cumulative_to_delta "${src}" "") diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta.c b/plugins/processor_cumulative_to_delta/cumulative_to_delta.c new file mode 100644 index 00000000000..c19cfed0d43 --- /dev/null +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta.c @@ -0,0 +1,280 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Fluent Bit + * ========== + * Copyright (C) 2015-2026 The Fluent Bit Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include + +#include "cumulative_to_delta.h" + +struct c2d_processor_ctx { + char *initial_value; + int drop_first; + int drop_on_reset; + int max_staleness; + int max_series; + int initial_value_mode; + struct flb_cumulative_to_delta_ctx *core_context; +}; + +static int parse_initial_value_mode(struct c2d_processor_ctx *context) +{ + if (context->initial_value == NULL || + strcasecmp(context->initial_value, "unset") == 0) { + if (context->drop_first == FLB_TRUE) { + context->initial_value_mode = FLB_C2D_INITIAL_VALUE_DROP; + } + else { + context->initial_value_mode = FLB_C2D_INITIAL_VALUE_KEEP; + } + + return 0; + } + + if (strcasecmp(context->initial_value, "auto") == 0) { + context->initial_value_mode = FLB_C2D_INITIAL_VALUE_AUTO; + } + else if (strcasecmp(context->initial_value, "keep") == 0) { + context->initial_value_mode = FLB_C2D_INITIAL_VALUE_KEEP; + } + else if (strcasecmp(context->initial_value, "drop") == 0) { + context->initial_value_mode = FLB_C2D_INITIAL_VALUE_DROP; + } + else { + return -1; + } + + return 0; +} + +static void destroy_context(struct c2d_processor_ctx *context) +{ + if (context == NULL) { + return; + } + + if (context->core_context != NULL) { + flb_cumulative_to_delta_ctx_destroy(context->core_context); + } + + flb_free(context); +} + +static struct c2d_processor_ctx *create_context(struct flb_processor_instance *processor_instance) +{ + int result; + struct c2d_processor_ctx *context; + + context = flb_calloc(1, sizeof(struct c2d_processor_ctx)); + if (context == NULL) { + flb_errno(); + return NULL; + } + + result = flb_processor_instance_config_map_set(processor_instance, context); + if (result != 0) { + destroy_context(context); + return NULL; + } + + result = parse_initial_value_mode(context); + if (result != 0) { + flb_plg_error(processor_instance, + "invalid 'initial_value' option: %s", + context->initial_value); + destroy_context(context); + return NULL; + } + + context->core_context = flb_cumulative_to_delta_ctx_create(context->initial_value_mode, + context->drop_on_reset, + cfl_time_now()); + if (context->core_context == NULL) { + destroy_context(context); + return NULL; + } + + result = flb_cumulative_to_delta_ctx_configure(context->core_context, + context->max_staleness, + context->max_series); + if (result != 0) { + flb_plg_error(processor_instance, + "invalid limits max_staleness=%d max_series=%d", + context->max_staleness, + context->max_series); + destroy_context(context); + return NULL; + } + + return context; +} + +static int cb_init(struct flb_processor_instance *processor_instance, + void *source_plugin_instance, + int source_plugin_type, + struct flb_config *config) +{ + processor_instance->context = create_context(processor_instance); + if (processor_instance->context == NULL) { + return FLB_PROCESSOR_FAILURE; + } + + return FLB_PROCESSOR_SUCCESS; +} + +static int cb_exit(struct flb_processor_instance *processor_instance, void *data) +{ + if (processor_instance != NULL && data != NULL) { + destroy_context(data); + } + + return FLB_PROCESSOR_SUCCESS; +} + +static int cb_process_metrics(struct flb_processor_instance *processor_instance, + struct cmt *metrics_context, + struct cmt **out_context, + const char *tag, + int tag_len) +{ + int has_cumulative_metrics; + int result; + struct cmt *out_cmt; + struct cfl_list *head; + struct cmt_counter *counter; + struct cmt_histogram *histogram; + struct cmt_exp_histogram *exp_histogram; + struct c2d_processor_ctx *context; + + context = (struct c2d_processor_ctx *) processor_instance->context; + + has_cumulative_metrics = FLB_FALSE; + + cfl_list_foreach(head, &metrics_context->counters) { + counter = cfl_list_entry(head, struct cmt_counter, _head); + + if (counter->aggregation_type == CMT_AGGREGATION_TYPE_CUMULATIVE && + counter->allow_reset == FLB_FALSE) { + has_cumulative_metrics = FLB_TRUE; + break; + } + } + + if (has_cumulative_metrics == FLB_FALSE) { + cfl_list_foreach(head, &metrics_context->histograms) { + histogram = cfl_list_entry(head, struct cmt_histogram, _head); + + if (histogram->aggregation_type == CMT_AGGREGATION_TYPE_CUMULATIVE) { + has_cumulative_metrics = FLB_TRUE; + break; + } + } + } + + if (has_cumulative_metrics == FLB_FALSE) { + cfl_list_foreach(head, &metrics_context->exp_histograms) { + exp_histogram = cfl_list_entry(head, + struct cmt_exp_histogram, + _head); + + if (exp_histogram->aggregation_type == + CMT_AGGREGATION_TYPE_CUMULATIVE) { + has_cumulative_metrics = FLB_TRUE; + break; + } + } + } + + if (has_cumulative_metrics == FLB_FALSE) { + *out_context = metrics_context; + return FLB_PROCESSOR_SUCCESS; + } + + out_cmt = cmt_create(); + if (out_cmt == NULL) { + flb_plg_error(processor_instance, "could not create out_cmt context"); + return FLB_PROCESSOR_FAILURE; + } + + result = cmt_cat(out_cmt, metrics_context); + if (result != 0) { + cmt_destroy(out_cmt); + return FLB_PROCESSOR_FAILURE; + } + + result = flb_cumulative_to_delta_ctx_process(context->core_context, out_cmt); + if (result != 0) { + cmt_destroy(out_cmt); + return FLB_PROCESSOR_FAILURE; + } + + /* + * The processor returns a replacement context and keeps ownership + * unchanged: the caller remains responsible for destroying both the + * input context and any returned replacement. + */ + *out_context = out_cmt; + return FLB_PROCESSOR_SUCCESS; +} + +static struct flb_config_map config_map[] = { + { + FLB_CONFIG_MAP_STR, "initial_value", "unset", + 0, FLB_TRUE, offsetof(struct c2d_processor_ctx, initial_value), + "First point behavior: auto, keep, drop. " + "If unset, drop_first compatibility mode is used." + }, + { + FLB_CONFIG_MAP_BOOL, "drop_first", "true", + 0, FLB_TRUE, offsetof(struct c2d_processor_ctx, drop_first), + "Compatibility option. Used only when initial_value is unset." + }, + { + FLB_CONFIG_MAP_BOOL, "drop_on_reset", "true", + 0, FLB_TRUE, offsetof(struct c2d_processor_ctx, drop_on_reset), + "Drop samples when monotonic sum/histogram reset is detected." + }, + { + FLB_CONFIG_MAP_TIME, "max_staleness", "1h", + 0, FLB_TRUE, offsetof(struct c2d_processor_ctx, max_staleness), + "State retention window. 0 disables staleness eviction." + }, + { + FLB_CONFIG_MAP_INT, "max_series", "65536", + 0, FLB_TRUE, offsetof(struct c2d_processor_ctx, max_series), + "Maximum tracked series in memory. 0 disables size-based eviction." + }, + {0} +}; + +struct flb_processor_plugin processor_cumulative_to_delta_plugin = { + .name = "cumulative_to_delta", + .description = "Convert cumulative monotonic sums and histograms to delta", + .cb_init = cb_init, + .cb_process_logs = NULL, + .cb_process_metrics = cb_process_metrics, + .cb_process_traces = NULL, + .cb_exit = cb_exit, + .config_map = config_map, + .flags = 0 +}; diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta.h b/plugins/processor_cumulative_to_delta/cumulative_to_delta.h new file mode 100644 index 00000000000..01ae00ac1ef --- /dev/null +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta.h @@ -0,0 +1,45 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Fluent Bit + * ========== + * Copyright (C) 2015-2026 The Fluent Bit Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLB_PROCESSOR_CUMULATIVE_TO_DELTA_H +#define FLB_PROCESSOR_CUMULATIVE_TO_DELTA_H + +#include + +struct flb_cumulative_to_delta_ctx; + +#define FLB_C2D_INITIAL_VALUE_AUTO 0 +#define FLB_C2D_INITIAL_VALUE_KEEP 1 +#define FLB_C2D_INITIAL_VALUE_DROP 2 + +struct flb_cumulative_to_delta_ctx *flb_cumulative_to_delta_ctx_create( + int initial_value_mode, + int drop_on_reset, + uint64_t processor_start_timestamp); +void flb_cumulative_to_delta_ctx_destroy( + struct flb_cumulative_to_delta_ctx *context); +int flb_cumulative_to_delta_ctx_process( + struct flb_cumulative_to_delta_ctx *context, + struct cmt *metrics_context); +int flb_cumulative_to_delta_ctx_configure( + struct flb_cumulative_to_delta_ctx *context, + int max_staleness_seconds, + int max_series); + +#endif diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c new file mode 100644 index 00000000000..ead1dd1e0f7 --- /dev/null +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c @@ -0,0 +1,1512 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* Fluent Bit + * ========== + * Copyright (C) 2015-2026 The Fluent Bit Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "cumulative_to_delta.h" + +#define FLB_C2D_KEEP 0 +#define FLB_C2D_DROP 1 +#define FLB_C2D_SERIES_TABLE_SIZE 1024 +#define FLB_C2D_MAX_SERIES 65536 +#define FLB_C2D_SERIES_TTL_SECONDS 3600 +#define FLB_C2D_GC_INTERVAL_SECONDS 30 +#define FLB_C2D_CONTEXT_HASH_MAX_DEPTH 16 + +struct flb_cumulative_to_delta_series { + int type; + uint64_t last_timestamp; + uint64_t last_update_timestamp; + double last_counter_value; + uint64_t last_hist_count; + double last_hist_sum; + size_t last_hist_bucket_count; + uint64_t *last_hist_buckets; + int32_t last_exp_hist_scale; + uint64_t last_exp_hist_zero_count; + double last_exp_hist_zero_threshold; + int32_t last_exp_hist_positive_offset; + size_t last_exp_hist_positive_count; + uint64_t *last_exp_hist_positive_buckets; + int32_t last_exp_hist_negative_offset; + size_t last_exp_hist_negative_count; + uint64_t *last_exp_hist_negative_buckets; + uint64_t last_exp_hist_count; + int last_exp_hist_sum_set; + double last_exp_hist_sum; + flb_sds_t key; + struct cfl_list _head; +}; + +struct flb_cumulative_to_delta_ctx { + int initial_value_mode; + int drop_on_reset; + uint64_t processor_start_timestamp; + uint64_t gc_interval; + uint64_t next_gc_timestamp; + uint64_t series_ttl; + size_t max_series; + struct flb_hash_table *series_table; + struct cfl_list series_list; +}; + +static int should_drop_initial_sample(struct flb_cumulative_to_delta_ctx *context, + struct cmt_metric *sample) +{ + uint64_t sample_timestamp; + + if (context->initial_value_mode == FLB_C2D_INITIAL_VALUE_DROP) { + return FLB_TRUE; + } + else if (context->initial_value_mode == FLB_C2D_INITIAL_VALUE_KEEP) { + return FLB_FALSE; + } + + /* cmetrics no longer exposes datapoint start timestamp, so auto mode + * falls back to sample timestamp vs processor start time. */ + sample_timestamp = cmt_metric_get_timestamp(sample); + if (sample_timestamp >= context->processor_start_timestamp) { + return FLB_FALSE; + } + + return FLB_TRUE; +} + +static void hash_variant(cfl_hash_state_t *state, + struct cfl_variant *variant, + size_t depth); + +static void hash_array(cfl_hash_state_t *state, + struct cfl_array *array, + size_t depth) +{ + size_t index; + size_t count; + + if (array == NULL || depth >= FLB_C2D_CONTEXT_HASH_MAX_DEPTH) { + count = 0; + cfl_hash_64bits_update(state, &count, sizeof(count)); + return; + } + + count = cfl_array_size(array); + cfl_hash_64bits_update(state, &count, sizeof(count)); + + for (index = 0; index < count; index++) { + hash_variant(state, + cfl_array_fetch_by_index(array, index), + depth + 1); + } +} + +static void hash_kvlist(cfl_hash_state_t *state, + struct cfl_kvlist *kvlist, + size_t depth) +{ + int entry_count; + size_t count; + size_t key_length; + struct cfl_list *head; + struct cfl_kvpair *pair; + + if (kvlist == NULL || depth >= FLB_C2D_CONTEXT_HASH_MAX_DEPTH) { + count = 0; + cfl_hash_64bits_update(state, &count, sizeof(count)); + return; + } + + entry_count = cfl_kvlist_count(kvlist); + if (entry_count < 0) { + count = 0; + } + else { + count = (size_t) entry_count; + } + + cfl_hash_64bits_update(state, &count, sizeof(count)); + + cfl_list_foreach(head, &kvlist->list) { + pair = cfl_list_entry(head, struct cfl_kvpair, _head); + + if (pair->key != NULL) { + key_length = cfl_sds_len(pair->key); + cfl_hash_64bits_update(state, &key_length, sizeof(key_length)); + cfl_hash_64bits_update(state, pair->key, key_length); + } + else { + key_length = 0; + cfl_hash_64bits_update(state, &key_length, sizeof(key_length)); + } + + hash_variant(state, pair->val, depth + 1); + } +} + +static void hash_variant(cfl_hash_state_t *state, + struct cfl_variant *variant, + size_t depth) +{ + int type; + size_t data_length; + + if (variant == NULL) { + type = CFL_VARIANT_NULL; + cfl_hash_64bits_update(state, &type, sizeof(type)); + return; + } + + type = variant->type; + cfl_hash_64bits_update(state, &type, sizeof(type)); + + switch (variant->type) { + case CFL_VARIANT_BOOL: + cfl_hash_64bits_update(state, + &variant->data.as_bool, + sizeof(variant->data.as_bool)); + break; + case CFL_VARIANT_INT: + cfl_hash_64bits_update(state, + &variant->data.as_int64, + sizeof(variant->data.as_int64)); + break; + case CFL_VARIANT_UINT: + cfl_hash_64bits_update(state, + &variant->data.as_uint64, + sizeof(variant->data.as_uint64)); + break; + case CFL_VARIANT_DOUBLE: + cfl_hash_64bits_update(state, + &variant->data.as_double, + sizeof(variant->data.as_double)); + break; + case CFL_VARIANT_STRING: + case CFL_VARIANT_BYTES: + if (variant->data.as_string != NULL) { + data_length = cfl_sds_len(variant->data.as_string); + cfl_hash_64bits_update(state, &data_length, sizeof(data_length)); + cfl_hash_64bits_update(state, variant->data.as_string, data_length); + } + else { + data_length = 0; + cfl_hash_64bits_update(state, &data_length, sizeof(data_length)); + } + break; + case CFL_VARIANT_ARRAY: + hash_array(state, variant->data.as_array, depth + 1); + break; + case CFL_VARIANT_KVLIST: + hash_kvlist(state, variant->data.as_kvlist, depth + 1); + break; + case CFL_VARIANT_NULL: + case CFL_VARIANT_REFERENCE: + default: + break; + } +} + +static uint64_t compute_context_identity(struct cmt *metrics_context) +{ + cfl_hash_state_t hash_state; + struct cfl_variant *resource_context; + struct cfl_variant *scope_context; + struct cfl_variant *resource_metrics_context; + struct cfl_variant *scope_metrics_context; + + if (metrics_context == NULL || metrics_context->external_metadata == NULL) { + return 0; + } + + cfl_hash_64bits_reset(&hash_state); + + resource_context = cfl_kvlist_fetch(metrics_context->external_metadata, + "resource"); + scope_context = cfl_kvlist_fetch(metrics_context->external_metadata, "scope"); + resource_metrics_context = cfl_kvlist_fetch(metrics_context->external_metadata, + "resource_metrics"); + scope_metrics_context = cfl_kvlist_fetch(metrics_context->external_metadata, + "scope_metrics"); + + hash_variant(&hash_state, resource_context, 0); + hash_variant(&hash_state, scope_context, 0); + hash_variant(&hash_state, resource_metrics_context, 0); + hash_variant(&hash_state, scope_metrics_context, 0); + + return cfl_hash_64bits_digest(&hash_state); +} + +static int series_state_update_counter( + struct flb_cumulative_to_delta_series *state, + uint64_t timestamp, + double value) +{ + state->last_timestamp = timestamp; + state->last_update_timestamp = cfl_time_now(); + state->last_counter_value = value; + + return 0; +} + +static int series_state_update_histogram( + struct flb_cumulative_to_delta_series *state, + uint64_t timestamp, + uint64_t count, + double sum, + size_t bucket_count, + uint64_t *buckets) +{ + int resize_buckets; + uint64_t *new_buckets; + + resize_buckets = + (state->last_hist_bucket_count != bucket_count || + (bucket_count > 0 && state->last_hist_buckets == NULL)); + + new_buckets = NULL; + + if (resize_buckets == FLB_TRUE && bucket_count > 0) { + new_buckets = flb_calloc(bucket_count, sizeof(uint64_t)); + if (new_buckets == NULL) { + return -1; + } + } + + if (resize_buckets == FLB_TRUE) { + if (state->last_hist_buckets != NULL) { + flb_free(state->last_hist_buckets); + } + + state->last_hist_buckets = new_buckets; + state->last_hist_bucket_count = bucket_count; + } + + if (bucket_count > 0) { + memcpy(state->last_hist_buckets, buckets, sizeof(uint64_t) * bucket_count); + } + + state->last_hist_count = count; + state->last_hist_sum = sum; + state->last_timestamp = timestamp; + state->last_update_timestamp = cfl_time_now(); + + return 0; +} + +static int series_state_update_exp_histogram( + struct flb_cumulative_to_delta_series *state, + uint64_t timestamp, + int32_t scale, + uint64_t zero_count, + double zero_threshold, + int32_t positive_offset, + size_t positive_count, + uint64_t *positive_buckets, + int32_t negative_offset, + size_t negative_count, + uint64_t *negative_buckets, + uint64_t count, + int sum_set, + double sum) +{ + int resize_positive; + int resize_negative; + uint64_t *new_positive_buckets; + uint64_t *new_negative_buckets; + + resize_positive = + (state->last_exp_hist_positive_count != positive_count || + (positive_count > 0 && state->last_exp_hist_positive_buckets == NULL)); + + resize_negative = + (state->last_exp_hist_negative_count != negative_count || + (negative_count > 0 && state->last_exp_hist_negative_buckets == NULL)); + + new_positive_buckets = NULL; + new_negative_buckets = NULL; + + if (resize_positive == FLB_TRUE && positive_count > 0) { + new_positive_buckets = flb_calloc(positive_count, sizeof(uint64_t)); + if (new_positive_buckets == NULL) { + return -1; + } + } + + if (resize_negative == FLB_TRUE && negative_count > 0) { + new_negative_buckets = flb_calloc(negative_count, sizeof(uint64_t)); + if (new_negative_buckets == NULL) { + if (new_positive_buckets != NULL) { + flb_free(new_positive_buckets); + } + return -1; + } + } + + if (resize_positive == FLB_TRUE) { + if (state->last_exp_hist_positive_buckets != NULL) { + flb_free(state->last_exp_hist_positive_buckets); + } + + state->last_exp_hist_positive_buckets = new_positive_buckets; + state->last_exp_hist_positive_count = positive_count; + } + + if (resize_negative == FLB_TRUE) { + if (state->last_exp_hist_negative_buckets != NULL) { + flb_free(state->last_exp_hist_negative_buckets); + } + + state->last_exp_hist_negative_buckets = new_negative_buckets; + state->last_exp_hist_negative_count = negative_count; + } + + if (positive_count > 0) { + memcpy(state->last_exp_hist_positive_buckets, + positive_buckets, + sizeof(uint64_t) * positive_count); + } + + if (negative_count > 0) { + memcpy(state->last_exp_hist_negative_buckets, + negative_buckets, + sizeof(uint64_t) * negative_count); + } + + state->last_exp_hist_scale = scale; + state->last_exp_hist_zero_count = zero_count; + state->last_exp_hist_zero_threshold = zero_threshold; + state->last_exp_hist_positive_offset = positive_offset; + state->last_exp_hist_negative_offset = negative_offset; + state->last_exp_hist_count = count; + state->last_exp_hist_sum_set = sum_set; + state->last_exp_hist_sum = sum; + state->last_timestamp = timestamp; + state->last_update_timestamp = cfl_time_now(); + + return 0; +} + +static flb_sds_t build_series_key(struct cmt_map *map, + struct cmt_metric *sample, + uint64_t context_identity) +{ + flb_sds_t key; + + key = flb_sds_create_size(cfl_sds_len(map->opts->fqname) + 96); + if (key == NULL) { + return NULL; + } + + if (flb_sds_printf(&key, "%d|%s|%d|%016" PRIx64 "|%" PRIu64, + map->type, + map->opts->fqname, + map->opts->resource_index, + context_identity, + sample->hash) == NULL) { + flb_sds_destroy(key); + return NULL; + } + + return key; +} + +static struct flb_cumulative_to_delta_series *series_state_create() +{ + struct flb_cumulative_to_delta_series *state; + + state = flb_calloc(1, sizeof(struct flb_cumulative_to_delta_series)); + if (state == NULL) { + return NULL; + } + + cfl_list_init(&state->_head); + + return state; +} + +static int series_state_table_del(struct flb_cumulative_to_delta_ctx *context, + struct flb_cumulative_to_delta_series *state) +{ + if (state->key == NULL) { + return 0; + } + + return flb_hash_table_del_ptr(context->series_table, + state->key, + cfl_sds_len(state->key), + state); +} + +static void series_state_destroy(struct flb_cumulative_to_delta_series *state) +{ + if (state == NULL) { + return; + } + + if (state->last_hist_buckets != NULL) { + flb_free(state->last_hist_buckets); + } + if (state->last_exp_hist_positive_buckets != NULL) { + flb_free(state->last_exp_hist_positive_buckets); + } + if (state->last_exp_hist_negative_buckets != NULL) { + flb_free(state->last_exp_hist_negative_buckets); + } + + if (state->key != NULL) { + flb_sds_destroy(state->key); + } + + if (state->_head.next != NULL && state->_head.prev != NULL) { + cfl_list_del(&state->_head); + } + flb_free(state); +} + +static struct flb_cumulative_to_delta_series *series_state_get( + struct flb_cumulative_to_delta_ctx *context, + struct cmt_map *map, + struct cmt_metric *sample, + uint64_t context_identity, + int create_if_missing) +{ + int ret; + flb_sds_t key; + struct flb_cumulative_to_delta_series *state; + + key = build_series_key(map, sample, context_identity); + if (key == NULL) { + return NULL; + } + + state = flb_hash_table_get_ptr(context->series_table, key, cfl_sds_len(key)); + if (state != NULL || create_if_missing == FLB_FALSE) { + flb_sds_destroy(key); + return state; + } + + state = series_state_create(); + if (state == NULL) { + flb_sds_destroy(key); + return NULL; + } + + state->type = map->type; + cfl_list_add(&state->_head, &context->series_list); + + ret = flb_hash_table_add(context->series_table, key, cfl_sds_len(key), + state, 0); + if (ret == -1) { + flb_sds_destroy(key); + series_state_destroy(state); + return NULL; + } + + state->key = key; + + return state; +} + +static void series_state_gc(struct flb_cumulative_to_delta_ctx *context, + uint64_t now) +{ + uint64_t age; + struct cfl_list *tmp; + struct cfl_list *head; + struct flb_cumulative_to_delta_series *state; + + cfl_list_foreach_safe(head, tmp, &context->series_list) { + state = cfl_list_entry(head, struct flb_cumulative_to_delta_series, _head); + + if (now >= state->last_update_timestamp) { + age = now - state->last_update_timestamp; + } + else { + continue; + } + + if (age > context->series_ttl) { + series_state_table_del(context, state); + series_state_destroy(state); + } + } +} + +static void series_state_evict_if_needed(struct flb_cumulative_to_delta_ctx *context) +{ + struct flb_cumulative_to_delta_series *state; + + while (cfl_list_size(&context->series_list) > context->max_series) { + if (cfl_list_is_empty(&context->series_list)) { + break; + } + + state = cfl_list_entry_first(&context->series_list, + struct flb_cumulative_to_delta_series, + _head); + + series_state_table_del(context, state); + series_state_destroy(state); + } +} + +static int process_counter_sample(struct flb_cumulative_to_delta_ctx *context, + struct cmt_counter *counter, + struct cmt_metric *sample, + uint64_t context_identity) +{ + int reset_detected; + double delta; + double current_value; + uint64_t timestamp; + struct flb_cumulative_to_delta_series *state; + + timestamp = cmt_metric_get_timestamp(sample); + current_value = cmt_metric_get_value(sample); + + state = series_state_get(context, + counter->map, + sample, + context_identity, + FLB_FALSE); + if (state == NULL) { + state = series_state_get(context, + counter->map, + sample, + context_identity, + FLB_TRUE); + if (state == NULL) { + return -1; + } + + if (series_state_update_counter(state, timestamp, current_value) != 0) { + return -1; + } + + if (should_drop_initial_sample(context, sample) == FLB_TRUE) { + return FLB_C2D_DROP; + } + + return FLB_C2D_KEEP; + } + + if (timestamp <= state->last_timestamp) { + return FLB_C2D_DROP; + } + + reset_detected = FLB_FALSE; + + if (counter->allow_reset == FLB_FALSE && + current_value < state->last_counter_value) { + reset_detected = FLB_TRUE; + } + + if (reset_detected == FLB_TRUE) { + if (series_state_update_counter(state, timestamp, current_value) != 0) { + return -1; + } + + if (context->drop_on_reset == FLB_TRUE) { + return FLB_C2D_DROP; + } + } + + if (reset_detected == FLB_TRUE) { + delta = current_value; + } + else { + delta = current_value - state->last_counter_value; + } + + cmt_metric_set(sample, timestamp, delta); + + if (series_state_update_counter(state, timestamp, current_value) != 0) { + return -1; + } + + return FLB_C2D_KEEP; +} + +static uint64_t exp_hist_bucket_get_value(int32_t offset, + size_t bucket_count, + uint64_t *buckets, + int64_t target_index) +{ + int64_t index; + + if (bucket_count == 0 || buckets == NULL) { + return 0; + } + + index = (int64_t) target_index - (int64_t) offset; + if (index < 0 || (size_t) index >= bucket_count) { + return 0; + } + + return buckets[index]; +} + +static int exp_hist_bucket_index_from_offset(int32_t offset, + size_t position, + int64_t *bucket_index) +{ + int64_t signed_offset; + int64_t signed_position; + + if (bucket_index == NULL) { + return -1; + } + + if (position > (size_t) INT64_MAX) { + return -1; + } + + signed_offset = (int64_t) offset; + signed_position = (int64_t) position; + + if (signed_offset > (INT64_MAX - signed_position)) { + return -1; + } + + *bucket_index = signed_offset + signed_position; + + return 0; +} + +static int process_histogram_sample(struct flb_cumulative_to_delta_ctx *context, + struct cmt_histogram *histogram, + struct cmt_metric *sample, + uint64_t context_identity) +{ + size_t bucket_index; + int reset_detected; + uint64_t bucket_delta; + uint64_t count_delta; + uint64_t current_count; + uint64_t timestamp; + double sum_delta; + double current_sum; + size_t bucket_count; + uint64_t *cumulative_buckets_snapshot; + struct flb_cumulative_to_delta_series *state; + + if (sample->hist_buckets == NULL) { + return FLB_C2D_DROP; + } + + timestamp = cmt_metric_get_timestamp(sample); + current_count = cmt_metric_hist_get_count_value(sample); + current_sum = cmt_metric_hist_get_sum_value(sample); + bucket_count = histogram->buckets->count + 1; + + if (bucket_count > (size_t) INT_MAX) { + return FLB_C2D_DROP; + } + + state = series_state_get(context, + histogram->map, + sample, + context_identity, + FLB_FALSE); + if (state == NULL) { + state = series_state_get(context, + histogram->map, + sample, + context_identity, + FLB_TRUE); + if (state == NULL) { + return -1; + } + + if (series_state_update_histogram(state, + timestamp, + current_count, + current_sum, + bucket_count, + sample->hist_buckets) != 0) { + return -1; + } + + if (should_drop_initial_sample(context, sample) == FLB_TRUE) { + return FLB_C2D_DROP; + } + + return FLB_C2D_KEEP; + } + + if (timestamp <= state->last_timestamp) { + return FLB_C2D_DROP; + } + + reset_detected = FLB_FALSE; + + if (bucket_count != state->last_hist_bucket_count) { + reset_detected = FLB_TRUE; + } + + if (current_count < state->last_hist_count) { + reset_detected = FLB_TRUE; + } + + if (reset_detected == FLB_FALSE) { + for (bucket_index = 0; bucket_index < bucket_count; bucket_index++) { + if (sample->hist_buckets[bucket_index] < + state->last_hist_buckets[bucket_index]) { + reset_detected = FLB_TRUE; + break; + } + } + } + + if (reset_detected == FLB_TRUE && context->drop_on_reset == FLB_TRUE) { + if (series_state_update_histogram(state, + timestamp, + current_count, + current_sum, + bucket_count, + sample->hist_buckets) != 0) { + return -1; + } + + return FLB_C2D_DROP; + } + + cumulative_buckets_snapshot = NULL; + + if (bucket_count > 0) { + cumulative_buckets_snapshot = flb_calloc(bucket_count, sizeof(uint64_t)); + if (cumulative_buckets_snapshot == NULL) { + return -1; + } + + memcpy(cumulative_buckets_snapshot, + sample->hist_buckets, + sizeof(uint64_t) * bucket_count); + } + + for (bucket_index = 0; bucket_index < bucket_count; bucket_index++) { + if (reset_detected == FLB_TRUE) { + bucket_delta = sample->hist_buckets[bucket_index]; + } + else { + bucket_delta = sample->hist_buckets[bucket_index] - + state->last_hist_buckets[bucket_index]; + } + + cmt_metric_hist_set(sample, timestamp, bucket_index, bucket_delta); + } + + if (reset_detected == FLB_TRUE) { + count_delta = current_count; + sum_delta = current_sum; + } + else { + count_delta = current_count - state->last_hist_count; + sum_delta = current_sum - state->last_hist_sum; + } + + cmt_metric_hist_count_set(sample, timestamp, count_delta); + cmt_metric_hist_sum_set(sample, timestamp, sum_delta); + + if (series_state_update_histogram(state, + timestamp, + current_count, + current_sum, + bucket_count, + cumulative_buckets_snapshot) != 0) { + if (cumulative_buckets_snapshot != NULL) { + flb_free(cumulative_buckets_snapshot); + } + return -1; + } + + if (cumulative_buckets_snapshot != NULL) { + flb_free(cumulative_buckets_snapshot); + } + + return FLB_C2D_KEEP; +} + +static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *context, + struct cmt_exp_histogram *exp_histogram, + struct cmt_metric *sample, + uint64_t context_identity) +{ + int reset_detected; + int current_sum_set; + uint64_t current_count; + uint64_t current_zero_count; + uint64_t count_delta; + uint64_t zero_count_delta; + uint64_t timestamp; + uint64_t current_bucket_value; + uint64_t previous_bucket_value; + int64_t bucket_index; + size_t index; + double current_sum; + double sum_delta; + uint64_t *cumulative_positive_snapshot; + uint64_t *cumulative_negative_snapshot; + struct flb_cumulative_to_delta_series *state; + + if (sample->exp_hist_positive_count > 0 && + sample->exp_hist_positive_buckets == NULL) { + return FLB_C2D_DROP; + } + + if (sample->exp_hist_negative_count > 0 && + sample->exp_hist_negative_buckets == NULL) { + return FLB_C2D_DROP; + } + + timestamp = cmt_metric_get_timestamp(sample); + current_count = sample->exp_hist_count; + current_zero_count = sample->exp_hist_zero_count; + current_sum_set = sample->exp_hist_sum_set; + current_sum = 0.0; + sum_delta = 0.0; + + if (current_sum_set == CMT_TRUE) { + current_sum = cmt_math_uint64_to_d64(sample->exp_hist_sum); + } + + state = series_state_get(context, + exp_histogram->map, + sample, + context_identity, + FLB_FALSE); + if (state == NULL) { + state = series_state_get(context, + exp_histogram->map, + sample, + context_identity, + FLB_TRUE); + if (state == NULL) { + return -1; + } + + if (series_state_update_exp_histogram(state, + timestamp, + sample->exp_hist_scale, + current_zero_count, + sample->exp_hist_zero_threshold, + sample->exp_hist_positive_offset, + sample->exp_hist_positive_count, + sample->exp_hist_positive_buckets, + sample->exp_hist_negative_offset, + sample->exp_hist_negative_count, + sample->exp_hist_negative_buckets, + current_count, + current_sum_set, + current_sum) != 0) { + return -1; + } + + if (should_drop_initial_sample(context, sample) == FLB_TRUE) { + return FLB_C2D_DROP; + } + + return FLB_C2D_KEEP; + } + + if (timestamp <= state->last_timestamp) { + return FLB_C2D_DROP; + } + + reset_detected = FLB_FALSE; + + if (sample->exp_hist_scale != state->last_exp_hist_scale || + sample->exp_hist_zero_threshold != state->last_exp_hist_zero_threshold || + current_sum_set != state->last_exp_hist_sum_set) { + reset_detected = FLB_TRUE; + } + + if (current_count < state->last_exp_hist_count || + current_zero_count < state->last_exp_hist_zero_count) { + reset_detected = FLB_TRUE; + } + + if (reset_detected == FLB_FALSE) { + for (index = 0; index < sample->exp_hist_positive_count; index++) { + if (exp_hist_bucket_index_from_offset(sample->exp_hist_positive_offset, + index, + &bucket_index) != 0) { + return FLB_C2D_DROP; + } + + previous_bucket_value = exp_hist_bucket_get_value( + state->last_exp_hist_positive_offset, + state->last_exp_hist_positive_count, + state->last_exp_hist_positive_buckets, + bucket_index); + + current_bucket_value = sample->exp_hist_positive_buckets[index]; + + if (current_bucket_value < previous_bucket_value) { + reset_detected = FLB_TRUE; + break; + } + } + } + + if (reset_detected == FLB_FALSE) { + for (index = 0; index < sample->exp_hist_negative_count; index++) { + if (exp_hist_bucket_index_from_offset(sample->exp_hist_negative_offset, + index, + &bucket_index) != 0) { + return FLB_C2D_DROP; + } + + previous_bucket_value = exp_hist_bucket_get_value( + state->last_exp_hist_negative_offset, + state->last_exp_hist_negative_count, + state->last_exp_hist_negative_buckets, + bucket_index); + + current_bucket_value = sample->exp_hist_negative_buckets[index]; + + if (current_bucket_value < previous_bucket_value) { + reset_detected = FLB_TRUE; + break; + } + } + } + + if (reset_detected == FLB_FALSE) { + for (index = 0; index < state->last_exp_hist_positive_count; index++) { + if (exp_hist_bucket_index_from_offset(state->last_exp_hist_positive_offset, + index, + &bucket_index) != 0) { + return FLB_C2D_DROP; + } + + previous_bucket_value = exp_hist_bucket_get_value( + state->last_exp_hist_positive_offset, + state->last_exp_hist_positive_count, + state->last_exp_hist_positive_buckets, + bucket_index); + current_bucket_value = exp_hist_bucket_get_value( + sample->exp_hist_positive_offset, + sample->exp_hist_positive_count, + sample->exp_hist_positive_buckets, + bucket_index); + + if (current_bucket_value < previous_bucket_value) { + reset_detected = FLB_TRUE; + break; + } + } + } + + if (reset_detected == FLB_FALSE) { + for (index = 0; index < state->last_exp_hist_negative_count; index++) { + if (exp_hist_bucket_index_from_offset(state->last_exp_hist_negative_offset, + index, + &bucket_index) != 0) { + return FLB_C2D_DROP; + } + + previous_bucket_value = exp_hist_bucket_get_value( + state->last_exp_hist_negative_offset, + state->last_exp_hist_negative_count, + state->last_exp_hist_negative_buckets, + bucket_index); + current_bucket_value = exp_hist_bucket_get_value( + sample->exp_hist_negative_offset, + sample->exp_hist_negative_count, + sample->exp_hist_negative_buckets, + bucket_index); + + if (current_bucket_value < previous_bucket_value) { + reset_detected = FLB_TRUE; + break; + } + } + } + + if (reset_detected == FLB_TRUE && context->drop_on_reset == FLB_TRUE) { + if (series_state_update_exp_histogram(state, + timestamp, + sample->exp_hist_scale, + current_zero_count, + sample->exp_hist_zero_threshold, + sample->exp_hist_positive_offset, + sample->exp_hist_positive_count, + sample->exp_hist_positive_buckets, + sample->exp_hist_negative_offset, + sample->exp_hist_negative_count, + sample->exp_hist_negative_buckets, + current_count, + current_sum_set, + current_sum) != 0) { + return -1; + } + + return FLB_C2D_DROP; + } + + cumulative_positive_snapshot = NULL; + cumulative_negative_snapshot = NULL; + + if (sample->exp_hist_positive_count > 0) { + cumulative_positive_snapshot = flb_calloc(sample->exp_hist_positive_count, + sizeof(uint64_t)); + if (cumulative_positive_snapshot == NULL) { + return -1; + } + + memcpy(cumulative_positive_snapshot, + sample->exp_hist_positive_buckets, + sizeof(uint64_t) * sample->exp_hist_positive_count); + } + + if (sample->exp_hist_negative_count > 0) { + cumulative_negative_snapshot = flb_calloc(sample->exp_hist_negative_count, + sizeof(uint64_t)); + if (cumulative_negative_snapshot == NULL) { + if (cumulative_positive_snapshot != NULL) { + flb_free(cumulative_positive_snapshot); + } + return -1; + } + + memcpy(cumulative_negative_snapshot, + sample->exp_hist_negative_buckets, + sizeof(uint64_t) * sample->exp_hist_negative_count); + } + + for (index = 0; index < sample->exp_hist_positive_count; index++) { + if (reset_detected == FLB_TRUE) { + continue; + } + + if (exp_hist_bucket_index_from_offset(sample->exp_hist_positive_offset, + index, + &bucket_index) != 0) { + if (cumulative_positive_snapshot != NULL) { + flb_free(cumulative_positive_snapshot); + } + if (cumulative_negative_snapshot != NULL) { + flb_free(cumulative_negative_snapshot); + } + return FLB_C2D_DROP; + } + + previous_bucket_value = exp_hist_bucket_get_value( + state->last_exp_hist_positive_offset, + state->last_exp_hist_positive_count, + state->last_exp_hist_positive_buckets, + bucket_index); + + sample->exp_hist_positive_buckets[index] -= previous_bucket_value; + } + + for (index = 0; index < sample->exp_hist_negative_count; index++) { + if (reset_detected == FLB_TRUE) { + continue; + } + + if (exp_hist_bucket_index_from_offset(sample->exp_hist_negative_offset, + index, + &bucket_index) != 0) { + if (cumulative_positive_snapshot != NULL) { + flb_free(cumulative_positive_snapshot); + } + if (cumulative_negative_snapshot != NULL) { + flb_free(cumulative_negative_snapshot); + } + return FLB_C2D_DROP; + } + + previous_bucket_value = exp_hist_bucket_get_value( + state->last_exp_hist_negative_offset, + state->last_exp_hist_negative_count, + state->last_exp_hist_negative_buckets, + bucket_index); + + sample->exp_hist_negative_buckets[index] -= previous_bucket_value; + } + + if (reset_detected == FLB_TRUE) { + count_delta = current_count; + zero_count_delta = current_zero_count; + sum_delta = current_sum; + } + else { + count_delta = current_count - state->last_exp_hist_count; + zero_count_delta = current_zero_count - state->last_exp_hist_zero_count; + + if (current_sum_set == CMT_TRUE) { + sum_delta = current_sum - state->last_exp_hist_sum; + } + } + + sample->exp_hist_count = count_delta; + sample->exp_hist_zero_count = zero_count_delta; + + if (current_sum_set == CMT_TRUE) { + sample->exp_hist_sum_set = CMT_TRUE; + sample->exp_hist_sum = cmt_math_d64_to_uint64(sum_delta); + } + else { + sample->exp_hist_sum_set = CMT_FALSE; + sample->exp_hist_sum = 0; + } + + if (series_state_update_exp_histogram(state, + timestamp, + sample->exp_hist_scale, + current_zero_count, + sample->exp_hist_zero_threshold, + sample->exp_hist_positive_offset, + sample->exp_hist_positive_count, + cumulative_positive_snapshot, + sample->exp_hist_negative_offset, + sample->exp_hist_negative_count, + cumulative_negative_snapshot, + current_count, + current_sum_set, + current_sum) != 0) { + if (cumulative_positive_snapshot != NULL) { + flb_free(cumulative_positive_snapshot); + } + if (cumulative_negative_snapshot != NULL) { + flb_free(cumulative_negative_snapshot); + } + return -1; + } + + if (cumulative_positive_snapshot != NULL) { + flb_free(cumulative_positive_snapshot); + } + if (cumulative_negative_snapshot != NULL) { + flb_free(cumulative_negative_snapshot); + } + + return FLB_C2D_KEEP; +} + +static int process_counter_map(struct flb_cumulative_to_delta_ctx *context, + struct cmt_counter *counter, + uint64_t context_identity) +{ + int result; + struct cfl_list *tmp; + struct cfl_list *head; + struct cmt_metric *sample; + struct cmt_map *map; + + map = counter->map; + + if (counter->aggregation_type != CMT_AGGREGATION_TYPE_CUMULATIVE) { + return 0; + } + + /* + * OTLP non-monotonic sums are decoded as counters with allow_reset=true. + * This processor only converts monotonic cumulative sums to delta. + */ + if (counter->allow_reset == FLB_TRUE) { + return 0; + } + + counter->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + + if (map->metric_static_set == FLB_TRUE) { + result = process_counter_sample(context, + counter, + &map->metric, + context_identity); + if (result == -1) { + return -1; + } + if (result == FLB_C2D_DROP) { + map->metric_static_set = FLB_FALSE; + } + } + + cfl_list_foreach_safe(head, tmp, &map->metrics) { + sample = cfl_list_entry(head, struct cmt_metric, _head); + + result = process_counter_sample(context, + counter, + sample, + context_identity); + if (result == -1) { + return -1; + } + if (result == FLB_C2D_DROP) { + cmt_map_metric_destroy(sample); + } + } + + return 0; +} + +static int process_histogram_map(struct flb_cumulative_to_delta_ctx *context, + struct cmt_histogram *histogram, + uint64_t context_identity) +{ + int result; + struct cfl_list *tmp; + struct cfl_list *head; + struct cmt_metric *sample; + struct cmt_map *map; + + map = histogram->map; + + if (histogram->aggregation_type != CMT_AGGREGATION_TYPE_CUMULATIVE) { + return 0; + } + + histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + + if (map->metric_static_set == FLB_TRUE) { + result = process_histogram_sample(context, + histogram, + &map->metric, + context_identity); + if (result == -1) { + return -1; + } + if (result == FLB_C2D_DROP) { + map->metric_static_set = FLB_FALSE; + } + } + + cfl_list_foreach_safe(head, tmp, &map->metrics) { + sample = cfl_list_entry(head, struct cmt_metric, _head); + + result = process_histogram_sample(context, + histogram, + sample, + context_identity); + if (result == -1) { + return -1; + } + if (result == FLB_C2D_DROP) { + cmt_map_metric_destroy(sample); + } + } + + return 0; +} + +static int process_exp_histogram_map(struct flb_cumulative_to_delta_ctx *context, + struct cmt_exp_histogram *exp_histogram, + uint64_t context_identity) +{ + int result; + struct cfl_list *tmp; + struct cfl_list *head; + struct cmt_metric *sample; + struct cmt_map *map; + + map = exp_histogram->map; + + if (exp_histogram->aggregation_type != CMT_AGGREGATION_TYPE_CUMULATIVE) { + return 0; + } + + exp_histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + + if (map->metric_static_set == FLB_TRUE) { + result = process_exp_histogram_sample(context, + exp_histogram, + &map->metric, + context_identity); + if (result == -1) { + return -1; + } + if (result == FLB_C2D_DROP) { + map->metric_static_set = FLB_FALSE; + } + } + + cfl_list_foreach_safe(head, tmp, &map->metrics) { + sample = cfl_list_entry(head, struct cmt_metric, _head); + + result = process_exp_histogram_sample(context, + exp_histogram, + sample, + context_identity); + if (result == -1) { + return -1; + } + if (result == FLB_C2D_DROP) { + cmt_map_metric_destroy(sample); + } + } + + return 0; +} + +struct flb_cumulative_to_delta_ctx *flb_cumulative_to_delta_ctx_create( + int initial_value_mode, + int drop_on_reset, + uint64_t processor_start_timestamp) +{ + struct flb_cumulative_to_delta_ctx *context; + + context = flb_calloc(1, sizeof(struct flb_cumulative_to_delta_ctx)); + if (context == NULL) { + return NULL; + } + + context->series_table = flb_hash_table_create(FLB_HASH_TABLE_EVICT_NONE, + FLB_C2D_SERIES_TABLE_SIZE, + 0); + if (context->series_table == NULL) { + flb_free(context); + return NULL; + } + + cfl_list_init(&context->series_list); + context->initial_value_mode = initial_value_mode; + context->drop_on_reset = drop_on_reset; + context->processor_start_timestamp = processor_start_timestamp; + context->gc_interval = (uint64_t) FLB_C2D_GC_INTERVAL_SECONDS * 1000000000ULL; + context->series_ttl = (uint64_t) FLB_C2D_SERIES_TTL_SECONDS * 1000000000ULL; + context->max_series = FLB_C2D_MAX_SERIES; + context->next_gc_timestamp = cfl_time_now() + context->gc_interval; + + return context; +} + +void flb_cumulative_to_delta_ctx_destroy( + struct flb_cumulative_to_delta_ctx *context) +{ + struct cfl_list *tmp; + struct cfl_list *head; + struct flb_cumulative_to_delta_series *state; + + if (context == NULL) { + return; + } + + cfl_list_foreach_safe(head, tmp, &context->series_list) { + state = cfl_list_entry(head, struct flb_cumulative_to_delta_series, _head); + series_state_table_del(context, state); + series_state_destroy(state); + } + + if (context->series_table != NULL) { + flb_hash_table_destroy(context->series_table); + } + + flb_free(context); +} + +int flb_cumulative_to_delta_ctx_process(struct flb_cumulative_to_delta_ctx *context, + struct cmt *metrics_context) +{ + int result; + uint64_t now; + uint64_t context_identity; + struct cfl_list *head; + struct cmt_counter *counter; + struct cmt_histogram *histogram; + struct cmt_exp_histogram *exp_histogram; + + now = cfl_time_now(); + context_identity = compute_context_identity(metrics_context); + + if (now >= context->next_gc_timestamp) { + series_state_gc(context, now); + context->next_gc_timestamp = now + context->gc_interval; + } + + cfl_list_foreach(head, &metrics_context->counters) { + counter = cfl_list_entry(head, struct cmt_counter, _head); + + result = process_counter_map(context, counter, context_identity); + if (result != 0) { + return -1; + } + } + + cfl_list_foreach(head, &metrics_context->histograms) { + histogram = cfl_list_entry(head, struct cmt_histogram, _head); + + result = process_histogram_map(context, histogram, context_identity); + if (result != 0) { + return -1; + } + } + + cfl_list_foreach(head, &metrics_context->exp_histograms) { + exp_histogram = cfl_list_entry(head, struct cmt_exp_histogram, _head); + + result = process_exp_histogram_map(context, + exp_histogram, + context_identity); + if (result != 0) { + return -1; + } + } + + series_state_evict_if_needed(context); + + return 0; +} + +int flb_cumulative_to_delta_ctx_configure( + struct flb_cumulative_to_delta_ctx *context, + int max_staleness_seconds, + int max_series) +{ + if (context == NULL) { + return -1; + } + + if (max_staleness_seconds < 0 || max_series < 0) { + return -1; + } + + if (max_staleness_seconds == 0) { + context->series_ttl = UINT64_MAX; + } + else { + context->series_ttl = (uint64_t) max_staleness_seconds * 1000000000ULL; + } + + if (max_series == 0) { + context->max_series = SIZE_MAX; + } + else { + context->max_series = (size_t) max_series; + } + + return 0; +} From 5879911ef3a1cc061a0a7cd1b3f88fdbc526c3fc Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 16 Feb 2026 17:59:33 -0600 Subject: [PATCH 02/14] tests: internal: cumulative_to_delta Signed-off-by: Eduardo Silva --- tests/internal/CMakeLists.txt | 13 + tests/internal/cumulative_to_delta.c | 407 +++++++++++++++++++++++++++ 2 files changed, 420 insertions(+) create mode 100644 tests/internal/cumulative_to_delta.c diff --git a/tests/internal/CMakeLists.txt b/tests/internal/CMakeLists.txt index 9b5b8fdd3d7..53510b54368 100644 --- a/tests/internal/CMakeLists.txt +++ b/tests/internal/CMakeLists.txt @@ -118,6 +118,7 @@ if(FLB_METRICS) set(UNIT_TESTS_FILES ${UNIT_TESTS_FILES} metrics.c + cumulative_to_delta.c ) endif() @@ -248,6 +249,18 @@ endfunction(prepare_unit_tests) prepare_unit_tests(flb-it- "${UNIT_TESTS_FILES}") +if(FLB_METRICS AND FLB_PROCESSOR_CUMULATIVE_TO_DELTA) + set(CUMULATIVE_TO_DELTA_PLUGIN_DIR + ${PROJECT_SOURCE_DIR}/plugins/processor_cumulative_to_delta) + set(CUMULATIVE_TO_DELTA_CORE_SOURCE + ${CUMULATIVE_TO_DELTA_PLUGIN_DIR}/cumulative_to_delta_core.c) + + target_sources(flb-it-cumulative_to_delta PRIVATE + ${CUMULATIVE_TO_DELTA_CORE_SOURCE}) + target_include_directories(flb-it-cumulative_to_delta PRIVATE + ${CUMULATIVE_TO_DELTA_PLUGIN_DIR}) +endif() + if(FLB_TESTS_INTERNAL_FUZZ) add_subdirectory(fuzzers) endif() diff --git a/tests/internal/cumulative_to_delta.c b/tests/internal/cumulative_to_delta.c new file mode 100644 index 00000000000..71ec9858532 --- /dev/null +++ b/tests/internal/cumulative_to_delta.c @@ -0,0 +1,407 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +#include + +#include +#include +#include +#include +#include + +#include "../../plugins/processor_cumulative_to_delta/cumulative_to_delta.h" + +#include "flb_tests_internal.h" + +static struct cmt_counter *get_first_counter(struct cmt *context) +{ + return cfl_list_entry(context->counters.next, struct cmt_counter, _head); +} + +static struct cmt_histogram *get_first_histogram(struct cmt *context) +{ + return cfl_list_entry(context->histograms.next, struct cmt_histogram, _head); +} + +static int map_sample_count(struct cmt_map *map) +{ + int count; + + count = cfl_list_size(&map->metrics); + + if (map->metric_static_set == FLB_TRUE) { + count++; + } + + return count; +} + +static struct cmt *create_counter_context(char *name, + uint64_t timestamp, + double value, + int allow_reset) +{ + struct cmt *context; + struct cmt_counter *counter; + + context = cmt_create(); + if (context == NULL) { + return NULL; + } + + counter = cmt_counter_create(context, "", "", name, "help", 0, NULL); + if (counter == NULL) { + cmt_destroy(context); + return NULL; + } + + counter->aggregation_type = CMT_AGGREGATION_TYPE_CUMULATIVE; + + if (allow_reset == FLB_TRUE) { + cmt_counter_allow_reset(counter); + } + + if (cmt_counter_set(counter, timestamp, value, 0, NULL) != 0) { + cmt_destroy(context); + return NULL; + } + + return context; +} + +static struct cmt *create_histogram_context(char *name, + uint64_t timestamp, + uint64_t *buckets, + double sum, + uint64_t count) +{ + double upper_bounds[2]; + struct cmt *context; + struct cmt_histogram *histogram; + struct cmt_histogram_buckets *bucket_definition; + + upper_bounds[0] = 1.0; + upper_bounds[1] = 2.0; + + context = cmt_create(); + if (context == NULL) { + return NULL; + } + + bucket_definition = cmt_histogram_buckets_create_size(upper_bounds, 2); + if (bucket_definition == NULL) { + cmt_destroy(context); + return NULL; + } + + histogram = cmt_histogram_create(context, "", "", name, "help", + bucket_definition, 0, NULL); + if (histogram == NULL) { + cmt_destroy(context); + return NULL; + } + + histogram->aggregation_type = CMT_AGGREGATION_TYPE_CUMULATIVE; + + if (cmt_histogram_set_default(histogram, timestamp, buckets, sum, count, + 0, NULL) != 0) { + cmt_destroy(context); + return NULL; + } + + return context; +} + +static void test_counter_drop_first_and_delta() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_counter_context("requests_total", 100, 10.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + context = create_counter_context("requests_total", 200, 16.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + counter = get_first_counter(context); + TEST_CHECK(counter->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + TEST_CHECK(map_sample_count(counter->map) == 1); + + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 6.0) < 0.0001); + + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_counter_reset_drop_and_keep() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_counter_context("errors_total", 100, 10.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_counter_context("errors_total", 200, 2.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + context = create_counter_context("errors_total", 300, 8.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 6.0) < 0.0001); + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_FALSE, 0); + TEST_CHECK(converter != NULL); + + context = create_counter_context("errors_total_keep", 100, 10.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_counter_context("errors_total_keep", 200, 2.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 2.0) < 0.0001); + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_histogram_drop_first_and_delta() +{ + double sum; + uint64_t count; + uint64_t buckets_1[3]; + uint64_t buckets_2[3]; + struct cmt *context; + struct cmt_histogram *histogram; + struct flb_cumulative_to_delta_ctx *converter; + + buckets_1[0] = 1; + buckets_1[1] = 2; + buckets_1[2] = 3; + + buckets_2[0] = 3; + buckets_2[1] = 5; + buckets_2[2] = 8; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_histogram_context("latency_seconds", 100, buckets_1, 2.0, 3); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + histogram = get_first_histogram(context); + TEST_CHECK(map_sample_count(histogram->map) == 0); + cmt_destroy(context); + + context = create_histogram_context("latency_seconds", 200, buckets_2, 7.0, 8); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + histogram = get_first_histogram(context); + TEST_CHECK(histogram->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + TEST_CHECK(map_sample_count(histogram->map) == 1); + TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 0) == 2); + TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 1) == 3); + TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 2) == 5); + + count = cmt_metric_hist_get_count_value(&histogram->map->metric); + sum = cmt_metric_hist_get_sum_value(&histogram->map->metric); + TEST_CHECK(count == 5); + TEST_CHECK(fabs(sum - 5.0) < 0.0001); + + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_counter_out_of_order_is_dropped() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_counter_context("out_of_order_total", 200, 5.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_counter_context("out_of_order_total", 100, 9.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + context = create_counter_context("out_of_order_total", 300, 12.0, FLB_FALSE); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 7.0) < 0.0001); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_counter_initial_value_auto() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_AUTO, + FLB_TRUE, 150); + TEST_CHECK(converter != NULL); + + context = create_counter_context("auto_drop_total", 100, 10.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + context = create_counter_context("auto_drop_total", 300, + 18.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 8.0) < 0.0001); + cmt_destroy(context); + + context = create_counter_context("auto_keep_total", 200, + 7.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 7.0) < 0.0001); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_histogram_sum_decrease_without_reset() +{ + double sum; + uint64_t count; + uint64_t buckets_1[3]; + uint64_t buckets_2[3]; + struct cmt *context; + struct cmt_histogram *histogram; + struct flb_cumulative_to_delta_ctx *converter; + + buckets_1[0] = 1; + buckets_1[1] = 2; + buckets_1[2] = 3; + + buckets_2[0] = 2; + buckets_2[1] = 4; + buckets_2[2] = 6; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_histogram_context("negative_sum_histogram", + 100, buckets_1, 10.0, 3); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_histogram_context("negative_sum_histogram", + 200, buckets_2, 8.0, 6); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + histogram = get_first_histogram(context); + TEST_CHECK(map_sample_count(histogram->map) == 1); + TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 0) == 1); + TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 1) == 2); + TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 2) == 3); + + count = cmt_metric_hist_get_count_value(&histogram->map->metric); + sum = cmt_metric_hist_get_sum_value(&histogram->map->metric); + TEST_CHECK(count == 3); + TEST_CHECK(fabs(sum - (-2.0)) < 0.0001); + + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_non_monotonic_sum_is_not_converted() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_counter_context("non_monotonic_sum", 100, 10.0, FLB_TRUE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + counter = get_first_counter(context); + TEST_CHECK(counter->aggregation_type == CMT_AGGREGATION_TYPE_CUMULATIVE); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 10.0) < 0.0001); + cmt_destroy(context); + + context = create_counter_context("non_monotonic_sum", 200, 3.0, FLB_TRUE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + counter = get_first_counter(context); + TEST_CHECK(counter->aggregation_type == CMT_AGGREGATION_TYPE_CUMULATIVE); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 3.0) < 0.0001); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + +TEST_LIST = { + {"counter_drop_first_and_delta", test_counter_drop_first_and_delta}, + {"counter_reset_drop_and_keep", test_counter_reset_drop_and_keep}, + {"histogram_drop_first_and_delta", test_histogram_drop_first_and_delta}, + {"counter_out_of_order_is_dropped", test_counter_out_of_order_is_dropped}, + {"counter_initial_value_auto", test_counter_initial_value_auto}, + {"histogram_sum_decrease_without_reset", test_histogram_sum_decrease_without_reset}, + {"non_monotonic_sum_is_not_converted", test_non_monotonic_sum_is_not_converted}, + {0} +}; From 2e3c712e3172e9dad1b605e9d6201972f09d77fd Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 16 Feb 2026 18:00:04 -0600 Subject: [PATCH 03/14] tests: runtime: add processor_cumulative_to_delta Signed-off-by: Eduardo Silva --- tests/runtime/processor_cumulative_to_delta.c | 811 ++++++++++++++++++ 1 file changed, 811 insertions(+) create mode 100644 tests/runtime/processor_cumulative_to_delta.c diff --git a/tests/runtime/processor_cumulative_to_delta.c b/tests/runtime/processor_cumulative_to_delta.c new file mode 100644 index 00000000000..81cdcb641ca --- /dev/null +++ b/tests/runtime/processor_cumulative_to_delta.c @@ -0,0 +1,811 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "flb_tests_runtime.h" + +#define PORT_OTEL 4318 +#define V1_ENDPOINT_METRICS "/v1/metrics" +#define OTLP_CONTENT_TYPE "application/x-protobuf" + +#define MAX_CAPTURED_VALUES 32 +#define MAX_CAPTURE_METRICS 8 + +struct http_client_ctx { + struct flb_upstream *upstream; + struct flb_connection *connection; + struct flb_config *config; + struct mk_event_loop *event_loop; +}; + +struct rt_ctx { + flb_ctx_t *flb; + int input_ffd; + int output_ffd; + struct flb_processor *processor; + struct http_client_ctx *http; +}; + +struct metric_capture { + const char *line_prefix; + double values[MAX_CAPTURED_VALUES]; + int value_count; +}; + +struct observation_state { + int callback_count; + int capture_count; + struct metric_capture captures[MAX_CAPTURE_METRICS]; +}; + +static pthread_mutex_t state_mutex = PTHREAD_MUTEX_INITIALIZER; +static struct observation_state observed; + +static int find_metric_value(const char *text, + const char *line_prefix, + double *value) +{ + const char *match; + const char *cursor; + const char *line_start; + const char *line_end; + const char *equal_sign; + + line_start = text; + + while (line_start != NULL && *line_start != '\0') { + line_end = strchr(line_start, '\n'); + if (line_end == NULL) { + line_end = line_start + strlen(line_start); + } + + match = strstr(line_start, line_prefix); + if (match != NULL && match < line_end) { + equal_sign = NULL; + + for (cursor = match; cursor < line_end; cursor++) { + if (*cursor == '=') { + equal_sign = cursor; + } + } + + if (equal_sign != NULL) { + equal_sign++; + + while (equal_sign < line_end && *equal_sign == ' ') { + equal_sign++; + } + + if (equal_sign < line_end && + ((*equal_sign >= '0' && *equal_sign <= '9') || + *equal_sign == '-' || *equal_sign == '+')) { + *value = strtod(equal_sign, NULL); + return 0; + } + } + } + + if (*line_end == '\0') { + break; + } + + line_start = line_end + 1; + } + + return -1; +} + +static void observation_reset(void) +{ + pthread_mutex_lock(&state_mutex); + memset(&observed, 0, sizeof(observed)); + pthread_mutex_unlock(&state_mutex); +} + +static int observation_add_capture(const char *line_prefix) +{ + int index; + + pthread_mutex_lock(&state_mutex); + + index = observed.capture_count; + + if (index >= MAX_CAPTURE_METRICS) { + pthread_mutex_unlock(&state_mutex); + return -1; + } + + observed.captures[index].line_prefix = line_prefix; + observed.captures[index].value_count = 0; + observed.capture_count++; + + pthread_mutex_unlock(&state_mutex); + + return index; +} + +static int observation_get_callback_count(void) +{ + int count; + + pthread_mutex_lock(&state_mutex); + count = observed.callback_count; + pthread_mutex_unlock(&state_mutex); + + return count; +} + +static int observation_get_value_count(int capture_index) +{ + int count; + + pthread_mutex_lock(&state_mutex); + + if (capture_index < 0 || capture_index >= observed.capture_count) { + count = 0; + } + else { + count = observed.captures[capture_index].value_count; + } + + pthread_mutex_unlock(&state_mutex); + + return count; +} + +static double observation_get_value(int capture_index, int value_index) +{ + double value; + + pthread_mutex_lock(&state_mutex); + + if (capture_index < 0 || + capture_index >= observed.capture_count || + value_index < 0 || + value_index >= observed.captures[capture_index].value_count) { + value = 0.0; + } + else { + value = observed.captures[capture_index].values[value_index]; + } + + pthread_mutex_unlock(&state_mutex); + + return value; +} + +static int wait_for_callback_growth(int baseline, int timeout_ms) +{ + int waited; + + waited = 0; + + while (waited < timeout_ms) { + if (observation_get_callback_count() > baseline) { + return 0; + } + + flb_time_msleep(50); + waited += 50; + } + + return -1; +} + +static int wait_for_value_count(int capture_index, int expected_count, int timeout_ms) +{ + int waited; + + waited = 0; + + while (waited < timeout_ms) { + if (observation_get_value_count(capture_index) >= expected_count) { + return 0; + } + + flb_time_msleep(50); + waited += 50; + } + + return -1; +} + +static struct http_client_ctx *http_client_ctx_create(void) +{ + struct http_client_ctx *context; + struct mk_event_loop *event_loop; + + context = flb_calloc(1, sizeof(struct http_client_ctx)); + if (context == NULL) { + flb_errno(); + return NULL; + } + + event_loop = mk_event_loop_create(16); + if (event_loop == NULL) { + flb_free(context); + return NULL; + } + + context->event_loop = event_loop; + + flb_engine_evl_init(); + flb_engine_evl_set(event_loop); + + context->config = flb_config_init(); + if (context->config == NULL) { + mk_event_loop_destroy(event_loop); + flb_free(context); + return NULL; + } + + context->upstream = flb_upstream_create(context->config, + "127.0.0.1", + PORT_OTEL, + 0, + NULL); + if (context->upstream == NULL) { + flb_config_exit(context->config); + mk_event_loop_destroy(event_loop); + flb_free(context); + return NULL; + } + + context->connection = flb_upstream_conn_get(context->upstream); + if (context->connection == NULL) { + flb_upstream_destroy(context->upstream); + flb_config_exit(context->config); + mk_event_loop_destroy(event_loop); + flb_free(context); + return NULL; + } + + context->connection->upstream = context->upstream; + + return context; +} + +static void http_client_ctx_destroy(struct http_client_ctx *context) +{ + if (context == NULL) { + return; + } + + if (context->upstream != NULL) { + flb_upstream_destroy(context->upstream); + } + + if (context->config != NULL) { + flb_config_exit(context->config); + } + + if (context->event_loop != NULL) { + mk_event_loop_destroy(context->event_loop); + } + + flb_free(context); +} + +static int cb_capture_metrics(void *record, size_t size, void *data) +{ + int ret; + int index; + double value; + size_t offset; + cfl_sds_t text; + struct cmt *context; + + (void) data; + + offset = 0; + text = NULL; + context = NULL; + + ret = cmt_decode_msgpack_create(&context, (char *) record, size, &offset); + if (ret != CMT_DECODE_MSGPACK_SUCCESS) { + if (record != NULL) { + flb_free(record); + } + + return -1; + } + + text = cmt_encode_text_create(context); + + pthread_mutex_lock(&state_mutex); + + observed.callback_count++; + + if (text != NULL) { + for (index = 0; index < observed.capture_count; index++) { + if (find_metric_value(text, + observed.captures[index].line_prefix, + &value) == 0) { + if (observed.captures[index].value_count < MAX_CAPTURED_VALUES) { + observed.captures[index].values[ + observed.captures[index].value_count] = value; + observed.captures[index].value_count++; + } + } + } + } + + pthread_mutex_unlock(&state_mutex); + + if (text != NULL) { + cmt_encode_text_destroy(text); + } + + cmt_destroy(context); + + if (record != NULL) { + flb_free(record); + } + + return 0; +} + +static struct rt_ctx *rt_ctx_create(const char *drop_first, + const char *drop_on_reset) +{ + int ret; + struct rt_ctx *context; + struct flb_processor_unit *unit; + struct flb_lib_out_cb cb_data; + + context = flb_calloc(1, sizeof(struct rt_ctx)); + if (context == NULL) { + flb_errno(); + return NULL; + } + + cb_data.cb = cb_capture_metrics; + cb_data.data = NULL; + + context->flb = flb_create(); + if (context->flb == NULL) { + flb_free(context); + return NULL; + } + + flb_service_set(context->flb, + "Flush", "0.200000000", + "Grace", "1", + "Log_Level", "error", + NULL); + + context->input_ffd = flb_input(context->flb, (char *) "opentelemetry", NULL); + if (context->input_ffd < 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + ret = flb_input_set(context->flb, + context->input_ffd, + "tag", "test", + "tag_from_uri", "false", + NULL); + if (ret != 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + context->processor = flb_processor_create(context->flb->config, + "unit_test", + NULL, + 0); + if (context->processor == NULL) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + unit = flb_processor_unit_create(context->processor, + FLB_PROCESSOR_METRICS, + "cumulative_to_delta"); + if (unit == NULL) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + if (drop_first != NULL) { + ret = flb_processor_unit_set_property_str(unit, + "drop_first", + (char *) drop_first); + if (ret != 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + } + + if (drop_on_reset != NULL) { + ret = flb_processor_unit_set_property_str(unit, + "drop_on_reset", + (char *) drop_on_reset); + if (ret != 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + } + + ret = flb_input_set_processor(context->flb, + context->input_ffd, + context->processor); + if (ret != 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + context->output_ffd = flb_output(context->flb, (char *) "lib", &cb_data); + if (context->output_ffd < 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + ret = flb_output_set(context->flb, + context->output_ffd, + "match", "*", + NULL); + if (ret != 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + ret = flb_start(context->flb); + if (ret != 0) { + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + context->http = http_client_ctx_create(); + if (context->http == NULL) { + flb_stop(context->flb); + flb_destroy(context->flb); + flb_free(context); + return NULL; + } + + return context; +} + +static void rt_ctx_destroy(struct rt_ctx *context) +{ + if (context == NULL) { + return; + } + + if (context->http != NULL) { + flb_upstream_conn_release(context->http->connection); + http_client_ctx_destroy(context->http); + } + + if (context->flb != NULL) { + flb_stop(context->flb); + flb_destroy(context->flb); + } + + flb_free(context); +} + +static struct cmt *create_counter_context(const char *name, + uint64_t timestamp, + double value, + int allow_reset, + int label_count, + char **label_keys, + char **label_values) +{ + struct cmt *context; + struct cmt_counter *counter; + + context = cmt_create(); + if (context == NULL) { + return NULL; + } + + counter = cmt_counter_create(context, + "", + "", + (char *) name, + "help", + label_count, + label_keys); + if (counter == NULL) { + cmt_destroy(context); + return NULL; + } + + counter->aggregation_type = CMT_AGGREGATION_TYPE_CUMULATIVE; + + if (allow_reset == FLB_TRUE) { + cmt_counter_allow_reset(counter); + } + + if (cmt_counter_set(counter, + timestamp, + value, + label_count, + label_values) != 0) { + cmt_destroy(context); + return NULL; + } + + return context; +} + +static int send_metrics_context(struct rt_ctx *context, struct cmt *metrics_context) +{ + int ret; + size_t bytes_sent; + cfl_sds_t payload; + struct flb_http_client *client; + + payload = cmt_encode_opentelemetry_create(metrics_context); + if (payload == NULL) { + return -1; + } + + client = flb_http_client(context->http->connection, + FLB_HTTP_POST, + V1_ENDPOINT_METRICS, + payload, + cfl_sds_len(payload), + "127.0.0.1", + PORT_OTEL, + NULL, + 0); + if (client == NULL) { + cmt_encode_opentelemetry_destroy(payload); + return -1; + } + + ret = flb_http_add_header(client, + FLB_HTTP_HEADER_CONTENT_TYPE, + strlen(FLB_HTTP_HEADER_CONTENT_TYPE), + OTLP_CONTENT_TYPE, + strlen(OTLP_CONTENT_TYPE)); + if (ret != 0) { + flb_http_client_destroy(client); + cmt_encode_opentelemetry_destroy(payload); + return -1; + } + + ret = flb_http_do(client, &bytes_sent); + + if (ret != 0 || bytes_sent == 0 || client->resp.status != 201) { + flb_http_client_destroy(client); + cmt_encode_opentelemetry_destroy(payload); + return -1; + } + + flb_http_client_destroy(client); + cmt_encode_opentelemetry_destroy(payload); + + return 0; +} + +static void flb_test_runtime_counter_default_behaviors(void) +{ + int baseline; + int count_before; + int metric_index; + struct cmt *context; + struct rt_ctx *rt; + + observation_reset(); + metric_index = observation_add_capture("rt_counter_total"); + TEST_CHECK(metric_index >= 0); + + rt = rt_ctx_create("true", "true"); + TEST_CHECK(rt != NULL); + + context = create_counter_context("rt_counter_total", 100, 1.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + baseline = observation_get_callback_count(); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_callback_growth(baseline, 2000) == 0); + TEST_CHECK(observation_get_value_count(metric_index) == 0); + + context = create_counter_context("rt_counter_total", 200, 2.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 1, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 0) - 1.0) < 0.0001); + + context = create_counter_context("rt_counter_total", 300, 3.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 2, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 1) - 1.0) < 0.0001); + + count_before = observation_get_value_count(metric_index); + context = create_counter_context("rt_counter_total", 400, 1.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + flb_time_msleep(500); + TEST_CHECK(observation_get_value_count(metric_index) == count_before); + + context = create_counter_context("rt_counter_total", 250, 5.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + flb_time_msleep(500); + TEST_CHECK(observation_get_value_count(metric_index) == count_before); + + context = create_counter_context("rt_counter_total", 500, 7.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 3, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 2) - 6.0) < 0.0001); + + rt_ctx_destroy(rt); +} + +static void flb_test_runtime_counter_reset_keep_and_first_sample(void) +{ + int metric_index; + struct cmt *context; + struct rt_ctx *rt; + + observation_reset(); + metric_index = observation_add_capture("rt_counter_keep_total"); + TEST_CHECK(metric_index >= 0); + + rt = rt_ctx_create("false", "false"); + TEST_CHECK(rt != NULL); + + context = create_counter_context("rt_counter_keep_total", 100, 10.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 1, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 0) - 10.0) < 0.0001); + + context = create_counter_context("rt_counter_keep_total", 200, 2.0, + FLB_FALSE, 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 2, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 1) - 2.0) < 0.0001); + + rt_ctx_destroy(rt); +} + +static void flb_test_runtime_multi_series(void) +{ + int index_a; + int index_b; + char *label_keys[1]; + char *label_values_a[1]; + char *label_values_b[1]; + struct cmt *context; + struct rt_ctx *rt; + + label_keys[0] = "instance"; + label_values_a[0] = "a"; + label_values_b[0] = "b"; + + observation_reset(); + + index_a = observation_add_capture("rt_series_total{instance=\"a\"}"); + index_b = observation_add_capture("rt_series_total{instance=\"b\"}"); + TEST_CHECK(index_a >= 0); + TEST_CHECK(index_b >= 0); + + rt = rt_ctx_create("true", "true"); + TEST_CHECK(rt != NULL); + + context = create_counter_context("rt_series_total", 100, 1.0, + FLB_FALSE, 1, + label_keys, label_values_a); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + + context = create_counter_context("rt_series_total", 100, 10.0, + FLB_FALSE, 1, + label_keys, label_values_b); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + + context = create_counter_context("rt_series_total", 200, 3.0, + FLB_FALSE, 1, + label_keys, label_values_a); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + + context = create_counter_context("rt_series_total", 200, 15.0, + FLB_FALSE, 1, + label_keys, label_values_b); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + + TEST_CHECK(wait_for_value_count(index_a, 1, 2000) == 0); + TEST_CHECK(wait_for_value_count(index_b, 1, 2000) == 0); + + TEST_CHECK(fabs(observation_get_value(index_a, 0) - 2.0) < 0.0001); + TEST_CHECK(fabs(observation_get_value(index_b, 0) - 5.0) < 0.0001); + + rt_ctx_destroy(rt); +} + +static void flb_test_runtime_non_monotonic_sum_passthrough(void) +{ + int metric_index; + struct cmt *context; + struct rt_ctx *rt; + + observation_reset(); + metric_index = observation_add_capture("rt_non_monotonic_total"); + TEST_CHECK(metric_index >= 0); + + rt = rt_ctx_create("true", "true"); + TEST_CHECK(rt != NULL); + + context = create_counter_context("rt_non_monotonic_total", + 100, 10.0, + FLB_TRUE, + 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 1, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 0) - 10.0) < 0.0001); + + context = create_counter_context("rt_non_monotonic_total", + 200, 3.0, + FLB_TRUE, + 0, NULL, NULL); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + TEST_CHECK(wait_for_value_count(metric_index, 2, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(metric_index, 1) - 3.0) < 0.0001); + + rt_ctx_destroy(rt); +} + +TEST_LIST = { + {"counter_default_behaviors", flb_test_runtime_counter_default_behaviors}, + {"counter_reset_keep_and_first_sample", flb_test_runtime_counter_reset_keep_and_first_sample}, + {"multi_series", flb_test_runtime_multi_series}, + {"non_monotonic_sum_passthrough", flb_test_runtime_non_monotonic_sum_passthrough}, + {NULL, NULL} +}; From a04039df74b3cf12627cd40a95d8d7cc8d5d463a Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 16 Feb 2026 18:00:25 -0600 Subject: [PATCH 04/14] build: add processor_cumulative_to_delta Signed-off-by: Eduardo Silva --- cmake/plugins_options.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/plugins_options.cmake b/cmake/plugins_options.cmake index 5cbe4733179..10ca8fdb230 100644 --- a/cmake/plugins_options.cmake +++ b/cmake/plugins_options.cmake @@ -69,6 +69,7 @@ DEFINE_OPTION(FLB_IN_EBPF "Enable Linux eBPF input plugin" # Processors # ========== DEFINE_OPTION(FLB_PROCESSOR_CONTENT_MODIFIER "Enable content modifier processor" ON) +DEFINE_OPTION(FLB_PROCESSOR_CUMULATIVE_TO_DELTA "Enable cumulative to delta metrics processor" ON) DEFINE_OPTION(FLB_PROCESSOR_LABELS "Enable metrics label manipulation processor" ON) DEFINE_OPTION(FLB_PROCESSOR_METRICS_SELECTOR "Enable metrics selector processor" ON) DEFINE_OPTION(FLB_PROCESSOR_OPENTELEMETRY_ENVELOPE "Enable OpenTelemetry envelope processor" ON) From 3231daaa870e449b3c3045204f23d84816ecb95c Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 16 Feb 2026 18:37:48 -0600 Subject: [PATCH 05/14] processor_cumulative_to_delta: free dropped static histogram buffers Signed-off-by: Eduardo Silva --- .../cumulative_to_delta_core.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c index ead1dd1e0f7..fe0c034541e 100644 --- a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c @@ -1301,6 +1301,10 @@ static int process_histogram_map(struct flb_cumulative_to_delta_ctx *context, return -1; } if (result == FLB_C2D_DROP) { + if (map->metric.hist_buckets != NULL) { + flb_free(map->metric.hist_buckets); + map->metric.hist_buckets = NULL; + } map->metric_static_set = FLB_FALSE; } } @@ -1350,6 +1354,14 @@ static int process_exp_histogram_map(struct flb_cumulative_to_delta_ctx *context return -1; } if (result == FLB_C2D_DROP) { + if (map->metric.exp_hist_positive_buckets != NULL) { + flb_free(map->metric.exp_hist_positive_buckets); + map->metric.exp_hist_positive_buckets = NULL; + } + if (map->metric.exp_hist_negative_buckets != NULL) { + flb_free(map->metric.exp_hist_negative_buckets); + map->metric.exp_hist_negative_buckets = NULL; + } map->metric_static_set = FLB_FALSE; } } From d60e022951d1b966ede93b7efe4be445c14cd6b3 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Mon, 16 Feb 2026 20:39:13 -0600 Subject: [PATCH 06/14] processor_cumulative_to_delta: fix order of aggregation type Signed-off-by: Eduardo Silva --- .../cumulative_to_delta_core.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c index fe0c034541e..459b2c41eed 100644 --- a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c @@ -1241,8 +1241,6 @@ static int process_counter_map(struct flb_cumulative_to_delta_ctx *context, return 0; } - counter->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; - if (map->metric_static_set == FLB_TRUE) { result = process_counter_sample(context, counter, @@ -1271,6 +1269,8 @@ static int process_counter_map(struct flb_cumulative_to_delta_ctx *context, } } + counter->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + return 0; } @@ -1290,8 +1290,6 @@ static int process_histogram_map(struct flb_cumulative_to_delta_ctx *context, return 0; } - histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; - if (map->metric_static_set == FLB_TRUE) { result = process_histogram_sample(context, histogram, @@ -1324,6 +1322,8 @@ static int process_histogram_map(struct flb_cumulative_to_delta_ctx *context, } } + histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + return 0; } @@ -1343,8 +1343,6 @@ static int process_exp_histogram_map(struct flb_cumulative_to_delta_ctx *context return 0; } - exp_histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; - if (map->metric_static_set == FLB_TRUE) { result = process_exp_histogram_sample(context, exp_histogram, @@ -1381,6 +1379,8 @@ static int process_exp_histogram_map(struct flb_cumulative_to_delta_ctx *context } } + exp_histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + return 0; } From 810d54f98b08f970d13bdf2f6f76bc3ed3617bef Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 20 Feb 2026 14:42:26 -0600 Subject: [PATCH 07/14] lib: cmetrics: upgrade to v2.0.3 Signed-off-by: Eduardo Silva --- lib/cmetrics/CMakeLists.txt | 2 +- lib/cmetrics/README.md | 143 ++++++++++++++- lib/cmetrics/include/cmetrics/cmt_metric.h | 34 +++- lib/cmetrics/include/cmetrics/cmt_summary.h | 4 + lib/cmetrics/src/cmt_atomic_generic.c | 44 ++--- lib/cmetrics/src/cmt_atomic_msvc.c | 46 +++-- lib/cmetrics/src/cmt_cat.c | 149 ++++++++++++---- lib/cmetrics/src/cmt_decode_msgpack.c | 165 +++++++++++++++--- lib/cmetrics/src/cmt_decode_opentelemetry.c | 120 ++++++++----- .../src/cmt_decode_prometheus_remote_write.c | 8 +- lib/cmetrics/src/cmt_encode_cloudwatch_emf.c | 5 +- lib/cmetrics/src/cmt_encode_influx.c | 7 +- lib/cmetrics/src/cmt_encode_msgpack.c | 69 ++++++-- lib/cmetrics/src/cmt_encode_opentelemetry.c | 95 +++++++--- lib/cmetrics/src/cmt_encode_prometheus.c | 111 ++++++++---- .../src/cmt_encode_prometheus_remote_write.c | 11 +- lib/cmetrics/src/cmt_encode_splunk_hec.c | 77 ++++++-- lib/cmetrics/src/cmt_encode_text.c | 34 ++-- lib/cmetrics/src/cmt_exp_histogram.c | 77 +++++--- lib/cmetrics/src/cmt_metric.c | 139 +++++++++++++++ lib/cmetrics/src/cmt_summary.c | 2 +- lib/cmetrics/tests/cloudwatch_emf_payload.bin | Bin 0 -> 2277 bytes lib/cmetrics/tests/encoding.c | 26 +-- lib/cmetrics/tests/opentelemetry_payload.bin | Bin 0 -> 795 bytes .../tests/prometheus_remote_write_payload.bin | Bin 0 -> 2695 bytes 25 files changed, 1060 insertions(+), 308 deletions(-) create mode 100644 lib/cmetrics/tests/cloudwatch_emf_payload.bin create mode 100644 lib/cmetrics/tests/opentelemetry_payload.bin create mode 100644 lib/cmetrics/tests/prometheus_remote_write_payload.bin diff --git a/lib/cmetrics/CMakeLists.txt b/lib/cmetrics/CMakeLists.txt index 3579ca6165c..0648af997a1 100644 --- a/lib/cmetrics/CMakeLists.txt +++ b/lib/cmetrics/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # CMetrics Version set(CMT_VERSION_MAJOR 2) set(CMT_VERSION_MINOR 0) -set(CMT_VERSION_PATCH 2) +set(CMT_VERSION_PATCH 3) set(CMT_VERSION_STR "${CMT_VERSION_MAJOR}.${CMT_VERSION_MINOR}.${CMT_VERSION_PATCH}") # Include helpers diff --git a/lib/cmetrics/README.md b/lib/cmetrics/README.md index 0ce732b3ed4..191d6b1c4e5 100644 --- a/lib/cmetrics/README.md +++ b/lib/cmetrics/README.md @@ -2,20 +2,149 @@ > DISCLAIMER: THIS LIBRARY IS STILL IN ACTIVE DEVELOPMENT -The [CMetrics](https://github.com/calyptia/cmetrics) project is a standalone C library to create and maintain a context of different sets of metrics with labels support such as: +[CMetrics](https://github.com/calyptia/cmetrics) is a standalone C library to +create, mutate, aggregate, encode, and decode metrics contexts. -- Counters -- Gauges -- Histograms -- Summaries +## Supported Metric Types -This project is heavily based on Go Prometheus Client API design: +- Counter +- Gauge +- Untyped +- Histogram +- Exponential Histogram +- Summary + +All metric points store a sample `timestamp` in nanoseconds. + +## Datapoint Start Timestamp (OTLP) + +CMetrics also supports an optional native `start_timestamp` per datapoint. +This is primarily relevant for OTLP cumulative streams. + +API (`cmt_metric.h`): + +- `cmt_metric_set_start_timestamp(...)` +- `cmt_metric_unset_start_timestamp(...)` +- `cmt_metric_has_start_timestamp(...)` +- `cmt_metric_get_start_timestamp(...)` + +Backward compatibility: existing code that only uses `timestamp` is unchanged. + +## Supported Encoders + +- OpenTelemetry Metrics (OTLP protobuf) +- Prometheus text exposition +- Prometheus Remote Write +- Influx line protocol +- Splunk HEC +- CloudWatch EMF +- CMetrics msgpack (internal format) +- Text (human-readable) + +## Supported Decoders + +- OpenTelemetry Metrics (OTLP protobuf) +- Prometheus text exposition +- Prometheus Remote Write +- StatsD +- CMetrics msgpack (internal format) + +## OTLP and `start_timestamp` + +- OTLP decoder populates native `start_timestamp` from + `start_time_unix_nano`. +- OTLP encoder prefers native `start_timestamp` and falls back to OTLP metadata + when needed. +- Internal CMetrics msgpack supports optional `start_ts` to preserve this value + across internal encode/decode flows. + +Non-OTLP formats (for example Prometheus text, Influx, Splunk HEC, and +CloudWatch EMF) do not define an OTLP-style start timestamp field, so they +serialize sample timestamps only. + +## C Usage Example + +```c +#include +#include + +#include +#include +#include +#include +#include + +int main(void) +{ + struct cmt *ctx; + struct cmt_counter *requests_total; + struct cmt_metric *sample; + cfl_sds_t otlp_payload; + uint64_t start_ns; + uint64_t sample_ns; + + ctx = cmt_create(); + if (ctx == NULL) { + return 1; + } + + requests_total = cmt_counter_create(ctx, + "demo", /* namespace */ + "service", /* subsystem */ + "requests_total", + "Total requests", + 0, /* label keys */ + NULL); + if (requests_total == NULL) { + cmt_destroy(ctx); + return 1; + } + + start_ns = 1700000000000000000ULL; + sample_ns = start_ns + 5000000000ULL; + + /* Write sample value (cumulative stream example). */ + if (cmt_counter_set(requests_total, sample_ns, 42.0, 0, NULL) != 0) { + cmt_destroy(ctx); + return 1; + } + + /* Access the same datapoint and attach native start timestamp. */ + sample = cmt_map_metric_get(&requests_total->opts, + requests_total->map, + 0, NULL, + CMT_FALSE); + if (sample == NULL) { + cmt_destroy(ctx); + return 1; + } + cmt_metric_set_start_timestamp(sample, start_ns); + + /* Encode OTLP metrics payload. */ + otlp_payload = cmt_encode_opentelemetry_create(ctx); + if (otlp_payload == NULL) { + cmt_destroy(ctx); + return 1; + } + + printf("Encoded OTLP payload size: %zu bytes\n", cfl_sds_len(otlp_payload)); + + cmt_encode_opentelemetry_destroy(otlp_payload); + cmt_destroy(ctx); + return 0; +} +``` + +## Design Reference + +CMetrics is heavily inspired by the Go Prometheus Client API design: - https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#section-documentation ## License -This program is under the terms of the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). +This program is under the terms of the +[Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). ## Authors diff --git a/lib/cmetrics/include/cmetrics/cmt_metric.h b/lib/cmetrics/include/cmetrics/cmt_metric.h index c9e4534ce3a..35ca44f8407 100644 --- a/lib/cmetrics/include/cmetrics/cmt_metric.h +++ b/lib/cmetrics/include/cmetrics/cmt_metric.h @@ -42,7 +42,7 @@ struct cmt_metric { uint64_t hist_sum; /* exponential histogram */ - int exp_hist_sum_set; + uint64_t exp_hist_sum_set; int32_t exp_hist_scale; uint64_t exp_hist_zero_count; double exp_hist_zero_threshold; @@ -56,7 +56,7 @@ struct cmt_metric { uint64_t exp_hist_sum; /* summary */ - int sum_quantiles_set; /* specify if quantive values has been set */ + uint64_t sum_quantiles_set; /* specify if quantive values has been set */ uint64_t *sum_quantiles; /* 0, 0.25, 0.5, 0.75 and 1 */ size_t sum_quantiles_count; uint64_t sum_count; @@ -65,10 +65,28 @@ struct cmt_metric { /* internal */ uint64_t hash; uint64_t timestamp; + uint64_t start_timestamp; + uint64_t start_timestamp_set; + uint64_t exp_hist_lock; struct cfl_list labels; struct cfl_list _head; }; +struct cmt_exp_histogram_snapshot { + int32_t scale; + uint64_t zero_count; + double zero_threshold; + int32_t positive_offset; + uint64_t *positive_buckets; + size_t positive_count; + int32_t negative_offset; + uint64_t *negative_buckets; + size_t negative_count; + uint64_t count; + uint64_t sum_set; + uint64_t sum; +}; + void cmt_metric_set(struct cmt_metric *metric, uint64_t timestamp, double val); void cmt_metric_set_double(struct cmt_metric *metric, uint64_t timestamp, double val); void cmt_metric_set_int64(struct cmt_metric *metric, uint64_t timestamp, int64_t val); @@ -86,6 +104,18 @@ void cmt_metric_get_value_snapshot(struct cmt_metric *metric, int64_t *out_int64, uint64_t *out_uint64); uint64_t cmt_metric_get_timestamp(struct cmt_metric *metric); +void cmt_metric_set_timestamp(struct cmt_metric *metric, uint64_t timestamp); +void cmt_metric_set_start_timestamp(struct cmt_metric *metric, uint64_t start_timestamp); +void cmt_metric_unset_start_timestamp(struct cmt_metric *metric); +int cmt_metric_has_start_timestamp(struct cmt_metric *metric); +uint64_t cmt_metric_get_start_timestamp(struct cmt_metric *metric); +void cmt_metric_set_exp_hist_count(struct cmt_metric *metric, uint64_t count); +void cmt_metric_set_exp_hist_sum(struct cmt_metric *metric, int sum_set, double sum); +void cmt_metric_exp_hist_lock(struct cmt_metric *metric); +void cmt_metric_exp_hist_unlock(struct cmt_metric *metric); +int cmt_metric_exp_hist_get_snapshot(struct cmt_metric *metric, + struct cmt_exp_histogram_snapshot *snapshot); +void cmt_metric_exp_hist_snapshot_destroy(struct cmt_exp_histogram_snapshot *snapshot); void cmt_metric_hist_inc(struct cmt_metric *metric, uint64_t timestamp, int bucket_id); diff --git a/lib/cmetrics/include/cmetrics/cmt_summary.h b/lib/cmetrics/include/cmetrics/cmt_summary.h index 641b837affd..1f9f5165096 100644 --- a/lib/cmetrics/include/cmetrics/cmt_summary.h +++ b/lib/cmetrics/include/cmetrics/cmt_summary.h @@ -65,6 +65,10 @@ uint64_t cmt_summary_get_count_value(struct cmt_metric *metric); void cmt_summary_quantile_set(struct cmt_metric *metric, uint64_t timestamp, int quantile_id, double val); +void cmt_summary_sum_set(struct cmt_metric *metric, uint64_t timestamp, + double val); +void cmt_summary_count_set(struct cmt_metric *metric, uint64_t timestamp, + uint64_t count); #endif diff --git a/lib/cmetrics/src/cmt_atomic_generic.c b/lib/cmetrics/src/cmt_atomic_generic.c index 40e59b615ef..72ec6f601f2 100644 --- a/lib/cmetrics/src/cmt_atomic_generic.c +++ b/lib/cmetrics/src/cmt_atomic_generic.c @@ -17,13 +17,13 @@ * limitations under the License. */ -#include -#include #include #include pthread_mutex_t atomic_operation_lock; -static int atomic_operation_system_initialized = 0; +static pthread_once_t atomic_operation_system_once = PTHREAD_ONCE_INIT; +static int atomic_operation_system_initialized = 0; +static int atomic_operation_system_status = 0; /* TODO: Determne if we want to keep this backend as well as how / if we want to handle * pthread_mutex_unlock errors (investigate and understand what could cause them), @@ -32,18 +32,22 @@ static int atomic_operation_system_initialized = 0; * */ -inline int cmt_atomic_initialize() +static void cmt_atomic_bootstrap() { - int result; + atomic_operation_system_status = + pthread_mutex_init(&atomic_operation_lock, NULL); - if (0 == atomic_operation_system_initialized) { - result = pthread_mutex_init(&atomic_operation_lock, NULL); + if (atomic_operation_system_status == 0) { + atomic_operation_system_initialized = 1; + } +} - if (0 != result) { - return 1; - } +inline int cmt_atomic_initialize() +{ + pthread_once(&atomic_operation_system_once, cmt_atomic_bootstrap); - atomic_operation_system_initialized = 1; + if (atomic_operation_system_status != 0) { + return 1; } return 0; @@ -54,9 +58,9 @@ inline int cmt_atomic_compare_exchange(uint64_t *storage, { int result; - if (0 == atomic_operation_system_initialized) { - printf("CMT ATOMIC : Atomic operation backend not initalized\n"); - exit(1); + if (cmt_atomic_initialize() != 0 || + atomic_operation_system_initialized == 0) { + return 0; } result = pthread_mutex_lock(&atomic_operation_lock); @@ -83,9 +87,9 @@ inline void cmt_atomic_store(uint64_t *storage, uint64_t new_value) { int result; - if (0 == atomic_operation_system_initialized) { - printf("CMT ATOMIC : Atomic operation backend not initalized\n"); - exit(1); + if (cmt_atomic_initialize() != 0 || + atomic_operation_system_initialized == 0) { + return; } result = pthread_mutex_lock(&atomic_operation_lock); @@ -104,9 +108,9 @@ inline uint64_t cmt_atomic_load(uint64_t *storage) int result; uint64_t retval; - if (0 == atomic_operation_system_initialized) { - printf("CMT ATOMIC : Atomic operation backend not initalized\n"); - exit(1); + if (cmt_atomic_initialize() != 0 || + atomic_operation_system_initialized == 0) { + return 0; } result = pthread_mutex_lock(&atomic_operation_lock); diff --git a/lib/cmetrics/src/cmt_atomic_msvc.c b/lib/cmetrics/src/cmt_atomic_msvc.c index 65bfd2f80db..3fd66dbf799 100644 --- a/lib/cmetrics/src/cmt_atomic_msvc.c +++ b/lib/cmetrics/src/cmt_atomic_msvc.c @@ -26,16 +26,33 @@ #ifndef _WIN64 CRITICAL_SECTION atomic_operation_lock; +static INIT_ONCE atomic_operation_system_once = INIT_ONCE_STATIC_INIT; static int atomic_operation_system_initialized = 0; +static int atomic_operation_system_status = 0; -int cmt_atomic_initialize() +static BOOL CALLBACK cmt_atomic_bootstrap(PINIT_ONCE once, PVOID parameter, + PVOID *context) { - if (0 == atomic_operation_system_initialized) { - InitializeCriticalSection(&atomic_operation_lock); + (void) once; + (void) parameter; + (void) context; + + InitializeCriticalSection(&atomic_operation_lock); + atomic_operation_system_initialized = 1; - atomic_operation_system_initialized = 1; + return TRUE; +} + +int cmt_atomic_initialize() +{ + if (!InitOnceExecuteOnce(&atomic_operation_system_once, + cmt_atomic_bootstrap, NULL, NULL)) { + atomic_operation_system_status = 1; + return 1; } + atomic_operation_system_status = 0; + return 0; } @@ -44,9 +61,10 @@ int cmt_atomic_compare_exchange(uint64_t *storage, { uint64_t result; - if (0 == atomic_operation_system_initialized) { - printf("CMT ATOMIC : Atomic operation backend not initalized\n"); - exit(1); + if (cmt_atomic_initialize() != 0 || + atomic_operation_system_initialized == 0 || + atomic_operation_system_status != 0) { + return 0; } EnterCriticalSection(&atomic_operation_lock); @@ -67,9 +85,10 @@ int cmt_atomic_compare_exchange(uint64_t *storage, void cmt_atomic_store(uint64_t *storage, uint64_t new_value) { - if (0 == atomic_operation_system_initialized) { - printf("CMT ATOMIC : Atomic operation backend not initalized\n"); - exit(1); + if (cmt_atomic_initialize() != 0 || + atomic_operation_system_initialized == 0 || + atomic_operation_system_status != 0) { + return; } EnterCriticalSection(&atomic_operation_lock); @@ -83,9 +102,10 @@ uint64_t cmt_atomic_load(uint64_t *storage) { uint64_t result; - if (0 == atomic_operation_system_initialized) { - printf("CMT ATOMIC : Atomic operation backend not initalized\n"); - exit(1); + if (cmt_atomic_initialize() != 0 || + atomic_operation_system_initialized == 0 || + atomic_operation_system_status != 0) { + return 0; } EnterCriticalSection(&atomic_operation_lock); diff --git a/lib/cmetrics/src/cmt_cat.c b/lib/cmetrics/src/cmt_cat.c index 8e65ff289be..9ec70176cdf 100644 --- a/lib/cmetrics/src/cmt_cat.c +++ b/lib/cmetrics/src/cmt_cat.c @@ -26,6 +26,7 @@ #include #include #include +#include int cmt_cat_copy_label_keys(struct cmt_map *map, char **out) { @@ -101,6 +102,9 @@ static inline int cat_histogram_values(struct cmt_metric *metric_dst, struct cmt struct cmt_metric *metric_src, struct cmt_histogram *histogram_dst) { int i; + int result; + uint64_t old_value; + uint64_t new_value; size_t bucket_count_src; size_t bucket_count_dst; @@ -129,17 +133,36 @@ static inline int cat_histogram_values(struct cmt_metric *metric_dst, struct cmt /* Concatenate bucket values including +Inf bucket at index bucket_count_dst */ for (i = 0; i <= bucket_count_dst; i++) { - /* histogram buckets are always integers, no need to convert them */ - metric_dst->hist_buckets[i] += metric_src->hist_buckets[i]; + do { + old_value = cmt_atomic_load(&metric_dst->hist_buckets[i]); + new_value = old_value + cmt_atomic_load(&metric_src->hist_buckets[i]); + result = cmt_atomic_compare_exchange(&metric_dst->hist_buckets[i], + old_value, new_value); + } + while (result == 0); } /* histogram count */ - metric_dst->hist_count = cmt_math_sum_native_uint64_as_d64(metric_dst->hist_count, - metric_src->hist_count); + do { + old_value = cmt_atomic_load(&metric_dst->hist_count); + new_value = cmt_math_sum_native_uint64_as_d64( + old_value, + cmt_atomic_load(&metric_src->hist_count)); + result = cmt_atomic_compare_exchange(&metric_dst->hist_count, + old_value, new_value); + } + while (result == 0); /* histoggram sum */ - metric_dst->hist_sum = cmt_math_sum_native_uint64_as_d64(metric_dst->hist_sum, - metric_src->hist_sum); + do { + old_value = cmt_atomic_load(&metric_dst->hist_sum); + new_value = cmt_math_sum_native_uint64_as_d64( + old_value, + cmt_atomic_load(&metric_src->hist_sum)); + result = cmt_atomic_compare_exchange(&metric_dst->hist_sum, + old_value, new_value); + } + while (result == 0); return 0; } @@ -161,15 +184,15 @@ static inline int cat_summary_values(struct cmt_metric *metric_dst, struct cmt_s } for (i = 0; i < summary->quantiles_count; i++) { - /* summary quantiles are always integers, no need to convert them */ - metric_dst->sum_quantiles[i] = metric_src->sum_quantiles[i]; + cmt_atomic_store(&metric_dst->sum_quantiles[i], + cmt_atomic_load(&metric_src->sum_quantiles[i])); } metric_dst->sum_quantiles_count = metric_src->sum_quantiles_count; - metric_dst->sum_quantiles_set = metric_src->sum_quantiles_set; + cmt_atomic_store(&metric_dst->sum_quantiles_set, cmt_atomic_load(&metric_src->sum_quantiles_set)); - metric_dst->sum_count = metric_src->sum_count; - metric_dst->sum_sum = metric_src->sum_sum; + cmt_atomic_store(&metric_dst->sum_count, cmt_atomic_load(&metric_src->sum_count)); + cmt_atomic_store(&metric_dst->sum_sum, cmt_atomic_load(&metric_src->sum_sum)); return 0; } @@ -177,6 +200,9 @@ static inline int cat_summary_values(struct cmt_metric *metric_dst, struct cmt_s static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, struct cmt_metric *metric_src) { + int result; + struct cmt_metric *first_lock_target; + struct cmt_metric *second_lock_target; int64_t dst_start; int64_t dst_end; int64_t src_start; @@ -185,36 +211,53 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, int64_t merged_end; size_t index; size_t merged_count; + uint64_t old_value; + uint64_t new_value; uint64_t *merged_buckets; uint64_t *tmp_buckets; + result = -1; + first_lock_target = metric_dst; + second_lock_target = metric_src; + + if (first_lock_target > second_lock_target) { + first_lock_target = metric_src; + second_lock_target = metric_dst; + } + + cmt_metric_exp_hist_lock(first_lock_target); + + if (second_lock_target != first_lock_target) { + cmt_metric_exp_hist_lock(second_lock_target); + } + if (metric_dst->exp_hist_positive_count > 0 && metric_dst->exp_hist_positive_buckets == NULL) { - return -1; + goto cleanup; } if (metric_dst->exp_hist_negative_count > 0 && metric_dst->exp_hist_negative_buckets == NULL) { - return -1; + goto cleanup; } if (metric_src->exp_hist_positive_count > 0 && metric_src->exp_hist_positive_buckets == NULL) { - return -1; + goto cleanup; } if (metric_src->exp_hist_negative_count > 0 && metric_src->exp_hist_negative_buckets == NULL) { - return -1; + goto cleanup; } if (metric_dst->exp_hist_positive_buckets == NULL && metric_dst->exp_hist_negative_buckets == NULL && metric_dst->exp_hist_positive_count == 0 && metric_dst->exp_hist_negative_count == 0 && - metric_dst->exp_hist_count == 0 && + cmt_atomic_load(&metric_dst->exp_hist_count) == 0 && metric_dst->exp_hist_zero_count == 0 && - metric_dst->exp_hist_sum == 0 && + cmt_atomic_load(&metric_dst->exp_hist_sum) == 0 && metric_dst->exp_hist_scale == 0 && metric_dst->exp_hist_positive_offset == 0 && metric_dst->exp_hist_negative_offset == 0 && @@ -223,7 +266,7 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, metric_dst->exp_hist_positive_buckets = calloc(metric_src->exp_hist_positive_count, sizeof(uint64_t)); if (metric_dst->exp_hist_positive_buckets == NULL) { - return -1; + goto cleanup; } memcpy(metric_dst->exp_hist_positive_buckets, @@ -238,7 +281,7 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, free(metric_dst->exp_hist_positive_buckets); metric_dst->exp_hist_positive_buckets = NULL; - return -1; + goto cleanup; } memcpy(metric_dst->exp_hist_negative_buckets, @@ -253,16 +296,20 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, metric_dst->exp_hist_positive_count = metric_src->exp_hist_positive_count; metric_dst->exp_hist_negative_offset = metric_src->exp_hist_negative_offset; metric_dst->exp_hist_negative_count = metric_src->exp_hist_negative_count; - metric_dst->exp_hist_count = metric_src->exp_hist_count; - metric_dst->exp_hist_sum_set = metric_src->exp_hist_sum_set; - metric_dst->exp_hist_sum = metric_src->exp_hist_sum; + cmt_atomic_store(&metric_dst->exp_hist_count, + cmt_atomic_load(&metric_src->exp_hist_count)); + cmt_atomic_store(&metric_dst->exp_hist_sum_set, + cmt_atomic_load(&metric_src->exp_hist_sum_set)); + cmt_atomic_store(&metric_dst->exp_hist_sum, + cmt_atomic_load(&metric_src->exp_hist_sum)); - return 0; + result = 0; + goto cleanup; } if (metric_dst->exp_hist_scale != metric_src->exp_hist_scale || metric_dst->exp_hist_zero_threshold != metric_src->exp_hist_zero_threshold) { - return -1; + goto cleanup; } if (metric_src->exp_hist_positive_count > 0) { @@ -270,7 +317,7 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, metric_dst->exp_hist_positive_buckets = calloc(metric_src->exp_hist_positive_count, sizeof(uint64_t)); if (metric_dst->exp_hist_positive_buckets == NULL) { - return -1; + goto cleanup; } memcpy(metric_dst->exp_hist_positive_buckets, @@ -291,7 +338,7 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, merged_buckets = calloc(merged_count, sizeof(uint64_t)); if (merged_buckets == NULL) { - return -1; + goto cleanup; } for (index = 0; index < metric_dst->exp_hist_positive_count; index++) { @@ -317,7 +364,7 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, metric_dst->exp_hist_negative_buckets = calloc(metric_src->exp_hist_negative_count, sizeof(uint64_t)); if (metric_dst->exp_hist_negative_buckets == NULL) { - return -1; + goto cleanup; } memcpy(metric_dst->exp_hist_negative_buckets, @@ -338,7 +385,7 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, merged_buckets = calloc(merged_count, sizeof(uint64_t)); if (merged_buckets == NULL) { - return -1; + goto cleanup; } for (index = 0; index < metric_dst->exp_hist_negative_count; index++) { @@ -360,19 +407,39 @@ static inline int cat_exp_histogram_values(struct cmt_metric *metric_dst, } metric_dst->exp_hist_zero_count += metric_src->exp_hist_zero_count; - metric_dst->exp_hist_count += metric_src->exp_hist_count; - if (metric_dst->exp_hist_sum_set && metric_src->exp_hist_sum_set) { - metric_dst->exp_hist_sum = cmt_math_d64_to_uint64( - cmt_math_uint64_to_d64(metric_dst->exp_hist_sum) + - cmt_math_uint64_to_d64(metric_src->exp_hist_sum)); + do { + old_value = cmt_atomic_load(&metric_dst->exp_hist_count); + new_value = old_value + cmt_atomic_load(&metric_src->exp_hist_count); + result = cmt_atomic_compare_exchange(&metric_dst->exp_hist_count, + old_value, new_value); + } + while (result == 0); + + if (cmt_atomic_load(&metric_dst->exp_hist_sum_set) && + cmt_atomic_load(&metric_src->exp_hist_sum_set)) { + cmt_atomic_store(&metric_dst->exp_hist_sum, + cmt_math_d64_to_uint64( + cmt_math_uint64_to_d64( + cmt_atomic_load(&metric_dst->exp_hist_sum)) + + cmt_math_uint64_to_d64( + cmt_atomic_load(&metric_src->exp_hist_sum)))); } - else if (metric_src->exp_hist_sum_set) { - metric_dst->exp_hist_sum_set = CMT_TRUE; - metric_dst->exp_hist_sum = metric_src->exp_hist_sum; + else if (cmt_atomic_load(&metric_src->exp_hist_sum_set)) { + cmt_atomic_store(&metric_dst->exp_hist_sum_set, CMT_TRUE); + cmt_atomic_store(&metric_dst->exp_hist_sum, + cmt_atomic_load(&metric_src->exp_hist_sum)); } - return 0; + result = 0; + +cleanup: + if (second_lock_target != first_lock_target) { + cmt_metric_exp_hist_unlock(second_lock_target); + } + cmt_metric_exp_hist_unlock(first_lock_target); + + return result; } static inline void cat_scalar_value(struct cmt_metric *metric_dst, @@ -393,6 +460,14 @@ static inline void cat_scalar_value(struct cmt_metric *metric_dst, val = cmt_metric_get_value(metric_src); cmt_metric_set_double(metric_dst, ts, val); } + + if (cmt_metric_has_start_timestamp(metric_src)) { + cmt_metric_set_start_timestamp(metric_dst, + cmt_metric_get_start_timestamp(metric_src)); + } + else { + cmt_metric_unset_start_timestamp(metric_dst); + } } int cmt_cat_copy_map(struct cmt_opts *opts, struct cmt_map *dst, struct cmt_map *src) diff --git a/lib/cmetrics/src/cmt_decode_msgpack.c b/lib/cmetrics/src/cmt_decode_msgpack.c index c36a12db3b9..a78573ec7cf 100644 --- a/lib/cmetrics/src/cmt_decode_msgpack.c +++ b/lib/cmetrics/src/cmt_decode_msgpack.c @@ -406,6 +406,30 @@ static int unpack_metric_label(mpack_reader_t *reader, size_t index, void *conte static int unpack_metric_ts(mpack_reader_t *reader, size_t index, void *context) { + int result; + uint64_t timestamp; + struct cmt_msgpack_decode_context *decode_context; + + if (NULL == reader || + NULL == context ) { + return CMT_DECODE_MSGPACK_INVALID_ARGUMENT_ERROR; + } + + decode_context = (struct cmt_msgpack_decode_context *) context; + + result = cmt_mpack_consume_uint_tag(reader, ×tamp); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_metric_set_timestamp(decode_context->metric, timestamp); + } + + return result; +} + +static int unpack_metric_start_ts(mpack_reader_t *reader, size_t index, void *context) +{ + int result; + uint64_t start_timestamp; struct cmt_msgpack_decode_context *decode_context; if (NULL == reader || @@ -415,7 +439,13 @@ static int unpack_metric_ts(mpack_reader_t *reader, size_t index, void *context) decode_context = (struct cmt_msgpack_decode_context *) context; - return cmt_mpack_consume_uint_tag(reader, &decode_context->metric->timestamp); + result = cmt_mpack_consume_uint_tag(reader, &start_timestamp); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_metric_set_start_timestamp(decode_context->metric, start_timestamp); + } + + return result; } static int unpack_metric_value(mpack_reader_t *reader, size_t index, void *context) @@ -528,7 +558,7 @@ static int unpack_summary_quantiles_set(mpack_reader_t *reader, size_t index, vo result = cmt_mpack_consume_uint_tag(reader, &value); if (result == CMT_DECODE_MSGPACK_SUCCESS) { - decode_context->metric->sum_quantiles_set = value; + cmt_atomic_store(&decode_context->metric->sum_quantiles_set, value); } return result; @@ -583,6 +613,8 @@ static int unpack_summary_quantiles(mpack_reader_t *reader, size_t index, void * static int unpack_summary_count(mpack_reader_t *reader, size_t index, void *context) { + int result; + uint64_t value; struct cmt_msgpack_decode_context *decode_context; if (NULL == reader || @@ -592,11 +624,19 @@ static int unpack_summary_count(mpack_reader_t *reader, size_t index, void *cont decode_context = (struct cmt_msgpack_decode_context *) context; - return cmt_mpack_consume_uint_tag(reader, &decode_context->metric->sum_count); + result = cmt_mpack_consume_uint_tag(reader, &value); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_atomic_store(&decode_context->metric->sum_count, value); + } + + return result; } static int unpack_summary_sum(mpack_reader_t *reader, size_t index, void *context) { + int result; + uint64_t value; struct cmt_msgpack_decode_context *decode_context; if (NULL == reader || @@ -606,7 +646,13 @@ static int unpack_summary_sum(mpack_reader_t *reader, size_t index, void *contex decode_context = (struct cmt_msgpack_decode_context *) context; - return cmt_mpack_consume_uint_tag(reader, &decode_context->metric->sum_sum); + result = cmt_mpack_consume_uint_tag(reader, &value); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_atomic_store(&decode_context->metric->sum_sum, value); + } + + return result; } static int unpack_metric_summary(mpack_reader_t *reader, size_t index, void *context) @@ -651,7 +697,8 @@ static int unpack_histogram_sum(mpack_reader_t *reader, size_t index, void *cont result = cmt_mpack_consume_double_tag(reader, &value); if (result == CMT_DECODE_MSGPACK_SUCCESS) { - decode_context->metric->hist_sum = cmt_math_d64_to_uint64(value); + cmt_atomic_store(&decode_context->metric->hist_sum, + cmt_math_d64_to_uint64(value)); } return result; @@ -659,6 +706,8 @@ static int unpack_histogram_sum(mpack_reader_t *reader, size_t index, void *cont static int unpack_histogram_count(mpack_reader_t *reader, size_t index, void *context) { + int result; + uint64_t value; struct cmt_msgpack_decode_context *decode_context; if (NULL == reader || @@ -668,7 +717,13 @@ static int unpack_histogram_count(mpack_reader_t *reader, size_t index, void *co decode_context = (struct cmt_msgpack_decode_context *) context; - return cmt_mpack_consume_uint_tag(reader, &decode_context->metric->hist_count); + result = cmt_mpack_consume_uint_tag(reader, &value); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_atomic_store(&decode_context->metric->hist_count, value); + } + + return result; } static int unpack_histogram_bucket(mpack_reader_t *reader, size_t index, void *context) @@ -883,9 +938,18 @@ static int unpack_exp_histogram_negative_buckets(mpack_reader_t *reader, size_t static int unpack_exp_histogram_count(mpack_reader_t *reader, size_t index, void *context) { + int result; + uint64_t value; struct cmt_msgpack_decode_context *decode_context; + decode_context = (struct cmt_msgpack_decode_context *) context; - return cmt_mpack_consume_uint_tag(reader, &decode_context->metric->exp_hist_count); + result = cmt_mpack_consume_uint_tag(reader, &value); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_atomic_store(&decode_context->metric->exp_hist_count, value); + } + + return result; } static int unpack_exp_histogram_sum_set(mpack_reader_t *reader, size_t index, void *context) @@ -898,7 +962,8 @@ static int unpack_exp_histogram_sum_set(mpack_reader_t *reader, size_t index, vo result = cmt_mpack_consume_uint_tag(reader, &value); if (result == CMT_DECODE_MSGPACK_SUCCESS) { - decode_context->metric->exp_hist_sum_set = value ? CMT_TRUE : CMT_FALSE; + cmt_atomic_store(&decode_context->metric->exp_hist_sum_set, + value ? CMT_TRUE : CMT_FALSE); } return result; @@ -906,13 +971,24 @@ static int unpack_exp_histogram_sum_set(mpack_reader_t *reader, size_t index, vo static int unpack_exp_histogram_sum(mpack_reader_t *reader, size_t index, void *context) { + int result; + uint64_t value; struct cmt_msgpack_decode_context *decode_context; + decode_context = (struct cmt_msgpack_decode_context *) context; - return cmt_mpack_consume_uint_tag(reader, &decode_context->metric->exp_hist_sum); + result = cmt_mpack_consume_uint_tag(reader, &value); + + if (result == CMT_DECODE_MSGPACK_SUCCESS) { + cmt_atomic_store(&decode_context->metric->exp_hist_sum, value); + } + + return result; } static int unpack_metric_exp_histogram(mpack_reader_t *reader, size_t index, void *context) { + int result; + struct cmt_msgpack_decode_context *decode_context; struct cmt_mpack_map_entry_callback_t callbacks[] = { {"scale", unpack_exp_histogram_scale}, {"zero_count", unpack_exp_histogram_zero_count}, @@ -927,7 +1003,12 @@ static int unpack_metric_exp_histogram(mpack_reader_t *reader, size_t index, voi {NULL, NULL} }; - return cmt_mpack_unpack_map(reader, callbacks, context); + decode_context = (struct cmt_msgpack_decode_context *) context; + cmt_metric_exp_hist_lock(decode_context->metric); + result = cmt_mpack_unpack_map(reader, callbacks, context); + cmt_metric_exp_hist_unlock(decode_context->metric); + + return result; } @@ -956,6 +1037,7 @@ static int unpack_metric(mpack_reader_t *reader, struct cmt_mpack_map_entry_callback_t callbacks[] = \ { {"ts", unpack_metric_ts}, + {"start_ts", unpack_metric_start_ts}, {"value", unpack_metric_value}, {"value_type", unpack_metric_value_type}, {"value_int64", unpack_metric_value_int64}, @@ -1052,6 +1134,8 @@ static int unpack_metric(mpack_reader_t *reader, static int unpack_metric_array_entry(mpack_reader_t *reader, size_t index, void *context) { int result; + uint64_t *old_negative_buckets; + uint64_t *old_positive_buckets; struct cmt_metric *metric; struct cmt_msgpack_decode_context *decode_context; @@ -1066,6 +1150,9 @@ static int unpack_metric_array_entry(mpack_reader_t *reader, size_t index, void result = unpack_metric(reader, decode_context, &metric); if (CMT_DECODE_MSGPACK_SUCCESS == result) { + old_positive_buckets = NULL; + old_negative_buckets = NULL; + if (0 == cfl_list_size(&metric->labels)) { /* Should we care about finding more than one "implicitly static metric" in * the array? @@ -1074,16 +1161,27 @@ static int unpack_metric_array_entry(mpack_reader_t *reader, size_t index, void if (decode_context->map->type == CMT_HISTOGRAM) { decode_context->map->metric.hist_buckets = metric->hist_buckets; - decode_context->map->metric.hist_count = metric->hist_count; - decode_context->map->metric.hist_sum = metric->hist_sum; + cmt_atomic_store(&decode_context->map->metric.hist_count, + cmt_atomic_load(&metric->hist_count)); + cmt_atomic_store(&decode_context->map->metric.hist_sum, + cmt_atomic_load(&metric->hist_sum)); } else if (decode_context->map->type == CMT_SUMMARY) { - decode_context->map->metric.sum_quantiles_set = metric->sum_quantiles_set; + cmt_atomic_store(&decode_context->map->metric.sum_quantiles_set, cmt_atomic_load(&metric->sum_quantiles_set)); decode_context->map->metric.sum_quantiles = metric->sum_quantiles; - decode_context->map->metric.sum_count = metric->sum_count; - decode_context->map->metric.sum_sum = metric->sum_sum; + cmt_atomic_store(&decode_context->map->metric.sum_count, + cmt_atomic_load(&metric->sum_count)); + cmt_atomic_store(&decode_context->map->metric.sum_sum, + cmt_atomic_load(&metric->sum_sum)); } else if (decode_context->map->type == CMT_EXP_HISTOGRAM) { + cmt_metric_exp_hist_lock(&decode_context->map->metric); + + old_positive_buckets = + decode_context->map->metric.exp_hist_positive_buckets; + old_negative_buckets = + decode_context->map->metric.exp_hist_negative_buckets; + decode_context->map->metric.exp_hist_scale = metric->exp_hist_scale; decode_context->map->metric.exp_hist_zero_count = metric->exp_hist_zero_count; decode_context->map->metric.exp_hist_zero_threshold = metric->exp_hist_zero_threshold; @@ -1093,17 +1191,38 @@ static int unpack_metric_array_entry(mpack_reader_t *reader, size_t index, void decode_context->map->metric.exp_hist_negative_offset = metric->exp_hist_negative_offset; decode_context->map->metric.exp_hist_negative_count = metric->exp_hist_negative_count; decode_context->map->metric.exp_hist_negative_buckets = metric->exp_hist_negative_buckets; - decode_context->map->metric.exp_hist_count = metric->exp_hist_count; - decode_context->map->metric.exp_hist_sum_set = metric->exp_hist_sum_set; - decode_context->map->metric.exp_hist_sum = metric->exp_hist_sum; + cmt_atomic_store(&decode_context->map->metric.exp_hist_count, + cmt_atomic_load(&metric->exp_hist_count)); + cmt_atomic_store(&decode_context->map->metric.exp_hist_sum_set, + cmt_atomic_load(&metric->exp_hist_sum_set)); + cmt_atomic_store(&decode_context->map->metric.exp_hist_sum, + cmt_atomic_load(&metric->exp_hist_sum)); + + cmt_metric_exp_hist_unlock(&decode_context->map->metric); + + if (old_positive_buckets != NULL) { + free(old_positive_buckets); + } + if (old_negative_buckets != NULL) { + free(old_negative_buckets); + } } - decode_context->map->metric.val = metric->val; - decode_context->map->metric.value_type = metric->value_type; - decode_context->map->metric.val_int64 = metric->val_int64; - decode_context->map->metric.val_uint64 = metric->val_uint64; + cmt_atomic_store(&decode_context->map->metric.val, + cmt_atomic_load(&metric->val)); + cmt_atomic_store(&decode_context->map->metric.value_type, + cmt_atomic_load(&metric->value_type)); + cmt_atomic_store(&decode_context->map->metric.val_int64, + cmt_atomic_load(&metric->val_int64)); + cmt_atomic_store(&decode_context->map->metric.val_uint64, + cmt_atomic_load(&metric->val_uint64)); decode_context->map->metric.hash = metric->hash; - decode_context->map->metric.timestamp = metric->timestamp; + cmt_atomic_store(&decode_context->map->metric.timestamp, + cmt_atomic_load(&metric->timestamp)); + cmt_atomic_store(&decode_context->map->metric.start_timestamp, + cmt_atomic_load(&metric->start_timestamp)); + cmt_atomic_store(&decode_context->map->metric.start_timestamp_set, + cmt_atomic_load(&metric->start_timestamp_set)); free(metric); } diff --git a/lib/cmetrics/src/cmt_decode_opentelemetry.c b/lib/cmetrics/src/cmt_decode_opentelemetry.c index 54c5cd29e38..4ed1e13a711 100644 --- a/lib/cmetrics/src/cmt_decode_opentelemetry.c +++ b/lib/cmetrics/src/cmt_decode_opentelemetry.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -741,6 +742,13 @@ static int decode_numerical_data_point(struct cmt *cmt, } clone_exemplars_to_kvlist(point_metadata, data_point->exemplars, data_point->n_exemplars); } + + if (data_point->start_time_unix_nano > 0) { + cmt_metric_set_start_timestamp(sample, data_point->start_time_unix_nano); + } + else { + cmt_metric_unset_start_timestamp(sample); + } } return result; @@ -839,7 +847,7 @@ static int decode_summary_data_point(struct cmt *cmt, if (result == CMT_DECODE_OPENTELEMETRY_SUCCESS) { struct cfl_kvlist *point_metadata; - if (sample->sum_quantiles_set == CMT_FALSE) { + if (cmt_atomic_load(&sample->sum_quantiles_set) == CMT_FALSE) { sample->sum_quantiles = calloc(data_point->n_quantile_values, sizeof(uint64_t)); @@ -847,7 +855,7 @@ static int decode_summary_data_point(struct cmt *cmt, return CMT_DECODE_OPENTELEMETRY_ALLOCATION_ERROR; } - sample->sum_quantiles_set = CMT_TRUE; + cmt_atomic_store(&sample->sum_quantiles_set, CMT_TRUE); sample->sum_quantiles_count = data_point->n_quantile_values; } @@ -858,14 +866,22 @@ static int decode_summary_data_point(struct cmt *cmt, index, data_point->quantile_values[index]->value); } - sample->sum_sum = cmt_math_d64_to_uint64(data_point->sum); - sample->sum_count = data_point->count; + cmt_summary_sum_set(sample, data_point->time_unix_nano, data_point->sum); + cmt_summary_count_set(sample, data_point->time_unix_nano, + data_point->count); point_metadata = get_or_create_data_point_metadata_context(cmt, map, sample, data_point->time_unix_nano); if (point_metadata != NULL) { cfl_kvlist_insert_uint64(point_metadata, "start_time_unix_nano", data_point->start_time_unix_nano); cfl_kvlist_insert_uint64(point_metadata, "flags", data_point->flags); } + + if (data_point->start_time_unix_nano > 0) { + cmt_metric_set_start_timestamp(sample, data_point->start_time_unix_nano); + } + else { + cmt_metric_unset_start_timestamp(sample); + } } return result; @@ -976,8 +992,10 @@ static int decode_histogram_data_point(struct cmt *cmt, index, data_point->bucket_counts[index]); } - sample->hist_sum = cmt_math_d64_to_uint64(data_point->sum); - sample->hist_count = data_point->count; + cmt_metric_hist_sum_set(sample, data_point->time_unix_nano, + data_point->sum); + cmt_metric_hist_count_set(sample, data_point->time_unix_nano, + data_point->count); point_metadata = get_or_create_data_point_metadata_context(cmt, map, sample, data_point->time_unix_nano); if (point_metadata != NULL) { @@ -994,6 +1012,13 @@ static int decode_histogram_data_point(struct cmt *cmt, } clone_exemplars_to_kvlist(point_metadata, data_point->exemplars, data_point->n_exemplars); } + + if (data_point->start_time_unix_nano > 0) { + cmt_metric_set_start_timestamp(sample, data_point->start_time_unix_nano); + } + else { + cmt_metric_unset_start_timestamp(sample); + } } return result; @@ -1135,6 +1160,10 @@ static int decode_exponential_histogram_data_point(struct cmt *cmt, { Opentelemetry__Proto__Metrics__V1__ExponentialHistogramDataPoint__Buckets *positive; Opentelemetry__Proto__Metrics__V1__ExponentialHistogramDataPoint__Buckets *negative; + uint64_t *old_positive_buckets; + uint64_t *old_negative_buckets; + uint64_t *new_positive_buckets; + uint64_t *new_negative_buckets; struct cmt_metric *sample; int static_metric_detected; int result; @@ -1143,6 +1172,8 @@ static int decode_exponential_histogram_data_point(struct cmt *cmt, result = CMT_DECODE_OPENTELEMETRY_SUCCESS; positive = data_point->positive; negative = data_point->negative; + new_positive_buckets = NULL; + new_negative_buckets = NULL; static_metric_detected = CMT_FALSE; @@ -1181,60 +1212,60 @@ static int decode_exponential_histogram_data_point(struct cmt *cmt, map->metric_static_set = CMT_TRUE; } - if (sample->exp_hist_positive_buckets != NULL) { - free(sample->exp_hist_positive_buckets); - sample->exp_hist_positive_buckets = NULL; - sample->exp_hist_positive_count = 0; - } - - if (sample->exp_hist_negative_buckets != NULL) { - free(sample->exp_hist_negative_buckets); - sample->exp_hist_negative_buckets = NULL; - sample->exp_hist_negative_count = 0; - } - if (positive != NULL && positive->n_bucket_counts > 0) { - sample->exp_hist_positive_buckets = calloc(positive->n_bucket_counts, sizeof(uint64_t)); - if (sample->exp_hist_positive_buckets == NULL) { + new_positive_buckets = calloc(positive->n_bucket_counts, sizeof(uint64_t)); + if (new_positive_buckets == NULL) { return CMT_DECODE_OPENTELEMETRY_ALLOCATION_ERROR; } for (index = 0 ; index < positive->n_bucket_counts ; index++) { - sample->exp_hist_positive_buckets[index] = positive->bucket_counts[index]; + new_positive_buckets[index] = positive->bucket_counts[index]; } - sample->exp_hist_positive_count = positive->n_bucket_counts; - sample->exp_hist_positive_offset = positive->offset; - } - else { - sample->exp_hist_positive_offset = 0; } if (negative != NULL && negative->n_bucket_counts > 0) { - sample->exp_hist_negative_buckets = calloc(negative->n_bucket_counts, sizeof(uint64_t)); - if (sample->exp_hist_negative_buckets == NULL) { + new_negative_buckets = calloc(negative->n_bucket_counts, sizeof(uint64_t)); + if (new_negative_buckets == NULL) { + free(new_positive_buckets); return CMT_DECODE_OPENTELEMETRY_ALLOCATION_ERROR; } for (index = 0 ; index < negative->n_bucket_counts ; index++) { - sample->exp_hist_negative_buckets[index] = negative->bucket_counts[index]; + new_negative_buckets[index] = negative->bucket_counts[index]; } - sample->exp_hist_negative_count = negative->n_bucket_counts; - sample->exp_hist_negative_offset = negative->offset; - } - else { - sample->exp_hist_negative_offset = 0; } + cmt_metric_exp_hist_lock(sample); + + old_positive_buckets = sample->exp_hist_positive_buckets; + old_negative_buckets = sample->exp_hist_negative_buckets; + + sample->exp_hist_positive_buckets = new_positive_buckets; + sample->exp_hist_positive_count = + positive != NULL ? positive->n_bucket_counts : 0; + sample->exp_hist_positive_offset = + positive != NULL ? positive->offset : 0; + + sample->exp_hist_negative_buckets = new_negative_buckets; + sample->exp_hist_negative_count = + negative != NULL ? negative->n_bucket_counts : 0; + sample->exp_hist_negative_offset = + negative != NULL ? negative->offset : 0; + sample->exp_hist_scale = data_point->scale; sample->exp_hist_zero_count = data_point->zero_count; sample->exp_hist_zero_threshold = data_point->zero_threshold; - sample->exp_hist_count = data_point->count; - sample->exp_hist_sum_set = data_point->has_sum ? CMT_TRUE : CMT_FALSE; - if (sample->exp_hist_sum_set == CMT_TRUE) { - sample->exp_hist_sum = cmt_math_d64_to_uint64(data_point->sum); + cmt_metric_set_exp_hist_count(sample, data_point->count); + cmt_metric_set_exp_hist_sum(sample, data_point->has_sum ? CMT_TRUE : CMT_FALSE, + data_point->sum); + cmt_metric_set_timestamp(sample, data_point->time_unix_nano); + + cmt_metric_exp_hist_unlock(sample); + + if (old_positive_buckets != NULL) { + free(old_positive_buckets); } - else { - sample->exp_hist_sum = 0; + if (old_negative_buckets != NULL) { + free(old_negative_buckets); } - sample->timestamp = data_point->time_unix_nano; { struct cfl_kvlist *point_metadata; @@ -1254,6 +1285,13 @@ static int decode_exponential_histogram_data_point(struct cmt *cmt, } clone_exemplars_to_kvlist(point_metadata, data_point->exemplars, data_point->n_exemplars); } + + if (data_point->start_time_unix_nano > 0) { + cmt_metric_set_start_timestamp(sample, data_point->start_time_unix_nano); + } + else { + cmt_metric_unset_start_timestamp(sample); + } } return result; diff --git a/lib/cmetrics/src/cmt_decode_prometheus_remote_write.c b/lib/cmetrics/src/cmt_decode_prometheus_remote_write.c index 3adeccefc4c..8c6a98679a8 100644 --- a/lib/cmetrics/src/cmt_decode_prometheus_remote_write.c +++ b/lib/cmetrics/src/cmt_decode_prometheus_remote_write.c @@ -452,12 +452,14 @@ static int decode_histogram_points(struct cmt *cmt, } } - metric->hist_sum = cmt_math_d64_to_uint64(hist->sum); + cmt_metric_hist_sum_set(metric, hist->timestamp * 1000000, hist->sum); if (hist->count_case == PROMETHEUS__HISTOGRAM__COUNT_COUNT_INT) { - metric->hist_count = hist->count_int; + cmt_metric_hist_count_set(metric, hist->timestamp * 1000000, + hist->count_int); } else if (hist->count_case == PROMETHEUS__HISTOGRAM__COUNT_COUNT_FLOAT) { - metric->hist_count = hist->count_float; + cmt_metric_hist_count_set(metric, hist->timestamp * 1000000, + hist->count_float); } else { if (static_metric_detected == CMT_FALSE) { diff --git a/lib/cmetrics/src/cmt_encode_cloudwatch_emf.c b/lib/cmetrics/src/cmt_encode_cloudwatch_emf.c index dae82ee8419..1918aa98ef8 100644 --- a/lib/cmetrics/src/cmt_encode_cloudwatch_emf.c +++ b/lib/cmetrics/src/cmt_encode_cloudwatch_emf.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -194,7 +195,7 @@ static void pack_histogram_metric(mpack_writer_t *writer, struct cmt *cmt, val = cmt_metric_hist_get_sum_value(metric); } else { - val = cmt_math_uint64_to_d64(metric->exp_hist_sum); + val = cmt_math_uint64_to_d64(cmt_atomic_load(&metric->exp_hist_sum)); } mpack_write_double(writer, val); mpack_write_cstr(writer, "Count"); @@ -202,7 +203,7 @@ static void pack_histogram_metric(mpack_writer_t *writer, struct cmt *cmt, val = cmt_metric_hist_get_count_value(metric); } else { - val = metric->exp_hist_count; + val = exp_bucket_counts[exp_bucket_count - 1]; } mpack_write_double(writer, val); mpack_finish_map(writer); diff --git a/lib/cmetrics/src/cmt_encode_influx.c b/lib/cmetrics/src/cmt_encode_influx.c index 97d9c52c1c6..d60cf2da6bd 100644 --- a/lib/cmetrics/src/cmt_encode_influx.c +++ b/lib/cmetrics/src/cmt_encode_influx.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -193,8 +194,8 @@ static void append_metric_value(struct cmt_map *map, fake_metric = *metric; fake_metric.hist_buckets = bucket_values; - fake_metric.hist_count = metric->exp_hist_count; - fake_metric.hist_sum = metric->exp_hist_sum; + fake_metric.hist_count = bucket_values[bucket_count - 1]; + fake_metric.hist_sum = cmt_atomic_load(&metric->exp_hist_sum); append_histogram_metric_value(&fake_map, buf, &fake_metric); @@ -276,7 +277,7 @@ static void format_metric(struct cmt *cmt, cfl_sds_t *buf, struct cmt_map *map, struct cmt_opts *opts; struct cmt_label *slabel; - if (map->type == CMT_SUMMARY && !metric->sum_quantiles_set) { + if (map->type == CMT_SUMMARY && !cmt_atomic_load(&metric->sum_quantiles_set)) { return; } diff --git a/lib/cmetrics/src/cmt_encode_msgpack.c b/lib/cmetrics/src/cmt_encode_msgpack.c index f6cb07a17fd..87fddf45324 100644 --- a/lib/cmetrics/src/cmt_encode_msgpack.c +++ b/lib/cmetrics/src/cmt_encode_msgpack.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -168,13 +169,17 @@ static void pack_header(mpack_writer_t *writer, struct cmt *cmt, struct cmt_map static int pack_metric(mpack_writer_t *writer, struct cmt_map *map, struct cmt_metric *metric) { int c_labels; + int has_start_timestamp; + int has_exp_hist_snapshot; int s; double val; size_t index; + uint64_t start_timestamp; struct cfl_list *head; struct cmt_map_label *label; struct cmt_summary *summary; struct cmt_histogram *histogram; + struct cmt_exp_histogram_snapshot snapshot; c_labels = cfl_list_size(&metric->labels); @@ -191,10 +196,33 @@ static int pack_metric(mpack_writer_t *writer, struct cmt_map *map, struct cmt_m s += 2; } + has_start_timestamp = cmt_metric_has_start_timestamp(metric); + if (has_start_timestamp) { + start_timestamp = cmt_metric_get_start_timestamp(metric); + s++; + } + else { + start_timestamp = 0; + } + + has_exp_hist_snapshot = CMT_FALSE; + + if (map->type == CMT_EXP_HISTOGRAM) { + if (cmt_metric_exp_hist_get_snapshot(metric, &snapshot) != 0) { + return -1; + } + has_exp_hist_snapshot = CMT_TRUE; + } + mpack_start_map(writer, s); mpack_write_cstr(writer, "ts"); - mpack_write_uint(writer, metric->timestamp); + mpack_write_uint(writer, cmt_metric_get_timestamp(metric)); + + if (has_start_timestamp) { + mpack_write_cstr(writer, "start_ts"); + mpack_write_uint(writer, start_timestamp); + } if (map->type == CMT_HISTOGRAM) { histogram = (struct cmt_histogram *) map->parent; @@ -225,42 +253,42 @@ static int pack_metric(mpack_writer_t *writer, struct cmt_map *map, struct cmt_m mpack_start_map(writer, 10); mpack_write_cstr(writer, "scale"); - mpack_write_int(writer, metric->exp_hist_scale); + mpack_write_int(writer, snapshot.scale); mpack_write_cstr(writer, "zero_count"); - mpack_write_uint(writer, metric->exp_hist_zero_count); + mpack_write_uint(writer, snapshot.zero_count); mpack_write_cstr(writer, "zero_threshold"); - mpack_write_double(writer, metric->exp_hist_zero_threshold); + mpack_write_double(writer, snapshot.zero_threshold); mpack_write_cstr(writer, "positive_offset"); - mpack_write_int(writer, metric->exp_hist_positive_offset); + mpack_write_int(writer, snapshot.positive_offset); mpack_write_cstr(writer, "positive_buckets"); - mpack_start_array(writer, metric->exp_hist_positive_count); - for (index = 0 ; index < metric->exp_hist_positive_count ; index++) { - mpack_write_uint(writer, metric->exp_hist_positive_buckets[index]); + mpack_start_array(writer, snapshot.positive_count); + for (index = 0 ; index < snapshot.positive_count ; index++) { + mpack_write_uint(writer, snapshot.positive_buckets[index]); } mpack_finish_array(writer); mpack_write_cstr(writer, "negative_offset"); - mpack_write_int(writer, metric->exp_hist_negative_offset); + mpack_write_int(writer, snapshot.negative_offset); mpack_write_cstr(writer, "negative_buckets"); - mpack_start_array(writer, metric->exp_hist_negative_count); - for (index = 0 ; index < metric->exp_hist_negative_count ; index++) { - mpack_write_uint(writer, metric->exp_hist_negative_buckets[index]); + mpack_start_array(writer, snapshot.negative_count); + for (index = 0 ; index < snapshot.negative_count ; index++) { + mpack_write_uint(writer, snapshot.negative_buckets[index]); } mpack_finish_array(writer); mpack_write_cstr(writer, "count"); - mpack_write_uint(writer, metric->exp_hist_count); + mpack_write_uint(writer, snapshot.count); mpack_write_cstr(writer, "sum_set"); - mpack_write_uint(writer, metric->exp_hist_sum_set); + mpack_write_uint(writer, snapshot.sum_set); mpack_write_cstr(writer, "sum"); - mpack_write_uint(writer, metric->exp_hist_sum); + mpack_write_uint(writer, snapshot.sum); mpack_finish_map(writer); /* 'exp_histogram' */ } @@ -271,13 +299,14 @@ static int pack_metric(mpack_writer_t *writer, struct cmt_map *map, struct cmt_m mpack_start_map(writer, 4); mpack_write_cstr(writer, "quantiles_set"); - mpack_write_uint(writer, metric->sum_quantiles_set); + mpack_write_uint(writer, cmt_atomic_load(&metric->sum_quantiles_set)); mpack_write_cstr(writer, "quantiles"); mpack_start_array(writer, summary->quantiles_count); for (index = 0 ; index < summary->quantiles_count ; index++) { - mpack_write_uint(writer, metric->sum_quantiles[index]); + mpack_write_uint(writer, + cmt_atomic_load(&metric->sum_quantiles[index])); } mpack_finish_array(writer); @@ -286,7 +315,7 @@ static int pack_metric(mpack_writer_t *writer, struct cmt_map *map, struct cmt_m mpack_write_uint(writer, cmt_summary_get_count_value(metric)); mpack_write_cstr(writer, "sum"); - mpack_write_uint(writer, metric->sum_sum); + mpack_write_uint(writer, cmt_atomic_load(&metric->sum_sum)); mpack_finish_map(writer); /* 'summary' */ } @@ -333,6 +362,10 @@ static int pack_metric(mpack_writer_t *writer, struct cmt_map *map, struct cmt_m mpack_finish_map(writer); + if (has_exp_hist_snapshot) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); + } + return 0; } diff --git a/lib/cmetrics/src/cmt_encode_opentelemetry.c b/lib/cmetrics/src/cmt_encode_opentelemetry.c index 87957c84da3..ee25fae568e 100644 --- a/lib/cmetrics/src/cmt_encode_opentelemetry.c +++ b/lib/cmetrics/src/cmt_encode_opentelemetry.c @@ -208,6 +208,22 @@ static int kvlist_fetch_double(struct cfl_kvlist *kvlist, const char *key, doubl return -1; } +static int resolve_data_point_start_timestamp(struct cmt_metric *sample, + struct cfl_kvlist *point_metadata, + uint64_t *start_timestamp) +{ + if (sample != NULL && cmt_metric_has_start_timestamp(sample)) { + *start_timestamp = cmt_metric_get_start_timestamp(sample); + return 0; + } + + if (point_metadata == NULL) { + return -1; + } + + return kvlist_fetch_uint64(point_metadata, "start_time_unix_nano", start_timestamp); +} + static int kvlist_fetch_bool(struct cfl_kvlist *kvlist, const char *key, int *out) { struct cfl_variant *value; @@ -567,7 +583,9 @@ static void apply_data_point_metadata_from_otlp_context(struct cmt *cmt, number_data_point = (Opentelemetry__Proto__Metrics__V1__NumberDataPoint *) data_point; number_value_case = NULL; - result = kvlist_fetch_uint64(point_metadata, "start_time_unix_nano", &uint64_value); + result = resolve_data_point_start_timestamp(sample, + point_metadata, + &uint64_value); if (result == 0) { number_data_point->start_time_unix_nano = uint64_value; } @@ -614,7 +632,9 @@ static void apply_data_point_metadata_from_otlp_context(struct cmt *cmt, summary_data_point = (Opentelemetry__Proto__Metrics__V1__SummaryDataPoint *) data_point; - result = kvlist_fetch_uint64(point_metadata, "start_time_unix_nano", &uint64_value); + result = resolve_data_point_start_timestamp(sample, + point_metadata, + &uint64_value); if (result == 0) { summary_data_point->start_time_unix_nano = uint64_value; } @@ -629,7 +649,9 @@ static void apply_data_point_metadata_from_otlp_context(struct cmt *cmt, histogram_data_point = (Opentelemetry__Proto__Metrics__V1__HistogramDataPoint *) data_point; - result = kvlist_fetch_uint64(point_metadata, "start_time_unix_nano", &uint64_value); + result = resolve_data_point_start_timestamp(sample, + point_metadata, + &uint64_value); if (result == 0) { histogram_data_point->start_time_unix_nano = uint64_value; } @@ -673,7 +695,9 @@ static void apply_data_point_metadata_from_otlp_context(struct cmt *cmt, exp_data_point = (Opentelemetry__Proto__Metrics__V1__ExponentialHistogramDataPoint *) data_point; - result = kvlist_fetch_uint64(point_metadata, "start_time_unix_nano", &uint64_value); + result = resolve_data_point_start_timestamp(sample, + point_metadata, + &uint64_value); if (result == 0) { exp_data_point->start_time_unix_nano = uint64_value; } @@ -3155,6 +3179,7 @@ int append_sample_to_metric(struct cmt_opentelemetry_context *context, struct cmt_metric *sample, size_t sample_index) { + uint64_t start_timestamp; size_t attribute_index; size_t attribute_count; Opentelemetry__Proto__Common__V1__KeyValue **attribute_list; @@ -3173,6 +3198,11 @@ int append_sample_to_metric(struct cmt_opentelemetry_context *context, attribute_count = cfl_list_size(&context->cmt->static_labels->list) + cfl_list_size(&sample->labels); + start_timestamp = 0; + + if (cmt_metric_has_start_timestamp(sample)) { + start_timestamp = cmt_metric_get_start_timestamp(sample); + } attribute_list = initialize_attribute_list(attribute_count); @@ -3183,7 +3213,7 @@ int append_sample_to_metric(struct cmt_opentelemetry_context *context, if (map->type == CMT_COUNTER || map->type == CMT_GAUGE || map->type == CMT_UNTYPED ) { - data_point = initialize_numerical_data_point(0, + data_point = initialize_numerical_data_point(start_timestamp, cmt_metric_get_timestamp(sample), sample, attribute_list, @@ -3192,7 +3222,7 @@ int append_sample_to_metric(struct cmt_opentelemetry_context *context, else if (map->type == CMT_SUMMARY) { summary = (struct cmt_summary *) map->parent; - data_point = initialize_summary_data_point(0, + data_point = initialize_summary_data_point(start_timestamp, cmt_metric_get_timestamp(sample), cmt_summary_get_count_value(sample), cmt_summary_get_sum_value(sample), @@ -3206,7 +3236,7 @@ int append_sample_to_metric(struct cmt_opentelemetry_context *context, else if (map->type == CMT_HISTOGRAM) { histogram = (struct cmt_histogram *) map->parent; - data_point = initialize_histogram_data_point(0, + data_point = initialize_histogram_data_point(start_timestamp, cmt_metric_get_timestamp(sample), cmt_metric_hist_get_count_value(sample), cmt_metric_hist_get_sum_value(sample), @@ -3218,70 +3248,83 @@ int append_sample_to_metric(struct cmt_opentelemetry_context *context, attribute_count); } else if (map->type == CMT_EXP_HISTOGRAM) { + struct cmt_exp_histogram_snapshot snapshot; + + if (cmt_metric_exp_hist_get_snapshot(sample, &snapshot) != 0) { + destroy_attribute_list(attribute_list); + return CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR; + } + exponential_data_point = calloc(1, sizeof(Opentelemetry__Proto__Metrics__V1__ExponentialHistogramDataPoint)); if (exponential_data_point == NULL) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); destroy_attribute_list(attribute_list); return CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR; } opentelemetry__proto__metrics__v1__exponential_histogram_data_point__init(exponential_data_point); - exponential_data_point->start_time_unix_nano = 0; + exponential_data_point->start_time_unix_nano = start_timestamp; exponential_data_point->time_unix_nano = cmt_metric_get_timestamp(sample); - exponential_data_point->count = sample->exp_hist_count; - exponential_data_point->scale = sample->exp_hist_scale; - exponential_data_point->zero_count = sample->exp_hist_zero_count; - exponential_data_point->zero_threshold = sample->exp_hist_zero_threshold; + exponential_data_point->count = snapshot.count; + exponential_data_point->scale = snapshot.scale; + exponential_data_point->zero_count = snapshot.zero_count; + exponential_data_point->zero_threshold = snapshot.zero_threshold; exponential_data_point->attributes = attribute_list; exponential_data_point->n_attributes = attribute_count; - if (sample->exp_hist_sum_set) { + if (snapshot.sum_set) { exponential_data_point->has_sum = CMT_TRUE; - exponential_data_point->sum = cmt_math_uint64_to_d64(sample->exp_hist_sum); + exponential_data_point->sum = cmt_math_uint64_to_d64(snapshot.sum); } - if (sample->exp_hist_positive_count > 0) { + if (snapshot.positive_count > 0) { positive = calloc(1, sizeof(Opentelemetry__Proto__Metrics__V1__ExponentialHistogramDataPoint__Buckets)); if (positive == NULL) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); destroy_data_point(exponential_data_point, map->type); return CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR; } opentelemetry__proto__metrics__v1__exponential_histogram_data_point__buckets__init(positive); - positive->offset = sample->exp_hist_positive_offset; - positive->n_bucket_counts = sample->exp_hist_positive_count; - positive->bucket_counts = calloc(sample->exp_hist_positive_count, sizeof(uint64_t)); + positive->offset = snapshot.positive_offset; + positive->n_bucket_counts = snapshot.positive_count; + positive->bucket_counts = calloc(snapshot.positive_count, sizeof(uint64_t)); if (positive->bucket_counts == NULL) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); free(positive); destroy_data_point(exponential_data_point, map->type); return CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR; } - memcpy(positive->bucket_counts, sample->exp_hist_positive_buckets, - sizeof(uint64_t) * sample->exp_hist_positive_count); + memcpy(positive->bucket_counts, snapshot.positive_buckets, + sizeof(uint64_t) * snapshot.positive_count); exponential_data_point->positive = positive; } - if (sample->exp_hist_negative_count > 0) { + if (snapshot.negative_count > 0) { negative = calloc(1, sizeof(Opentelemetry__Proto__Metrics__V1__ExponentialHistogramDataPoint__Buckets)); if (negative == NULL) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); destroy_data_point(exponential_data_point, map->type); return CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR; } opentelemetry__proto__metrics__v1__exponential_histogram_data_point__buckets__init(negative); - negative->offset = sample->exp_hist_negative_offset; - negative->n_bucket_counts = sample->exp_hist_negative_count; - negative->bucket_counts = calloc(sample->exp_hist_negative_count, sizeof(uint64_t)); + negative->offset = snapshot.negative_offset; + negative->n_bucket_counts = snapshot.negative_count; + negative->bucket_counts = calloc(snapshot.negative_count, sizeof(uint64_t)); if (negative->bucket_counts == NULL) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); free(negative); destroy_data_point(exponential_data_point, map->type); return CMT_ENCODE_OPENTELEMETRY_DATA_POINT_INIT_ERROR; } - memcpy(negative->bucket_counts, sample->exp_hist_negative_buckets, - sizeof(uint64_t) * sample->exp_hist_negative_count); + memcpy(negative->bucket_counts, snapshot.negative_buckets, + sizeof(uint64_t) * snapshot.negative_count); exponential_data_point->negative = negative; } + cmt_metric_exp_hist_snapshot_destroy(&snapshot); data_point = exponential_data_point; } diff --git a/lib/cmetrics/src/cmt_encode_prometheus.c b/lib/cmetrics/src/cmt_encode_prometheus.c index d1a8cab87b9..b5f76149c75 100644 --- a/lib/cmetrics/src/cmt_encode_prometheus.c +++ b/lib/cmetrics/src/cmt_encode_prometheus.c @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -177,10 +178,11 @@ static void append_metric_value(cfl_sds_t *buf, } else if (map->type == CMT_EXP_HISTOGRAM) { if (fmt->value_from == PROM_FMT_VAL_FROM_SUM) { - val = cmt_math_uint64_to_d64(metric->exp_hist_sum); + val = cmt_math_uint64_to_d64( + cmt_atomic_load(&metric->exp_hist_sum)); } else if (fmt->value_from == PROM_FMT_VAL_FROM_COUNT) { - val = metric->exp_hist_count; + val = cmt_atomic_load(&metric->exp_hist_count); } } else if (map->type == CMT_SUMMARY) { @@ -237,6 +239,54 @@ static int add_static_labels(struct cmt *cmt, cfl_sds_t *buf) return count; } +static void destroy_temporary_metric_labels(struct cmt_metric *metric) +{ + struct cfl_list *head; + struct cfl_list *tmp; + struct cmt_map_label *label; + + cfl_list_foreach_safe(head, tmp, &metric->labels) { + label = cfl_list_entry(head, struct cmt_map_label, _head); + cfl_list_del(&label->_head); + cfl_sds_destroy(label->name); + free(label); + } +} + +static int initialize_temporary_metric(struct cmt_metric *destination, + struct cmt_metric *source) +{ + struct cfl_list *head; + struct cmt_map_label *source_label; + struct cmt_map_label *destination_label; + + memset(destination, 0, sizeof(struct cmt_metric)); + cfl_list_init(&destination->labels); + + cfl_list_foreach(head, &source->labels) { + source_label = cfl_list_entry(head, struct cmt_map_label, _head); + + destination_label = calloc(1, sizeof(struct cmt_map_label)); + if (destination_label == NULL) { + destroy_temporary_metric_labels(destination); + return -1; + } + + destination_label->name = cfl_sds_create(source_label->name); + if (destination_label->name == NULL) { + free(destination_label); + destroy_temporary_metric_labels(destination); + return -1; + } + + cfl_list_add(&destination_label->_head, &destination->labels); + } + + cmt_metric_set_timestamp(destination, cmt_metric_get_timestamp(source)); + + return 0; +} + static void format_metric(struct cmt *cmt, cfl_sds_t *buf, struct cmt_map *map, struct cmt_metric *metric, int add_timestamp, @@ -410,7 +460,7 @@ static void format_summary_quantiles(struct cmt *cmt, summary = (struct cmt_summary *) map->parent; opts = map->opts; - if (metric->sum_quantiles_set) { + if (cmt_atomic_load(&metric->sum_quantiles_set)) { for (i = 0; i < summary->quantiles_count; i++) { /* metric name */ cfl_sds_cat_safe(buf, opts->fqname, cfl_sds_len(opts->fqname)); @@ -472,11 +522,9 @@ static void format_metrics(struct cmt *cmt, cfl_sds_t *buf, struct cmt_map *map, } else if (map->type == CMT_EXP_HISTOGRAM) { struct cmt_map fake_map; + struct cmt_metric fake_metric; struct cmt_histogram fake_histogram; struct cmt_histogram_buckets fake_buckets; - uint64_t *original_hist_buckets; - uint64_t original_hist_count; - uint64_t original_hist_sum; size_t bucket_count; size_t upper_bounds_count; uint64_t *bucket_values; @@ -498,21 +546,17 @@ static void format_metrics(struct cmt *cmt, cfl_sds_t *buf, struct cmt_map *map, fake_map = *map; fake_map.type = CMT_HISTOGRAM; fake_map.parent = &fake_histogram; + if (initialize_temporary_metric(&fake_metric, &map->metric) == 0) { + fake_metric.hist_buckets = bucket_values; + fake_metric.hist_count = bucket_values[bucket_count - 1]; + fake_metric.hist_sum = cmt_atomic_load(&map->metric.exp_hist_sum); - original_hist_buckets = map->metric.hist_buckets; - original_hist_count = map->metric.hist_count; - original_hist_sum = map->metric.hist_sum; + format_histogram_bucket(cmt, buf, &fake_map, &fake_metric, + add_timestamp, + cmt_atomic_load(&map->metric.exp_hist_sum_set)); - map->metric.hist_buckets = bucket_values; - map->metric.hist_count = map->metric.exp_hist_count; - map->metric.hist_sum = map->metric.exp_hist_sum; - - format_histogram_bucket(cmt, buf, &fake_map, &map->metric, - add_timestamp, map->metric.exp_hist_sum_set); - - map->metric.hist_buckets = original_hist_buckets; - map->metric.hist_count = original_hist_count; - map->metric.hist_sum = original_hist_sum; + destroy_temporary_metric_labels(&fake_metric); + } free(bucket_values); free(upper_bounds); @@ -545,11 +589,9 @@ static void format_metrics(struct cmt *cmt, cfl_sds_t *buf, struct cmt_map *map, } else if (map->type == CMT_EXP_HISTOGRAM) { struct cmt_map fake_map; + struct cmt_metric fake_metric; struct cmt_histogram fake_histogram; struct cmt_histogram_buckets fake_buckets; - uint64_t *original_hist_buckets; - uint64_t original_hist_count; - uint64_t original_hist_sum; size_t bucket_count; size_t upper_bounds_count; uint64_t *bucket_values; @@ -571,22 +613,21 @@ static void format_metrics(struct cmt *cmt, cfl_sds_t *buf, struct cmt_map *map, fake_map = *map; fake_map.type = CMT_HISTOGRAM; fake_map.parent = &fake_histogram; + if (initialize_temporary_metric(&fake_metric, metric) != 0) { + free(bucket_values); + free(upper_bounds); + continue; + } - original_hist_buckets = metric->hist_buckets; - original_hist_count = metric->hist_count; - original_hist_sum = metric->hist_sum; - - metric->hist_buckets = bucket_values; - metric->hist_count = metric->exp_hist_count; - metric->hist_sum = metric->exp_hist_sum; - - format_histogram_bucket(cmt, buf, &fake_map, metric, add_timestamp, - metric->exp_hist_sum_set); + fake_metric.hist_buckets = bucket_values; + fake_metric.hist_count = bucket_values[bucket_count - 1]; + fake_metric.hist_sum = cmt_atomic_load(&metric->exp_hist_sum); - metric->hist_buckets = original_hist_buckets; - metric->hist_count = original_hist_count; - metric->hist_sum = original_hist_sum; + format_histogram_bucket(cmt, buf, &fake_map, &fake_metric, + add_timestamp, + cmt_atomic_load(&metric->exp_hist_sum_set)); + destroy_temporary_metric_labels(&fake_metric); free(bucket_values); free(upper_bounds); } diff --git a/lib/cmetrics/src/cmt_encode_prometheus_remote_write.c b/lib/cmetrics/src/cmt_encode_prometheus_remote_write.c index 8eea8206393..f974d557f7b 100644 --- a/lib/cmetrics/src/cmt_encode_prometheus_remote_write.c +++ b/lib/cmetrics/src/cmt_encode_prometheus_remote_write.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -773,7 +774,7 @@ int pack_complex_metric_sample(struct cmt_prometheus_remote_write_context *conte memset(&dummy_metric, 0, sizeof(struct cmt_metric)); memcpy(&dummy_metric.labels, &metric->labels, sizeof(struct cfl_list)); - dummy_metric.timestamp = metric->timestamp; + dummy_metric.timestamp = cmt_metric_get_timestamp(metric); if (map->type == CMT_SUMMARY) { summary = (struct cmt_summary *) map->parent; @@ -922,7 +923,7 @@ int pack_complex_metric_sample(struct cmt_prometheus_remote_write_context *conte count_value = cmt_metric_hist_get_count_value(metric); } else { - count_value = metric->exp_hist_count; + count_value = exp_bucket_counts[exp_bucket_count - 1]; } cmt_metric_set(&dummy_metric, dummy_metric.timestamp, count_value); @@ -943,7 +944,8 @@ int pack_complex_metric_sample(struct cmt_prometheus_remote_write_context *conte if (result == CMT_ENCODE_PROMETHEUS_REMOTE_WRITE_SUCCESS && (map->type == CMT_HISTOGRAM || - (map->type == CMT_EXP_HISTOGRAM && metric->exp_hist_sum_set == CMT_TRUE))) { + (map->type == CMT_EXP_HISTOGRAM && + cmt_atomic_load(&metric->exp_hist_sum_set) == CMT_TRUE))) { context->sequence_number += SYNTHETIC_METRIC_HISTOGRAM_SUM_SEQUENCE_DELTA; cfl_sds_len_set(synthetized_metric_name, @@ -956,7 +958,8 @@ int pack_complex_metric_sample(struct cmt_prometheus_remote_write_context *conte sum_value = cmt_metric_hist_get_sum_value(metric); } else { - sum_value = cmt_math_uint64_to_d64(metric->exp_hist_sum); + sum_value = cmt_math_uint64_to_d64( + cmt_atomic_load(&metric->exp_hist_sum)); } cmt_metric_set(&dummy_metric, dummy_metric.timestamp, sum_value); diff --git a/lib/cmetrics/src/cmt_encode_splunk_hec.c b/lib/cmetrics/src/cmt_encode_splunk_hec.c index 2f5730f9af0..4c7e7433b4f 100644 --- a/lib/cmetrics/src/cmt_encode_splunk_hec.c +++ b/lib/cmetrics/src/cmt_encode_splunk_hec.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,54 @@ static cfl_sds_t double_to_string(double val) return str; } +static void destroy_temporary_metric_labels(struct cmt_metric *metric) +{ + struct cfl_list *head; + struct cfl_list *tmp; + struct cmt_map_label *label; + + cfl_list_foreach_safe(head, tmp, &metric->labels) { + label = cfl_list_entry(head, struct cmt_map_label, _head); + cfl_list_del(&label->_head); + cfl_sds_destroy(label->name); + free(label); + } +} + +static int initialize_temporary_metric(struct cmt_metric *destination, + struct cmt_metric *source) +{ + struct cfl_list *head; + struct cmt_map_label *source_label; + struct cmt_map_label *destination_label; + + memset(destination, 0, sizeof(struct cmt_metric)); + cfl_list_init(&destination->labels); + + cfl_list_foreach(head, &source->labels) { + source_label = cfl_list_entry(head, struct cmt_map_label, _head); + + destination_label = calloc(1, sizeof(struct cmt_map_label)); + if (destination_label == NULL) { + destroy_temporary_metric_labels(destination); + return -1; + } + + destination_label->name = cfl_sds_create(source_label->name); + if (destination_label->name == NULL) { + free(destination_label); + destroy_temporary_metric_labels(destination); + return -1; + } + + cfl_list_add(&destination_label->_head, &destination->labels); + } + + cmt_metric_set_timestamp(destination, cmt_metric_get_timestamp(source)); + + return 0; +} + static void format_metric_name(cfl_sds_t *buf, struct cmt_map *map, const char *suffix) { int mlen = 0; @@ -441,7 +490,7 @@ static void format_summary_metric(struct cmt_splunk_hec_context *context, cfl_sd summary = (struct cmt_summary *) map->parent; - if (metric->sum_quantiles_set) { + if (cmt_atomic_load(&metric->sum_quantiles_set)) { for (index = 0; index < summary->quantiles_count; index++) { /* Common fields */ format_context_common(context, buf, map, metric); @@ -568,15 +617,13 @@ static void format_metric(struct cmt_splunk_hec_context *context, cfl_sds_t *buf } else if (map->type == CMT_EXP_HISTOGRAM) { struct cmt_map fake_map; + struct cmt_metric fake_metric; struct cmt_histogram fake_histogram; struct cmt_histogram_buckets fake_buckets; uint64_t *bucket_counts = NULL; double *upper_bounds = NULL; size_t upper_bounds_count = 0; size_t bucket_count = 0; - uint64_t *orig_hist_buckets; - uint64_t orig_hist_count; - uint64_t orig_hist_sum; if (cmt_exp_histogram_to_explicit(metric, &upper_bounds, @@ -593,21 +640,19 @@ static void format_metric(struct cmt_splunk_hec_context *context, cfl_sds_t *buf memcpy(&fake_map, map, sizeof(struct cmt_map)); fake_map.type = CMT_HISTOGRAM; fake_map.parent = &fake_histogram; + if (initialize_temporary_metric(&fake_metric, metric) != 0) { + free(bucket_counts); + free(upper_bounds); + return; + } - orig_hist_buckets = metric->hist_buckets; - orig_hist_count = metric->hist_count; - orig_hist_sum = metric->hist_sum; - - metric->hist_buckets = bucket_counts; - metric->hist_count = metric->exp_hist_count; - metric->hist_sum = metric->exp_hist_sum; - - format_histogram_bucket(context, buf, &fake_map, metric); + fake_metric.hist_buckets = bucket_counts; + fake_metric.hist_count = bucket_counts[bucket_count - 1]; + fake_metric.hist_sum = cmt_atomic_load(&metric->exp_hist_sum); - metric->hist_buckets = orig_hist_buckets; - metric->hist_count = orig_hist_count; - metric->hist_sum = orig_hist_sum; + format_histogram_bucket(context, buf, &fake_map, &fake_metric); + destroy_temporary_metric_labels(&fake_metric); free(bucket_counts); free(upper_bounds); diff --git a/lib/cmetrics/src/cmt_encode_text.c b/lib/cmetrics/src/cmt_encode_text.c index 0de654f5d62..0b1b5b2e126 100644 --- a/lib/cmetrics/src/cmt_encode_text.c +++ b/lib/cmetrics/src/cmt_encode_text.c @@ -389,32 +389,37 @@ static void append_summary_metric_value(cfl_sds_t *buf, static void append_exp_histogram_metric_value(cfl_sds_t *buf, struct cmt_metric *metric) { + struct cmt_exp_histogram_snapshot snapshot; size_t entry_buffer_length; char entry_buffer[256]; size_t index; + if (cmt_metric_exp_hist_get_snapshot(metric, &snapshot) != 0) { + return; + } + cfl_sds_cat_safe(buf, " = { ", 5); entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, "scale=%d, zero_count=%" PRIu64 ", zero_threshold=%.17g, ", - metric->exp_hist_scale, - metric->exp_hist_zero_count, - metric->exp_hist_zero_threshold); + snapshot.scale, + snapshot.zero_count, + snapshot.zero_threshold); cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, "positive={offset=%d, bucket_counts=[", - metric->exp_hist_positive_offset); + snapshot.positive_offset); cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); - for (index = 0; index < metric->exp_hist_positive_count; index++) { + for (index = 0; index < snapshot.positive_count; index++) { entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, "%" PRIu64 "%s", - metric->exp_hist_positive_buckets[index], - (index + 1 < metric->exp_hist_positive_count) ? ", " : ""); + snapshot.positive_buckets[index], + (index + 1 < snapshot.positive_count) ? ", " : ""); cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); } @@ -423,15 +428,15 @@ static void append_exp_histogram_metric_value(cfl_sds_t *buf, entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, "negative={offset=%d, bucket_counts=[", - metric->exp_hist_negative_offset); + snapshot.negative_offset); cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); - for (index = 0; index < metric->exp_hist_negative_count; index++) { + for (index = 0; index < snapshot.negative_count; index++) { entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, "%" PRIu64 "%s", - metric->exp_hist_negative_buckets[index], - (index + 1 < metric->exp_hist_negative_count) ? ", " : ""); + snapshot.negative_buckets[index], + (index + 1 < snapshot.negative_count) ? ", " : ""); cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); } @@ -440,14 +445,14 @@ static void append_exp_histogram_metric_value(cfl_sds_t *buf, entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, "count=%" PRIu64, - metric->exp_hist_count); + snapshot.count); cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); - if (metric->exp_hist_sum_set) { + if (snapshot.sum_set) { entry_buffer_length = snprintf(entry_buffer, sizeof(entry_buffer) - 1, ", sum=%.17g", - cmt_math_uint64_to_d64(metric->exp_hist_sum)); + cmt_math_uint64_to_d64(snapshot.sum)); } else { entry_buffer_length = snprintf(entry_buffer, @@ -457,6 +462,7 @@ static void append_exp_histogram_metric_value(cfl_sds_t *buf, cfl_sds_cat_safe(buf, entry_buffer, entry_buffer_length); cfl_sds_cat_safe(buf, " }\n", 3); + cmt_metric_exp_hist_snapshot_destroy(&snapshot); } static void append_metric_value(cfl_sds_t *buf, struct cmt_map *map, diff --git a/lib/cmetrics/src/cmt_exp_histogram.c b/lib/cmetrics/src/cmt_exp_histogram.c index 9fea05a2bf7..dd3030e4378 100644 --- a/lib/cmetrics/src/cmt_exp_histogram.c +++ b/lib/cmetrics/src/cmt_exp_histogram.c @@ -118,6 +118,8 @@ int cmt_exp_histogram_set_default(struct cmt_exp_histogram *exp_histogram, struct cmt_metric *metric; uint64_t *new_positive_buckets; uint64_t *new_negative_buckets; + uint64_t *old_positive_buckets; + uint64_t *old_negative_buckets; metric = exp_histogram_get_metric(exp_histogram, labels_count, label_vals); if (!metric) { @@ -166,12 +168,10 @@ int cmt_exp_histogram_set_default(struct cmt_exp_histogram *exp_histogram, return -1; } - if (metric->exp_hist_positive_buckets != NULL) { - free(metric->exp_hist_positive_buckets); - } - if (metric->exp_hist_negative_buckets != NULL) { - free(metric->exp_hist_negative_buckets); - } + cmt_metric_exp_hist_lock(metric); + + old_positive_buckets = metric->exp_hist_positive_buckets; + old_negative_buckets = metric->exp_hist_negative_buckets; metric->exp_hist_positive_buckets = new_positive_buckets; metric->exp_hist_negative_buckets = new_negative_buckets; @@ -183,10 +183,18 @@ int cmt_exp_histogram_set_default(struct cmt_exp_histogram *exp_histogram, metric->exp_hist_zero_threshold = zero_threshold; metric->exp_hist_positive_offset = positive_offset; metric->exp_hist_negative_offset = negative_offset; - metric->exp_hist_count = count; - metric->exp_hist_sum_set = sum_set ? CMT_TRUE : CMT_FALSE; - metric->exp_hist_sum = cmt_math_d64_to_uint64(sum); - metric->timestamp = timestamp; + cmt_metric_set_exp_hist_count(metric, count); + cmt_metric_set_exp_hist_sum(metric, sum_set, sum); + cmt_metric_set_timestamp(metric, timestamp); + + cmt_metric_exp_hist_unlock(metric); + + if (old_positive_buckets != NULL) { + free(old_positive_buckets); + } + if (old_negative_buckets != NULL) { + free(old_negative_buckets); + } return 0; } @@ -211,6 +219,7 @@ int cmt_exp_histogram_to_explicit(struct cmt_metric *metric, uint64_t **bucket_counts, size_t *bucket_count) { + struct cmt_exp_histogram_snapshot snapshot; double base; double *local_upper_bounds; uint64_t *local_bucket_counts; @@ -230,98 +239,108 @@ int cmt_exp_histogram_to_explicit(struct cmt_metric *metric, return -1; } - base = pow(2.0, pow(2.0, (double) -metric->exp_hist_scale)); + if (cmt_metric_exp_hist_get_snapshot(metric, &snapshot) != 0) { + return -1; + } + + base = pow(2.0, pow(2.0, (double) -snapshot.scale)); if (!isfinite(base) || base <= 1.0) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); return -1; } - include_zero_threshold = (metric->exp_hist_zero_count > 0 || - metric->exp_hist_zero_threshold > 0.0 || - (metric->exp_hist_negative_count > 0 && metric->exp_hist_positive_count > 0) || - (metric->exp_hist_negative_count == 0 && metric->exp_hist_positive_count == 0)); + include_zero_threshold = (snapshot.zero_count > 0 || + snapshot.zero_threshold > 0.0 || + (snapshot.negative_count > 0 && snapshot.positive_count > 0) || + (snapshot.negative_count == 0 && snapshot.positive_count == 0)); - local_upper_bounds_count = metric->exp_hist_negative_count + metric->exp_hist_positive_count; + local_upper_bounds_count = snapshot.negative_count + snapshot.positive_count; if (include_zero_threshold) { - local_upper_bounds_count += (metric->exp_hist_zero_threshold > 0.0) ? 3 : 1; + local_upper_bounds_count += (snapshot.zero_threshold > 0.0) ? 3 : 1; } local_bucket_count = local_upper_bounds_count + 1; local_upper_bounds = calloc(local_upper_bounds_count, sizeof(double)); if (local_upper_bounds == NULL) { + cmt_metric_exp_hist_snapshot_destroy(&snapshot); return -1; } local_bucket_counts = calloc(local_bucket_count, sizeof(uint64_t)); if (local_bucket_counts == NULL) { free(local_upper_bounds); + cmt_metric_exp_hist_snapshot_destroy(&snapshot); return -1; } target_index = 0; cumulative_count = 0; - for (index = metric->exp_hist_negative_count ; index > 0 ; index--) { - bucket_index = (int64_t) metric->exp_hist_negative_offset + (int64_t) index - 1; + for (index = snapshot.negative_count ; index > 0 ; index--) { + bucket_index = (int64_t) snapshot.negative_offset + (int64_t) index - 1; local_upper_bounds[target_index] = -pow(base, (double) bucket_index); if (!isfinite(local_upper_bounds[target_index])) { free(local_bucket_counts); free(local_upper_bounds); + cmt_metric_exp_hist_snapshot_destroy(&snapshot); return -1; } - cumulative_count += metric->exp_hist_negative_buckets[index - 1]; + cumulative_count += snapshot.negative_buckets[index - 1]; local_bucket_counts[target_index] = cumulative_count; target_index++; } if (include_zero_threshold) { - if (metric->exp_hist_zero_threshold > 0.0) { - local_upper_bounds[target_index] = -metric->exp_hist_zero_threshold; + if (snapshot.zero_threshold > 0.0) { + local_upper_bounds[target_index] = -snapshot.zero_threshold; local_bucket_counts[target_index] = cumulative_count; target_index++; - cumulative_count += metric->exp_hist_zero_count; + cumulative_count += snapshot.zero_count; local_upper_bounds[target_index] = 0.0; local_bucket_counts[target_index] = cumulative_count; target_index++; - local_upper_bounds[target_index] = metric->exp_hist_zero_threshold; + local_upper_bounds[target_index] = snapshot.zero_threshold; local_bucket_counts[target_index] = cumulative_count; target_index++; } else { - cumulative_count += metric->exp_hist_zero_count; + cumulative_count += snapshot.zero_count; local_upper_bounds[target_index] = 0.0; local_bucket_counts[target_index] = cumulative_count; target_index++; } } - for (index = 0 ; index < metric->exp_hist_positive_count ; index++) { - bucket_index = (int64_t) metric->exp_hist_positive_offset + (int64_t) index + 1; + for (index = 0 ; index < snapshot.positive_count ; index++) { + bucket_index = (int64_t) snapshot.positive_offset + (int64_t) index + 1; local_upper_bounds[target_index] = pow(base, (double) bucket_index); if (!isfinite(local_upper_bounds[target_index])) { free(local_bucket_counts); free(local_upper_bounds); + cmt_metric_exp_hist_snapshot_destroy(&snapshot); return -1; } - cumulative_count += metric->exp_hist_positive_buckets[index]; + cumulative_count += snapshot.positive_buckets[index]; local_bucket_counts[target_index] = cumulative_count; target_index++; } - local_bucket_counts[local_bucket_count - 1] = metric->exp_hist_count; + local_bucket_counts[local_bucket_count - 1] = snapshot.count; *upper_bounds = local_upper_bounds; *upper_bounds_count = local_upper_bounds_count; *bucket_counts = local_bucket_counts; *bucket_count = local_bucket_count; + cmt_metric_exp_hist_snapshot_destroy(&snapshot); return 0; } diff --git a/lib/cmetrics/src/cmt_metric.c b/lib/cmetrics/src/cmt_metric.c index 7a94e22fa0b..ec892e3b55d 100644 --- a/lib/cmetrics/src/cmt_metric.c +++ b/lib/cmetrics/src/cmt_metric.c @@ -21,6 +21,8 @@ #include #include #include +#include +#include static inline int metric_exchange(struct cmt_metric *metric, double new_value, double old_value) @@ -247,3 +249,140 @@ uint64_t cmt_metric_get_timestamp(struct cmt_metric *metric) return val; } + +void cmt_metric_set_timestamp(struct cmt_metric *metric, uint64_t timestamp) +{ + cmt_atomic_store(&metric->timestamp, timestamp); +} + +void cmt_metric_set_start_timestamp(struct cmt_metric *metric, uint64_t start_timestamp) +{ + cmt_atomic_store(&metric->start_timestamp, start_timestamp); + cmt_atomic_store(&metric->start_timestamp_set, 1); +} + +void cmt_metric_unset_start_timestamp(struct cmt_metric *metric) +{ + cmt_atomic_store(&metric->start_timestamp, 0); + cmt_atomic_store(&metric->start_timestamp_set, 0); +} + +int cmt_metric_has_start_timestamp(struct cmt_metric *metric) +{ + return cmt_atomic_load(&metric->start_timestamp_set) != 0; +} + +uint64_t cmt_metric_get_start_timestamp(struct cmt_metric *metric) +{ + return cmt_atomic_load(&metric->start_timestamp); +} + +void cmt_metric_set_exp_hist_count(struct cmt_metric *metric, uint64_t count) +{ + cmt_atomic_store(&metric->exp_hist_count, count); +} + +void cmt_metric_set_exp_hist_sum(struct cmt_metric *metric, int sum_set, double sum) +{ + cmt_atomic_store(&metric->exp_hist_sum_set, sum_set ? CMT_TRUE : CMT_FALSE); + + if (sum_set) { + cmt_atomic_store(&metric->exp_hist_sum, cmt_math_d64_to_uint64(sum)); + } + else { + cmt_atomic_store(&metric->exp_hist_sum, 0); + } +} + +void cmt_metric_exp_hist_lock(struct cmt_metric *metric) +{ + while (cmt_atomic_compare_exchange(&metric->exp_hist_lock, 0, 1) == 0) { + } +} + +void cmt_metric_exp_hist_unlock(struct cmt_metric *metric) +{ + cmt_atomic_store(&metric->exp_hist_lock, 0); +} + +int cmt_metric_exp_hist_get_snapshot(struct cmt_metric *metric, + struct cmt_exp_histogram_snapshot *snapshot) +{ + if (metric == NULL || snapshot == NULL) { + return -1; + } + + memset(snapshot, 0, sizeof(struct cmt_exp_histogram_snapshot)); + + cmt_metric_exp_hist_lock(metric); + + snapshot->scale = metric->exp_hist_scale; + snapshot->zero_count = metric->exp_hist_zero_count; + snapshot->zero_threshold = metric->exp_hist_zero_threshold; + snapshot->positive_offset = metric->exp_hist_positive_offset; + snapshot->positive_count = metric->exp_hist_positive_count; + snapshot->negative_offset = metric->exp_hist_negative_offset; + snapshot->negative_count = metric->exp_hist_negative_count; + snapshot->count = cmt_atomic_load(&metric->exp_hist_count); + snapshot->sum_set = cmt_atomic_load(&metric->exp_hist_sum_set); + snapshot->sum = cmt_atomic_load(&metric->exp_hist_sum); + + if (snapshot->positive_count > 0) { + if (metric->exp_hist_positive_buckets == NULL) { + cmt_metric_exp_hist_unlock(metric); + return -1; + } + + snapshot->positive_buckets = calloc(snapshot->positive_count, + sizeof(uint64_t)); + if (snapshot->positive_buckets == NULL) { + cmt_metric_exp_hist_unlock(metric); + return -1; + } + + memcpy(snapshot->positive_buckets, metric->exp_hist_positive_buckets, + sizeof(uint64_t) * snapshot->positive_count); + } + + if (snapshot->negative_count > 0) { + if (metric->exp_hist_negative_buckets == NULL) { + free(snapshot->positive_buckets); + snapshot->positive_buckets = NULL; + cmt_metric_exp_hist_unlock(metric); + return -1; + } + + snapshot->negative_buckets = calloc(snapshot->negative_count, + sizeof(uint64_t)); + if (snapshot->negative_buckets == NULL) { + free(snapshot->positive_buckets); + snapshot->positive_buckets = NULL; + cmt_metric_exp_hist_unlock(metric); + return -1; + } + + memcpy(snapshot->negative_buckets, metric->exp_hist_negative_buckets, + sizeof(uint64_t) * snapshot->negative_count); + } + + cmt_metric_exp_hist_unlock(metric); + + return 0; +} + +void cmt_metric_exp_hist_snapshot_destroy(struct cmt_exp_histogram_snapshot *snapshot) +{ + if (snapshot == NULL) { + return; + } + + if (snapshot->positive_buckets != NULL) { + free(snapshot->positive_buckets); + snapshot->positive_buckets = NULL; + } + + if (snapshot->negative_buckets != NULL) { + free(snapshot->negative_buckets); + snapshot->negative_buckets = NULL; + } +} diff --git a/lib/cmetrics/src/cmt_summary.c b/lib/cmetrics/src/cmt_summary.c index f2a9eec0450..014ad0bbd02 100644 --- a/lib/cmetrics/src/cmt_summary.c +++ b/lib/cmetrics/src/cmt_summary.c @@ -291,7 +291,7 @@ int cmt_summary_set_default(struct cmt_summary *summary, /* set quantile values */ if (quantile_values) { /* yes, quantile values are set */ - metric->sum_quantiles_set = CMT_TRUE; + cmt_atomic_store(&metric->sum_quantiles_set, CMT_TRUE); /* populate each quantile */ for (i = 0; i < summary->quantiles_count; i++) { diff --git a/lib/cmetrics/tests/cloudwatch_emf_payload.bin b/lib/cmetrics/tests/cloudwatch_emf_payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..8077bc2ae5f1d108fe37490ca6a0a06e15f08279 GIT binary patch literal 2277 zcmds&Jx{_w7{`f|gM)){G!DjS`~nUp)SxCrF;O&*%~1|$==GYrE69qXqd^_iwIDAO z3?_!exVaSlA};y~^xE>8073*3eYWQA{?Bv2`#(=_QW+7%X|ix2BS;xQnyc(lupT@D zOp5p*BeF!(Cxuxsf<;czRpK5Wh&og08P6OVS)fNf-shA zXk;vn=jL&di^MUBiV6lnQNp^)Ajur6GRt|KkFuX(0>W?~Qmp8VCt9m3Z~C?@^^&J1 zKfkWC2qv_o5sXA+*DbPy26HvxpAwtGq|1);zXhbhy`W$TDAp@jFOZiU26S}5+#ml5 zScV&#jSiNuv{U8rI;3{HtXJCFVM)8gp1O5d80cX*jF!sUO1<-#b$1MUVI#WQHdwCm zC?P5!0F~}ne&A=spiFC{gz5+Zk{HHSb2Y8b8BPtKteWo6TPyAiA2>FJS*Ixszm817 z`$qlMujQ;g&rD&#SvwnW!i*G26EEi+0}aak57<5s35%Y$7K%EXknQg_hI_#D)u>JE5*ZKF}* F`UZRJz-|Bl literal 0 HcmV?d00001 diff --git a/lib/cmetrics/tests/encoding.c b/lib/cmetrics/tests/encoding.c index f0553528159..85905dfecaa 100644 --- a/lib/cmetrics/tests/encoding.c +++ b/lib/cmetrics/tests/encoding.c @@ -540,10 +540,10 @@ curl -v 'http://localhost:9090/receive' -H 'Content-Type: application/x-protobuf --data-binary '@prometheus_remote_write_payload.snp'\n\n"); sample_file = fopen("prometheus_remote_write_payload.bin", "wb+"); - - fwrite(payload, 1, cfl_sds_len(payload), sample_file); - - fclose(sample_file); + if (sample_file != NULL) { + fwrite(payload, 1, cfl_sds_len(payload), sample_file); + fclose(sample_file); + } cmt_encode_prometheus_remote_write_destroy(payload); @@ -604,12 +604,12 @@ curl -v 'http://localhost:9090/v1/metrics' -H 'Content-Type: application/x-proto --data-binary '@opentelemetry_payload.bin'\n\n"); sample_file = fopen("opentelemetry_payload.bin", "wb+"); + if (sample_file != NULL) { + fwrite(payload, 1, cfl_sds_len(payload), sample_file); + fclose(sample_file); + } - fwrite(payload, 1, cfl_sds_len(payload), sample_file); - - fclose(sample_file); - - cmt_encode_prometheus_remote_write_destroy(payload); + cmt_encode_opentelemetry_destroy(payload); cmt_destroy(cmt); } @@ -644,10 +644,10 @@ we need to encode it as JSON and to send AWS Cloudwatch with out_cloudwatch plug fluent-bit\n\n"); sample_file = fopen("cloudwatch_emf_payload.bin", "wb+"); - - fwrite(mp_buf, 1, mp_size, sample_file); - - fclose(sample_file); + if (sample_file != NULL) { + fwrite(mp_buf, 1, mp_size, sample_file); + fclose(sample_file); + } cmt_encode_cloudwatch_emf_destroy(mp_buf); diff --git a/lib/cmetrics/tests/opentelemetry_payload.bin b/lib/cmetrics/tests/opentelemetry_payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..6720d211bdafcd837a0d31ca8fb3359c914dab42 GIT binary patch literal 795 zcmd;@!6r17O=uG%mwa|gG^F3C&GO%>wi;>^iUPRs!bSP615Cl(Y4adB}Z z=cbkvWhNJM87iJV0|Y)A4!E`PLbS1Qu>c)dBETfUDCEE;i_6XFiKXeOLP97m28(F{ z-2!){&}t?w(QJ$2_>|0IsAr2yb8{1mDuo1HfI<+TLWQFKF>;kDazXscpsD%)OmOiz zPX{9*&RH`-V5hwxgz?cHE_M+! z5x>Pdsj-i8(@V8M&lDp@otPmvWCZo52rn6y{) oI~XZ2L4}y1Gz*l5DYcS-gy%|oh=hm(gjR9jN};ZKUReDL05XZ+Jpcdz literal 0 HcmV?d00001 diff --git a/lib/cmetrics/tests/prometheus_remote_write_payload.bin b/lib/cmetrics/tests/prometheus_remote_write_payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..24eba8cf820662271ae4a3567aae5c81644a03cc GIT binary patch literal 2695 zcmb_dO-~b16oo=$xR_9fj|7b%iONTznHEVP2I!=ntHk1`(`*2cAK zT)1&#+`2LD+~@|~`Xk)vFVNQ+xicN7la76N(WHxW&b#NHdj~e501_c4H5eiCOv9@& z+hi_t2>*R%*$tvwbdRW(XS&Rm^TU09NtOJk*U#U7S%G>c0+;~lz;fJ{o;<8uD%D$m zVH5^vvnfle!CYHY9TfVxc_yR^5gG}E26=}r7x{Y1>+U>uvjGbcJ1%#!qfWg#OL*f= zn2W$KHP#%mr#XK;bUeeL_Hp1Pg0G_XI$Vo@r;@>(ghLm8pWb)ARS{Z)*$8wZ5efj3 zaZGetV>--$bmURf)pRD0DaDe~UL~&a>ZDb_(EnaF%Iy+LsL?O1kMeq~cpv+IfiR65 zu|CS^)dkep$vThG$UFv=;wtA17Htwax4O<2$l;{rbA0Acsg8tU-tKjow+?ji3?6pV zbHdBI$>N9U7eBVwuA>)Jkk-GM96CEUqS{G{Bz%9<+;~^Pc0_3X`P|AugHPsxK`b!M(hTTBf~9Nj4({6Fa(G2WA%Kl@b)F*HCT?| zXH)PsPi-)_?I>P^96yKSN>N#j?f43=PajG>>_uwGm+BAqJp7V;SGd2=NNADqVr zKkkJXeB!}E)!awwaBL5<4Zg!g2`*m9x|1ag9XR6wIm&86(#6UalqVu2QvyRo@TUcb JiV#Q*&>x!`Oke;2 literal 0 HcmV?d00001 From bfa2d57b96b2d67bd1bd6cf1746e150c7949efb6 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 20 Feb 2026 14:43:07 -0600 Subject: [PATCH 08/14] processor_cumulative_to_delta: use new timestamp API Signed-off-by: Eduardo Silva --- .../cumulative_to_delta_core.c | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c index 459b2c41eed..b9059d13cf7 100644 --- a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c @@ -95,9 +95,13 @@ static int should_drop_initial_sample(struct flb_cumulative_to_delta_ctx *contex return FLB_FALSE; } - /* cmetrics no longer exposes datapoint start timestamp, so auto mode - * falls back to sample timestamp vs processor start time. */ - sample_timestamp = cmt_metric_get_timestamp(sample); + if (cmt_metric_has_start_timestamp(sample)) { + sample_timestamp = cmt_metric_get_start_timestamp(sample); + } + else { + sample_timestamp = cmt_metric_get_timestamp(sample); + } + if (sample_timestamp >= context->processor_start_timestamp) { return FLB_FALSE; } @@ -649,6 +653,15 @@ static int process_counter_sample(struct flb_cumulative_to_delta_ctx *context, delta = current_value - state->last_counter_value; } + if (reset_detected == FLB_TRUE) { + if (cmt_metric_has_start_timestamp(sample) == FLB_FALSE) { + cmt_metric_set_start_timestamp(sample, timestamp); + } + } + else { + cmt_metric_set_start_timestamp(sample, state->last_timestamp); + } + cmt_metric_set(sample, timestamp, delta); if (series_state_update_counter(state, timestamp, current_value) != 0) { @@ -836,6 +849,15 @@ static int process_histogram_sample(struct flb_cumulative_to_delta_ctx *context, sum_delta = current_sum - state->last_hist_sum; } + if (reset_detected == FLB_TRUE) { + if (cmt_metric_has_start_timestamp(sample) == FLB_FALSE) { + cmt_metric_set_start_timestamp(sample, timestamp); + } + } + else { + cmt_metric_set_start_timestamp(sample, state->last_timestamp); + } + cmt_metric_hist_count_set(sample, timestamp, count_delta); cmt_metric_hist_sum_set(sample, timestamp, sum_delta); @@ -1172,6 +1194,15 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont } } + if (reset_detected == FLB_TRUE) { + if (cmt_metric_has_start_timestamp(sample) == FLB_FALSE) { + cmt_metric_set_start_timestamp(sample, timestamp); + } + } + else { + cmt_metric_set_start_timestamp(sample, state->last_timestamp); + } + sample->exp_hist_count = count_delta; sample->exp_hist_zero_count = zero_count_delta; From 4a52bbf85de9095e8be015345adc5ecf9f10faee Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Fri, 20 Feb 2026 14:43:38 -0600 Subject: [PATCH 09/14] tests: internal: cumulative_to_delta: use new timestamp api Signed-off-by: Eduardo Silva --- tests/internal/cumulative_to_delta.c | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/internal/cumulative_to_delta.c b/tests/internal/cumulative_to_delta.c index 71ec9858532..2e8084fcabb 100644 --- a/tests/internal/cumulative_to_delta.c +++ b/tests/internal/cumulative_to_delta.c @@ -140,6 +140,8 @@ static void test_counter_drop_first_and_delta() value = cmt_metric_get_value(&counter->map->metric); TEST_CHECK(fabs(value - 6.0) < 0.0001); + TEST_CHECK(cmt_metric_has_start_timestamp(&counter->map->metric) == CMT_TRUE); + TEST_CHECK(cmt_metric_get_start_timestamp(&counter->map->metric) == 100); cmt_destroy(context); flb_cumulative_to_delta_ctx_destroy(converter); @@ -187,6 +189,8 @@ static void test_counter_reset_drop_and_keep() counter = get_first_counter(context); value = cmt_metric_get_value(&counter->map->metric); TEST_CHECK(fabs(value - 2.0) < 0.0001); + TEST_CHECK(cmt_metric_has_start_timestamp(&counter->map->metric) == CMT_TRUE); + TEST_CHECK(cmt_metric_get_start_timestamp(&counter->map->metric) == 200); cmt_destroy(context); flb_cumulative_to_delta_ctx_destroy(converter); } @@ -227,6 +231,8 @@ static void test_histogram_drop_first_and_delta() histogram = get_first_histogram(context); TEST_CHECK(histogram->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); TEST_CHECK(map_sample_count(histogram->map) == 1); + TEST_CHECK(cmt_metric_has_start_timestamp(&histogram->map->metric) == CMT_TRUE); + TEST_CHECK(cmt_metric_get_start_timestamp(&histogram->map->metric) == 100); TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 0) == 2); TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 1) == 3); TEST_CHECK(cmt_metric_hist_get_value(&histogram->map->metric, 2) == 5); @@ -311,6 +317,38 @@ static void test_counter_initial_value_auto() flb_cumulative_to_delta_ctx_destroy(converter); } +static void test_counter_initial_value_auto_uses_start_timestamp() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_AUTO, + FLB_TRUE, 150); + TEST_CHECK(converter != NULL); + + context = create_counter_context("auto_start_time_total", 200, 10.0, FLB_FALSE); + TEST_CHECK(context != NULL); + counter = get_first_counter(context); + cmt_metric_set_start_timestamp(&counter->map->metric, 100); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + context = create_counter_context("auto_start_time_total", 300, 18.0, FLB_FALSE); + TEST_CHECK(context != NULL); + counter = get_first_counter(context); + cmt_metric_set_start_timestamp(&counter->map->metric, 100); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 8.0) < 0.0001); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + static void test_histogram_sum_decrease_without_reset() { double sum; @@ -401,6 +439,8 @@ TEST_LIST = { {"histogram_drop_first_and_delta", test_histogram_drop_first_and_delta}, {"counter_out_of_order_is_dropped", test_counter_out_of_order_is_dropped}, {"counter_initial_value_auto", test_counter_initial_value_auto}, + {"counter_initial_value_auto_uses_start_timestamp", + test_counter_initial_value_auto_uses_start_timestamp}, {"histogram_sum_decrease_without_reset", test_histogram_sum_decrease_without_reset}, {"non_monotonic_sum_is_not_converted", test_non_monotonic_sum_is_not_converted}, {0} From 47cb9161f144926e0b4c9c24baf3584734c6fbe7 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 10 Mar 2026 13:15:11 -0600 Subject: [PATCH 10/14] processor_cumulative_to_delta: complete exp histogram conversion Signed-off-by: Eduardo Silva --- .../cumulative_to_delta_core.c | 681 +++++++++++++----- 1 file changed, 520 insertions(+), 161 deletions(-) diff --git a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c index b9059d13cf7..87a8ea1e850 100644 --- a/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c +++ b/plugins/processor_cumulative_to_delta/cumulative_to_delta_core.c @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -113,6 +114,24 @@ static void hash_variant(cfl_hash_state_t *state, struct cfl_variant *variant, size_t depth); +static int compare_uint64_values(const void *left, const void *right) +{ + uint64_t left_value; + uint64_t right_value; + + left_value = *(const uint64_t *) left; + right_value = *(const uint64_t *) right; + + if (left_value < right_value) { + return -1; + } + else if (left_value > right_value) { + return 1; + } + + return 0; +} + static void hash_array(cfl_hash_state_t *state, struct cfl_array *array, size_t depth) @@ -142,9 +161,13 @@ static void hash_kvlist(cfl_hash_state_t *state, { int entry_count; size_t count; + size_t index; size_t key_length; + uint64_t *entry_hashes; + uint64_t value_hash; struct cfl_list *head; struct cfl_kvpair *pair; + cfl_hash_state_t entry_state; if (kvlist == NULL || depth >= FLB_C2D_CONTEXT_HASH_MAX_DEPTH) { count = 0; @@ -162,20 +185,54 @@ static void hash_kvlist(cfl_hash_state_t *state, cfl_hash_64bits_update(state, &count, sizeof(count)); + if (count == 0) { + return; + } + + entry_hashes = flb_calloc(count, sizeof(uint64_t)); + if (entry_hashes == NULL) { + entry_hashes = NULL; + } + + index = 0; cfl_list_foreach(head, &kvlist->list) { pair = cfl_list_entry(head, struct cfl_kvpair, _head); - if (pair->key != NULL) { + cfl_hash_64bits_reset(&entry_state); + + if (pair->key == NULL) { + key_length = 0; + cfl_hash_64bits_update(&entry_state, &key_length, sizeof(key_length)); + } + else { key_length = cfl_sds_len(pair->key); - cfl_hash_64bits_update(state, &key_length, sizeof(key_length)); - cfl_hash_64bits_update(state, pair->key, key_length); + cfl_hash_64bits_update(&entry_state, &key_length, sizeof(key_length)); + cfl_hash_64bits_update(&entry_state, pair->key, key_length); + } + + hash_variant(&entry_state, pair->val, depth + 1); + value_hash = cfl_hash_64bits_digest(&entry_state); + + if (entry_hashes != NULL) { + entry_hashes[index] = value_hash; } else { - key_length = 0; - cfl_hash_64bits_update(state, &key_length, sizeof(key_length)); + cfl_hash_64bits_update(state, &value_hash, sizeof(value_hash)); + } + + index++; + } + + if (entry_hashes != NULL) { + qsort(entry_hashes, count, sizeof(uint64_t), compare_uint64_values); + + for (index = 0; index < count; index++) { + cfl_hash_64bits_update(state, + &entry_hashes[index], + sizeof(entry_hashes[index])); } - hash_variant(state, pair->val, depth + 1); + flb_free(entry_hashes); } } @@ -283,6 +340,25 @@ static int series_state_update_counter( return 0; } +static void series_state_mark_recent(struct flb_cumulative_to_delta_ctx *context, + struct flb_cumulative_to_delta_series *state) +{ + if (context == NULL || state == NULL) { + return; + } + + if (state->_head.next == NULL || state->_head.prev == NULL) { + return; + } + + if (context->series_list.prev == &state->_head) { + return; + } + + cfl_list_del(&state->_head); + cfl_list_add(&state->_head, &context->series_list); +} + static int series_state_update_histogram( struct flb_cumulative_to_delta_series *state, uint64_t timestamp, @@ -517,6 +593,11 @@ static struct flb_cumulative_to_delta_series *series_state_get( state = flb_hash_table_get_ptr(context->series_table, key, cfl_sds_len(key)); if (state != NULL || create_if_missing == FLB_FALSE) { flb_sds_destroy(key); + + if (state != NULL) { + series_state_mark_recent(context, state); + } + return state; } @@ -615,6 +696,8 @@ static int process_counter_sample(struct flb_cumulative_to_delta_ctx *context, } if (series_state_update_counter(state, timestamp, current_value) != 0) { + series_state_table_del(context, state); + series_state_destroy(state); return -1; } @@ -717,6 +800,350 @@ static int exp_hist_bucket_index_from_offset(int32_t offset, return 0; } +struct flb_c2d_exp_hist_bucket_layout { + int32_t offset; + size_t count; + uint64_t *buckets; + int owns_buckets; +}; + +struct flb_c2d_exp_hist_layout { + int32_t scale; + struct flb_c2d_exp_hist_bucket_layout positive; + struct flb_c2d_exp_hist_bucket_layout negative; +}; + +static void exp_hist_bucket_layout_destroy( + struct flb_c2d_exp_hist_bucket_layout *layout) +{ + if (layout == NULL) { + return; + } + + if (layout->owns_buckets == FLB_TRUE && + layout->buckets != NULL) { + flb_free(layout->buckets); + } + + memset(layout, 0, sizeof(struct flb_c2d_exp_hist_bucket_layout)); +} + +static void exp_hist_layout_destroy(struct flb_c2d_exp_hist_layout *layout) +{ + if (layout == NULL) { + return; + } + + exp_hist_bucket_layout_destroy(&layout->positive); + exp_hist_bucket_layout_destroy(&layout->negative); + memset(layout, 0, sizeof(struct flb_c2d_exp_hist_layout)); +} + +static int exp_hist_floor_div(int64_t value, + int64_t divisor, + int64_t *result) +{ + int64_t quotient; + int64_t remainder; + + if (result == NULL || divisor <= 0) { + return -1; + } + + quotient = value / divisor; + remainder = value % divisor; + + if (remainder != 0 && value < 0) { + quotient--; + } + + *result = quotient; + + return 0; +} + +static int exp_hist_bucket_layout_downscale( + struct flb_c2d_exp_hist_bucket_layout *source, + int32_t scale_delta, + struct flb_c2d_exp_hist_bucket_layout *destination) +{ + int64_t first_index; + int64_t group_index; + int64_t last_index; + int64_t new_first_index; + int64_t new_last_index; + int64_t signed_position; + int64_t width; + size_t index; + + if (source == NULL || destination == NULL) { + return -1; + } + + memset(destination, 0, sizeof(struct flb_c2d_exp_hist_bucket_layout)); + + if (source->count == 0) { + return 0; + } + + if (source->buckets == NULL || scale_delta < 0 || scale_delta > 62) { + return -1; + } + + if (scale_delta == 0) { + destination->offset = source->offset; + destination->count = source->count; + destination->buckets = source->buckets; + destination->owns_buckets = FLB_FALSE; + + return 0; + } + + width = ((int64_t) 1) << scale_delta; + signed_position = (int64_t) source->count - 1; + first_index = (int64_t) source->offset; + last_index = first_index + signed_position; + + if (last_index < first_index) { + return -1; + } + + if (exp_hist_floor_div(first_index, width, &new_first_index) != 0 || + exp_hist_floor_div(last_index, width, &new_last_index) != 0) { + return -1; + } + + if (new_first_index < INT32_MIN || new_first_index > INT32_MAX || + new_last_index < INT32_MIN || new_last_index > INT32_MAX || + new_last_index < new_first_index) { + return -1; + } + + destination->count = (size_t) (new_last_index - new_first_index + 1); + destination->offset = (int32_t) new_first_index; + destination->owns_buckets = FLB_TRUE; + destination->buckets = flb_calloc(destination->count, sizeof(uint64_t)); + if (destination->buckets == NULL) { + return -1; + } + + for (index = 0; index < source->count; index++) { + signed_position = (int64_t) index; + group_index = first_index + signed_position; + + if (exp_hist_floor_div(group_index, width, &group_index) != 0) { + exp_hist_bucket_layout_destroy(destination); + return -1; + } + + destination->buckets[(size_t) (group_index - new_first_index)] += + source->buckets[index]; + } + + return 0; +} + +static int exp_hist_layout_downscale(struct flb_c2d_exp_hist_layout *source, + int32_t target_scale, + struct flb_c2d_exp_hist_layout *destination) +{ + int32_t scale_delta; + + if (source == NULL || destination == NULL || target_scale > source->scale) { + return -1; + } + + memset(destination, 0, sizeof(struct flb_c2d_exp_hist_layout)); + destination->scale = target_scale; + scale_delta = source->scale - target_scale; + + if (exp_hist_bucket_layout_downscale(&source->positive, + scale_delta, + &destination->positive) != 0) { + return -1; + } + + if (exp_hist_bucket_layout_downscale(&source->negative, + scale_delta, + &destination->negative) != 0) { + exp_hist_layout_destroy(destination); + return -1; + } + + return 0; +} + +static int exp_hist_layout_init_from_sample(struct flb_c2d_exp_hist_layout *layout, + struct cmt_metric *sample) +{ + if (layout == NULL || sample == NULL) { + return -1; + } + + memset(layout, 0, sizeof(struct flb_c2d_exp_hist_layout)); + layout->scale = sample->exp_hist_scale; + + layout->positive.offset = sample->exp_hist_positive_offset; + layout->positive.count = sample->exp_hist_positive_count; + layout->positive.buckets = sample->exp_hist_positive_buckets; + layout->positive.owns_buckets = FLB_FALSE; + + layout->negative.offset = sample->exp_hist_negative_offset; + layout->negative.count = sample->exp_hist_negative_count; + layout->negative.buckets = sample->exp_hist_negative_buckets; + layout->negative.owns_buckets = FLB_FALSE; + + return 0; +} + +static int exp_hist_layout_init_from_state(struct flb_c2d_exp_hist_layout *layout, + struct flb_cumulative_to_delta_series *state) +{ + if (layout == NULL || state == NULL) { + return -1; + } + + memset(layout, 0, sizeof(struct flb_c2d_exp_hist_layout)); + layout->scale = state->last_exp_hist_scale; + + layout->positive.offset = state->last_exp_hist_positive_offset; + layout->positive.count = state->last_exp_hist_positive_count; + layout->positive.buckets = state->last_exp_hist_positive_buckets; + layout->positive.owns_buckets = FLB_FALSE; + + layout->negative.offset = state->last_exp_hist_negative_offset; + layout->negative.count = state->last_exp_hist_negative_count; + layout->negative.buckets = state->last_exp_hist_negative_buckets; + layout->negative.owns_buckets = FLB_FALSE; + + return 0; +} + +static int exp_hist_bucket_layout_is_monotonic( + struct flb_c2d_exp_hist_bucket_layout *current, + struct flb_c2d_exp_hist_bucket_layout *previous) +{ + int64_t bucket_index; + int64_t current_end; + int64_t current_start; + int64_t previous_end; + int64_t previous_start; + uint64_t current_value; + uint64_t previous_value; + + if (current == NULL || previous == NULL) { + return FLB_FALSE; + } + + current_start = (int64_t) current->offset; + previous_start = (int64_t) previous->offset; + current_end = current_start + (int64_t) current->count - 1; + previous_end = previous_start + (int64_t) previous->count - 1; + + if (current->count == 0) { + current_end = current_start - 1; + } + if (previous->count == 0) { + previous_end = previous_start - 1; + } + + for (bucket_index = current_start; bucket_index <= current_end; bucket_index++) { + current_value = exp_hist_bucket_get_value(current->offset, + current->count, + current->buckets, + bucket_index); + previous_value = exp_hist_bucket_get_value(previous->offset, + previous->count, + previous->buckets, + bucket_index); + + if (current_value < previous_value) { + return FLB_FALSE; + } + } + + for (bucket_index = previous_start; bucket_index <= previous_end; bucket_index++) { + current_value = exp_hist_bucket_get_value(current->offset, + current->count, + current->buckets, + bucket_index); + previous_value = exp_hist_bucket_get_value(previous->offset, + previous->count, + previous->buckets, + bucket_index); + + if (current_value < previous_value) { + return FLB_FALSE; + } + } + + return FLB_TRUE; +} + +static void exp_hist_bucket_layout_subtract( + struct flb_c2d_exp_hist_bucket_layout *current, + struct flb_c2d_exp_hist_bucket_layout *previous) +{ + int64_t bucket_index; + size_t index; + uint64_t previous_value; + + if (current == NULL || previous == NULL) { + return; + } + + for (index = 0; index < current->count; index++) { + if (exp_hist_bucket_index_from_offset(current->offset, + index, + &bucket_index) != 0) { + continue; + } + + previous_value = exp_hist_bucket_get_value(previous->offset, + previous->count, + previous->buckets, + bucket_index); + current->buckets[index] -= previous_value; + } +} + +static void exp_hist_sample_apply_layout(struct cmt_metric *sample, + struct flb_c2d_exp_hist_layout *layout) +{ + uint64_t *old_negative; + uint64_t *old_positive; + + if (sample == NULL || layout == NULL) { + return; + } + + old_positive = sample->exp_hist_positive_buckets; + old_negative = sample->exp_hist_negative_buckets; + + sample->exp_hist_scale = layout->scale; + sample->exp_hist_positive_offset = layout->positive.offset; + sample->exp_hist_positive_count = layout->positive.count; + sample->exp_hist_positive_buckets = layout->positive.buckets; + sample->exp_hist_negative_offset = layout->negative.offset; + sample->exp_hist_negative_count = layout->negative.count; + sample->exp_hist_negative_buckets = layout->negative.buckets; + + if (layout->positive.owns_buckets == FLB_TRUE && + old_positive != NULL && + old_positive != sample->exp_hist_positive_buckets) { + flb_free(old_positive); + } + + if (layout->negative.owns_buckets == FLB_TRUE && + old_negative != NULL && + old_negative != sample->exp_hist_negative_buckets) { + flb_free(old_negative); + } + + layout->positive.owns_buckets = FLB_FALSE; + layout->negative.owns_buckets = FLB_FALSE; +} + static int process_histogram_sample(struct flb_cumulative_to_delta_ctx *context, struct cmt_histogram *histogram, struct cmt_metric *sample, @@ -768,6 +1195,8 @@ static int process_histogram_sample(struct flb_cumulative_to_delta_ctx *context, current_sum, bucket_count, sample->hist_buckets) != 0) { + series_state_table_del(context, state); + series_state_destroy(state); return -1; } @@ -885,6 +1314,10 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont struct cmt_metric *sample, uint64_t context_identity) { + int32_t original_negative_offset; + int32_t original_positive_offset; + int32_t original_scale; + int32_t output_scale; int reset_detected; int current_sum_set; uint64_t current_count; @@ -892,15 +1325,17 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont uint64_t count_delta; uint64_t zero_count_delta; uint64_t timestamp; - uint64_t current_bucket_value; - uint64_t previous_bucket_value; - int64_t bucket_index; - size_t index; double current_sum; double sum_delta; + double original_zero_threshold; uint64_t *cumulative_positive_snapshot; uint64_t *cumulative_negative_snapshot; + struct flb_c2d_exp_hist_layout current_layout; + struct flb_c2d_exp_hist_layout normalized_current_layout; + struct flb_c2d_exp_hist_layout normalized_previous_layout; struct flb_cumulative_to_delta_series *state; + size_t original_negative_count; + size_t original_positive_count; if (sample->exp_hist_positive_count > 0 && sample->exp_hist_positive_buckets == NULL) { @@ -918,6 +1353,16 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont current_sum_set = sample->exp_hist_sum_set; current_sum = 0.0; sum_delta = 0.0; + original_scale = sample->exp_hist_scale; + original_zero_threshold = sample->exp_hist_zero_threshold; + original_positive_offset = sample->exp_hist_positive_offset; + original_positive_count = sample->exp_hist_positive_count; + original_negative_offset = sample->exp_hist_negative_offset; + original_negative_count = sample->exp_hist_negative_count; + + memset(¤t_layout, 0, sizeof(struct flb_c2d_exp_hist_layout)); + memset(&normalized_current_layout, 0, sizeof(struct flb_c2d_exp_hist_layout)); + memset(&normalized_previous_layout, 0, sizeof(struct flb_c2d_exp_hist_layout)); if (current_sum_set == CMT_TRUE) { current_sum = cmt_math_uint64_to_d64(sample->exp_hist_sum); @@ -952,6 +1397,8 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont current_count, current_sum_set, current_sum) != 0) { + series_state_table_del(context, state); + series_state_destroy(state); return -1; } @@ -968,8 +1415,7 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont reset_detected = FLB_FALSE; - if (sample->exp_hist_scale != state->last_exp_hist_scale || - sample->exp_hist_zero_threshold != state->last_exp_hist_zero_threshold || + if (sample->exp_hist_zero_threshold != state->last_exp_hist_zero_threshold || current_sum_set != state->last_exp_hist_sum_set) { reset_detected = FLB_TRUE; } @@ -979,115 +1425,51 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont reset_detected = FLB_TRUE; } - if (reset_detected == FLB_FALSE) { - for (index = 0; index < sample->exp_hist_positive_count; index++) { - if (exp_hist_bucket_index_from_offset(sample->exp_hist_positive_offset, - index, - &bucket_index) != 0) { - return FLB_C2D_DROP; - } - - previous_bucket_value = exp_hist_bucket_get_value( - state->last_exp_hist_positive_offset, - state->last_exp_hist_positive_count, - state->last_exp_hist_positive_buckets, - bucket_index); - - current_bucket_value = sample->exp_hist_positive_buckets[index]; - - if (current_bucket_value < previous_bucket_value) { - reset_detected = FLB_TRUE; - break; - } - } + output_scale = sample->exp_hist_scale; + if (state->last_exp_hist_scale < output_scale) { + output_scale = state->last_exp_hist_scale; } if (reset_detected == FLB_FALSE) { - for (index = 0; index < sample->exp_hist_negative_count; index++) { - if (exp_hist_bucket_index_from_offset(sample->exp_hist_negative_offset, - index, - &bucket_index) != 0) { - return FLB_C2D_DROP; - } - - previous_bucket_value = exp_hist_bucket_get_value( - state->last_exp_hist_negative_offset, - state->last_exp_hist_negative_count, - state->last_exp_hist_negative_buckets, - bucket_index); - - current_bucket_value = sample->exp_hist_negative_buckets[index]; - - if (current_bucket_value < previous_bucket_value) { - reset_detected = FLB_TRUE; - break; - } + if (exp_hist_layout_init_from_sample(¤t_layout, sample) != 0 || + exp_hist_layout_downscale(¤t_layout, + output_scale, + &normalized_current_layout) != 0) { + return -1; } - } - if (reset_detected == FLB_FALSE) { - for (index = 0; index < state->last_exp_hist_positive_count; index++) { - if (exp_hist_bucket_index_from_offset(state->last_exp_hist_positive_offset, - index, - &bucket_index) != 0) { - return FLB_C2D_DROP; - } - - previous_bucket_value = exp_hist_bucket_get_value( - state->last_exp_hist_positive_offset, - state->last_exp_hist_positive_count, - state->last_exp_hist_positive_buckets, - bucket_index); - current_bucket_value = exp_hist_bucket_get_value( - sample->exp_hist_positive_offset, - sample->exp_hist_positive_count, - sample->exp_hist_positive_buckets, - bucket_index); - - if (current_bucket_value < previous_bucket_value) { - reset_detected = FLB_TRUE; - break; - } + if (exp_hist_layout_init_from_state(¤t_layout, state) != 0 || + exp_hist_layout_downscale(¤t_layout, + output_scale, + &normalized_previous_layout) != 0) { + exp_hist_layout_destroy(&normalized_current_layout); + return -1; } - } - - if (reset_detected == FLB_FALSE) { - for (index = 0; index < state->last_exp_hist_negative_count; index++) { - if (exp_hist_bucket_index_from_offset(state->last_exp_hist_negative_offset, - index, - &bucket_index) != 0) { - return FLB_C2D_DROP; - } - previous_bucket_value = exp_hist_bucket_get_value( - state->last_exp_hist_negative_offset, - state->last_exp_hist_negative_count, - state->last_exp_hist_negative_buckets, - bucket_index); - current_bucket_value = exp_hist_bucket_get_value( - sample->exp_hist_negative_offset, - sample->exp_hist_negative_count, - sample->exp_hist_negative_buckets, - bucket_index); - - if (current_bucket_value < previous_bucket_value) { - reset_detected = FLB_TRUE; - break; - } + if (exp_hist_bucket_layout_is_monotonic(&normalized_current_layout.positive, + &normalized_previous_layout.positive) == + FLB_FALSE || + exp_hist_bucket_layout_is_monotonic(&normalized_current_layout.negative, + &normalized_previous_layout.negative) == + FLB_FALSE) { + reset_detected = FLB_TRUE; } } if (reset_detected == FLB_TRUE && context->drop_on_reset == FLB_TRUE) { + exp_hist_layout_destroy(&normalized_current_layout); + exp_hist_layout_destroy(&normalized_previous_layout); + if (series_state_update_exp_histogram(state, timestamp, - sample->exp_hist_scale, + original_scale, current_zero_count, - sample->exp_hist_zero_threshold, - sample->exp_hist_positive_offset, - sample->exp_hist_positive_count, + original_zero_threshold, + original_positive_offset, + original_positive_count, sample->exp_hist_positive_buckets, - sample->exp_hist_negative_offset, - sample->exp_hist_negative_count, + original_negative_offset, + original_negative_count, sample->exp_hist_negative_buckets, current_count, current_sum_set, @@ -1120,6 +1502,8 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont if (cumulative_positive_snapshot != NULL) { flb_free(cumulative_positive_snapshot); } + exp_hist_layout_destroy(&normalized_current_layout); + exp_hist_layout_destroy(&normalized_previous_layout); return -1; } @@ -1128,56 +1512,13 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont sizeof(uint64_t) * sample->exp_hist_negative_count); } - for (index = 0; index < sample->exp_hist_positive_count; index++) { - if (reset_detected == FLB_TRUE) { - continue; - } - - if (exp_hist_bucket_index_from_offset(sample->exp_hist_positive_offset, - index, - &bucket_index) != 0) { - if (cumulative_positive_snapshot != NULL) { - flb_free(cumulative_positive_snapshot); - } - if (cumulative_negative_snapshot != NULL) { - flb_free(cumulative_negative_snapshot); - } - return FLB_C2D_DROP; - } - - previous_bucket_value = exp_hist_bucket_get_value( - state->last_exp_hist_positive_offset, - state->last_exp_hist_positive_count, - state->last_exp_hist_positive_buckets, - bucket_index); - - sample->exp_hist_positive_buckets[index] -= previous_bucket_value; - } - - for (index = 0; index < sample->exp_hist_negative_count; index++) { - if (reset_detected == FLB_TRUE) { - continue; - } - - if (exp_hist_bucket_index_from_offset(sample->exp_hist_negative_offset, - index, - &bucket_index) != 0) { - if (cumulative_positive_snapshot != NULL) { - flb_free(cumulative_positive_snapshot); - } - if (cumulative_negative_snapshot != NULL) { - flb_free(cumulative_negative_snapshot); - } - return FLB_C2D_DROP; - } - - previous_bucket_value = exp_hist_bucket_get_value( - state->last_exp_hist_negative_offset, - state->last_exp_hist_negative_count, - state->last_exp_hist_negative_buckets, - bucket_index); + if (reset_detected == FLB_FALSE) { + exp_hist_sample_apply_layout(sample, &normalized_current_layout); - sample->exp_hist_negative_buckets[index] -= previous_bucket_value; + exp_hist_bucket_layout_subtract(&normalized_current_layout.positive, + &normalized_previous_layout.positive); + exp_hist_bucket_layout_subtract(&normalized_current_layout.negative, + &normalized_previous_layout.negative); } if (reset_detected == FLB_TRUE) { @@ -1215,16 +1556,19 @@ static int process_exp_histogram_sample(struct flb_cumulative_to_delta_ctx *cont sample->exp_hist_sum = 0; } + exp_hist_layout_destroy(&normalized_current_layout); + exp_hist_layout_destroy(&normalized_previous_layout); + if (series_state_update_exp_histogram(state, timestamp, - sample->exp_hist_scale, + original_scale, current_zero_count, - sample->exp_hist_zero_threshold, - sample->exp_hist_positive_offset, - sample->exp_hist_positive_count, + original_zero_threshold, + original_positive_offset, + original_positive_count, cumulative_positive_snapshot, - sample->exp_hist_negative_offset, - sample->exp_hist_negative_count, + original_negative_offset, + original_negative_count, cumulative_negative_snapshot, current_count, current_sum_set, @@ -1529,6 +1873,8 @@ int flb_cumulative_to_delta_ctx_configure( int max_staleness_seconds, int max_series) { + uint64_t default_gc_interval; + if (context == NULL) { return -1; } @@ -1539,9 +1885,20 @@ int flb_cumulative_to_delta_ctx_configure( if (max_staleness_seconds == 0) { context->series_ttl = UINT64_MAX; + context->gc_interval = + (uint64_t) FLB_C2D_GC_INTERVAL_SECONDS * 1000000000ULL; } else { context->series_ttl = (uint64_t) max_staleness_seconds * 1000000000ULL; + default_gc_interval = + (uint64_t) FLB_C2D_GC_INTERVAL_SECONDS * 1000000000ULL; + + if (context->series_ttl < default_gc_interval) { + context->gc_interval = context->series_ttl; + } + else { + context->gc_interval = default_gc_interval; + } } if (max_series == 0) { @@ -1551,5 +1908,7 @@ int flb_cumulative_to_delta_ctx_configure( context->max_series = (size_t) max_series; } + context->next_gc_timestamp = cfl_time_now() + context->gc_interval; + return 0; } From 2219faaba63db21aeb6971ea6414e9376cfddff4 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 12 Mar 2026 12:33:26 -0600 Subject: [PATCH 11/14] lib: cmetrics: upgrade to v2.0.4 Signed-off-by: Eduardo Silva --- lib/cmetrics/CMakeLists.txt | 2 +- lib/cmetrics/src/cmt_cat.c | 7 ++ lib/cmetrics/tests/cat.c | 95 ++++++++++++++++++ .../tests/prometheus_remote_write_payload.bin | Bin 2695 -> 2695 bytes 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/lib/cmetrics/CMakeLists.txt b/lib/cmetrics/CMakeLists.txt index 0648af997a1..894d6f24aa8 100644 --- a/lib/cmetrics/CMakeLists.txt +++ b/lib/cmetrics/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # CMetrics Version set(CMT_VERSION_MAJOR 2) set(CMT_VERSION_MINOR 0) -set(CMT_VERSION_PATCH 3) +set(CMT_VERSION_PATCH 4) set(CMT_VERSION_STR "${CMT_VERSION_MAJOR}.${CMT_VERSION_MINOR}.${CMT_VERSION_PATCH}") # Include helpers diff --git a/lib/cmetrics/src/cmt_cat.c b/lib/cmetrics/src/cmt_cat.c index 9ec70176cdf..49f44a60acf 100644 --- a/lib/cmetrics/src/cmt_cat.c +++ b/lib/cmetrics/src/cmt_cat.c @@ -689,6 +689,9 @@ int cmt_cat_counter(struct cmt *cmt, struct cmt_counter *counter, return -1; } + c->aggregation_type = counter->aggregation_type; + c->allow_reset = counter->allow_reset; + if (filtered_map != NULL) { ret = cmt_cat_copy_map(&c->opts, c->map, filtered_map); if (ret == -1) { @@ -837,6 +840,8 @@ int cmt_cat_histogram(struct cmt *cmt, struct cmt_histogram *histogram, return -1; } + hist->aggregation_type = histogram->aggregation_type; + if (filtered_map != NULL) { ret = cmt_cat_copy_map(&hist->opts, hist->map, filtered_map); if (ret == -1) { @@ -945,6 +950,8 @@ int cmt_cat_exp_histogram(struct cmt *cmt, struct cmt_exp_histogram *exp_histogr return -1; } + eh->aggregation_type = exp_histogram->aggregation_type; + if (filtered_map != NULL) { ret = cmt_cat_copy_map(&eh->opts, eh->map, filtered_map); } diff --git a/lib/cmetrics/tests/cat.c b/lib/cmetrics/tests/cat.c index 3ef59fca3e1..397125277bd 100644 --- a/lib/cmetrics/tests/cat.c +++ b/lib/cmetrics/tests/cat.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -611,6 +612,99 @@ void test_histogram_populated_to_empty() cmt_destroy(cmt2); } +void test_cat_preserves_aggregation_metadata() +{ + int ret; + uint64_t ts; + uint64_t positive_counts[2] = {3, 4}; + struct cmt *src; + struct cmt *dst; + struct cmt_counter *src_counter; + struct cmt_counter *dst_counter; + struct cmt_histogram *src_histogram; + struct cmt_histogram *dst_histogram; + struct cmt_exp_histogram *src_exp_histogram; + struct cmt_exp_histogram *dst_exp_histogram; + struct cmt_histogram_buckets *buckets; + + src = cmt_create(); + TEST_CHECK(src != NULL); + + dst = cmt_create(); + TEST_CHECK(dst != NULL); + + buckets = cmt_histogram_buckets_create(3, 1.0, 5.0, 10.0); + TEST_CHECK(buckets != NULL); + + src_counter = cmt_counter_create(src, "otlp", "cat", "counter", "counter", + 0, NULL); + TEST_CHECK(src_counter != NULL); + + src_histogram = cmt_histogram_create(src, "otlp", "cat", "histogram", "histogram", + buckets, 0, NULL); + TEST_CHECK(src_histogram != NULL); + + src_exp_histogram = cmt_exp_histogram_create(src, "otlp", "cat", "exp_histogram", + "exp histogram", 0, NULL); + TEST_CHECK(src_exp_histogram != NULL); + + ts = cfl_time_now(); + cmt_counter_allow_reset(src_counter); + src_counter->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + cmt_counter_add(src_counter, ts, 4.0, 0, NULL); + + src_histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + cmt_histogram_observe(src_histogram, ts, 2.5, 0, NULL); + + src_exp_histogram->aggregation_type = CMT_AGGREGATION_TYPE_DELTA; + ret = cmt_exp_histogram_set_default(src_exp_histogram, + ts, + 2, + 1, + 0.0, + 0, + 2, + positive_counts, + 0, + 0, + NULL, + CMT_TRUE, + 7.5, + 7, + 0, + NULL); + TEST_CHECK(ret == 0); + + ret = cmt_cat(dst, src); + TEST_CHECK(ret == 0); + + TEST_CHECK(cfl_list_size(&dst->counters) == 1); + dst_counter = cfl_list_entry_first(&dst->counters, struct cmt_counter, _head); + TEST_CHECK(dst_counter != NULL); + if (dst_counter != NULL) { + TEST_CHECK(dst_counter->allow_reset == CMT_TRUE); + TEST_CHECK(dst_counter->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + } + + TEST_CHECK(cfl_list_size(&dst->histograms) == 1); + dst_histogram = cfl_list_entry_first(&dst->histograms, struct cmt_histogram, _head); + TEST_CHECK(dst_histogram != NULL); + if (dst_histogram != NULL) { + TEST_CHECK(dst_histogram->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + } + + TEST_CHECK(cfl_list_size(&dst->exp_histograms) == 1); + dst_exp_histogram = cfl_list_entry_first(&dst->exp_histograms, + struct cmt_exp_histogram, _head); + TEST_CHECK(dst_exp_histogram != NULL); + if (dst_exp_histogram != NULL) { + TEST_CHECK(dst_exp_histogram->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + } + + cmt_destroy(src); + cmt_destroy(dst); +} + TEST_LIST = { {"cat", test_cat}, {"duplicate_metrics", test_duplicate_metrics}, @@ -618,5 +712,6 @@ TEST_LIST = { {"histogram_mismatched_buckets", test_histogram_mismatched_buckets}, {"histogram_empty_to_populated", test_histogram_empty_to_populated}, {"histogram_populated_to_empty", test_histogram_populated_to_empty}, + {"cat_preserves_aggregation_metadata", test_cat_preserves_aggregation_metadata}, { 0 } }; diff --git a/lib/cmetrics/tests/prometheus_remote_write_payload.bin b/lib/cmetrics/tests/prometheus_remote_write_payload.bin index 24eba8cf820662271ae4a3567aae5c81644a03cc..60b7f2b52cc3a56e8f36e6baac8abbd61ec7bf8a 100644 GIT binary patch delta 440 zcmZn{Z5N$j&HCWy(OKsvI;21uD-$4$$$X4fP?j&F1(emrXa;3{WOAC!&LoS(`_CdY zxt56=N#s0|d^`u(9%HVc$rG3iCU-E)zy$m!2QX?*F5{5pP;n3dE1LX^*$XC{1yrsw zxq!t1CXfgfP@epsMFb`YQ>8rlJd=nZ*Y(8(wyPYVrWtb~WY4j}T>ODG9O_aZHd`pG zj}00clh3iG!Hf=@e38v&as|5xRMABC5SUaXSW1&a93~Jl*`G}h=oJN+K+xogY&w%q qaVS7Fzvsw;;p}0xDOT zT)^T06G(&#C{O;+A_5bHsZySNo=HTI>-ypX+f@!w(~P+gvgcS~F8;t84t1#yn=O>p z#|90J$>-S8U`B^czQ|@Xxq@8;s%Rp62uvyxETzdI4igBO?9ZkL^ojyZAZYSLHl4|* qI253o-*e=_q~d^@4JL Date: Thu, 12 Mar 2026 12:35:22 -0600 Subject: [PATCH 12/14] metrics: consider exp histograms in empty-context checks Signed-off-by: Eduardo Silva --- src/flb_metrics.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/flb_metrics.c b/src/flb_metrics.c index d4a4b6e0b16..856a23c901d 100644 --- a/src/flb_metrics.c +++ b/src/flb_metrics.c @@ -377,6 +377,7 @@ bool flb_metrics_is_empty(struct cmt *cmt) return cfl_list_is_empty(&cmt->counters) && cfl_list_is_empty(&cmt->gauges) && cfl_list_is_empty(&cmt->histograms) && + cfl_list_is_empty(&cmt->exp_histograms) && cfl_list_is_empty(&cmt->summaries) && cfl_list_is_empty(&cmt->untypeds); } From f38dfe319c6c5bf51ece4fd8edc0e0cedc88aff8 Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 12 Mar 2026 12:35:35 -0600 Subject: [PATCH 13/14] tests: internal: add cumulative to delta exp histogram coverage Signed-off-by: Eduardo Silva --- tests/internal/cumulative_to_delta.c | 632 ++++++++++++++++++++++++++- 1 file changed, 631 insertions(+), 1 deletion(-) diff --git a/tests/internal/cumulative_to_delta.c b/tests/internal/cumulative_to_delta.c index 2e8084fcabb..374174c9b86 100644 --- a/tests/internal/cumulative_to_delta.c +++ b/tests/internal/cumulative_to_delta.c @@ -1,12 +1,17 @@ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ #include +#include #include -#include #include +#include +#include #include +#include #include +#include +#include #include "../../plugins/processor_cumulative_to_delta/cumulative_to_delta.h" @@ -111,6 +116,144 @@ static struct cmt *create_histogram_context(char *name, return context; } +static struct cmt_exp_histogram *get_first_exp_histogram(struct cmt *context) +{ + return cfl_list_entry(context->exp_histograms.next, + struct cmt_exp_histogram, + _head); +} + +static struct cmt *roundtrip_context_through_otlp(struct cmt *input) +{ + size_t offset; + cfl_sds_t payload; + struct cfl_list contexts; + struct cmt *context; + + payload = cmt_encode_opentelemetry_create(input); + if (payload == NULL) { + return NULL; + } + + offset = 0; + if (cmt_decode_opentelemetry_create(&contexts, + payload, + cfl_sds_len(payload), + &offset) != 0) { + cmt_encode_opentelemetry_destroy(payload); + return NULL; + } + + cmt_encode_opentelemetry_destroy(payload); + + if (cfl_list_size(&contexts) != 1) { + cmt_decode_opentelemetry_destroy(&contexts); + return NULL; + } + + context = cfl_list_entry_first(&contexts, struct cmt, _head); + cfl_list_del(&context->_head); + + return context; +} + +static struct cmt *create_exp_histogram_context(char *name, + uint64_t timestamp, + int32_t scale, + uint64_t zero_count, + double zero_threshold, + int32_t positive_offset, + size_t positive_count, + uint64_t *positive_buckets, + int32_t negative_offset, + size_t negative_count, + uint64_t *negative_buckets, + int sum_set, + double sum, + uint64_t count) +{ + struct cmt *context; + struct cmt_exp_histogram *exp_histogram; + + context = cmt_create(); + if (context == NULL) { + return NULL; + } + + exp_histogram = cmt_exp_histogram_create(context, "", "", name, "help", 0, NULL); + if (exp_histogram == NULL) { + cmt_destroy(context); + return NULL; + } + + exp_histogram->aggregation_type = CMT_AGGREGATION_TYPE_CUMULATIVE; + + if (cmt_exp_histogram_set_default(exp_histogram, + timestamp, + scale, + zero_count, + zero_threshold, + positive_offset, + positive_count, + positive_buckets, + negative_offset, + negative_count, + negative_buckets, + sum_set, + sum, + count, + 0, + NULL) != 0) { + cmt_destroy(context); + return NULL; + } + + return context; +} + +static int set_context_resource_attributes(struct cmt *context, + char *first_key, + char *first_value, + char *second_key, + char *second_value) +{ + struct cfl_variant *root_variant; + struct cfl_kvlist *resource_root; + struct cfl_kvlist *attributes; + + root_variant = cfl_kvlist_fetch(context->external_metadata, "resource"); + if (root_variant == NULL || root_variant->type != CFL_VARIANT_KVLIST) { + resource_root = cfl_kvlist_create(); + if (resource_root == NULL) { + return -1; + } + + if (cfl_kvlist_insert_kvlist(context->external_metadata, + "resource", + resource_root) != 0) { + cfl_kvlist_destroy(resource_root); + return -1; + } + } + else { + resource_root = root_variant->data.as_kvlist; + } + + attributes = cfl_kvlist_create(); + if (attributes == NULL) { + return -1; + } + + if (cfl_kvlist_insert_string(attributes, first_key, first_value) != 0 || + cfl_kvlist_insert_string(attributes, second_key, second_value) != 0 || + cfl_kvlist_insert_kvlist(resource_root, "attributes", attributes) != 0) { + cfl_kvlist_destroy(attributes); + return -1; + } + + return 0; +} + static void test_counter_drop_first_and_delta() { double value; @@ -433,6 +576,479 @@ static void test_non_monotonic_sum_is_not_converted() flb_cumulative_to_delta_ctx_destroy(converter); } +static void test_context_attribute_order_does_not_split_series() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_counter_context("ordered_resource_total", 100, 10.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(set_context_resource_attributes(context, + "service.name", "checkout", + "host.name", "node-a") == 0); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_counter_context("ordered_resource_total", 200, 16.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(set_context_resource_attributes(context, + "host.name", "node-a", + "service.name", "checkout") == 0); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 6.0) < 0.0001); + + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_max_series_evicts_least_recently_used() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_configure(converter, 0, 2) == 0); + + context = create_counter_context("series_a_total", 100, 1.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_counter_context("series_b_total", 100, 10.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_counter_context("series_a_total", 200, 3.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 2.0) < 0.0001); + cmt_destroy(context); + + context = create_counter_context("series_c_total", 100, 20.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + context = create_counter_context("series_a_total", 300, 6.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 3.0) < 0.0001); + cmt_destroy(context); + + context = create_counter_context("series_b_total", 200, 14.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 0); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_max_staleness_evicts_series() +{ + double value; + struct cmt *context; + struct cmt_counter *counter; + struct flb_cumulative_to_delta_ctx *converter; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_KEEP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_configure(converter, 1, 0) == 0); + + context = create_counter_context("stale_series_total", 100, 10.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + sleep(2); + + context = create_counter_context("stale_series_total", 200, 16.0, FLB_FALSE); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + counter = get_first_counter(context); + TEST_CHECK(map_sample_count(counter->map) == 1); + value = cmt_metric_get_value(&counter->map->metric); + TEST_CHECK(fabs(value - 16.0) < 0.0001); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_exp_histogram_scale_change_is_coarsened() +{ + double sum; + uint64_t positive_a[2]; + uint64_t positive_b[4]; + struct cmt *context; + struct cmt_exp_histogram *exp_histogram; + struct cmt_metric *metric; + struct flb_cumulative_to_delta_ctx *converter; + + positive_a[0] = 4; + positive_a[1] = 8; + + positive_b[0] = 5; + positive_b[1] = 8; + positive_b[2] = 11; + positive_b[3] = 15; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_exp_histogram_context("exp_scale_total", + 100, + 1, + 2, + 0.0, + 0, + 2, + positive_a, + 0, + 0, + NULL, + CMT_TRUE, + 40.0, + 10); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + exp_histogram = get_first_exp_histogram(context); + TEST_CHECK(map_sample_count(exp_histogram->map) == 0); + cmt_destroy(context); + + context = create_exp_histogram_context("exp_scale_total", + 200, + 2, + 3, + 0.0, + 0, + 4, + positive_b, + 0, + 0, + NULL, + CMT_TRUE, + 58.0, + 18); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + exp_histogram = get_first_exp_histogram(context); + TEST_CHECK(exp_histogram->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + TEST_CHECK(map_sample_count(exp_histogram->map) == 1); + + metric = &exp_histogram->map->metric; + TEST_CHECK(metric->exp_hist_scale == 1); + TEST_CHECK(metric->exp_hist_positive_offset == 0); + TEST_CHECK(metric->exp_hist_positive_count == 2); + TEST_CHECK(metric->exp_hist_positive_buckets[0] == 9); + TEST_CHECK(metric->exp_hist_positive_buckets[1] == 18); + TEST_CHECK(metric->exp_hist_zero_count == 1); + TEST_CHECK(metric->exp_hist_count == 8); + sum = cmt_math_uint64_to_d64(metric->exp_hist_sum); + TEST_CHECK(fabs(sum - 18.0) < 0.0001); + TEST_CHECK(cmt_metric_has_start_timestamp(metric) == CMT_TRUE); + TEST_CHECK(cmt_metric_get_start_timestamp(metric) == 100); + + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_exp_histogram_scale_change_is_coarsened_after_otlp_roundtrip() +{ + double sum; + uint64_t positive_a[2]; + uint64_t positive_b[4]; + struct cmt *context; + struct cmt *roundtrip; + struct cmt_exp_histogram *exp_histogram; + struct cmt_metric *metric; + struct flb_cumulative_to_delta_ctx *converter; + + positive_a[0] = 4; + positive_a[1] = 8; + + positive_b[0] = 5; + positive_b[1] = 8; + positive_b[2] = 11; + positive_b[3] = 15; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_exp_histogram_context("exp_scale_roundtrip_total", + 100, + 1, + 2, + 0.0, + 0, + 2, + positive_a, + 0, + 0, + NULL, + CMT_TRUE, + 40.0, + 10); + TEST_CHECK(context != NULL); + roundtrip = roundtrip_context_through_otlp(context); + TEST_CHECK(roundtrip != NULL); + cmt_destroy(context); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, roundtrip) == 0); + exp_histogram = get_first_exp_histogram(roundtrip); + TEST_CHECK(map_sample_count(exp_histogram->map) == 0); + cmt_destroy(roundtrip); + + context = create_exp_histogram_context("exp_scale_roundtrip_total", + 200, + 2, + 3, + 0.0, + 0, + 4, + positive_b, + 0, + 0, + NULL, + CMT_TRUE, + 58.0, + 18); + TEST_CHECK(context != NULL); + roundtrip = roundtrip_context_through_otlp(context); + TEST_CHECK(roundtrip != NULL); + cmt_destroy(context); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, roundtrip) == 0); + + exp_histogram = get_first_exp_histogram(roundtrip); + TEST_CHECK(exp_histogram->aggregation_type == CMT_AGGREGATION_TYPE_DELTA); + TEST_CHECK(map_sample_count(exp_histogram->map) == 1); + + metric = &exp_histogram->map->metric; + TEST_CHECK(metric->exp_hist_scale == 1); + TEST_CHECK(metric->exp_hist_positive_offset == 0); + TEST_CHECK(metric->exp_hist_positive_count == 2); + TEST_CHECK(metric->exp_hist_positive_buckets[0] == 9); + TEST_CHECK(metric->exp_hist_positive_buckets[1] == 18); + TEST_CHECK(metric->exp_hist_zero_count == 1); + TEST_CHECK(metric->exp_hist_count == 8); + sum = cmt_math_uint64_to_d64(metric->exp_hist_sum); + TEST_CHECK(fabs(sum - 18.0) < 0.0001); + + cmt_destroy(roundtrip); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_exp_histogram_scale_change_reset_detection() +{ + uint64_t positive_a[2]; + uint64_t positive_b[4]; + uint64_t positive_c[4]; + struct cmt *context; + struct cmt_exp_histogram *exp_histogram; + struct cmt_metric *metric; + struct flb_cumulative_to_delta_ctx *converter; + + positive_a[0] = 4; + positive_a[1] = 8; + + positive_b[0] = 3; + positive_b[1] = 0; + positive_b[2] = 11; + positive_b[3] = 15; + + positive_c[0] = 4; + positive_c[1] = 1; + positive_c[2] = 12; + positive_c[3] = 17; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_exp_histogram_context("exp_scale_reset_total", + 100, + 1, + 0, + 0.0, + 0, + 2, + positive_a, + 0, + 0, + NULL, + CMT_TRUE, + 10.0, + 12); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_exp_histogram_context("exp_scale_reset_total", + 200, + 2, + 0, + 0.0, + 0, + 4, + positive_b, + 0, + 0, + NULL, + CMT_TRUE, + 16.0, + 16); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + exp_histogram = get_first_exp_histogram(context); + TEST_CHECK(map_sample_count(exp_histogram->map) == 0); + cmt_destroy(context); + + context = create_exp_histogram_context("exp_scale_reset_total", + 300, + 2, + 0, + 0.0, + 0, + 4, + positive_c, + 0, + 0, + NULL, + CMT_TRUE, + 20.0, + 19); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + + exp_histogram = get_first_exp_histogram(context); + metric = &exp_histogram->map->metric; + TEST_CHECK(map_sample_count(exp_histogram->map) == 1); + TEST_CHECK(metric->exp_hist_scale == 2); + TEST_CHECK(metric->exp_hist_positive_count == 4); + TEST_CHECK(metric->exp_hist_positive_buckets[0] == 1); + TEST_CHECK(metric->exp_hist_positive_buckets[1] == 1); + TEST_CHECK(metric->exp_hist_positive_buckets[2] == 1); + TEST_CHECK(metric->exp_hist_positive_buckets[3] == 2); + TEST_CHECK(metric->exp_hist_count == 3); + + cmt_destroy(context); + flb_cumulative_to_delta_ctx_destroy(converter); +} + +static void test_exp_histogram_malformed_sample_is_dropped() +{ + uint64_t positive_a[2]; + uint64_t positive_c[2]; + struct cmt *context; + struct cmt_exp_histogram *exp_histogram; + struct cmt_metric *metric; + struct flb_cumulative_to_delta_ctx *converter; + + positive_a[0] = 4; + positive_a[1] = 8; + + positive_c[0] = 7; + positive_c[1] = 13; + + converter = flb_cumulative_to_delta_ctx_create(FLB_C2D_INITIAL_VALUE_DROP, + FLB_TRUE, 0); + TEST_CHECK(converter != NULL); + + context = create_exp_histogram_context("exp_malformed_total", + 100, + 1, + 1, + 0.0, + 0, + 2, + positive_a, + 0, + 0, + NULL, + CMT_TRUE, + 20.0, + 13); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + cmt_destroy(context); + + context = create_exp_histogram_context("exp_malformed_total", + 200, + 1, + 2, + 0.0, + 0, + 2, + positive_a, + 0, + 0, + NULL, + CMT_TRUE, + 22.0, + 14); + TEST_CHECK(context != NULL); + exp_histogram = get_first_exp_histogram(context); + metric = &exp_histogram->map->metric; + flb_free(metric->exp_hist_positive_buckets); + metric->exp_hist_positive_buckets = NULL; + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + TEST_CHECK(map_sample_count(exp_histogram->map) == 0); + cmt_destroy(context); + + context = create_exp_histogram_context("exp_malformed_total", + 300, + 1, + 3, + 0.0, + 0, + 2, + positive_c, + 0, + 0, + NULL, + CMT_TRUE, + 30.0, + 21); + TEST_CHECK(context != NULL); + TEST_CHECK(flb_cumulative_to_delta_ctx_process(converter, context) == 0); + exp_histogram = get_first_exp_histogram(context); + metric = &exp_histogram->map->metric; + TEST_CHECK(map_sample_count(exp_histogram->map) == 1); + TEST_CHECK(metric->exp_hist_positive_buckets[0] == 3); + TEST_CHECK(metric->exp_hist_positive_buckets[1] == 5); + TEST_CHECK(metric->exp_hist_zero_count == 2); + TEST_CHECK(metric->exp_hist_count == 8); + cmt_destroy(context); + + flb_cumulative_to_delta_ctx_destroy(converter); +} + TEST_LIST = { {"counter_drop_first_and_delta", test_counter_drop_first_and_delta}, {"counter_reset_drop_and_keep", test_counter_reset_drop_and_keep}, @@ -443,5 +1059,19 @@ TEST_LIST = { test_counter_initial_value_auto_uses_start_timestamp}, {"histogram_sum_decrease_without_reset", test_histogram_sum_decrease_without_reset}, {"non_monotonic_sum_is_not_converted", test_non_monotonic_sum_is_not_converted}, + {"context_attribute_order_does_not_split_series", + test_context_attribute_order_does_not_split_series}, + {"max_series_evicts_least_recently_used", + test_max_series_evicts_least_recently_used}, + {"max_staleness_evicts_series", + test_max_staleness_evicts_series}, + {"exp_histogram_scale_change_is_coarsened", + test_exp_histogram_scale_change_is_coarsened}, + {"exp_histogram_scale_change_is_coarsened_after_otlp_roundtrip", + test_exp_histogram_scale_change_is_coarsened_after_otlp_roundtrip}, + {"exp_histogram_scale_change_reset_detection", + test_exp_histogram_scale_change_reset_detection}, + {"exp_histogram_malformed_sample_is_dropped", + test_exp_histogram_malformed_sample_is_dropped}, {0} }; From 4047dce05de08da078bf15433e18ffcf59d3e34f Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Thu, 12 Mar 2026 12:35:42 -0600 Subject: [PATCH 14/14] tests: runtime: add cumulative to delta exp histogram coverage Signed-off-by: Eduardo Silva --- tests/runtime/CMakeLists.txt | 5 + tests/runtime/processor_cumulative_to_delta.c | 296 ++++++++++++++++-- 2 files changed, 270 insertions(+), 31 deletions(-) diff --git a/tests/runtime/CMakeLists.txt b/tests/runtime/CMakeLists.txt index dd76c16faee..86360d4ed49 100644 --- a/tests/runtime/CMakeLists.txt +++ b/tests/runtime/CMakeLists.txt @@ -254,6 +254,11 @@ if (FLB_PROCESSOR_CONTENT_MODIFIER) FLB_RT_TEST(FLB_PROCESSOR_CONTENT_MODIFIER "processor_content_modifier.c") endif() +if (FLB_PROCESSOR_CUMULATIVE_TO_DELTA) + FLB_RT_TEST(FLB_PROCESSOR_CUMULATIVE_TO_DELTA + "processor_cumulative_to_delta.c") +endif() + # HTTP Client Debug (requires -DFLB_HTTP_CLIENT_DEBUG=On) if(FLB_HTTP_CLIENT_DEBUG) FLB_RT_TEST(FLB_OUT_TD "http_callbacks.c") diff --git a/tests/runtime/processor_cumulative_to_delta.c b/tests/runtime/processor_cumulative_to_delta.c index 81cdcb641ca..da65c7479a2 100644 --- a/tests/runtime/processor_cumulative_to_delta.c +++ b/tests/runtime/processor_cumulative_to_delta.c @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,9 @@ #define MAX_CAPTURED_VALUES 32 #define MAX_CAPTURE_METRICS 8 +#define CAPTURE_TYPE_VALUE 0 +#define CAPTURE_TYPE_EXP_HIST_FIELD 1 + struct http_client_ctx { struct flb_upstream *upstream; struct flb_connection *connection; @@ -40,6 +44,8 @@ struct rt_ctx { struct metric_capture { const char *line_prefix; + const char *field_name; + int capture_type; double values[MAX_CAPTURED_VALUES]; int value_count; }; @@ -107,6 +113,60 @@ static int find_metric_value(const char *text, return -1; } +static int find_exp_hist_field_value(const char *text, + const char *line_prefix, + const char *field_name, + double *value) +{ + const char *field_match; + const char *line_start; + const char *line_end; + char needle[64]; + + line_start = text; + + snprintf(needle, sizeof(needle) - 1, "%s=", field_name); + + while (line_start != NULL && *line_start != '\0') { + line_end = strchr(line_start, '\n'); + if (line_end == NULL) { + line_end = line_start + strlen(line_start); + } + + if (strstr(line_start, line_prefix) != NULL) { + field_match = line_start; + + while (field_match != NULL && field_match < line_end) { + field_match = strstr(field_match, needle); + + if (field_match == NULL || field_match >= line_end) { + break; + } + + if (field_match == line_start || + (*(field_match - 1) != '_' && + (*(field_match - 1) < '0' || *(field_match - 1) > '9') && + (*(field_match - 1) < 'A' || *(field_match - 1) > 'Z') && + (*(field_match - 1) < 'a' || *(field_match - 1) > 'z'))) { + field_match += strlen(needle); + *value = strtod(field_match, NULL); + return 0; + } + + field_match++; + } + } + + if (*line_end == '\0') { + break; + } + + line_start = line_end + 1; + } + + return -1; +} + static void observation_reset(void) { pthread_mutex_lock(&state_mutex); @@ -128,6 +188,33 @@ static int observation_add_capture(const char *line_prefix) } observed.captures[index].line_prefix = line_prefix; + observed.captures[index].field_name = NULL; + observed.captures[index].capture_type = CAPTURE_TYPE_VALUE; + observed.captures[index].value_count = 0; + observed.capture_count++; + + pthread_mutex_unlock(&state_mutex); + + return index; +} + +static int observation_add_exp_hist_capture(const char *line_prefix, + const char *field_name) +{ + int index; + + pthread_mutex_lock(&state_mutex); + + index = observed.capture_count; + + if (index >= MAX_CAPTURE_METRICS) { + pthread_mutex_unlock(&state_mutex); + return -1; + } + + observed.captures[index].line_prefix = line_prefix; + observed.captures[index].field_name = field_name; + observed.captures[index].capture_type = CAPTURE_TYPE_EXP_HIST_FIELD; observed.captures[index].value_count = 0; observed.capture_count++; @@ -309,49 +396,62 @@ static int cb_capture_metrics(void *record, size_t size, void *data) (void) data; + pthread_mutex_lock(&state_mutex); + observed.callback_count++; + pthread_mutex_unlock(&state_mutex); + offset = 0; - text = NULL; - context = NULL; - ret = cmt_decode_msgpack_create(&context, (char *) record, size, &offset); - if (ret != CMT_DECODE_MSGPACK_SUCCESS) { - if (record != NULL) { - flb_free(record); + while (offset < size) { + text = NULL; + context = NULL; + + ret = cmt_decode_msgpack_create(&context, (char *) record, size, &offset); + if (ret != CMT_DECODE_MSGPACK_SUCCESS) { + if (context != NULL) { + cmt_destroy(context); + } + + return -1; } - return -1; - } + text = cmt_encode_text_create(context); - text = cmt_encode_text_create(context); + if (text != NULL) { + pthread_mutex_lock(&state_mutex); - pthread_mutex_lock(&state_mutex); + for (index = 0; index < observed.capture_count; index++) { + ret = -1; - observed.callback_count++; + if (observed.captures[index].capture_type == + CAPTURE_TYPE_EXP_HIST_FIELD) { + ret = find_exp_hist_field_value( + text, + observed.captures[index].line_prefix, + observed.captures[index].field_name, + &value); + } + else { + ret = find_metric_value(text, + observed.captures[index].line_prefix, + &value); + } - if (text != NULL) { - for (index = 0; index < observed.capture_count; index++) { - if (find_metric_value(text, - observed.captures[index].line_prefix, - &value) == 0) { - if (observed.captures[index].value_count < MAX_CAPTURED_VALUES) { - observed.captures[index].values[ - observed.captures[index].value_count] = value; - observed.captures[index].value_count++; + if (ret == 0) { + if (observed.captures[index].value_count < + MAX_CAPTURED_VALUES) { + observed.captures[index].values[ + observed.captures[index].value_count] = value; + observed.captures[index].value_count++; + } } } - } - } - - pthread_mutex_unlock(&state_mutex); - if (text != NULL) { - cmt_encode_text_destroy(text); - } - - cmt_destroy(context); + pthread_mutex_unlock(&state_mutex); + cmt_encode_text_destroy(text); + } - if (record != NULL) { - flb_free(record); + cmt_destroy(context); } return 0; @@ -464,6 +564,7 @@ static struct rt_ctx *rt_ctx_create(const char *drop_first, ret = flb_output_set(context->flb, context->output_ffd, "match", "*", + "data_mode", "chunk", NULL); if (ret != 0) { flb_destroy(context->flb); @@ -554,6 +655,66 @@ static struct cmt *create_counter_context(const char *name, return context; } +static struct cmt *create_exp_histogram_context(const char *name, + uint64_t timestamp, + int32_t scale, + uint64_t zero_count, + double zero_threshold, + int32_t positive_offset, + size_t positive_count, + uint64_t *positive_buckets, + int32_t negative_offset, + size_t negative_count, + uint64_t *negative_buckets, + int sum_set, + double sum, + uint64_t count) +{ + struct cmt *context; + struct cmt_exp_histogram *exp_histogram; + + context = cmt_create(); + if (context == NULL) { + return NULL; + } + + exp_histogram = cmt_exp_histogram_create(context, + "", + "", + (char *) name, + "help", + 0, + NULL); + if (exp_histogram == NULL) { + cmt_destroy(context); + return NULL; + } + + exp_histogram->aggregation_type = CMT_AGGREGATION_TYPE_CUMULATIVE; + + if (cmt_exp_histogram_set_default(exp_histogram, + timestamp, + scale, + zero_count, + zero_threshold, + positive_offset, + positive_count, + positive_buckets, + negative_offset, + negative_count, + negative_buckets, + sum_set, + sum, + count, + 0, + NULL) != 0) { + cmt_destroy(context); + return NULL; + } + + return context; +} + static int send_metrics_context(struct rt_ctx *context, struct cmt *metrics_context) { int ret; @@ -802,10 +963,83 @@ static void flb_test_runtime_non_monotonic_sum_passthrough(void) rt_ctx_destroy(rt); } +static void flb_test_runtime_exp_histogram_scale_change(void) +{ + int count_index; + int sum_index; + uint64_t positive_a[2]; + uint64_t positive_b[4]; + struct cmt *context; + struct rt_ctx *rt; + + positive_a[0] = 4; + positive_a[1] = 8; + + positive_b[0] = 5; + positive_b[1] = 8; + positive_b[2] = 11; + positive_b[3] = 15; + + observation_reset(); + count_index = observation_add_exp_hist_capture("rt_exp_hist", "count"); + sum_index = observation_add_exp_hist_capture("rt_exp_hist", "sum"); + TEST_CHECK(count_index >= 0); + TEST_CHECK(sum_index >= 0); + + rt = rt_ctx_create("true", "true"); + TEST_CHECK(rt != NULL); + + context = create_exp_histogram_context("rt_exp_hist", + 100, + 1, + 2, + 0.0, + 0, + 2, + positive_a, + 0, + 0, + NULL, + CMT_TRUE, + 40.0, + 10); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + flb_time_msleep(500); + TEST_CHECK(observation_get_value_count(count_index) == 0); + + context = create_exp_histogram_context("rt_exp_hist", + 200, + 2, + 3, + 0.0, + 0, + 4, + positive_b, + 0, + 0, + NULL, + CMT_TRUE, + 58.0, + 18); + TEST_CHECK(context != NULL); + TEST_CHECK(send_metrics_context(rt, context) == 0); + cmt_destroy(context); + + TEST_CHECK(wait_for_value_count(count_index, 1, 2000) == 0); + TEST_CHECK(wait_for_value_count(sum_index, 1, 2000) == 0); + TEST_CHECK(fabs(observation_get_value(count_index, 0) - 8.0) < 0.0001); + TEST_CHECK(fabs(observation_get_value(sum_index, 0) - 18.0) < 0.0001); + + rt_ctx_destroy(rt); +} + TEST_LIST = { {"counter_default_behaviors", flb_test_runtime_counter_default_behaviors}, {"counter_reset_keep_and_first_sample", flb_test_runtime_counter_reset_keep_and_first_sample}, {"multi_series", flb_test_runtime_multi_series}, {"non_monotonic_sum_passthrough", flb_test_runtime_non_monotonic_sum_passthrough}, + {"exp_histogram_scale_change", flb_test_runtime_exp_histogram_scale_change}, {NULL, NULL} };