Plataformas
Python
O Runlight roda em Python 3.11 ou mais recente, dentro de um app Django, Flask ou FastAPI ou de qualquer app WSGI ou ASGI, ou como servidor independente.
O pacote Python é o Runlight reescrito em Python. Ele 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 um dos dois consegue ler um banco de dados que o outro escreveu. Ele precisa do Python 3.11 ou mais recente e não tem dependências próprias.
Instalação
Adicione o pacote com o pip.
bash
pip install runlightO SQLite vem com o Python. Para Postgres, instale o extra postgres, que adiciona o psycopg 3, e para MySQL ou MariaDB, instale o extra mysql, que adiciona o PyMySQL.
bash
pip install "runlight[postgres]"
pip install "runlight[mysql]"Os fusos horários vêm do banco de dados de fusos horários do sistema. O Windows não tem um, então adicione o extra tzdata nele.
Crie a instância
Crie um módulo que cria a instância e importe-o onde você precisar do Runlight.
myproject/analytics.py
from runlight import Runlight
from runlight.store import Stores
rl = Runlight(
store=Stores.sqlite("data/runlight.db"),
site={"name": "example.com", "hostnames": ["example.com"], "timezone": "Europe/London"},
)As opções têm os mesmos nomes que em Configuração. Passe-as como argumentos nomeados em snake case, como trust_proxy=False e rate_limit=60, ou como um único dict com os nomes do TypeScript, como Runlight({"store": ..., "trustProxy": False}). As tabelas são criadas na primeira requisição.
Stores
As tabelas do Runlight começam com rl_. Todo store aceita prefix para usar outro prefixo no lugar, como Stores.sqlite(path, prefix="wp_rl_"), de modo que dois apps que compartilham um banco de dados mantêm cada um as suas próprias 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
python
Stores.sqlite("data/runlight.db")Mantenha o arquivo fora de qualquer pasta que o seu servidor web sirva, em uma pasta em que o app possa escrever, já que o SQLite grava um journal ao lado dele. A pasta é criada se não existir.
Postgres
python
Stores.postgres("postgres://runlight:password@127.0.0.1:5432/runlight")statement_timeout interrompe qualquer comando depois desse número de milissegundos, e o padrão é 120000. schema mantém as tabelas em um schema próprio, como em Stores.postgres(url, schema="analytics"). No lugar de uma URL, você pode passar uma conexão psycopg que o seu app já tenha, que o Runlight usa como está e nunca fecha.
MySQL e MariaDB
python
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. Ele aceita statement_timeout como o Postgres, que o MySQL aplica às leituras e o MariaDB a todos os comandos, como em Configuração.
Stores.url() escolhe o store a partir de uma DATABASE_URL que comece com postgres://, mysql://, mariadb:// ou sqlite:.
Django
Adicione o app do Runlight e o middleware dele às suas configurações, e indique a instância em RUNLIGHT. O middleware vem primeiro, para que nenhum outro middleware altere as requisições do Runlight nem as respostas dele.
settings.py
INSTALLED_APPS = [
# ...
"runlight.django",
]
MIDDLEWARE = [
"runlight.django.RunlightMiddleware",
# ...
]
RUNLIGHT = "myproject.analytics.rl"RUNLIGHT guarda a própria instância ou o caminho pontuado até ela, e um caminho para uma função que cria uma também funciona. RUNLIGHT_ROUTES passa opções para as rotas com os nomes do TypeScript, como {"accounts": True}. O middleware serve o painel e a API sob /runlight, os links curtos em /go e todos os domínios de links que você adiciona em Configurações, antes das URLs do próprio Django. Cada domínio de links também precisa estar em ALLOWED_HOSTS.
Rode a verificação agendada pelo cron com o comando de gerenciamento.
bash
*/5 * * * * cd /srv/myproject && python manage.py runlight_checkFlask
Passe o app e a instância para init_app, que coloca o Runlight na frente das views do próprio app. Ele responde sob /runlight, em /go e em todos os domínios de links que você adiciona em Configurações, e passa todo o resto para o seu app.
app.py
from flask import Flask
import runlight.flask
from myproject.analytics import rl
app = Flask(__name__)
runlight.flask.init_app(app, rl)As opções das rotas entram como argumentos nomeados, como runlight.flask.init_app(app, rl, accounts=True). init_app também adiciona um comando que roda a verificação agendada, para o cron.
bash
*/5 * * * * cd /srv/myproject && flask --app app runlight checkFastAPI
init_app adiciona o Runlight como middleware na frente das rotas do app, então ele responde sob /runlight, em /go e em todos os domínios de links que você adiciona em Configurações. Funciona também para apps Starlette.
main.py
from fastapi import FastAPI
import runlight.fastapi
from myproject.analytics import rl
app = FastAPI()
runlight.fastapi.init_app(app, rl)O trabalho do próprio Runlight roda em threads de worker, então ele nunca bloqueia o event loop. Para a verificação agendada, chame rl.check() a partir de um script que o cron executa, ou defina CRON_SECRET e chame a verificação por HTTP.
Qualquer app WSGI ou ASGI
Bottle, Pyramid e qualquer outro app WSGI usam o middleware WSGI.
python
from runlight.wsgi import RunlightMiddleware
application = RunlightMiddleware(application, rl)Quart, Django sob ASGI e qualquer outro app ASGI usam o middleware ASGI, que recebe a instância como runlight=.
python
from runlight.asgi import RunlightMiddleware
application = RunlightMiddleware(application, runlight=rl)Os dois respondem sob /runlight, em /go e em todos os domínios de links, e passam todo o resto para o seu app. As opções das rotas entram como argumentos nomeados, como no Flask. Para rodar o Runlight como um app próprio, sem um app seu por trás, use runlight.wsgi.app(rl) com um servidor WSGI como o gunicorn, ou runlight.asgi.app(rl) com um servidor ASGI como o uvicorn. Os caminhos fora dos do Runlight respondem então com 404.
Um link curto em /go/<name> sem nenhum link com esse nome segue para o seu app, então a sua própria página 404 responde, seja a do Django, a do Flask, a do FastAPI ou a do seu app WSGI ou ASGI. runlight.wsgi.app() e runlight.asgi.app() não têm nenhum app por trás e respondem eles mesmos com um 404. Num domínio de links, /go/<name> é um 404 como qualquer outro caminho que não seja um dos links desse domínio.
Seja qual for o jeito como você o adiciona, coloque 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 e um RUNLIGHT_SECRET, as pessoas entram com o próprio e-mail e senha, como em Contas.
Agentes de IA
O middleware registra as leituras de agentes de IA enquanto o seu app serve as páginas, e deixa passar intacta qualquer outra requisição. Passe observe=False para init_app ou para o middleware para desativar isso, ou defina "observe": False em RUNLIGHT_ROUTES no Django. Sem middleware, chame rl.observe(request) com um runlight.http.Request para cada requisição de página.
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. Rode-a a cada poucos minutos com python manage.py runlight_check no Django, flask runlight check no Flask ou rl.check() a partir de qualquer script. Quando nada consegue rodar um comando em um timer, defina CRON_SECRET e faça um agendador chamar /runlight/api/check com ele, como mostra Verificação agendada.
O servidor independente
O pacote inclui o servidor independente, que roda o Runlight em um domínio próprio, como stats.example.com, com o painel na raiz, sites adicionados pelo painel, contas com login e links curtos em qualquer domínio que você apontar para ele. Instale o pacote e rode o comando runlight.
bash
pip install runlight
runlightO servidor escuta na porta 3000 e guarda os dados em uma pasta chamada runlight-data no diretório de onde você o inicia. Na primeira inicialização, 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.
| 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. |
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 própria de cada site, mostrada em Configurações, Instalação, é a melhor escolha. |
TRUST_PROXY |
Defina como false quando não há nenhum proxy na frente do servidor, ou como x-real-ip ou cf-connecting-ip quando esse cabeçalho traz o endereço do visitante. |
RUNLIGHT_GEO |
De onde vêm as localizações quando nenhum cabeçalho as informa, como em Localizações. O padrão é city, 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
runlight check roda a verificação agendada uma vez e baixa os dados de localização do mês. runlight 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 de pip install --upgrade runlight e 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. Ele também desativa o login em duas etapas dessa conta, e cria a conta quando ela não existe, como proprietário em um servidor ainda sem ninguém e como administrador nos outros casos.
bash
runlight password someone@example.comAgentes de IA a partir de um log
Agentes de IA como o GPTBot e o ClaudeBot leem páginas sem rodar JavaScript, então a tag do script nunca os vê. Um app Python com o Runlight dentro os conta onde a página é servida, como mostra Agentes de IA. Para qualquer outro site que você hospeda, o log de acesso do servidor web os registra, e o comando agents lê esse log.
bash
runlight 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 continuar 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
O pacote Python passa nos testes de conformidade a que a biblioteca TypeScript é submetida, em SQLite, Postgres, MySQL e MariaDB. As diferenças vêm do jeito como o Python roda.
- O trabalho do Runlight é síncrono e roda em threads. O middleware ASGI entrega cada uma das suas requisições a uma thread de worker, então o event loop fica livre para o resto do seu app.
- O SQLite mantém uma conexão, que todas as threads compartilham sob um lock. O Postgres e o MySQL abrem uma conexão para cada thread, então um servidor com dezesseis threads mantém até dezesseis conexões.
- O que a biblioteca TypeScript faz em segundo plano, como apagar visitas depois que uma retenção menor é escolhida, roda depois que a resposta é enviada.
- O limite de requisições do script de rastreamento e as listas em cache de domínios de links e de ícones dos sites ficam na memória, então cada processo mantém as suas. Um servidor com vários processos de worker permite a cada endereço o limite uma vez em cada processo. Os limites de senha errada ficam no banco de dados, então todos os processos os compartilham.
- As senhas recebem hash com scrypt pelo
hashlibdo Python. Os hashes batem com os da biblioteca TypeScript, então as contas são aproveitadas.