Plataformas
.NET
Runlight funciona con .NET 10 o posterior, dentro de una aplicación ASP.NET Core, o en un dominio propio como servidor independiente.
El paquete de .NET es Runlight escrito de nuevo en C#. 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 .NET 10 o posterior y el driver ADO.NET de tu base de datos, que tu aplicación ya tiene si usa esa base de datos.
Instalación
Añade el paquete de ASP.NET Core y el driver de tu base de datos. El paquete trae consigo el paquete principal Runlight.
bash
dotnet add package Runlight.AspNetCore
dotnet add package Microsoft.Data.SqliteEl paquete principal no tiene dependencias propias. Llega a la base de datos a través del driver que le pases, y comparte el pool de conexiones de ese driver.
Crea la instancia
Crea una instancia cuando arranque la aplicación y regístrala como singleton.
Program.cs
using Microsoft.Data.Sqlite;
using Runlight;
using Runlight.Store;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(new Runlight.Runlight(new RunlightOptions
{
Store = Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db"),
Site = new SiteOptions { Name = "example.com", Hostnames = ["example.com"], Timezone = "Europe/London" },
}));Las opciones tienen los nombres de Configuración, escritos en PascalCase, así que Sites, TrustProxy, RateLimit, Mail y ManagedSites funcionan como allí. Las tablas se crean en la primera petición.
Almacenes
Las tablas de Runlight empiezan por rl_. Cada almacén acepta un argumento prefix para usar otro en su lugar, como Stores.Sqlite(SqliteFactory.Instance, path, prefix: "wp_rl_"), 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. store.Tables() lista las tablas del almacén por nombre, para una aplicación que quiera quitar Runlight.
SQLite
csharp
Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db");SQLite necesita Microsoft.Data.Sqlite. Guarda el archivo en una carpeta en la que la aplicación pueda escribir, ya que SQLite escribe un journal a su lado. La carpeta se crea si no existe.
Postgres
csharp
Stores.Postgres(NpgsqlDataSource.Create("Host=127.0.0.1;Username=runlight;Password=password;Database=runlight"));Este almacén necesita Npgsql. Pasa el NpgsqlDataSource propio de la aplicación para compartir su pool. Un segundo argumento como 30000 detiene cualquier consulta pasados esos milisegundos, y Search Path=analytics en la cadena de conexión guarda las tablas en un schema propio.
MySQL y MariaDB
csharp
Stores.MySql(new MySqlDataSource("Server=127.0.0.1;User ID=runlight;Password=password;Database=runlight"));Este almacén necesita MySqlConnector y funciona con MySQL 8.0.20 o posterior y MariaDB 11.4 o posterior. Acepta el mismo segundo argumento que el almacén de Postgres.
ASP.NET Core
Mapea Runlight después de construir la aplicación.
Program.cs
var app = builder.Build();
app.MapRunlight();
app.Run();MapRunlight() sirve el panel, la API y el script bajo /runlight, y los enlaces cortos en /go. También responde a cualquier dominio de enlaces que añadas en Ajustes, y registra los accesos de los agentes de IA mientras se sirven tus páginas, lo que no le cuesta tiempo a nadie más. Runlight comprueba sus propias peticiones, así que sus endpoints se saltan la comprobación antiforgery de ASP.NET Core, que rechazaría al tracker. Permiten peticiones anónimas, así que una aplicación cuya política por defecto exige un usuario con sesión iniciada sigue contando a sus visitantes. 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. Las opciones de las rutas van en un RoutesOptions, con los nombres de Configuración.
csharp
app.MapRunlight(new RoutesOptions { BasePath = "/stats", Accounts = true, Origin = "https://example.com" });Con Accounts = true y un RUNLIGHT_SECRET, cada persona inicia sesión con su propio correo y contraseña, como en Cuentas. Con Token = null, las rutas no tienen inicio de sesión propio, y sus endpoints siguen entonces la política de autorización de la aplicación.
app.UseRunlight() hace el mismo trabajo como middleware, para una aplicación que quiere Runlight antes que el resto de su middleware o que no usa endpoint routing. Las peticiones que no son de Runlight siguen hacia la aplicación. El middleware no lleva autorización de endpoint, así que llama a UseAuthentication() y UseAuthorization() antes que él cuando el panel dependa del inicio de sesión de la aplicación.
MapRunlight() también funciona dentro de UseEndpoints, donde responde a los dominios de enlaces justo antes de tus endpoints. En un grupo de rutas de MapGroup se niega a arrancar, ya que las rutas necesitan su ruta completa, así que mapéalo en la aplicación y pon en BasePath la ruta completa, como /admin/runlight. Un enlace corto que nadie tiene queda para tu aplicación. MapRunlight() le responde con un 404 desnudo, como ASP.NET Core responde a cualquier ruta sin endpoint, así que UseStatusCodePages muestra ahí tu propia página, y UseRunlight() pasa la petición a la aplicación.
Un dominio de enlaces es un host propio, así que añádelo a AllowedHosts cuando la aplicación define esa lista, o ASP.NET Core rechazará antes la petición.
Otras aplicaciones .NET
Cualquier aplicación .NET puede pasarle una petición a Runlight y devolver su respuesta. Construye un Runlight.Http.Request a partir de la URL absoluta, el método, las cabeceras, el cuerpo y la dirección de la conexión, y pásalo a FrontController.AnswerAsync().
csharp
using Runlight.Http;
using Runlight.Server;
var request = new Request(url, method, headers, body, remoteAddress);
Response answer = await FrontController.AnswerAsync(rl, routes, request);
await rl.IdleAsync();answer.Status, answer.HeaderLines() y answer.WriteToAsync() dan lo que hay que enviar. IdleAsync() ejecuta el trabajo que la biblioteca de TypeScript hace después de responder, así que llámalo una vez enviada la respuesta. En un endpoint o un middleware propio de ASP.NET Core, RunlightHttp.ServeAsync(context, rl, routes) hace todo esto para un HttpContext.
La comprobación programada
La comprobación programada puede ejecutarse dentro de la aplicación como un hosted service.
RunlightCheck.cs
public sealed class RunlightCheck(Runlight.Runlight rl) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
do
{
await rl.CheckAsync(stoppingToken);
await rl.IdleAsync();
}
while (await timer.WaitForNextTickAsync(stoppingToken));
}
}Regístralo con builder.Services.AddHostedService<RunlightCheck>(). Una aplicación que se ejecuta en varias copias puede prescindir de esto y definir CRON_SECRET, y hacer que un solo programador de tareas llame a /runlight/api/check.
El servidor independiente
La herramienta runlight ejecuta Runlight en un dominio propio, como stats.example.com, igual que el servidor independiente. Sirve el panel en la raíz, gestiona los sitios desde el panel, y tiene cuentas con inicio de sesión y enlaces cortos en cualquier dominio que apuntes hacia él.
bash
dotnet tool install --global Runlight.Server
mkdir stats && cd stats
runlight serveEl servidor escucha en el puerto 3000, o en PORT, con Kestrel. Los datos viven en runlight-data, dentro de la carpeta desde la que lo arrancas. En su primer arranque muestra un enlace con un código de un solo uso que crea la primera cuenta y escribe el enlace en setup.txt, dentro de la carpeta de datos, y runlight setup lo vuelve a mostrar. El enlace solo funciona mientras no haya ninguna cuenta. Los roles, las invitaciones, el inicio de sesión en dos pasos y la forma de añadir sitios funcionan como en el servidor independiente.
Ajustes
Los ajustes vienen de variables de entorno, o de un config.json con los mismos nombres en la carpeta desde la que arrancas el servidor. Si ambos están definidos, ganan las variables de entorno.
config.json
{
"RUNLIGHT_URL": "https://stats.example.com",
"DATABASE_URL": "postgres://runlight:password@localhost:5432/runlight"
}| Ajuste | Qué hace |
|---|---|
PORT |
El puerto en el que escucha el servidor, 3000 por defecto. |
HOST |
La dirección en la que escucha, 0.0.0.0 por defecto. |
RUNLIGHT_URL |
La dirección pública del panel, como https://stats.example.com. Nunca puede convertirse en dominio de enlaces, y los correos de invitación y de informes enlazan a ella. |
DATABASE_URL |
Una dirección postgres://, mysql:// o mariadb:// guarda los datos en esa base de datos en lugar de SQLite. |
DATA_DIR |
La carpeta de datos, runlight-data por defecto. Una ruta relativa se lee desde la carpeta en la que arranca el servidor. |
RUNLIGHT_SECRET |
La clave que firma los inicios de sesión y cifra las claves guardadas. Sin ella, el servidor crea una y la guarda en la carpeta de datos. |
RUNLIGHT_TOKEN |
Un token bearer para scripts, que además crea la primera cuenta en lugar del enlace de configuración. |
TRUST_PROXY |
Defínela como false cuando no haya nada delante del servidor, o como x-real-ip o cf-connecting-ip cuando esa cabecera lleve la dirección del visitante. |
RUNLIGHT_GEO |
De dónde salen las ubicaciones, como en el servidor independiente. Por defecto es city, y puede ser country, off o la ruta a un archivo MMDB. |
CRON_SECRET |
Permite que un programador de tareas ejecute la comprobación en POST /api/check. |
RUNLIGHT_OBSERVE_KEY |
Una sola clave para los informes de agentes de IA de todos los sitios. La clave de agentes propia de cada sitio, que aparece en Ajustes, Instalación, es la mejor opción. |
RUNLIGHT_CONFIG |
La ruta de un archivo de configuración que no está en la carpeta en la que arranca el servidor. |
El servidor ejecuta la comprobación programada cada cinco minutos y descarga los nuevos datos de ubicación una vez al mes. Para un servidor que no está siempre en marcha, runlight cron hace ese mismo trabajo una vez. No muestra nada cuando todo va bien, salvo una línea cuando están listos los datos de ubicación de un mes nuevo.
bash
*/5 * * * * cd /srv/stats && runlight cronUn servicio de systemd o cualquier gestor de procesos mantiene el servidor en marcha. Se detiene con SIGTERM y da cinco segundos a las respuestas que ha empezado para que terminen.
Contraseñas olvidadas
Ejecuta el comando password en la carpeta del servidor para darle a una cuenta una contraseña nueva, que el comando muestra. También desactiva el inicio de sesión en dos pasos de esa cuenta, y crea la cuenta si no existe.
bash
runlight password someone@example.comActualizaciones y copias de seguridad
Ejecuta dotnet tool update --global Runlight.Server y reinicia el servidor. Las tablas se actualizan solas en el siguiente arranque, o ejecuta runlight migrate para actualizarlas al momento. Haz copia de seguridad de la carpeta de datos y de config.json, ya que las claves guardadas no se pueden leer sin el secreto.
Agentes de IA desde un log
Los agentes de IA como GPTBot y ClaudeBot acceden a las páginas sin ejecutar JavaScript, así que la etiqueta de script nunca los ve. Una aplicación ASP.NET Core con Runlight dentro los cuenta mientras sirve sus páginas. Para cualquier otro sitio que alojes tú, el log de acceso del servidor web los tiene, y el comando agents lo lee.
bash
runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --followLa clave es la clave de agentes del sitio, que aparece en Runlight en Ajustes, Instalación, y solo puede informar de accesos a ese sitio. Cuando falta --to o --key, el comando lee RUNLIGHT_URL o RUNLIGHT_OBSERVE_KEY del entorno o de config.json. --site es la dirección del sitio, para logs en el formato habitual de nginx o Apache, que no incluyen el host. Los logs JSON de Caddy sí llevan el host, así que ahí no hace falta.
Con --follow, el comando sigue en marcha, envía los accesos a medida que ocurren y continúa cuando se rota el log. Añade --state agents.json para que un reinicio continúe donde se quedó. Sin --follow, lee el log una vez y se detiene, y con --state la siguiente ejecución empieza donde terminó la anterior, lo que encaja con cron. Solo una ejecución a la vez puede usar un archivo de estado, y un bloqueo que dejó una ejecución que falló se toma de nuevo. Solo salen de la máquina los accesos correctos a páginas de agentes de IA conocidos, cada uno con la dirección de la página, el user agent del agente y cuándo se sirvió.
En qué se diferencia de la biblioteca de TypeScript
El paquete de .NET 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 ejecutan las aplicaciones .NET.
- El pool de conexiones es el del driver. Postgres y MySQL toman una conexión del pool del data source para cada consulta, y SQLite mantiene una conexión abierta y ejecuta una sentencia cada vez.
- 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 una vez que ASP.NET Core ha enviado la respuesta.
- El hash de las contraseñas se calcula con scrypt escrito en C#, ya que .NET no lo incluye. Los hashes coinciden con los de la biblioteca de TypeScript, así que las cuentas se mantienen.
- El límite de peticiones del tracker se guarda en memoria, así que cada copia de la aplicación cuenta por su cuenta. Los límites de contraseñas incorrectas se guardan en la base de datos, así que todas las copias los comparten.
- Los informes por email dan formato a números, fechas, importes y nombres de países a partir de una copia de los datos de ICU que usa Node, así que se leen igual en cualquier sistema.