Packages
metastatic
0.8.6
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/analysis/business_logic/telemetry_in_recursive_function.ex
defmodule Metastatic.Analysis.BusinessLogic.TelemetryInRecursiveFunction do
@moduledoc """
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
"""
@behaviour Metastatic.Analysis.Analyzer
alias Metastatic.Analysis.Analyzer
# Common telemetry/metrics function keywords
@telemetry_keywords [
# Generic
:telemetry,
:metrics,
:statsd,
:emit,
:record,
:increment,
:gauge,
:timing,
:histogram,
:counter,
# Elixir-specific
:execute,
:span,
# Prometheus/OpenTelemetry
:observe,
:add,
:inc,
:mark
]
@impl true
def info do
%{
name: :telemetry_in_recursive_function,
category: :performance,
description: "Detects telemetry emissions inside recursive functions",
severity: :warning,
explanation: """
Emitting telemetry or metrics inside recursive functions causes metric
spam and performance issues. The telemetry overhead is multiplied by
the recursion depth.
Instead:
- Wrap the entire recursive operation with telemetry at top level
- Aggregate metrics and emit once
- Use telemetry spans for duration measurement
- Move instrumentation out of the recursive path
This applies to all telemetry/metrics systems across all languages.
""",
configurable: false
}
end
@impl true
def run_before(context) do
# Initialize tracking for functions we're analyzing
{:ok, Map.put(context, :function_stack, [])}
end
@impl true
# New 3-tuple format: {:function_def, [name: name, params: [...], ...], body_list}
# Body is the entire children list (function body statements)
def analyze({:function_def, meta, children} = node, _context) when is_list(meta) do
name = Keyword.get(meta, :name)
if name != nil do
# Check against all children (the function body statements)
check_recursive_telemetry(name, children, node)
else
[]
end
end
# Function definition without location metadata (6-tuple)
def analyze({:function_def, name, _params, _return_type, _opts, body} = node, _context)
when is_atom(name) or is_binary(name) do
check_recursive_telemetry(name, body, node)
end
# Function definition with location metadata (7-tuple)
def analyze({:function_def, name, _params, _return_type, _opts, body, _loc} = node, _context)
when is_atom(name) or is_binary(name) do
check_recursive_telemetry(name, body, node)
end
def analyze(_node, _context), do: []
# Check if function is recursive and contains telemetry
defp check_recursive_telemetry(name, body, node) do
if recursive?(name, body) and contains_telemetry?(body) do
[
Analyzer.issue(
analyzer: __MODULE__,
category: :performance,
severity: :warning,
message:
"Telemetry emitted in recursive function '#{name}' - wrap entire operation instead",
node: node,
metadata: %{
function_name: name,
suggestion:
"Move telemetry outside recursion, use span at top level, or aggregate metrics"
}
)
]
else
[]
end
end
# ----- Private Helpers -----
# Check if function calls itself (direct recursion)
defp recursive?(func_name, body) do
contains_call_to?(body, func_name)
end
# Check if AST contains call to specific function
# New 3-tuple format: {:block, meta, statements}
defp contains_call_to?({:block, _meta, statements}, target_name) when is_list(statements) do
Enum.any?(statements, &contains_call_to?(&1, target_name))
end
defp contains_call_to?({:block, statements}, target_name) when is_list(statements) do
Enum.any?(statements, &contains_call_to?(&1, target_name))
end
# New 3-tuple function_call: {:function_call, [name: name], args}
defp contains_call_to?({:function_call, meta, _args}, target_name) when is_list(meta) do
name = Keyword.get(meta, :name, "")
normalize_name(name) == normalize_name(target_name)
end
defp contains_call_to?({:function_call, name, _args}, target_name) do
normalize_name(name) == normalize_name(target_name)
end
# New 3-tuple: {:conditional, meta, [cond, then, else]}
defp contains_call_to?({:conditional, _meta, [_cond, then_branch, else_branch]}, target_name) do
contains_call_to?(then_branch, target_name) or
contains_call_to?(else_branch || {:block, [], []}, target_name)
end
defp contains_call_to?({:conditional, _cond, then_branch, else_branch}, target_name) do
contains_call_to?(then_branch, target_name) or
contains_call_to?(else_branch || {:block, []}, target_name)
end
defp contains_call_to?(tuple, target_name) when is_tuple(tuple) do
tuple
|> Tuple.to_list()
|> Enum.any?(&contains_call_to?(&1, target_name))
end
defp contains_call_to?(list, target_name) when is_list(list) do
Enum.any?(list, &contains_call_to?(&1, target_name))
end
defp contains_call_to?(_, _), do: false
# Check if body contains telemetry calls
# New 3-tuple format: {:block, meta, statements}
defp contains_telemetry?({:block, _meta, statements}) when is_list(statements) do
Enum.any?(statements, &contains_telemetry?/1)
end
defp contains_telemetry?({:block, statements}) when is_list(statements) do
Enum.any?(statements, &contains_telemetry?/1)
end
# New 3-tuple function_call: {:function_call, [name: name], args}
defp contains_telemetry?({:function_call, meta, _args}) when is_list(meta) do
func_name = Keyword.get(meta, :name, "")
telemetry_function?(func_name)
end
defp contains_telemetry?({:function_call, func_name, _args}) do
telemetry_function?(func_name)
end
# New 3-tuple: {:attribute_access, meta, [obj, method]}
defp contains_telemetry?({:attribute_access, _meta, [_obj, method]}) when is_atom(method) do
telemetry_function?(method)
end
defp contains_telemetry?({:attribute_access, _obj, method}) when is_atom(method) do
telemetry_function?(method)
end
# New 3-tuple: {:conditional, meta, [cond, then, else]}
defp contains_telemetry?({:conditional, _meta, [_cond, then_branch, else_branch]}) do
contains_telemetry?(then_branch) or contains_telemetry?(else_branch || {:block, [], []})
end
defp contains_telemetry?({:conditional, _cond, then_branch, else_branch}) do
contains_telemetry?(then_branch) or contains_telemetry?(else_branch || {:block, []})
end
defp contains_telemetry?(tuple) when is_tuple(tuple) do
tuple
|> Tuple.to_list()
|> Enum.any?(&contains_telemetry?/1)
end
defp contains_telemetry?(list) when is_list(list) do
Enum.any?(list, &contains_telemetry?/1)
end
defp contains_telemetry?(_), do: false
# Check if function name suggests telemetry/metrics
defp telemetry_function?(func_name) when is_atom(func_name) do
# Direct match
if func_name in @telemetry_keywords do
true
else
# Pattern match
func_str = Atom.to_string(func_name) |> String.downcase()
check_telemetry_patterns(func_str)
end
end
defp telemetry_function?(func_name) when is_binary(func_name) do
# String function names (e.g. "telemetry.execute")
func_str = String.downcase(func_name)
check_telemetry_patterns(func_str)
end
defp telemetry_function?(_), do: false
defp check_telemetry_patterns(func_str) do
Enum.any?(
[
"telemetry",
"metric",
"statsd",
"emit",
"record",
"increment",
"gauge",
"counter",
"timing",
"observe"
],
&String.contains?(func_str, &1)
)
end
# Normalize function names for comparison (atom or string)
defp normalize_name(name) when is_atom(name), do: Atom.to_string(name)
defp normalize_name(name) when is_binary(name), do: name
defp normalize_name(_), do: ""
end