Plataformas
Elixir
O Runlight roda no Elixir 1.18 ou mais recente, sobre Erlang/OTP 27 ou mais recente, dentro de um app Phoenix ou Plug, com as tabelas no seu próprio repo Ecto.
O pacote Elixir é o Runlight reescrito em Elixir. 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 ficam nas mesmas tabelas, então qualquer um dos dois consegue ler um banco de dados que o outro escreveu. Ele precisa do Elixir 1.18 ou mais recente, sobre Erlang/OTP 27 ou mais recente.
Instalação
Adicione o pacote ao mix.exs, junto com o adapter do Ecto para o seu banco de dados, se o seu app ainda não tiver um.
mix.exs
def deps do
[
{:runlight, "~> 0.1"},
{:ecto_sqlite3, "~> 0.17"}
]
endO Runlight guarda as tabelas no próprio repo Ecto do seu app, por meio de ecto_sqlite3, postgrex ou myxql. O painel dele é um Plug, que todo app Phoenix já tem. Os fusos horários vêm do pacote tz, que é compilado junto, então uma release funciona em qualquer imagem.
Inicie a instância
O Runlight é um filho da árvore de supervisão da sua aplicação, iniciado depois do seu repo.
lib/my_app/application.ex
children = [
MyApp.Repo,
{Runlight,
store: {Runlight.Store, repo: MyApp.Repo},
site: [name: "example.com", hostnames: ["example.com"], timezone: "Europe/London"],
check_every: :timer.hours(1)}
]As opções têm os mesmos nomes de Configuração, escritos em snake case, então sites, trust_proxy, rate_limit, mail e managed_sites funcionam como lá. As tabelas são criadas quando a instância inicia, e um store que não pode ser alcançado impede o app de subir. check_every roda a verificação agendada em um intervalo, o que 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. Deixe-o de fora quando um agendador chamar /runlight/api/check com CRON_SECRET, como mostra Verificação agendada, ou chame Runlight.check(Runlight.instance()) a partir de um job seu.
Stores
O store é o seu repo, e o adapter dele define o SQL que o Runlight escreve. Todas as tabelas do Runlight começam com rl_, então ficam ao lado das suas. prefix: "wp_rl_", como em {Runlight.Store, repo: MyApp.Repo, prefix: "wp_rl_"}, usa outro prefixo no lugar, para que dois apps que dividem um banco de dados mantenham cada um suas próprias tabelas. Um prefixo tem até 32 letras, dígitos e sublinhados, e não começa com um dígito, e Runlight.Store.tables(store) lista as tabelas do store pelo nome, para um app que está removendo o Runlight.
No SQLite, mantenha o journal write-ahead do ecto_sqlite3 e dê ao repo um busy_timeout de pelo menos 5000. Um banco de dados em memória existe um por conexão, então um repo com database: ":memory:" precisa de pool_size: 1.
O Postgres funciona como está. O Runlight cria os índices de forma concorrente, então uma tabela grande continua recebendo escritas enquanto uma atualização roda.
O MySQL precisa da versão 8.0.20 ou mais recente, e o MariaDB da 11.4 ou mais recente, já que as tabelas usam a collation binária utf8mb4_0900_bin para que o texto seja ordenado do mesmo jeito em todo banco de dados.
Phoenix
Encaminhe /runlight para o Plug fora do pipeline :browser. Esse pipeline pede um token CSRF em todo POST, que o script de rastreamento não consegue enviar, e o Runlight verifica as requisições do seu próprio jeito.
lib/my_app_web/router.ex
scope "/" do
forward "/runlight", Runlight.Plug, token: System.get_env("RUNLIGHT_TOKEN", "")
forward "/go", Runlight.Plug.Links
endO token é lido uma vez, quando o router compila. Em uma release, deixe a opção de fora e defina RUNLIGHT_TOKEN onde o app roda, já que o Runlight o lê na primeira requisição. Um token vazio mantém o painel fechado, e token: false o deixa aberto para um app que verifica as pessoas por conta própria.
Adicione o script a todas as páginas, logo antes de </head>.
html
<script defer src="/runlight/s.js"></script>Depois defina RUNLIGHT_TOKEN como uma string aleatória longa e abra uma vez /runlight/?token= seguido dessa string 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.
Para contar os agentes de IA que leem suas páginas, adicione o observer ao pipeline :browser. Ele registra as leituras deles e retorna na hora para todos os outros.
lib/my_app_web/router.ex
pipeline :browser do
plug :accepts, ["html"]
plug Runlight.Plug.Observer
endUm domínio de links responde a links curtos em um nome próprio. Cada Plug do Runlight responde a uma requisição em um domínio de links antes de qualquer outra coisa e repassa todas as outras requisições sem mexer nelas. Um router do Phoenix só os executa para os caminhos que ele reconhece, então coloque o Plug de domínio de links no seu endpoint, antes do router, e toda requisição nesse nome é respondida ali.
lib/my_app_web/endpoint.ex
plug Runlight.Plug.LinkDomains
plug MyAppWeb.RouterUm nome que nenhum link tem fica para o seu app responder. Encaminhado de um router do Phoenix, Runlight.Plug.Links levanta o NoRouteError do próprio Phoenix, então a sua página 404 aparece, e encaminhado de um Plug.Router simples ele responde 404. Você também pode colocar plug Runlight.Plug.Links no seu endpoint antes do router, onde ele substitui tanto o forward quanto o Plug de domínio de links. Ali ele responde aos domínios de links e a /go/<name> para os links que existem, e repassa todas as outras requisições para o seu app, inclusive um nome desconhecido. Num domínio de links, /go/<name> é sempre um 404, já que os links de um domínio de links ficam na raiz do nome dele.
Plug.Router
Um Plug.Router encaminha do mesmo jeito. O Plug de domínio de links vem antes de :match, para ver todas as requisições.
lib/my_app/router.ex
defmodule MyApp.Router do
use Plug.Router
plug Runlight.Plug.LinkDomains
plug :match
plug :dispatch
forward "/runlight", to: Runlight.Plug
forward "/go", to: Runlight.Plug.Links
endAtrás de um proxy
O Runlight lê o endereço do visitante na última entrada de X-Forwarded-For, que o proxy mais próximo escreveu, para que um visitante conte uma vez por dia. Sem nada na frente do app, defina trust_proxy: false e ele lê o endereço da própria conexão. Atrás da Cloudflare e de outro proxy, trust_proxy: "cf-connecting-ip" lê só esse cabeçalho.
Várias instâncias
Dê um nome a cada uma e passe-o para os Plugs.
elixir
{Runlight, name: MyApp.Stats, store: {Runlight.Store, repo: MyApp.Repo}, site: [name: "Docs"]}
forward "/stats", Runlight.Plug, instance: MyApp.StatsDiferenças em relação à biblioteca TypeScript
O pacote Elixir passa nos testes de conformidade aos quais a biblioteca TypeScript é submetida, no SQLite, Postgres, MySQL e MariaDB. As diferenças vêm do jeito como o Elixir roda.
- Cada requisição roda em um processo próprio, e a instância vive na sua árvore de supervisão. O estado dela, como o limite de requisições do script de rastreamento e as listas de domínios de links em cache, fica em uma tabela ETS, então todos os processos de um nó o compartilham e cada nó mantém o seu.
- O que a biblioteca TypeScript faz em segundo plano, como apagar visitas depois que uma retenção menor é escolhida, roda em uma task própria assim que a resposta é enviada.
- As senhas novas recebem hash com PBKDF2-SHA-256, que o crypto do Erlang calcula nativamente. Os hashes que a biblioteca TypeScript fez com scrypt continuam permitindo o login, então as contas continuam valendo.
- Não existe servidor independente em Elixir. Para um único painel com vários sites, rode o servidor Node ou o servidor de outra linguagem, e conecte o site deste app a ele.