Plataformas
Java
Runlight funciona con Java 21 o posterior, en el propio servidor HTTP del JDK, en un contenedor de servlets o en una aplicación Spring Boot, o como servidor independiente.
La biblioteca de Java es Runlight escrito de nuevo en Java. 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 Java 21 o posterior, y el núcleo no tiene dependencias propias.
Instalación
Añade el núcleo a tu build.
pom.xml
<dependency>
<groupId>sh.runlight</groupId>
<artifactId>runlight</artifactId>
<version>0.1.1</version>
</dependency>build.gradle.kts
implementation("sh.runlight:runlight:0.1.1")Runlight llega a tu base de datos a través de JDBC, así que tu aplicación aporta el driver. Es org.xerial:sqlite-jdbc para SQLite, org.postgresql:postgresql para Postgres, y com.mysql:mysql-connector-j u org.mariadb.jdbc:mariadb-java-client para MySQL y MariaDB. Otros dos artefactos integran Runlight en un framework. sh.runlight:runlight-servlet lo sirve en Tomcat 10.1, Jetty 12 o cualquier otro contenedor Jakarta Servlet 6, y sh.runlight:runlight-spring-boot-starter lo configura en Spring Boot 3.5 o 4.
Crea la instancia
Crea una instancia cuando arranque tu aplicación, y consérvala mientras la aplicación esté en marcha.
Analytics.java
import java.util.List;
import java.util.Map;
import sh.runlight.Runlight;
import sh.runlight.store.Stores;
Runlight rl = new Runlight(new Runlight.Options()
.store(Stores.sqlite("data/runlight.db"))
.site(Map.of("name", "example.com", "hostnames", List.of("example.com"), "timezone", "Europe/London")));Las opciones tienen los mismos nombres que en Configuración, como setters de Runlight.Options, así que sites, trustProxy, rateLimit, mail y managedSites funcionan como allí. Una sola instancia sirve a la vez a todos los hilos de tu aplicación. Las tablas se crean en la primera petición.
Almacenes
Las tablas de Runlight empiezan por rl_. Cada método de almacén tiene una variante que recibe como último argumento un prefijo para usar en su lugar, como Stores.sqlite(path, "wp_rl_") o Stores.dataSource(dataSource, dialect, "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
java
Stores.sqlite("data/runlight.db");Guarda el archivo fuera de cualquier carpeta que sirva tu servidor web, 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
java
Stores.postgres("postgres://runlight:password@127.0.0.1:5432/runlight");Cualquier sentencia se detiene a los dos minutos. Stores.postgres(url, 30_000, "analytics") define otro límite en milisegundos y guarda las tablas en un schema propio.
MySQL y MariaDB
java
Stores.mysql("mysql://runlight:password@127.0.0.1:3306/runlight");Este almacén funciona con MySQL 8.0.20 o posterior y MariaDB 11.4 o posterior. También vale una URL mariadb://. Stores.mysql(url, 30_000) define el timeout de sentencia, que MySQL aplica a las lecturas y MariaDB a todas las sentencias, como en Configuración.
El DataSource de tu propia aplicación
java
Stores.dataSource(dataSource);Runlight toma prestada una conexión del pool de tu aplicación para cada sentencia y la devuelve, y todas sus tablas empiezan por rl_, así que pueden vivir en la propia base de datos de tu aplicación. Stores.url() elige el almacén a partir de un DATABASE_URL que empiece por postgres://, mysql://, mariadb:// o sqlite:.
El propio servidor HTTP del JDK
Una aplicación sobre com.sun.net.httpserver.HttpServer monta el panel, la API y el tracker en /runlight.
Main.java
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import sh.runlight.Routes;
import sh.runlight.server.JdkServer;
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
var routes = rl.routes(new Routes.Options().basePath("/runlight"));
var app = JdkServer.mount(server, rl, routes, yourHandler);
app.getFilters().add(JdkServer.observer(rl));
server.start();Dale un executor al servidor, ya que las rutas leen la base de datos y el executor por defecto del JDK ejecuta todas las peticiones en un solo hilo. mount también responde a los enlaces cortos en /go/, y sirve tu handler en / detrás de un filtro que responde a los dominios de enlaces añadidos en Ajustes, así que una petición en cualquier otro host llega intacta a tu aplicación. El filtro observer registra los accesos de los agentes de IA mientras tu aplicación sirve sus páginas.
Contenedores de servlets
Añade sh.runlight:runlight-servlet y después registra el servlet en /runlight/* y el filtro en todas las rutas, en un ServletContainerInitializer o en un listener.
java
import sh.runlight.servlet.RunlightFilter;
import sh.runlight.servlet.RunlightServlet;
context.addServlet("runlight", new RunlightServlet(rl)).addMapping("/runlight/*");
context.addFilter("runlight-links", new RunlightFilter(rl)).addMappingForUrlPatterns(null, false, "/*");El servlet toma su ruta base de su mapping. El filtro responde a los enlaces cortos y a los dominios de enlaces y registra los accesos de los agentes de IA, y nunca lee el cuerpo de una petición, que sigue siendo de tu aplicación. Los dominios de enlaces necesitan la aplicación en el contexto raíz, ya que sus slugs se leen de la ruta completa.
Spring Boot
Añade sh.runlight:runlight-spring-boot-starter y configura Runlight en application.properties.
application.properties
runlight.site.name=example.com
runlight.site.hostnames=example.com
runlight.site.timezone=Europe/London
runlight.token=${RUNLIGHT_TOKEN}El starter guarda sus tablas en el DataSource propio de tu aplicación, o en la base de datos que indique runlight.database-url. Sirve las rutas en runlight.base-path (/runlight) y los enlaces cortos en runlight.link-path (/go), responde a los dominios de enlaces añadidos en Ajustes antes que el enrutado propio de tu aplicación y ejecuta la comprobación programada cada minuto. Sus filtros se ejecutan antes que los de Spring Security. Define runlight.open=true para servir el panel sin token, detrás del inicio de sesión propio de tu aplicación, y su filtro pasa a ir detrás de Spring Security para que tus reglas lo protejan. Un bean Runlight o Routes propio sustituye al que crea el starter. Los demás ajustes son runlight.sites[0].*, managed-sites, secret, trust-proxy, rate-limit, origin, accounts, cron-secret, observe-key, observe, check.every y web.enabled.
El filtro y el starter solo responden a un enlace corto en /go/<name> cuando el enlace existe. Un nombre sin enlace sigue hacia tu aplicación, así que responde tu propia página 404, o la página de error de Spring Boot si no tienes una. En el servidor del JDK, mount le da la ruta de los enlaces a Runlight, que responde él mismo a ese nombre con un 404, como hace el servidor independiente. En un dominio de enlaces, /go/<name> es un 404 como cualquier otra ruta que no sea uno de los enlaces de ese dominio.
Añade el script
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) en Routes.Options y un RUNLIGHT_SECRET, cada persona inicia sesión con su propio correo y contraseña, como en Cuentas.
La comprobación programada
La comprobación programada 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. El starter de Spring Boot la ejecuta por ti. En cualquier otro caso, llama a rl.check() cada pocos minutos desde un ScheduledExecutorService, o define CRON_SECRET y haz que un programador de tareas llame a /runlight/api/check con él, como muestra Comprobación programada.
El servidor independiente
El jar principal incluye el servidor independiente, que ejecuta Runlight en un dominio propio, como stats.example.com, con el panel en la raíz, sitios añadidos desde el panel, cuentas con inicio de sesión y enlaces cortos en cualquier dominio que apuntes hacia él. Descarga de Maven Central el jar runlight y el driver JDBC de tu base de datos, ponlos uno junto al otro y arráncalo.
bash
java -cp runlight.jar:sqlite-jdbc.jar sh.runlight.server.CliEn Windows, separa los jars con ;. El servidor escucha en el puerto 3000 y guarda sus datos en una carpeta llamada runlight-data dentro del directorio desde el que lo arrancas. En su primer arranque muestra un enlace de configuración con un código de un solo uso. Abre ese enlace y crea tu cuenta, como describe Crea tu cuenta. El servidor ejecuta él mismo la comprobación programada cada cinco minutos, y descarga los datos de ubicación del mes igual que el servidor de Node. Ponlo detrás de un proxy que añada HTTPS, como en Publícalo en internet.
Ajustes
El servidor lee sus ajustes de variables de entorno, o de líneas como PORT=8080 en runlight.properties, dentro del directorio desde el que lo arrancas. El entorno tiene prioridad sobre el archivo, y --config indica otro archivo.
| Variable | Qué hace |
|---|---|
PORT |
El puerto en el que escucha. Por defecto, 3000. |
HOST |
La dirección en la que escucha. Por defecto, 0.0.0.0. |
DATA_DIR |
La carpeta para el archivo SQLite, el secreto y los datos de ubicación. Por defecto, ./runlight-data. |
DATABASE_URL |
Una dirección postgres:// guarda los datos en Postgres en lugar de SQLite, y una dirección mysql:// o mariadb:// los guarda en MySQL o MariaDB. Pon el driver de esa base de datos en el class path. |
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 DATA_DIR, legible solo por el usuario que lo ejecuta. |
RUNLIGHT_URL |
La dirección pública del servidor, como https://stats.example.com, que nunca puede convertirse en dominio de enlaces. Los correos de invitación y de informes enlazan a ella. |
RUNLIGHT_TOKEN |
Un token que los scripts pueden enviar como bearer, además de los tokens de API creados en el panel. |
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. |
TRUST_PROXY |
Defínela como false cuando no haya ningún proxy 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 cuando ninguna cabecera las da, como en Ubicaciones. 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. |
Comandos
Cada comando va después de sh.runlight.server.Cli. cron ejecuta la comprobación programada una vez y obtiene los datos de ubicación del mes, y migrate crea o actualiza las tablas al momento. Las tablas también se actualizan solas cuando arranca el servidor, así que para actualizar basta con el jar nuevo y un reinicio. Haz copia de seguridad de la carpeta de datos, ya que las claves guardadas no se pueden leer sin el secreto que contiene.
Contraseñas olvidadas
Ejecuta el comando password donde se ejecuta el 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, como propietario en un servidor que aún no tiene a nadie y como administrador en los demás casos.
bash
java -cp runlight.jar:sqlite-jdbc.jar sh.runlight.server.Cli password someone@example.comAgentes 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 Java con Runlight dentro los cuenta donde se sirve la página, como hacen los filtros de arriba. Para cualquier otro sitio que alojes tú, el log de acceso del servidor web los tiene, y el comando agents lo lee. No necesita ningún driver de base de datos.
bash
java -jar runlight.jar 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. El comando funciona igual que en el servidor independiente, con --follow para seguir en marcha y --state para continuar donde se quedó la última ejecución. Solo salen de la máquina los accesos correctos a páginas de agentes de IA conocidos, y las direcciones de los visitantes se quedan donde están.
En qué se diferencia de la biblioteca de TypeScript
La biblioteca de Java 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 Java.
- El trabajo de Runlight es síncrono y se ejecuta en los hilos de tu servidor, de plataforma o virtuales. Una sola instancia se puede compartir con seguridad entre ellos.
- SQLite mantiene una sola conexión, que los hilos usan por turnos. Postgres y MySQL mantienen un pequeño pool propio, o toman conexiones prestadas del
DataSourcede tu aplicación. - 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 enviada la respuesta. Los adaptadores llaman a
rl.idle()para eso, y una aplicación que responde a las peticiones a su manera también debería llamarlo. - El límite de peticiones del tracker y las listas en caché de dominios de enlaces y de iconos de sitios se guardan en memoria, así que cada proceso tiene las suyas. Los límites de contraseñas incorrectas se guardan en la base de datos, así que todos los procesos los comparten.
- El hash de las contraseñas se calcula con scrypt, escrito en Java ya que el JDK no lo incluye. Los hashes coinciden con los de la biblioteca de TypeScript, así que las cuentas se mantienen.