Plattformen
Elixir
Runlight läuft mit Elixir 1.18 oder neuer auf Erlang/OTP 27 oder neuer, in einer Phoenix- oder Plug-App, mit seinen Tabellen in deinem eigenen Ecto-Repo.
Das Elixir-Paket ist Runlight, noch einmal in Elixir geschrieben. Es liefert dasselbe Dashboard und dieselbe API aus und gibt auf jede Anfrage die Antwort, die auch die TypeScript-Bibliothek gibt. Seine Zahlen landen in denselben Tabellen, deshalb kann jede der beiden eine Datenbank lesen, die die andere geschrieben hat. Es braucht Elixir 1.18 oder neuer auf Erlang/OTP 27 oder neuer.
Installieren
Füge das Paket zu mix.exs hinzu, dazu den Ecto-Adapter für deine Datenbank, falls deine App noch keinen hat.
mix.exs
def deps do
[
{:runlight, "~> 0.1"},
{:ecto_sqlite3, "~> 0.17"}
]
endRunlight legt seine Tabellen in das eigene Ecto-Repo deiner App, über ecto_sqlite3, postgrex oder myxql. Sein Dashboard ist ein Plug, und Plug hat jede Phoenix-App schon. Zeitzonen kommen aus dem Paket tz, das mit einkompiliert wird, deshalb funktioniert ein Release auf jedem Image.
Die Instanz starten
Runlight ist ein Kind im Supervision Tree deiner Anwendung und startet nach deinem 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)}
]Die Optionen heißen genauso wie unter Konfiguration, in Snake Case geschrieben, deshalb funktionieren sites, trust_proxy, rate_limit, mail und managed_sites alle so wie dort. Die Tabellen werden beim Start der Instanz angelegt, und ein Store, der nicht erreichbar ist, verhindert den Start der App. check_every führt den stündlichen Check in einem Intervall aus. Er wechselt die täglichen Salts, verschickt fällige E-Mail-Berichte, wendet an, wie lange jede Website ihre Besuche behält, und baut die Rollups, die lange Zeiträume schnell halten. Lass die Option weg, wenn stattdessen ein Scheduler /runlight/api/check mit CRON_SECRET aufruft, wie Stündlicher Check zeigt, oder ruf Runlight.check(Runlight.instance()) aus einem eigenen Job auf.
Stores
Der Store ist dein Repo, und sein Adapter bestimmt, welches SQL Runlight schreibt. Runlights Tabellen beginnen alle mit rl_, sodass sie neben deinen eigenen stehen. prefix: "wp_rl_", wie in {Runlight.Store, repo: MyApp.Repo, prefix: "wp_rl_"}, verwendet stattdessen ein anderes Präfix, damit zwei Apps in einer gemeinsamen Datenbank jeweils ihre eigenen Tabellen behalten. Ein Präfix besteht aus bis zu 32 Buchstaben, Ziffern und Unterstrichen und beginnt nicht mit einer Ziffer, und Runlight.Store.tables(store) listet die Tabellen des Stores mit Namen auf, für eine App, die Runlight entfernt.
Behalte auf SQLite das Write-Ahead-Journal von ecto_sqlite3 bei und gib dem Repo einen busy_timeout von mindestens 5000. Eine In-Memory-Datenbank gibt es einmal pro Verbindung, deshalb braucht ein Repo mit database: ":memory:" die Einstellung pool_size: 1.
Postgres funktioniert, wie es ist. Runlight baut seine Indizes nebenläufig auf, sodass eine große Tabelle weiter Schreibvorgänge annimmt, während ein Upgrade läuft.
MySQL muss 8.0.20 oder neuer sein und MariaDB 11.4 oder neuer, weil die Tabellen die binäre Kollation utf8mb4_0900_bin verwenden, damit Text auf jeder Datenbank gleich sortiert wird.
Phoenix
Leite /runlight außerhalb der Pipeline :browser an den Plug weiter. Diese Pipeline verlangt bei jedem POST ein CSRF-Token, das der Tracker nicht senden kann, und Runlight prüft Anfragen auf seine eigene Weise.
lib/my_app_web/router.ex
scope "/" do
forward "/runlight", Runlight.Plug, token: System.get_env("RUNLIGHT_TOKEN", "")
forward "/go", Runlight.Plug.Links
endDas Token wird einmal gelesen, wenn der Router kompiliert wird. Lass die Option in einem Release weg und setze RUNLIGHT_TOKEN dort, wo die App läuft, denn Runlight liest es bei der ersten Anfrage. Ein leeres Token hält das Dashboard geschlossen, und token: false lässt es offen, für eine App, die Leute selbst prüft.
Füge das Skript in jede Seite ein, direkt vor </head>.
html
<script defer src="/runlight/s.js"></script>Setze dann RUNLIGHT_TOKEN auf eine lange zufällige Zeichenkette und öffne einmal /runlight/?token=, gefolgt von dieser Zeichenkette, um dich anzumelden, wie es Erste Schritte beschreibt. Mit accounts: true und einem RUNLIGHT_SECRET melden sich Leute mit ihrer eigenen E-Mail-Adresse und ihrem Passwort an, wie unter Konten.
Um die KI-Agenten zu zählen, die deine Seiten lesen, füge den Observer zur Pipeline :browser hinzu. Er zeichnet ihre Abrufe auf und kehrt für alle anderen sofort zurück.
lib/my_app_web/router.ex
pipeline :browser do
plug :accepts, ["html"]
plug Runlight.Plug.Observer
endEine Link-Domain beantwortet Kurzlinks unter einem eigenen Namen. Jeder von Runlights Plugs beantwortet eine Anfrage auf einer Link-Domain vor allem anderen und reicht jede andere Anfrage unverändert weiter. Ein Phoenix-Router führt sie nur für Pfade aus, die er erkennt, setze den Plug für Link-Domains also in deinen Endpoint, vor den Router, dann wird jede Anfrage unter diesem Namen dort beantwortet.
lib/my_app_web/endpoint.ex
plug Runlight.Plug.LinkDomains
plug MyAppWeb.RouterEinen Namen, zu dem es keinen Link gibt, beantwortet deine App. Von einem Phoenix-Router weitergeleitet, löst Runlight.Plug.Links den eigenen NoRouteError von Phoenix aus, sodass deine 404-Seite erscheint, und von einem schlichten Plug.Router weitergeleitet, antwortet es mit 404. Du kannst stattdessen plug Runlight.Plug.Links in deinen Endpoint vor den Router setzen, wo es sowohl das forward als auch den Plug für Link-Domains ersetzt. Dort beantwortet es Link-Domains und /go/<name> für Links, die es gibt, und reicht jede andere Anfrage an deine App weiter, auch einen unbekannten Namen. Auf einer Link-Domain ist /go/<name> immer ein 404, weil die Links einer Link-Domain an der Wurzel ihres Namens liegen.
Plug.Router
Ein Plug.Router leitet genauso weiter. Der Plug für Link-Domains kommt vor :match, damit er jede Anfrage sieht.
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
endHinter einem Proxy
Runlight liest die Adresse des Besuchers aus dem letzten Eintrag von X-Forwarded-For, den der nächstgelegene Proxy geschrieben hat, sodass ein Besucher einmal pro Tag zählt. Steht nichts vor der App, setze trust_proxy: false, dann liest es die eigene Adresse der Verbindung. Hinter Cloudflare und einem weiteren Proxy liest trust_proxy: "cf-connecting-ip" nur diesen Header.
Mehrere Instanzen
Gib jeder einen Namen und übergib ihn an die Plugs.
elixir
{Runlight, name: MyApp.Stats, store: {Runlight.Store, repo: MyApp.Repo}, site: [name: "Docs"]}
forward "/stats", Runlight.Plug, instance: MyApp.StatsWas anders ist als bei der TypeScript-Bibliothek
Das Elixir-Paket besteht dieselben Konformitätstests wie die TypeScript-Bibliothek, auf SQLite, Postgres, MySQL und MariaDB. Die Unterschiede kommen daher, wie Elixir läuft.
- Jede Anfrage läuft in einem eigenen Prozess, und die Instanz lebt in deinem Supervision Tree. Ihr Zustand, etwa das Rate-Limit des Trackers und die zwischengespeicherten Listen von Link-Domains, liegt in einer ETS-Tabelle, deshalb teilen sich alle Prozesse auf einem Node ihn, und jeder Node hat seinen eigenen.
- Was die TypeScript-Bibliothek im Hintergrund erledigt, etwa das Löschen von Besuchen, nachdem eine kürzere Aufbewahrungsdauer gewählt wurde, läuft in einem eigenen Task, sobald die Antwort gesendet ist.
- Neue Passwörter werden mit PBKDF2-SHA-256 gehasht, das Erlangs crypto nativ berechnet. Hashes, die die TypeScript-Bibliothek mit scrypt erstellt hat, funktionieren weiterhin bei der Anmeldung, Konten lassen sich also übernehmen.
- Einen eigenständigen Server gibt es in Elixir nicht. Für ein Dashboard über mehrere Websites betreibst du den Node-Server oder den Server einer anderen Sprache und verbindest die Website dieser App damit.