Packages
ex_esdb
0.10.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14-alpha
0.0.13-alpha
0.0.12-alpha
0.0.11-alpha
0.0.10-alpha
0.0.9-alpha
0.0.8-alpha
0.0.6-alpha
0.0.5-alpha
0.0.4-alpha
0.0.3-alpha
0.0.2-alfa
0.0.1-alfa
ExESDB is a reincarnation of rabbitmq/khepri, specialized for use as a BEAM-native event store.
Current section
Files
Jump to
Current section
Files
lib/ex_esdb/debugger.ex
defmodule ExESDB.Debugger do
@moduledoc """
Comprehensive debugging and inspection API for ExESDB systems.
This module provides REPL-friendly functions to investigate all aspects
of an ExESDB Event Sourcing Database by delegating to specialized subsystems:
- Process supervision tree inspection (Inspector)
- Health and performance monitoring (Monitor)
- Scenario testing and simulation (ScenarioManager)
- Real-time observation (Observer)
## Usage in REPL
iex> ExESDB.Debugger.overview()
iex> ExESDB.Debugger.start_scenario(:high_load, intensity: :medium)
iex> ExESDB.Debugger.observe(:system_metrics)
"""
require Logger
# ===================
# System Overview
# ===================
@doc """
Display a comprehensive overview of the ExESDB system.
"""
def overview(store_id \\ nil) do
IO.puts("\nð ExESDB System Overview")
IO.puts("=" <> String.duplicate("=", 40))
# Get data from ProcessInspector
case ExESDB.Inspection.ProcessInspector.list_processes() do
processes when is_list(processes) ->
alive_count = Enum.count(processes, fn {_, pid, _} -> Process.alive?(pid) end)
total_memory = processes |> Enum.map(fn {_, _, info} -> info[:memory] || 0 end) |> Enum.sum()
IO.puts("âïļ Processes: #{alive_count}/#{length(processes)} alive")
IO.puts("ðū Memory: #{format_memory(total_memory)}")
_ ->
IO.puts("â Unable to retrieve process information")
end
# Get health status from HealthChecker
case ExESDB.Monitoring.HealthChecker.perform_health_check(store_id) do
{:ok, %{status: status}} ->
icon = case status do
:healthy -> "â
"
:degraded -> "â ïļ"
:unhealthy -> "â"
end
IO.puts("#{icon} Health: #{status}")
_ ->
IO.puts("â Unable to retrieve health status")
end
IO.puts("\nðĄ Use ExESDB.Debugger.help() for available commands")
end
@doc """
Show help information for all available debugging commands.
"""
def help do
IO.puts("\nð ExESDB Debugger Help")
IO.puts("=" <> String.duplicate("=", 50))
sections = [
{"ð System Inspection", [
{"overview(store_id \\\\ nil)", "Complete system overview"},
{"processes(store_id \\\\ nil)", "List all ExESDB processes"},
{"supervision_tree(store_id \\\\ nil)", "Display supervision tree"},
{"config(store_id \\\\ nil)", "Show configuration details"},
]},
{"ðĨ Health & Performance", [
{"health(store_id \\\\ nil)", "Comprehensive health check"},
{"performance(store_id \\\\ nil)", "Performance metrics"},
]},
{"ðŊ Scenario Testing", [
{"start_scenario(name, opts \\\\ [])", "Start test scenario"},
{"stop_scenario(scenario_id)", "Stop running scenario"},
{"list_scenarios()", "List active scenarios"},
]},
{"ðïļ Real-time Observation", [
{"observe(target, opts \\\\ [])", "Start real-time observation"},
{"stop_observation(obs_id)", "Stop observation"},
{"list_observations()", "List active observations"},
]},
{"ð§ Advanced Tools", [
{"trace(module, function, opts \\\\ [])", "Trace function calls"},
{"benchmark(fun, opts \\\\ [])", "Benchmark a function"},
{"top(opts \\\\ [])", "Show top processes by memory/CPU"},
]}
]
Enum.each(sections, fn {section_title, commands} ->
IO.puts("\n#{section_title}")
Enum.each(commands, fn {cmd, desc} ->
IO.puts(" #{String.pad_trailing(cmd, 35)} - #{desc}")
end)
end)
IO.puts("\nð Examples:")
IO.puts(" ExESDB.Debugger.start_scenario(:high_load, intensity: :medium, duration: 30_000)")
IO.puts(" ExESDB.Debugger.observe(:system_metrics, interval: 2000)")
IO.puts(" ExESDB.Debugger.trace(ExESDB.StreamsWriter, :append_events)")
end
# ===================
# Process Inspection
# ===================
@doc """
List all ExESDB-related processes with formatted output.
"""
def processes(_store_id \\ nil) do
IO.puts("\nâïļ ExESDB Processes")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Inspection.ProcessInspector.list_processes() do
%{processes: processes, total_memory: memory, alive_count: alive} ->
if Enum.empty?(processes) do
IO.puts("ð No ExESDB processes found")
else
Enum.each(processes, fn {type, procs} ->
IO.puts("\nðĶ #{type} (#{length(procs)} processes)")
Enum.each(procs, fn proc ->
status = if proc.alive, do: "â
", else: "â"
memory_str = format_memory(proc.memory)
IO.puts(" #{status} #{String.pad_trailing(to_string(proc.name), 30)} #{inspect(proc.pid)} #{memory_str}")
end)
end)
IO.puts("\nð Total: #{alive} alive, #{format_memory(memory)} memory")
end
{:error, reason} ->
IO.puts("â Error: #{inspect(reason)}")
end
end
@doc """
Display the supervision tree for ExESDB.
"""
def supervision_tree(store_id \\ nil) do
IO.puts("\nðģ ExESDB Supervision Tree")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Inspection.TreeInspector.view_supervision_tree(store_id) do
{:ok, tree} ->
display_tree(tree, 0)
{:error, reason} ->
IO.puts("â #{reason}")
end
end
@doc """
Show detailed configuration for the store.
"""
def config(store_id \\ nil) do
IO.puts("\nâïļ ExESDB Configuration")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Inspection.ConfigInspector.get_config(store_id) do
{:ok, config} when is_map(config) ->
Enum.each(config, fn {key, value} ->
if key != :topologies do
IO.puts(" #{String.pad_trailing(to_string(key), 20)} : #{inspect(value)}")
end
end)
IO.puts("\nð LibCluster Topologies:")
case Map.get(config, :topologies) do
nil -> IO.puts(" None configured")
topologies when is_list(topologies) ->
Enum.each(topologies, fn {name, topo_config} ->
strategy = topo_config[:strategy] || "unknown"
IO.puts(" #{name}: #{strategy}")
end)
_ -> IO.puts(" Invalid topology configuration")
end
{:error, reason} ->
IO.puts("â Error: #{inspect(reason)}")
end
end
# ===================
# Health & Performance
# ===================
@doc """
Perform comprehensive health check.
"""
def health(store_id \\ nil) do
IO.puts("\nðĨ ExESDB Health Check")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Monitoring.HealthChecker.perform_health_check(store_id) do
%{status: status, details: details} ->
icon = if status == "Healthy", do: "â
", else: "â ïļ"
IO.puts("#{icon} Overall Status: #{status}")
IO.puts(" Details: #{details}")
{:error, reason} ->
IO.puts("â Health check failed: #{inspect(reason)}")
end
end
@doc """
Show performance metrics.
"""
def performance(_store_id \\ nil) do
IO.puts("\nð Performance Metrics")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Observation.MetricsCollector.collect_system_metrics() do
metrics when is_map(metrics) ->
Enum.each(metrics, fn {key, value} ->
IO.puts(" #{String.pad_trailing(to_string(key), 15)} : #{value}")
end)
{:error, reason} ->
IO.puts("â Performance metrics unavailable: #{inspect(reason)}")
end
end
# ===================
# Scenario Management
# ===================
@doc """
Start a test scenario.
Available scenarios:
- `:high_load` - Simulate high CPU/memory load
- `:node_failure` - Simulate node failures
- `:custom` - Load custom scenario from config
## Examples
ExESDB.Debugger.start_scenario(:high_load, intensity: :medium, duration: 30_000)
ExESDB.Debugger.start_scenario(:custom, config_path: "scenarios/custom.json")
"""
def start_scenario(scenario_name, opts \\ []) do
case ExESDB.Debugger.ScenarioManager.start_scenario(scenario_name, opts) do
{:ok, scenario_id} ->
IO.puts("â
Started scenario #{inspect(scenario_name)} with ID: #{scenario_id}")
{:ok, scenario_id}
{:error, reason} ->
IO.puts("â Failed to start scenario: #{reason}")
{:error, reason}
end
end
@doc """
Stop a running scenario.
"""
def stop_scenario(scenario_id) do
case ExESDB.Debugger.ScenarioManager.stop_scenario(scenario_id) do
:ok ->
IO.puts("â
Stopped scenario: #{scenario_id}")
:ok
{:error, reason} ->
IO.puts("â Failed to stop scenario: #{inspect(reason)}")
{:error, reason}
end
end
@doc """
List all running scenarios.
"""
def list_scenarios do
IO.puts("\nðŊ Active Scenarios")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Debugger.ScenarioManager.list_scenarios() do
[] ->
IO.puts("ð No active scenarios")
scenarios ->
Enum.each(scenarios, fn scenario ->
uptime = System.system_time(:millisecond) - scenario.started_at
IO.puts("ðŊ #{scenario.id}")
IO.puts(" Name: #{scenario.name}")
IO.puts(" Status: #{scenario.status}")
IO.puts(" Uptime: #{format_uptime(uptime)}")
end)
end
end
# ===================
# Real-time Observation
# ===================
@doc """
Start real-time observation of system components.
Available targets:
- `:system_metrics` - Overall system metrics
- `:process_metrics` - Specific process metrics (requires :target_process opt)
- `:memory_usage` - Memory usage patterns
## Examples
ExESDB.Debugger.observe(:system_metrics, interval: 1000)
ExESDB.Debugger.observe(:process_metrics, target_process: :my_process, interval: 2000)
"""
def observe(target, opts \\ []) do
case ExESDB.Debugger.Observer.start_observation(target, opts) do
{:ok, observation_id} ->
IO.puts("ðïļ Started observing #{inspect(target)} with ID: #{observation_id}")
{:ok, observation_id}
{:error, reason} ->
IO.puts("â Failed to start observation: #{reason}")
{:error, reason}
end
end
@doc """
Stop a running observation.
"""
def stop_observation(observation_id) do
case ExESDB.Debugger.Observer.stop_observation(observation_id) do
:ok ->
IO.puts("â
Stopped observation: #{observation_id}")
:ok
{:error, reason} ->
IO.puts("â Failed to stop observation: #{inspect(reason)}")
{:error, reason}
end
end
@doc """
List all active observations.
"""
def list_observations do
IO.puts("\nðïļ Active Observations")
IO.puts("=" <> String.duplicate("=", 40))
case ExESDB.Debugger.Observer.list_observations() do
[] ->
IO.puts("ð No active observations")
observations ->
Enum.each(observations, fn obs ->
uptime = System.system_time(:millisecond) - obs.started_at
IO.puts("ðïļ #{obs.id}")
IO.puts(" Target: #{obs.target}")
IO.puts(" Data Points: #{obs.data_points}")
IO.puts(" Uptime: #{format_uptime(uptime)}")
end)
end
end
@doc """
Get observation data for analysis.
"""
def observation_data(observation_id) do
case ExESDB.Debugger.Observer.get_observation_data(observation_id) do
{:ok, data} -> data
{:error, reason} ->
IO.puts("â Failed to get observation data: #{inspect(reason)}")
{:error, reason}
end
end
# ===================
# Advanced Tools
# ===================
@doc """
Trace function calls for debugging.
"""
def trace(module, function, opts \\ []) do
duration = Keyword.get(opts, :duration, 5000)
match_spec = Keyword.get(opts, :match_spec, :_)
IO.puts("ð Tracing #{module}.#{function} for #{duration}ms...")
# Start tracing
:dbg.start()
:dbg.tracer()
:dbg.p(:all, :c)
:dbg.tpl(module, function, match_spec)
# Wait for specified duration
Process.sleep(duration)
# Stop tracing
:dbg.stop()
IO.puts("â
Tracing complete")
end
@doc """
Benchmark a function.
"""
def benchmark(fun, opts \\ []) when is_function(fun) do
times = Keyword.get(opts, :times, 1000)
IO.puts("âąïļ Benchmarking function #{times} times...")
{total_time, _} = :timer.tc(fn ->
Enum.each(1..times, fn _ -> fun.() end)
end)
avg_time = total_time / times
IO.puts("ð Results:")
IO.puts(" Total time: #{format_time(total_time)}")
IO.puts(" Average time: #{format_time(avg_time)}")
IO.puts(" Calls per second: #{Float.round(1_000_000 / avg_time, 2)}")
end
@doc """
Show top processes by memory/CPU usage.
"""
def top(opts \\ []) do
limit = Keyword.get(opts, :limit, 10)
sort_by = Keyword.get(opts, :sort_by, :memory)
IO.puts("\nð Top #{limit} Processes by #{sort_by}")
IO.puts("=" <> String.duplicate("=", 60))
processes = :recon.proc_count(sort_by, limit)
IO.puts("#{String.pad_trailing("PID", 15)} #{String.pad_trailing("Name/Function", 30)} #{String.pad_trailing("Value", 15)}")
IO.puts(String.duplicate("-", 60))
Enum.each(processes, fn {pid, value, [name | _]} ->
name_str = case name do
atom when is_atom(atom) -> to_string(atom)
other -> inspect(other)
end
formatted_value = case sort_by do
:memory -> format_memory(value)
_ -> to_string(value)
end
IO.puts("#{String.pad_trailing(inspect(pid), 15)} #{String.pad_trailing(name_str, 30)} #{formatted_value}")
end)
end
# ===================
# Helper Functions
# ===================
defp display_tree(tree, depth) do
indent = String.duplicate(" ", depth)
status = if tree.alive, do: "â
", else: "â"
IO.puts("#{indent}#{status} #{tree.name} (#{inspect(tree.pid)})")
if tree.children do
Enum.each(tree.children, fn child ->
if child.children do
display_tree(child, depth + 1)
else
child_indent = String.duplicate(" ", depth + 1)
child_status = if child.alive, do: "â
", else: "â"
type_icon = case child.type do
:supervisor -> "ð"
:worker -> "âïļ"
_ -> "â"
end
IO.puts("#{child_indent}#{child_status} #{type_icon} #{child.id} (#{inspect(child.pid)})")
end
end)
end
end
defp format_memory(bytes) when is_integer(bytes) do
cond do
bytes >= 1_073_741_824 -> "#{Float.round(bytes / 1_073_741_824, 1)}GB"
bytes >= 1_048_576 -> "#{Float.round(bytes / 1_048_576, 1)}MB"
bytes >= 1024 -> "#{Float.round(bytes / 1024, 1)}KB"
true -> "#{bytes}B"
end
end
defp format_memory(_), do: "0B"
defp format_time(microseconds) when is_number(microseconds) do
cond do
microseconds >= 1_000_000 -> "#{Float.round(microseconds / 1_000_000, 3)}s"
microseconds >= 1_000 -> "#{Float.round(microseconds / 1_000, 3)}ms"
true -> "#{Float.round(microseconds, 3)}Ξs"
end
end
defp format_uptime(milliseconds) when is_number(milliseconds) do
seconds = div(milliseconds, 1000)
minutes = div(seconds, 60)
hours = div(minutes, 60)
days = div(hours, 24)
cond do
days > 0 -> "#{days}d #{rem(hours, 24)}h"
hours > 0 -> "#{hours}h #{rem(minutes, 60)}m"
minutes > 0 -> "#{minutes}m #{rem(seconds, 60)}s"
true -> "#{seconds}s"
end
end
end