Plataformas
Java
O Runlight roda em Java 21 ou mais recente, no servidor HTTP do próprio JDK, num servlet container ou num app Spring Boot, ou como servidor independente.
A biblioteca Java é o Runlight reescrito em Java. Ela serve o mesmo painel e a mesma API, e dá a cada requisição a mesma resposta que a biblioteca TypeScript dá. Os números vão para as mesmas tabelas, então qualquer uma das duas consegue ler um banco de dados que a outra escreveu. Ela precisa do Java 21 ou mais recente, e o núcleo não tem dependências próprias.
Instalação
Adicione o núcleo ao seu 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")O Runlight acessa o seu banco de dados por JDBC, então o driver vem do seu app. É org.xerial:sqlite-jdbc para SQLite, org.postgresql:postgresql para Postgres e com.mysql:mysql-connector-j ou org.mariadb.jdbc:mariadb-java-client para MySQL e MariaDB. Outros dois artefatos encaixam o Runlight num framework. sh.runlight:runlight-servlet o serve no Tomcat 10.1, no Jetty 12 ou em qualquer outro container Jakarta Servlet 6, e sh.runlight:runlight-spring-boot-starter o configura no Spring Boot 3.5 ou 4.
Crie a instância
Crie uma instância quando o seu app inicia e mantenha-a enquanto o app estiver rodando.
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")));As opções têm os mesmos nomes de Configuração, como setters em Runlight.Options, então sites, trustProxy, rateLimit, mail e managedSites funcionam como lá. Uma instância atende todas as threads do seu app ao mesmo tempo. As tabelas são criadas na primeira requisição.
Stores
As tabelas do Runlight começam com rl_. Cada método de store tem uma forma que recebe, como último argumento, um prefixo para usar no lugar, como Stores.sqlite(path, "wp_rl_") ou Stores.dataSource(dataSource, dialect, "wp_rl_"), para que dois apps que compartilham um banco de dados mantenham cada um as suas tabelas. Um prefixo tem até 32 letras, dígitos e sublinhados e não começa com dígito. store.tables() lista as tabelas do store pelo nome, para um app que esteja removendo o Runlight.
SQLite
java
Stores.sqlite("data/runlight.db");Mantenha o arquivo fora de qualquer pasta que o seu servidor web sirva, numa pasta em que o app possa escrever, já que o SQLite escreve um journal ao lado dele. A pasta é criada quando não existe.
Postgres
java
Stores.postgres("postgres://runlight:password@127.0.0.1:5432/runlight");Qualquer comando é interrompido depois de dois minutos. Stores.postgres(url, 30_000, "analytics") define outro limite em milissegundos e mantém as tabelas num schema próprio.
MySQL e MariaDB
java
Stores.mysql("mysql://runlight:password@127.0.0.1:3306/runlight");Este store funciona com MySQL 8.0.20 ou mais recente e MariaDB 11.4 ou mais recente. Uma URL mariadb:// também funciona. Stores.mysql(url, 30_000) define o timeout de comando, que o MySQL aplica às leituras e o MariaDB a todos os comandos, como em Configuração.
O DataSource do seu próprio app
java
Stores.dataSource(dataSource);O Runlight pega emprestada uma conexão do pool do seu app para cada comando e a devolve, e todas as tabelas dele começam com rl_, então elas podem ficar no banco de dados do seu próprio app. Stores.url() escolhe o store a partir de uma DATABASE_URL que começa com postgres://, mysql://, mariadb:// ou sqlite:.
O servidor HTTP do próprio JDK
Um app em com.sun.net.httpserver.HttpServer monta o painel, a API e o script de rastreamento em /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();Dê um executor ao servidor, já que as rotas leem o banco de dados e o padrão do JDK roda todas as requisições numa só thread. mount também responde aos links curtos em /go/, e serve o seu handler em / atrás de um filtro que responde aos domínios de links adicionados em Configurações, então uma requisição em qualquer outro host chega ao seu app sem alteração. O filtro observer registra as leituras de agentes de IA enquanto o seu app serve as páginas dele.
Servlet containers
Adicione sh.runlight:runlight-servlet e então registre o servlet em /runlight/* e o filtro em todos os caminhos, num ServletContainerInitializer ou num 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, "/*");O servlet pega o caminho base do próprio mapeamento. O filtro responde aos links curtos e aos domínios de links e registra as leituras de agentes de IA, e nunca lê o corpo de uma requisição, que continua sendo do seu app. Os domínios de links precisam do app no contexto raiz, já que os slugs deles são lidos do caminho inteiro.
Spring Boot
Adicione sh.runlight:runlight-spring-boot-starter e configure o Runlight em application.properties.
application.properties
runlight.site.name=example.com
runlight.site.hostnames=example.com
runlight.site.timezone=Europe/London
runlight.token=${RUNLIGHT_TOKEN}O starter guarda as tabelas no DataSource do seu próprio app, ou no banco de dados que runlight.database-url indicar. Ele serve as rotas em runlight.base-path (/runlight) e os links curtos em runlight.link-path (/go), responde aos domínios de links adicionados em Configurações antes do roteamento do seu app e roda a verificação agendada a cada minuto. Os filtros dele rodam antes dos do Spring Security. Defina runlight.open=true para servir o painel sem token, atrás do login do seu próprio app, e o filtro dele passa para trás do Spring Security, para que as suas regras o protejam. Um bean Runlight ou Routes seu substitui o que o starter cria. As outras configurações são runlight.sites[0].*, managed-sites, secret, trust-proxy, rate-limit, origin, accounts, cron-secret, observe-key, observe, check.every e web.enabled.
O filtro e o starter só respondem a um link curto em /go/<name> quando o link existe. Um nome sem link segue para o seu app, então a sua própria página 404 responde, ou a página de erro do Spring Boot quando você não tem uma. No servidor do JDK, mount entrega o caminho dos links ao Runlight, que responde ele mesmo a esse nome com um 404, como faz o servidor independente. Num domínio de links, /go/<name> é um 404 como qualquer outro caminho que não seja um dos links desse domínio.
Adicione o script
Adicione o script em todas as páginas, logo antes de </head>.
html
<script defer src="/runlight/s.js"></script>Depois defina RUNLIGHT_TOKEN como uma string longa e aleatória e abra /runlight/?token= seguido dessa string uma vez para entrar, como descreve Primeiros passos. Com .accounts(true) em Routes.Options e um RUNLIGHT_SECRET, as pessoas entram com o próprio e-mail e senha, como em Contas.
A verificação agendada
A verificação agendada troca os salts diários, envia os relatórios por email que estão na hora, aplica por quanto tempo cada site guarda as visitas e soma os totais diários que deixam os períodos longos rápidos. O starter do Spring Boot a roda para você. Em qualquer outro lugar, chame rl.check() a cada poucos minutos de um ScheduledExecutorService, ou defina CRON_SECRET e faça um agendador chamar /runlight/api/check com ele, como mostra Verificação agendada.
O servidor independente
O jar principal inclui o servidor independente, que roda o Runlight num domínio próprio, como stats.example.com, com o painel na raiz, sites adicionados no painel, contas com login e links curtos em qualquer domínio que você apontar para ele. Baixe o jar runlight e o driver JDBC do seu banco de dados no Maven Central, coloque os dois lado a lado e inicie o servidor.
bash
java -cp runlight.jar:sqlite-jdbc.jar sh.runlight.server.CliNo Windows, separe os jars com ;. O servidor escuta na porta 3000 e guarda os dados numa pasta chamada runlight-data, no diretório de onde você o inicia. Na primeira vez que inicia, ele mostra um link de configuração com um código de uso único. Abra esse link e crie sua conta, como descreve Crie sua conta. O servidor roda a verificação agendada sozinho a cada cinco minutos e baixa os dados de localização do mês como o servidor Node faz. Coloque-o atrás de um proxy que adicione HTTPS, como em Coloque na internet.
Configurações
O servidor lê as configurações de variáveis de ambiente, ou de linhas como PORT=8080 em runlight.properties, no diretório de onde você o inicia. O ambiente tem prioridade sobre o arquivo, e --config indica outro arquivo.
| Variável | O que faz |
|---|---|
PORT |
A porta em que escutar. O padrão é 3000. |
HOST |
O endereço em que escutar. O padrão é 0.0.0.0. |
DATA_DIR |
A pasta do arquivo SQLite, do segredo e dos dados de localização. O padrão é ./runlight-data. |
DATABASE_URL |
Um endereço postgres:// guarda os dados no Postgres em vez do SQLite, e um endereço mysql:// ou mariadb:// os guarda no MySQL ou no MariaDB. Coloque o driver desse banco de dados no class path. |
RUNLIGHT_SECRET |
A chave que assina os logins e criptografa as chaves salvas. Sem ela, o servidor cria uma e a guarda em DATA_DIR, legível só pelo usuário que o roda. |
RUNLIGHT_URL |
O endereço público do servidor, como https://stats.example.com, que nunca pode virar um domínio de links. Os e-mails de convite e de relatório apontam para ele. |
RUNLIGHT_TOKEN |
Um token que scripts podem enviar como bearer, além dos tokens de API criados no painel. |
RUNLIGHT_OBSERVE_KEY |
Uma única chave com que qualquer site pode informar agentes de IA. A chave de agentes de cada site, mostrada em Configurações, Instalação, é a melhor escolha. |
TRUST_PROXY |
Defina como false quando não houver proxy na frente do servidor, ou como x-real-ip ou cf-connecting-ip quando esse cabeçalho trouxer o endereço do visitante. |
RUNLIGHT_GEO |
De onde vêm as localizações quando nenhum cabeçalho as informa, como em Localizações. É city por padrão, e pode ser country, off ou o caminho de um arquivo MMDB. |
CRON_SECRET |
Permite que um agendador rode a verificação em POST /api/check. |
Comandos
Cada comando vem depois de sh.runlight.server.Cli. cron roda a verificação agendada uma vez e baixa os dados de localização do mês, e migrate cria ou atualiza as tabelas na hora. As tabelas também se atualizam sozinhas quando o servidor inicia, então uma atualização só precisa do novo jar e de um reinício. Faça backup da pasta de dados, já que as chaves salvas não podem ser lidas sem o segredo que está nela.
Senhas esquecidas
Rode o comando password onde o servidor roda para dar uma nova senha a uma conta, que ele mostra na tela. Ele também desativa o login em duas etapas dessa conta, e cria a conta quando ela não existe, como proprietário num servidor que ainda não tem ninguém e como administrador nos outros casos.
bash
java -cp runlight.jar:sqlite-jdbc.jar sh.runlight.server.Cli password someone@example.comAgentes de IA a partir de um log
Agentes de IA como GPTBot e ClaudeBot leem páginas sem rodar JavaScript, então a tag do script nunca os vê. Um app Java com o Runlight dentro os conta onde a página é servida, como fazem os filtros acima. Para qualquer outro site que você hospeda, o log de acesso do servidor web os registra, e o comando agents lê esse log. Ele não precisa de driver de banco de dados.
bash
java -jar runlight.jar agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --followA chave é a chave de agentes do site, mostrada no Runlight em Configurações, Instalação, e ela só pode informar leituras desse site. Quando --to ou --key ficam de fora, o comando lê RUNLIGHT_URL ou RUNLIGHT_OBSERVE_KEY do ambiente. O comando funciona como no servidor independente, com --follow para continuar rodando e --state para retomar de onde a última execução parou. Só as leituras de página bem-sucedidas de agentes de IA conhecidos saem da máquina, e os endereços dos visitantes ficam onde estão.
Diferenças em relação à biblioteca TypeScript
A biblioteca Java passa nos testes de conformidade aos quais a biblioteca TypeScript é submetida, em SQLite, Postgres, MySQL e MariaDB. As diferenças vêm da forma como o Java roda.
- O trabalho do Runlight é síncrono e roda nas threads do seu servidor, sejam de plataforma ou virtuais. Uma instância pode ser compartilhada entre elas com segurança.
- O SQLite mantém uma conexão, que as threads usam uma de cada vez. O Postgres e o MySQL mantêm um pequeno pool próprio, ou pegam conexões emprestadas do
DataSourcedo seu app. - O que a biblioteca TypeScript faz em segundo plano, como apagar visitas depois que um período de retenção menor é escolhido, roda depois que a resposta foi enviada. Os adaptadores chamam
rl.idle()para isso, e um app que responde às requisições do seu próprio jeito também deve chamá-lo. - O limite de requisições do script de rastreamento e as listas em cache de domínios de links e ícones de sites ficam na memória, então cada processo mantém as suas. Os limites de senha errada ficam no banco de dados, então todos os processos os compartilham.
- As senhas recebem hash com scrypt, escrito em Java já que o JDK não tem um. Os hashes são iguais aos da biblioteca TypeScript, então as contas continuam valendo.