Powered by AppSignal & Oban Pro

Monitoreo distribuido en tiempo real con Elixir: Control de una constelación de satélites

monitoreo distribuídos.livemd

Monitoreo distribuido en tiempo real con Elixir: Control de una constelación de satélites

Mix.install([
  {:kino, "~> 0.12"},
  {:vega_lite, "~> 0.1.11"},
  {:kino_vega_lite, "~> 0.1.8"}
])

Concepto

Vamos a simular una red de satélites orbitando la Tierra, donde cada satélite es un proceso que transmite métricas (temperatura del núcleo, señal, posición orbital, batería…). El sistema podrá:

Visualizar cada satélite en un mapa 2D del globo con coordenadas.

Mostrar su estado vital (con color verde/amarillo/rojo según anomalías).

Supervisar y reiniciar satélites caídos (simulación de fallos).

Producir logs y alertas visuales de comportamiento anómalo.

Incluir una vista tipo “sala de control” para ver todo en tiempo real.

🛰️ Monitoreo distribuido en tiempo real con Elixir

¿Qué veremos?
Simularemos una constelación de satélites orbitando y enviando telemetría en tiempo real. Cada satélite será un proceso concurrente, y visualizaremos en Livebook su estado y posición.

Este proyecto demostrará el poder de Elixir para sistemas de monitoreo distribuidos, altamente concurrentes, con una interfaz visual e interactiva.

Monitor central

Recibe todos los datos y actualiza un Kino.Frame con la telemetría en tiempo real.

defmodule Monitor do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{data: []}, name: __MODULE__)
  end

  def receive_data(id, data) do
    GenServer.cast(__MODULE__, {:nuevo_dato, id, data})
  end

  def init(state) do
    {:ok, Map.put(state, :frame, Kino.Frame.new())}
  end

  def handle_cast({:nuevo_dato, id, data}, state) do
    new_data = [%{id: id, data: data} | state.data] |> Enum.take(50)

    markdown =
      new_data
      |> Enum.map(fn %{id: id, data: data} ->
        "- **#{id}** - Pos: (#{elem(data.position, 0)}, #{elem(data.position, 1)}), Temp: #{data.temperature}°C, ⚡ #{data.battery}%"
      end)
      |> Enum.join("\n")

    Kino.Frame.clear(state.frame)
    Kino.Frame.append(state.frame, Kino.Markdown.new("""
    ### 📡 Telemetría de satélites (últimos datos):
    #{markdown}
    """))

    {:noreply, %{state | data: new_data}}
  end
end

{:ok, monitor_pid} = Monitor.start_link(nil)

monitor_pid |> :sys.get_state() |> Map.get(:frame)

Representación de la constelación

Vamos a modelar 10 satélites como procesos GenServer que mandan datos periódicamente. Estos datos se centralizan en un proceso de monitoreo que los agrupa y visualiza.

defmodule Satelite do
  use GenServer

  def start_link(id) do
    GenServer.start_link(__MODULE__, %{id: id}, name: via(id))
  end

  defp via(id), do: {:via, Registry, {:satelites, id}}

  def init(state) do
    send(self(), :emitir)
    {:ok, Map.put(state, :data, [])}
  end

  def handle_info(:emitir, state) do
    data = %{
      timestamp: DateTime.utc_now(),
      position: {Enum.random(0..100), Enum.random(0..100)},
      temperature: :rand.uniform(50) + 10,
      battery: Enum.random(30..100)
    }

    Monitor.receive_data(state.id, data)
    Process.send_after(self(), :emitir, 1000)
    {:noreply, %{state | data: [data | state.data] |> Enum.take(10)}}
  end

  def handle_call(:get_data, _from, state) do
    {:reply, state.data, state}
  end

end

Iniciar la constelación

{:ok, _} = Registry.start_link(keys: :unique, name: :satelites)

for id <- 1..10 do
  Satelite.start_link("SAT-#{id}")
end

Visualización gráfica de posiciones

Mostrar las posiciones en un gráfico VegaLite animado y real.

graph_data = Kino.DataTable.new([])

frame_posiciones = Kino.Frame.new()
Kino.render(frame_posiciones)

posicion_fn = fn ->
  data =
    :satelites
    |> Registry.select([{{:"$1", :_, :_}, [], [:"$1"]}])
    |> Enum.map(fn id ->
      [last | _] = GenServer.call({:via, Registry, {:satelites, id}}, :get_data)
      %{id: id, x: elem(last.position, 0), y: elem(last.position, 1)}
    end)

  vl =
    VegaLite.new()
    |> VegaLite.data_from_values(data)
    |> VegaLite.encode_field(:x, "x")
    |> VegaLite.encode_field(:y, "y")
    |> VegaLite.encode_field(:text, "id")
    |> VegaLite.mark(:circle)
    |> then(fn vl ->                # Aumentar tamaño del gráfico
    %{vl | spec: Map.merge(vl.spec, %{"width" => 1000, "height" => 500})}
  end)

  Kino.Frame.clear(frame_posiciones)
  Kino.Frame.append(frame_posiciones, Kino.VegaLite.new(vl))
end

:timer.send_interval(2000, self(), :update_graph)

receive do
  :update_graph -> posicion_fn.()
end

Interacción: inspeccionar un satélite

Permitir elegir un satélite y ver sus últimos datos.

opciones = Enum.map(1..10, &amp;{"SAT-#{&amp;1}", "Satélite #{&amp;1}"})
selector = Kino.Input.select("Selecciona un satélite", opciones)

Kino.render(selector)

detalle = Kino.Frame.new()
Kino.render(detalle)

Kino.listen(selector, fn %{value: id} ->
  IO.puts("🔘 Botón presionado")

  #id = Kino.Input.get(selector)
  IO.puts("Click #{id}")

  case GenServer.call({:via, Registry, {:satelites, id}}, :get_data) do
     datos  ->
      resumen =
        datos
        |> Enum.map(fn d ->
          "- #{d.timestamp} → Temp: #{d.temperature}°C, ⚡ #{d.battery}%, Pos: #{inspect(d.position)}"
        end)
        |> Enum.join("\n")

      #IO.puts("Click #{resumen}")

      Kino.Frame.clear(detalle)

      Kino.Frame.append(detalle, Kino.Markdown.new("""
      ### 📄 Historial de `#{id}`

      #{resumen}
      """))

    _ ->
      Kino.Frame.clear(detalle)
      Kino.Frame.append(detalle, Kino.Markdown.new("❌ No se encontró el satélite `#{id}`."))
  end
end)

✅ Conclusión

Hemos creado una simulación distribuida en Elixir que:

  • Lanza múltiples procesos representando satélites.
  • Cada uno reporta su estado periódicamente.
  • Un monitor central coordina y visualiza sus datos.
  • Livebook sirve como panel visual interactivo.

🚀 ¡Este es solo el comienzo! Podrías conectar sensores reales, simular fallos, o exportar la telemetría como métricas Prometheus.