Toda la documentación

Empezar

  1. 01Primeros pasos
  2. 02Instalación
  3. 03Seguimiento

Funciones

  1. 04El panel
  2. 05Objetivos
  3. 06Enlaces cortos
  4. 07Fuentes y agentes de IA
  5. 08Informes por email
  6. 09Comprobación programada
  7. 10Pregunta a tu IA

Referencia

  1. 11Configuración
  2. 12API HTTP

Plataformas

  1. 13Servidor independiente
  2. 14PHP
  3. 15WordPress
  4. 16Drupal
  5. 17Craft CMS
  6. 18Python
  7. 19Rails
  8. 20Ruby
  9. 21Go
  10. 22Java
  11. 23.NET
  12. 24Elixir
  13. 25Rust

Confianza

  1. 26Privacidad

Plataformas

Elixir

Runlight funciona con Elixir 1.18 o posterior sobre Erlang/OTP 27 o posterior, dentro de una aplicación Phoenix o Plug, con sus tablas en tu propio repo de Ecto.

El paquete de Elixir es Runlight escrito de nuevo en Elixir. Sirve el mismo panel y la misma API, y da a cada petición la misma respuesta que da la biblioteca de TypeScript. Sus cifras van en las mismas tablas, así que cualquiera de los dos puede leer una base de datos que haya escrito el otro. Necesita Elixir 1.18 o posterior sobre Erlang/OTP 27 o posterior.

Instalación

Añade el paquete a mix.exs, con el adaptador de Ecto para tu base de datos si tu aplicación aún no tiene ninguno.

mix.exs

def deps do
  [
    {:runlight, "~> 0.1"},
    {:ecto_sqlite3, "~> 0.17"}
  ]
end

Runlight guarda sus tablas en el propio repo de Ecto de tu aplicación, a través de ecto_sqlite3, postgrex o myxql. Su panel es un Plug, algo que toda aplicación Phoenix ya tiene. Las zonas horarias salen del paquete tz, que va compilado dentro, así que un release funciona en cualquier imagen.

Arranca la instancia

Runlight es un hijo del árbol de supervisión de tu aplicación, que arranca después de tu repo.

lib/my_app/application.ex

children = [
  MyApp.Repo,
  {Runlight,
   store: {Runlight.Store, repo: MyApp.Repo},
   site: [name: "example.com", hostnames: ["example.com"], timezone: "Europe/London"],
   check_every: :timer.hours(1)}
]

Las opciones tienen los mismos nombres que en Configuración, escritos en snake case, así que sites, trust_proxy, rate_limit, mail y managed_sites funcionan como allí. Las tablas se crean cuando arranca la instancia, y un almacén inalcanzable impide que la aplicación arranque. check_every ejecuta la comprobación programada a intervalos, que rota las sales diarias, envía los informes por email pendientes, aplica cuánto tiempo conserva cada sitio sus visitas y construye los resúmenes que mantienen rápidos los rangos largos. Omítelo cuando un programador de tareas llame a /runlight/api/check con CRON_SECRET, como muestra Comprobación programada, o llama a Runlight.check(Runlight.instance()) desde un job propio.

Almacenes

El almacén es tu repo, y su adaptador decide el SQL que escribe Runlight. Todas las tablas de Runlight empiezan por rl_, así que conviven con las tuyas. prefix: "wp_rl_", como en {Runlight.Store, repo: MyApp.Repo, prefix: "wp_rl_"}, usa otro prefijo en su lugar, así que dos aplicaciones que comparten una base de datos mantienen cada una sus propias tablas. Un prefijo tiene como máximo 32 letras, dígitos y guiones bajos, y no empieza por un dígito, y Runlight.Store.tables(store) lista las tablas del almacén por nombre, para una aplicación que quiera quitar Runlight.

En SQLite, mantén el journal write-ahead de ecto_sqlite3 y dale al repo un busy_timeout de al menos 5000. Una base de datos en memoria es una por conexión, así que un repo con database: ":memory:" necesita pool_size: 1.

Postgres funciona tal cual. Runlight crea sus índices de forma concurrente, así que una tabla grande sigue aceptando escrituras mientras se ejecuta una actualización.

MySQL necesita la versión 8.0.20 o posterior, y MariaDB la 11.4 o posterior, ya que las tablas usan la collation binaria utf8mb4_0900_bin para que el texto se ordene igual en todas las bases de datos.

Phoenix

Reenvía /runlight al Plug fuera de la pipeline :browser. Esa pipeline pide un token CSRF en cada POST, que el tracker no puede enviar, y Runlight comprueba las peticiones a su manera.

lib/my_app_web/router.ex

scope "/" do
  forward "/runlight", Runlight.Plug, token: System.get_env("RUNLIGHT_TOKEN", "")
  forward "/go", Runlight.Plug.Links
end

El token se lee una sola vez, al compilar el router. En un release, omite la opción y define RUNLIGHT_TOKEN donde se ejecuta la aplicación, ya que Runlight lo lee en la primera petición. Un token vacío mantiene cerrado el panel, y token: false lo deja abierto para una aplicación que comprueba ella misma a las personas.

Añade el script a cada página, justo antes de </head>.

html

<script defer src="/runlight/s.js"></script>

Después define RUNLIGHT_TOKEN como una cadena larga y aleatoria y abre una vez /runlight/?token= seguido de esa cadena para iniciar sesión, como describe Primeros pasos. Con accounts: true y un RUNLIGHT_SECRET, cada persona inicia sesión con su propio correo y contraseña, como en Cuentas.

Para contar los agentes de IA que leen tus páginas, añade el observer a la pipeline :browser. Registra sus accesos y vuelve al instante para todos los demás.

lib/my_app_web/router.ex

pipeline :browser do
  plug :accepts, ["html"]
  plug Runlight.Plug.Observer
end

Un dominio de enlaces responde a los enlaces cortos en un nombre propio. Cada uno de los Plugs de Runlight responde a una petición en un dominio de enlaces antes que nada y deja pasar intacta cualquier otra petición. Un router de Phoenix solo los ejecuta para las rutas con las que coincide, así que pon el Plug de dominios de enlaces en tu endpoint, antes del router, y cada petición a ese nombre se responde ahí.

lib/my_app_web/endpoint.ex

plug Runlight.Plug.LinkDomains
plug MyAppWeb.Router

Un nombre que no tiene ningún enlace le toca responderlo a tu aplicación. Reenviado desde un router de Phoenix, Runlight.Plug.Links lanza el NoRouteError propio de Phoenix, así que se muestra tu página 404, y reenviado desde un Plug.Router simple responde 404. También puedes poner plug Runlight.Plug.Links en tu endpoint antes del router, donde sustituye tanto al forward como al Plug de dominios de enlaces. Ahí responde a los dominios de enlaces y a /go/<name> para los enlaces que existen, y pasa cualquier otra petición a tu aplicación, también un nombre desconocido. En un dominio de enlaces, /go/<name> siempre es un 404, ya que los enlaces de un dominio de enlaces están en la raíz de su nombre.

Plug.Router

Un Plug.Router reenvía de la misma forma. El Plug de dominios de enlaces va antes de :match, para que vea todas las peticiones.

lib/my_app/router.ex

defmodule MyApp.Router do
  use Plug.Router

  plug Runlight.Plug.LinkDomains
  plug :match
  plug :dispatch

  forward "/runlight", to: Runlight.Plug
  forward "/go", to: Runlight.Plug.Links
end

Detrás de un proxy

Runlight lee la dirección del visitante de la última entrada de X-Forwarded-For, la que escribió el proxy más cercano, así que cada visitante cuenta una vez al día. Si no hay nada delante de la aplicación, define trust_proxy: false y lee la propia dirección de la conexión. Detrás de Cloudflare y otro proxy, trust_proxy: "cf-connecting-ip" lee solo esa cabecera.

Varias instancias

Dale un nombre a cada una y pásalo a los Plugs.

elixir

{Runlight, name: MyApp.Stats, store: {Runlight.Store, repo: MyApp.Repo}, site: [name: "Docs"]}

forward "/stats", Runlight.Plug, instance: MyApp.Stats

En qué se diferencia de la biblioteca de TypeScript

El paquete de Elixir pasa las pruebas de conformidad que se exigen a la biblioteca de TypeScript, en SQLite, Postgres, MySQL y MariaDB. Las diferencias vienen de cómo se ejecuta Elixir.

  • Cada petición se ejecuta en su propio proceso, y la instancia vive en tu árbol de supervisión. Su estado, como el límite de peticiones del tracker y las listas en caché de dominios de enlaces, está en una tabla ETS, así que todos los procesos de un nodo lo comparten y cada nodo tiene el suyo.
  • Lo que la biblioteca de TypeScript hace en segundo plano, como borrar visitas después de elegir una retención más corta, se ejecuta en una task propia una vez enviada la respuesta.
  • El hash de las contraseñas nuevas se calcula con PBKDF2-SHA-256, que el módulo crypto de Erlang calcula de forma nativa. Los hashes que la biblioteca de TypeScript creó con scrypt siguen sirviendo para iniciar sesión, así que las cuentas se mantienen.
  • No hay servidor independiente en Elixir. Para tener un solo panel para varios sitios, ejecuta el servidor de Node o el servidor de otro lenguaje, y conecta a él el sitio de esta aplicación.