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.increment in 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 count

Bad (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: :ok

Good (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: :ok

Bad (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

  1. Identify recursive functions (functions that call themselves)
  2. Check if function body contains telemetry/metrics calls
  3. 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