Skip to content

Commit 08b3196

Browse files
committed
processor_tda: Add documentation for processor_tda
Signed-off-by: Hiroshi Hatake <hiroshi@chronosphere.io>
1 parent 17989cc commit 08b3196

File tree

2 files changed

+284
-0
lines changed

2 files changed

+284
-0
lines changed

pipeline/processors.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Fluent Bit offers the following processors:
2121
- [Sampling](./processors/sampling.md): Apply head or tail sampling to incoming traces.
2222
- [SQL](./processors/sql.md): Use SQL queries to extract log content.
2323
- [Filters as processors](./processors/filters.md): Use filters as processors.
24+
- [TDA](./processors/tda.md): Do Topological Data Analysis calculations.
2425

2526
## Features
2627

pipeline/processors/tda.md

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# TDA
2+
3+
The `tda` processor applies **Topological Data Analysis (TDA)** – specifically, **persistent homology** – to Fluent Bit’s metrics stream and exports **Betti numbers** that summarize the shape of recent behavior in metric space.
4+
5+
This processor is intended for detecting **phase transitions**, **regime changes**, and **intermittent instabilities** that are hard to see from individual counters, gauges, or standard statistical aggregates. It can, for example, differentiate between a single, one-off failure and an extended period of intermittent failures where the system never settles into a stable regime.
6+
7+
Currently, `tda` works only in the **metrics pipeline** (`processors.metrics`).
8+
9+
---
10+
11+
## Configuration parameters
12+
13+
The `tda_metrics` processor supports the following configuration parameters:
14+
15+
| Key | Description | Default |
16+
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
17+
| `window_size` | Number of samples to keep in the TDA sliding window. This controls how far back in time the topology is estimated. | `60` |
18+
| `min_points` | Minimum number of samples required in the window before running TDA. Until this limit is reached, no Betti metrics are emitted. | `10` |
19+
| `embed_dim` | Delay embedding dimension `m`. `m = 1` disables embedding (original behavior). For example, `m = 3` reconstructs state vectors `(x_t, x_{t-τ}, x_{t-2τ})` as suggested by Takens’ theorem. | `3` |
20+
| `embed_delay` | Delay `τ` in samples between successive lags used in delay embedding. | `1` |
21+
| `threshold` | Distance scale selector. `0` enables an automatic **multi-quantile scan** across several candidate thresholds; a value in `(0, 1)` is interpreted as a single quantile used to pick the Rips radius. | `0` |
22+
23+
All parameters are optional; defaults are suitable as a starting point for many workloads.
24+
25+
---
26+
27+
## How it works
28+
29+
### 1. Metric aggregation and normalization
30+
31+
On each metrics flush, `tda`:
32+
33+
1. **Groups metrics by `(namespace, subsystem)`**
34+
All counters, gauges, and untyped metrics are traversed. For each `cmt_map`, the pair `(ns, subsystem)` is hashed and assigned a **feature index**. This produces a fixed-dimensional feature vector of length `feature_dim` (number of `(ns, subsystem)` groups).
35+
36+
2. **Aggregates values per group**
37+
For each group, all static and labeled metrics are summed into the corresponding feature dimension.
38+
39+
3. **Converts counters to approximate rates**
40+
The processor keeps the previous raw snapshot `last_vec` and timestamp `last_ts`. For each dimension:
41+
42+
* `diff = now_raw - prev_raw`
43+
* `dt_sec = (ts_now - ts_prev) / 1e9`
44+
* `rate = diff / dt_sec`
45+
A safeguard ensures `dt_sec > 0`.
46+
47+
4. **Applies signed `log1p` normalization**
48+
To stabilize very different magnitudes and bursty traffic, each rate is mapped to
49+
`norm = log1p(|rate|)`, and the sign of `rate` is reattached. This yields a vector that is roughly scale-invariant but still sensitive to relative changes in rates across groups.
50+
51+
The resulting normalized vector is written into a **ring buffer window** (`tda_window`), implemented via a lightweight circular buffer (`lwrb`) that stores timestamped samples. The window maintains at most `window_size` samples; older samples are dropped when the buffer is full.
52+
53+
### 2. Sliding window and delay embedding
54+
55+
Let the ring buffer contain `n_raw` samples and the feature dimension be `D = feature_dim`. To capture temporal structure, `tda` supports an optional **delay embedding**:
56+
57+
* Embedding dimension: `m = embed_dim` (forced to `1` if `embed_dim <= 0`)
58+
* Lag (in integer samples): `τ = embed_delay` (ignored when `m = 1`)
59+
60+
For each valid time index `t`, a reconstructed state vector is built as:
61+
62+
$$
63+
x_t ;\to; (x_t,; x_{t-\tau},; \dots,; x_{t-(m-1)\tau})
64+
$$
65+
66+
where each `x_·` is the **D-dimensional normalized metrics vector** at that time. This yields embedded points in (\mathbb{R}^{mD}).
67+
68+
Because we need all lags to be inside the window, the number of embedded points is:
69+
70+
$$
71+
n_{\text{embed}} = n_{\text{raw}} - (m - 1)\tau
72+
$$
73+
74+
If `n_raw < (m − 1)τ + 1`, TDA is skipped until enough data has accumulated.
75+
76+
This embedding follows the idea of **Takens’ theorem**, which states that, under mild conditions, the dynamics of a system can be reconstructed from delay-embedded observations of a single time series or a low-dimensional observable [2]. In this plugin, the observable is the multi-dimensional vector of aggregated metrics.
77+
78+
Intuitively:
79+
80+
* `embed_dim = 1`: you see only the current “snapshot” geometry.
81+
* `embed_dim > 1`: you expose **loops and recurrent trajectories** in the joint evolution of metrics, which later show up as **H₁ (Betti₁) features**.
82+
83+
### 3. Distance matrix construction
84+
85+
For the embedded points $ x_i \in \mathbb{R}^{mD} $ (`i = 0..n_embed-1`), `tda` builds a **dense Euclidean distance matrix**:
86+
87+
$$
88+
d(i, j) = \left| x_i - x_j \right|_2
89+
$$
90+
91+
The implementation iterates over all pairs `(i, j)` with `i > j`, accumulates squared differences across both feature dimensions and lags, and then takes the square root; the matrix is stored symmetrically with zeros on the diagonal.
92+
93+
### 4. Threshold selection (Rips scale)
94+
95+
Persistent homology requires a **scale parameter** (Rips radius / distance threshold). The plugin supports two modes:
96+
97+
1. **Automatic multi-quantile scan** (`threshold = 0`, default)
98+
99+
* The off-diagonal distances are collected, sorted, and several quantiles are evaluated, e.g. `q ∈ {0.10, 0.20, …, 0.90}`.
100+
* For each candidate quantile `q`, a threshold `r_q` is chosen and Betti numbers are computed using Ripser.
101+
* The plugin prefers the scale where **Betti₁** (loops) is maximized; if all Betti₁ are zero, it falls back to Betti₀ as a secondary indicator.
102+
103+
2. **Fixed quantile mode** (`0 < threshold < 1`)
104+
105+
* `threshold` is interpreted as a single quantile `q`. The Rips radius is set at this quantile of all pairwise distances.
106+
* The multi-quantile scan still runs internally for robustness, but reported diagnostics (e.g., debug logs) will reflect the user-selected quantile.
107+
108+
Internally, quantile selection is handled by `tda_choose_threshold_from_dist`, which gathers all `i > j` entries of the distance matrix, sorts them, and picks the specified quantile index.
109+
110+
### 5. Persistent homology via Ripser
111+
112+
Once the compressed lower-triangular distance matrix is built, it is passed to a thin wrapper around **Ripser**, a well-known implementation of Vietoris–Rips persistent homology:
113+
114+
1. **Compression and C API**
115+
116+
* The dense `n_embed × n_embed` matrix is converted into Ripser’s `compressed_lower_distance_matrix`.
117+
* The wrapper function `flb_ripser_compute_betti_from_dense_distance` runs Ripser up to `max_dim = 2` (H₀, H₁, H₂), using coefficients in (\mathbb{Z}/2\mathbb{Z}), and accumulates persistence intervals into Betti numbers with a small persistence cutoff to ignore very short-lived noise features.
118+
119+
2. **Interval aggregation**
120+
121+
* A callback (`interval_recorder`) receives all persistence intervals ((\text{birth}, \text{death})) from Ripser.
122+
* Intervals with very small persistence are filtered out, and the remaining ones are counted per homology dimension to form Betti numbers.
123+
124+
3. **Multi-scale selection**
125+
126+
* For each candidate threshold, Betti numbers are computed.
127+
* The “best” scale is chosen as the one with the largest Betti₁ (loops); if Betti₁ is zero across scales, the plugin picks the scale where Betti₀ is largest.
128+
* The corresponding Betti₀, Betti₁, and Betti₂ values are then exported as Fluent Bit gauges.
129+
130+
### 6. Exported metrics
131+
132+
`tda` creates (lazily) three gauge metrics in the `fluentbit_tda_*` namespace:
133+
134+
| Metric name | Type | Description |
135+
| ---------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
136+
| `fluentbit_tda_betti0` | gauge | Approximate Betti₀ – number of connected components (clusters) in the embedded point cloud at the selected scale. Large values indicate fragmentation into many “micro-regimes”. |
137+
| `fluentbit_tda_betti1` | gauge | Approximate Betti₁ – number of 1-dimensional loops / cycles in the Rips complex. Non-zero values often signal **recurrent, quasi-periodic, or cycling behavior**, typical of intermittent failure / recovery patterns and other regime switches. |
138+
| `fluentbit_tda_betti2` | gauge | Approximate Betti₂ – number of 2-dimensional voids (higher-order structures). These can appear when the system explores different “surfaces” in state space, e.g., transitioning between distinct operating modes. |
139+
140+
Each metric is timestamped with the current time at the moment of TDA computation and is exported via the same metrics context it received, so downstream metric outputs can scrape or forward them like any other Fluent Bit metric.
141+
142+
---
143+
144+
## Interpreting Betti numbers
145+
146+
Topologically, Betti numbers count the number of “holes” of each dimension in a space:
147+
148+
* **Betti₀** – connected components (0-dimensional clusters).
149+
* **Betti₁** – 1-dimensional holes (loops / cycles).
150+
* **Betti₂** – 2-dimensional voids, and so on.
151+
152+
In our context:
153+
154+
* The sliding window of metrics is a **point cloud in phase space**.
155+
* The Rips complex at a given scale connects points that are close in this space.
156+
* Betti numbers summarize the topology of this complex.
157+
158+
Some practical patterns:
159+
160+
1. **Stable regime**
161+
162+
* Metrics fluctuate near a single attractor.
163+
* Betti₀ is small (often close to 1–few), Betti₁ and Betti₂ are typically `0` or very small.
164+
165+
2. **Single, one-off failure**
166+
167+
* A brief outage or spike happens once and resolves.
168+
* The embedding sees a short excursion but no sustained cycling, so Betti₁ and Betti₂ often remain near `0`.
169+
* In the provided HTTP example, a single failing chunk does not significantly raise Betti₁/₂.
170+
171+
3. **Intermittent failure / unstable regime**
172+
173+
* The system repeatedly bounces between “healthy” and “unhealthy” states (e.g., repeated `Connection refused` / `broken connection` errors interspersed with 200 responses).
174+
* The trajectory in phase space forms **loops**: metrics move away from the healthy region and then return, many times.
175+
* Betti₁ (and occasionally Betti₂) increases noticeably while this behavior persists, reflecting the emergence of non-trivial cycles in the metric dynamics.
176+
177+
In the sample output, as the HTTP output oscillates between success and various `Connection refused` / `broken connection` errors, `fluentbit_tda_betti1` and `fluentbit_tda_betti2` grow from small values to larger plateaus (e.g., Betti₁ around 10–13, Betti₂ around 1–2) while Betti₀ also increases. This is a direct signature of a **phase transition** from a stable regime to one with persistent, intermittent instability.
178+
179+
These interpretations are consistent with results from condensed matter physics and dynamical systems, where persistent homology has been used to detect phase transitions and changes in underlying order purely from data [1][2].
180+
181+
---
182+
183+
## Configuration examples
184+
185+
### Basic setup with `fluentbit_metrics`
186+
187+
The following example computes TDA on Fluent Bit’s own internal metrics, using `metrics_selector` to remove a few high-cardinality or uninteresting metrics before feeding them into `tda`:
188+
189+
```yaml
190+
service:
191+
http_server: On
192+
http_port: 2021
193+
194+
pipeline:
195+
inputs:
196+
- name: dummy
197+
tag: log.raw
198+
samples: 10000
199+
200+
- name: fluentbit_metrics
201+
tag: metrics.raw
202+
203+
processors:
204+
metrics:
205+
# Optionally exclude metrics you don't want in the TDA feature vector
206+
- name: metrics_selector
207+
metric_name: /process_start_time_seconds/
208+
action: exclude
209+
210+
- name: metrics_selector
211+
metric_name: /build_info/
212+
action: exclude
213+
214+
# Perform TDA on the remaining metrics
215+
- name: tda
216+
# window_size: 60 # optional tuning
217+
# min_points: 10
218+
# embed_dim: 3
219+
# embed_delay: 1
220+
# threshold: 0 # auto multi-quantile scan
221+
222+
outputs:
223+
- name: stdout
224+
match: '*'
225+
```
226+
227+
With this configuration, you will see time series like:
228+
229+
```text
230+
fluentbit_tda_betti0 = 39
231+
fluentbit_tda_betti1 = 7
232+
fluentbit_tda_betti2 = 0
233+
...
234+
fluentbit_tda_betti0 = 56
235+
fluentbit_tda_betti1 = 13
236+
fluentbit_tda_betti2 = 2
237+
```
238+
239+
These Betti metrics can be scraped by Prometheus, forwarded to an observability backend, and used in alerts (for example, triggering on sudden increases in `fluentbit_tda_betti1` as a signal of emerging instability in the pipeline).
240+
241+
### Emphasizing short-term cycles with delay embedding
242+
243+
To focus on shorter-term cyclic behavior—for example, oscillations in retry logic and error counters—you can lower `window_size` and adjust the embedding parameters:
244+
245+
```yaml
246+
processors:
247+
metrics:
248+
- name: tda_metrics
249+
window_size: 30 # shorter temporal horizon
250+
min_points: 15 # require at least half the window
251+
embed_dim: 4 # look at 4 successive states
252+
embed_delay: 1 # each lag = 1 metrics interval
253+
threshold: 0.2 # use 20th percentile of distances
254+
```
255+
256+
This configuration reconstructs the system in an effective dimension of `4 × feature_dim` and tends to highlight tight loops that occur within roughly 4–10 sampling intervals.
257+
258+
---
259+
260+
## When to use `tda_metrics`
261+
262+
`tda_metrics` is particularly useful when:
263+
264+
* You suspect **non-linear or multi-modal behavior** in your system (e.g., on/off regimes, congestion collapse, periodic retries).
265+
* Standard indicators (mean, percentiles, error rates) show “noise,” but you want to know whether that noise hides **coherent structure**.
266+
* You want to build alerts not just on “levels” of metrics, but on **changes in the topology** of system behavior – for example:
267+
268+
* “Raise an alert if Betti₁ remains above 5 for more than 5 minutes.”
269+
* “Mark windows where Betti₂ becomes non-zero as potential phase transitions.”
270+
271+
Because the plugin operates on an arbitrary selection of metrics (chosen upstream via `metrics_selector` or by how you configure `fluentbit_metrics`), you can tailor the TDA to focus on:
272+
273+
* Network health (latency histograms, connection failures, TLS handshake errors),
274+
* Resource saturation (CPU, memory, buffer usage),
275+
* Pipeline-level signals (retries, DLQ usage, chunk failures),
276+
* Or any other metric subset that meaningfully characterizes the state of your system.
277+
278+
---
279+
280+
## References
281+
282+
1. I. Donato, M. Gori, A. Sarti, “Persistent homology analysis of phase transitions,” *Physical Review E*, 93, 052138, 2016.
283+
2. F. Takens, “Detecting strange attractors in turbulence,” in D. Rand and L.-S. Young (eds.), *Dynamical Systems and Turbulence*, Lecture Notes in Mathematics, vol. 898, Springer, 1981, pp. 366–381.

0 commit comments

Comments
 (0)