From 8757d4b0c87cd25fe55c182aaffc818a789375d8 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 20:10:46 +0200 Subject: [PATCH] feat(introspection): add Excache.Command.Introspection.Info and render --- lib/excache/command/introspection/info.ex | 163 ++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 lib/excache/command/introspection/info.ex diff --git a/lib/excache/command/introspection/info.ex b/lib/excache/command/introspection/info.ex new file mode 100644 index 0000000..eb069be --- /dev/null +++ b/lib/excache/command/introspection/info.ex @@ -0,0 +1,163 @@ +defmodule Excache.Command.Introspection.Info do + @moduledoc false + + alias Excache.Aof + alias Excache.Clients + alias Excache.Config + alias Excache.Eviction + alias Excache.Eviction.Runner + alias Excache.Memory + alias Excache.Persistence + alias Excache.Store + + @redisCompat "7.2.0" + + @sections %{ + "server" => :server, + "clients" => :clients, + "memory" => :memory, + "persistence" => :persistence, + "stats" => :stats, + "replication" => :replication, + "keyspace" => :keyspace, + "cpu" => :cpu + } + + @default [:server, :clients, :memory, :persistence, :stats, :replication, :keyspace] + @everything @default ++ [:cpu] + + @titles %{ + server: "Server", + clients: "Clients", + memory: "Memory", + persistence: "Persistence", + stats: "Stats", + replication: "Replication", + keyspace: "Keyspace", + cpu: "Cpu" + } + + @clientSections [:server, :clients, :stats] + @keyspaceSections [:memory, :keyspace] + + def render(args), do: args |> requested() |> emit() + + defp requested([]), do: @default + + defp requested(args) do + args + |> Enum.flat_map(fn arg -> + case String.downcase(arg) do + "all" -> @everything + "everything" -> @everything + "default" -> @default + name -> @sections |> Map.get(name) |> List.wrap() + end + end) + |> Enum.uniq() + end + + defp emit([]), do: "" + + defp emit(sections) do + context = context(sections) + + sections + |> Enum.map(fn section -> block(Map.fetch!(@titles, section), lines(section, context)) end) + |> IO.iodata_to_binary() + end + + defp block(title, fields) do + [ + "# ", + title, + "\r\n", + Enum.map(fields, fn {key, value} -> [key, ":", text(value), "\r\n"] end), + "\r\n" + ] + end + + defp text(value) when is_binary(value), do: value + defp text(value) when is_integer(value), do: Integer.to_string(value) + defp text(value) when is_atom(value), do: Atom.to_string(value) + defp text(value) when is_list(value), do: List.to_string(value) + + defp context(sections) do + %{} + |> withClients(Enum.any?(sections, &(&1 in @clientSections))) + |> withKeyspace(Enum.any?(sections, &(&1 in @keyspaceSections))) + end + + defp withClients(context, false), do: context + defp withClients(context, true), do: Map.put(context, :clients, Clients.snapshot()) + + defp withKeyspace(context, false), do: context + + defp withKeyspace(context, true) do + entries = Store.dump() + now = System.system_time(:millisecond) + remaining = for {_key, _kind, _value, at} <- entries, at != nil and at > now, do: at - now + + Map.merge(context, %{ + memory: Memory.stats(entries), + volatile: length(remaining), + avgTtl: averageTtl(remaining) + }) + end + + defp averageTtl([]), do: 0 + defp averageTtl(remaining), do: div(Enum.sum(remaining), length(remaining)) + + defp lines(:server, context) do + snapshot = context.clients + uptime = div(System.system_time(:millisecond) - snapshot.startedAt, 1000) + + [ + {"excache_version", version()}, + {"redis_version", @redisCompat}, + {"redis_mode", "standalone"}, + {"os", :erlang.system_info(:system_architecture)}, + {"arch_bits", :erlang.system_info(:wordsize) * 8}, + {"process_id", :os.getpid()}, + {"run_id", snapshot.runId}, + {"tcp_port", tcpPort()}, + {"uptime_in_seconds", uptime}, + {"uptime_in_days", div(uptime, 86_400)}, + {"otp_release", :erlang.system_info(:otp_release)}, + {"erts_version", :erlang.system_info(:version)}, + {"elixir_version", System.version()} + ] + end + + defp lines(:clients, context) do + clients = context.clients.clients + + [ + {"connected_clients", length(clients)}, + {"blocked_clients", Enum.count(clients, fn client -> Clients.blocked?(client.pid) end)}, + {"pubsub_clients", + Enum.count(clients, fn client -> Clients.subscriptionCount(client.pid) > 0 end)} + ] + end + + defp lines(:memory, context) do + stats = context.memory + limit = Config.maxmemory() + + [ + {"used_memory", stats.usedMemory}, + {"used_memory_human", humanBytes(stats.usedMemory)}, + {"used_memory_dataset", stats.datasetBytes}, + {"used_memory_overhead", stats.overheadBytes}, + {"used_memory_beam", :erlang.memory(:total)}, + {"used_memory_beam_processes", :erlang.memory(:processes)}, + {"used_memory_beam_binary", :erlang.memory(:binary)}, + {"used_memory_beam_ets", :erlang.memory(:ets)}, + {"maxmemory", limit}, + {"maxmemory_human", humanBytes(limit)}, + {"maxmemory_policy", Eviction.policyName(Config.policy())}, + {"maxmemory_samples", Config.maxmemorySamples()}, + {"mem_allocator", "erts"} + ] + end +