Metastatic.Analysis.BusinessLogic.TelemetryInRecursiveFunction
(Metastatic v0.10.4)
View Source
Detects telemetry/metrics emissions inside recursive functions.
This analyzer identifies recursive functions that emit telemetry events or metrics on each iteration, causing metric spam and performance degradation.
Cross-Language Applicability
This is a universal observability anti-pattern across all languages:
- Python:
metrics.emit()in recursive function - JavaScript:
statsd.increment()in recursive function - Elixir:
:telemetry.execute()in recursive function - Go:
metrics.Inc()in recursive function - C#:
meter.RecordValue()in recursive function - Java:
meter.mark()in recursive function - Ruby:
StatsD.incrementin recursive function
Problem
Emitting telemetry inside recursive functions causes:
- Metric spam: N emissions for N iterations
- Performance degradation: Telemetry overhead per recursion
- Misleading metrics: Inflated counts
- Backend overload: Too many metric events
For recursive traversal of 10,000 nodes:
- Bad: 10,000 telemetry events
- Good: 1 telemetry event with aggregate data
Examples
Bad (Python)
def process_tree(node):
metrics.increment('tree.node') # Emitted N times!
if node.children:
for child in node.children:
process_tree(child)Good (Python)
def process_tree_wrapper(root):
count = process_tree(root, 0)
metrics.increment('tree.nodes', count)
def process_tree(node, count):
count += 1
for child in node.children:
count = process_tree(child, count)
return countBad (JavaScript)
function fibonacci(n) {
statsd.increment('fib.calls'); // Exponential spam!
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}Good (JavaScript)
function fibonacci(n) {
const start = Date.now();
const result = fib_internal(n);
statsd.timing('fib.duration', Date.now() - start);
return result;
}
function fib_internal(n) {
if (n <= 1) return n;
return fib_internal(n-1) + fib_internal(n-2);
}Bad (Elixir)
def traverse([head | tail]) do
:telemetry.execute([:app, :item], %{}) # N times!
process(head)
traverse(tail)
end
def traverse([]), do: :okGood (Elixir)
def traverse(items) do
:telemetry.span([:app, :traverse], %{count: length(items)}, fn ->
{do_traverse(items), %{}}
end)
end
defp do_traverse([head | tail]) do
process(head)
do_traverse(tail)
end
defp do_traverse([]), do: :okBad (Go)
func process(node *Node) {
metrics.Inc("nodes") // Called N times
if node.Left != nil {
process(node.Left)
}
if node.Right != nil {
process(node.Right)
}
}Good (Go)
func processTree(root *Node) {
count := processNode(root, 0)
metrics.Add("nodes", float64(count))
}
func processNode(node *Node, count int) int {
count++
if node.Left != nil {
count = processNode(node.Left, count)
}
if node.Right != nil {
count = processNode(node.Right, count)
}
return count
}Detection Strategy
- Identify recursive functions (functions that call themselves)
- Check if function body contains telemetry/metrics calls
- Flag if both conditions are met
Telemetry Function Heuristics
Function names suggesting telemetry/metrics:
*telemetry*,*metric*,*statsd**emit*,*record*,*increment*,*gauge**.execute*,*.span*,*.timing*
Solution
Wrap the recursive operation with telemetry at the top level:
- Use telemetry spans for duration
- Aggregate counts and emit once
- Move instrumentation out of recursion
Limitations
- Requires structural recursion detection (function calling itself)
- Cannot detect mutual recursion easily
- May miss indirect telemetry calls