Todas as docs

Início

  1. 01Primeiros passos
  2. 02Instalação
  3. 03Rastreamento

Recursos

  1. 04O painel
  2. 05Metas
  3. 06Links curtos
  4. 07Origens e agentes de IA
  5. 08Relatórios por email
  6. 09Verificação agendada
  7. 10Pergunte à sua IA

Referência

  1. 11Configuração
  2. 12API HTTP

Plataformas

  1. 13Servidor independente
  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

Confiança

  1. 26Privacidade

Plataformas

Go

O Runlight roda em Go 1.25 ou mais recente, dentro de qualquer app net/http, chi ou Echo, ou como servidor independente.

O módulo Go é o Runlight reescrito em Go. 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 Go 1.25 ou mais recente, e o módulo em si não depende de nenhum outro módulo.

Instalação

Adicione o módulo com go get, junto com o driver do seu banco de dados.

bash

go get runlight.sh/go
go get modernc.org/sqlite

O Runlight fala com o banco de dados por database/sql, então qualquer driver funciona. Os testes rodam com modernc.org/sqlite para SQLite, github.com/jackc/pgx/v5/stdlib para Postgres e github.com/go-sql-driver/mysql para MySQL e MariaDB.

Crie a instância

Abra o banco de dados, envolva-o num store e crie uma instância para o app inteiro.

analytics.go

package main

import (
	"database/sql"

	_ "modernc.org/sqlite"
	runlight "runlight.sh/go"
)

func newRunlight() (*runlight.Runlight, error) {
	db, err := sql.Open("sqlite", "data/runlight.db")
	if err != nil {
		return nil, err
	}
	return runlight.New(runlight.Options{
		Store: runlight.NewStore(runlight.SQLite(db).Owned(0)),
		Site:  &runlight.SiteOptions{Name: "example.com", Hostnames: []string{"example.com"}, Timezone: "Europe/London"},
	})
}

As opções têm os mesmos nomes de Configuração, escritos como campos Go, como Sites, IgnoreProxy, RateLimit, Mail e ManagedSites. Todo método que lê ou escreve recebe um context.Context como primeiro argumento, e as tabelas são criadas na primeira requisição.

Stores

Owned entrega a conexão ao Runlight, que a fecha quando o store é fechado. Deixe de fora para compartilhar um *sql.DB que o seu app já tem, e então o Runlight nunca o fecha.

As tabelas do Runlight começam com rl_. runlight.NewStoreWithPrefix(runlight.SQLite(db).Owned(0), "wp_rl_") usa outro prefixo no lugar, 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, e store.Tables() lista as tabelas do store pelo nome, para um app que esteja removendo o Runlight.

SQLite

go

db, err := sql.Open("sqlite", "data/runlight.db")
store := runlight.NewStore(runlight.SQLite(db).Owned(0))

O Runlight mantém uma conexão SQLite e a coloca em modo WAL com um busy timeout. 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.

Postgres

go

db, err := sql.Open("pgx", "postgres://runlight:password@127.0.0.1:5432/runlight?statement_timeout=120000")
store := runlight.NewStore(runlight.Postgres(db).Owned(0))

Importe github.com/jackc/pgx/v5/stdlib para ter o driver pgx. O parâmetro statement_timeout interrompe qualquer comando depois desse número de milissegundos, como a biblioteca TypeScript faz depois de 120000. Para manter as tabelas num schema próprio, adicione search_path=analytics à URL. O pool precisa de pelo menos duas conexões.

MySQL e MariaDB

go

db, err := sql.Open("mysql", "runlight:password@tcp(127.0.0.1:3306)/runlight?charset=utf8mb4&loc=UTC")
store := runlight.NewStore(runlight.MySQL(db).Owned(120000))

Este store funciona com MySQL 8.0.20 ou mais recente e MariaDB 11.4 ou mais recente. O número passado para Owned é o timeout de comando de cada sessão em milissegundos, e 0 o desativa. O pool precisa de pelo menos duas conexões.

net/http

As rotas são um http.Handler. Monte-as no caminho base delas, /runlight por padrão, e adicione os links curtos em /go/{slug}.

main.go

rl, err := newRunlight()
if err != nil {
	log.Fatal(err)
}
routes, err := rl.Routes(runlight.RoutesOptions{})
if err != nil {
	log.Fatal(err)
}

mux := http.NewServeMux()
mux.Handle("/runlight", routes)
mux.Handle("/runlight/", routes)
mux.Handle("GET /go/{slug}", rl.LinksHTTP())
mux.Handle("/", yourApp)

log.Fatal(http.ListenAndServe(":8080", rl.LinkDomains(rl.Observer(mux))))

rl.Observer registra as leituras de agentes de IA enquanto o seu app serve as páginas dele, e passa todas as requisições adiante. rl.LinkDomains responde aos links curtos nos seus domínios de links, os nomes próprios que você aponta para o app, antes que o mux veja a requisição. Ele passa todos os outros hosts adiante sem mexer neles, então as suas próprias páginas não são afetadas. Num app que manda todas as requisições por um único handler, routes.Middleware(next) responde aos domínios de links e aos caminhos do próprio Runlight, e passa o resto para next sem ler o corpo. Ele também responde aos documentos OAuth que os clientes MCP procuram na raiz do site.

rl.LinksHTTP() responde a um link curto que ninguém tem com um 404 simples. Para mostrar a sua própria página 404 ali, use rl.LinksHTTPOr(notFound), que entrega essas requisições ao seu handler notFound. Num domínio de links, /go/{slug} é um 404 como qualquer outro caminho sem link.

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. RoutesOptions aceita as mesmas configurações que routes() em Configuração, então Token, Authorize, BasePath e Origin funcionam como lá. Com Accounts: true e um RUNLIGHT_SECRET, as pessoas entram com o próprio e-mail e senha, como em Contas.

chi

O adaptador para chi é um módulo à parte, então o chi nunca é uma exigência do módulo principal.

bash

go get runlight.sh/go/chi

go

r := chi.NewRouter()
r.Use(rl.Observer)
runlightchi.Mount(r, rl, routes)

Mount adiciona as rotas sob o caminho base delas, os documentos OAuth e os links curtos, e responde aos domínios de links antes das suas próprias rotas. Chame-o no router principal antes das suas rotas, já que as rotas leem o caminho completo de cada requisição e o chi só aceita middleware até a primeira rota. Chamado depois de uma rota, ele entra em pânico com esse conselho, porque senão um domínio de links perderia cada link curto cujo caminho uma das suas rotas atende. Quando as suas rotas precisam vir primeiro, envolva o router inteiro em rl.LinkDomains e chame runlightchi.MountRoutes, que adiciona o resto. Um link curto que ninguém tem vai para o handler NotFound do router, então a sua própria página 404 responde a ele.

Echo

O adaptador para Echo também é um módulo à parte, e funciona com o Echo v5.

bash

go get runlight.sh/go/echo

go

e := echo.New()
e.Use(runlightecho.Observer(rl))
runlightecho.Register(e, rl, routes)

Register adiciona as rotas, os documentos OAuth e os links curtos, como Mount faz para o chi. Ele responde aos domínios de links antes que o Echo roteie cada requisição, em qualquer ponto do app em que for chamado. Um link curto que ninguém tem devolve echo.ErrNotFound, então o seu tratador de erros responde a ele como responde a qualquer página que o app não tem.

Outros routers

Qualquer router que aceite um http.Handler consegue servir o Runlight do mesmo jeito que o http.ServeMux, envolvido em rl.LinkDomains para os domínios de links. Para responder você mesmo a uma requisição, transforme-a numa requisição do Runlight com runlight.FromHTTP, passe-a para routes.Handle e envie a resposta de volta com runlight.WriteHTTP. FromHTTP lê o corpo com os limites que o adaptador do Node usa, 16 KB para o script de rastreamento e 10 MB para todo o resto, e guarda o endereço de onde veio a conexão. O Runlight lê os cabeçalhos de encaminhamento por conta própria, a menos que IgnoreProxy esteja definido.

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. Chame rl.Check(ctx) a cada poucos minutos de uma goroutine com um time.Ticker. Quando nada no app roda num timer, defina CRON_SECRET e faça um agendador chamar /runlight/api/check com ele, como mostra Verificação agendada.

O servidor independente

O comando runlight é o servidor independente em Go. Ele 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. Instale e rode.

bash

go install runlight.sh/go/cmd/runlight@latest
runlight

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.

Para rodar o servidor dentro de um programa seu, runlight.sh/go/server o oferece como um http.Handler, com server.New recebendo as mesmas configurações como campos.

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 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.

As tabelas se atualizam sozinhas quando o servidor inicia, então uma atualização só precisa de um novo build 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

runlight password someone@example.com

Agentes 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 Go com o Runlight dentro os conta onde a página é servida, com rl.Observer. 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 --follow

A 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

O módulo Go 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 Go roda.

  • As requisições rodam em goroutines, e a instância é segura para muitas ao mesmo tempo. O SQLite mantém uma conexão, que todas as requisições compartilham sob um lock, e o Postgres e o MySQL usam o pool que o database/sql mantém.
  • O que a biblioteca TypeScript faz em segundo plano, como apagar visitas depois que um período de retenção menor é escolhido, roda numa goroutine ao lado da resposta. rl.Idle() espera por ela, e um programa pode chamá-lo antes de sair.
  • Os fusos horários vêm do banco de dados de fusos do próprio Go. Um programa compilado para um sistema que não tem um importa time/tzdata.
  • O texto é passado para maiúsculas ou minúsculas com tabelas feitas a partir das do Node, então os nomes se comparam do mesmo jeito nos dois. Os sites são listados por nome com as regras principais da ordem padrão do Unicode, então de vez em quando um nome numa escrita mais rara fica uma posição distante de onde o Node o coloca.
  • 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 em Go. Os hashes são iguais aos da biblioteca TypeScript, então as contas continuam valendo.