Plattformen
Python
Runlight läuft mit Python 3.11 oder neuer, in einer Django-, Flask- oder FastAPI-App oder jeder WSGI- oder ASGI-App, oder als eigenständiger Server.
Das Python-Paket ist Runlight, noch einmal in Python 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 Python 3.11 oder neuer und hat keine eigenen Abhängigkeiten.
Installieren
Füge das Paket mit pip hinzu.
bash
pip install runlightSQLite bringt Python mit. Für Postgres installierst du das Extra postgres, das psycopg 3 hinzufügt, und für MySQL oder MariaDB das Extra mysql, das PyMySQL hinzufügt.
bash
pip install "runlight[postgres]"
pip install "runlight[mysql]"Zeitzonen kommen aus der Zeitzonendatenbank des Systems. Windows hat keine, füge dort also das Extra tzdata hinzu.
Die Instanz erstellen
Leg ein Modul an, das die Instanz erstellt, und importiere es überall, wo du Runlight brauchst.
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"},
)Die Optionen heißen genauso wie unter Konfiguration. Übergib sie als Keyword-Argumente in Snake Case, etwa trust_proxy=False und rate_limit=60, oder als ein einziges Dict mit den TypeScript-Namen, etwa Runlight({"store": ..., "trustProxy": False}). Die Tabellen werden bei der ersten Anfrage angelegt.
Stores
Runlights Tabellen beginnen mit rl_. Jeder Store nimmt prefix, das stattdessen verwendet wird, etwa Stores.sqlite(path, prefix="wp_rl_"), 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. store.tables() listet die Tabellen des Stores mit Namen auf, für eine App, die Runlight entfernt.
SQLite
python
Stores.sqlite("data/runlight.db")Leg die Datei außerhalb jedes Ordners ab, den dein Webserver ausliefert, in einem Ordner, in den die App schreiben darf, weil SQLite daneben ein Journal schreibt. Fehlt der Ordner, wird er angelegt.
Postgres
python
Stores.postgres("postgres://runlight:password@127.0.0.1:5432/runlight")statement_timeout bricht jede einzelne Anweisung nach so vielen Millisekunden ab, Standard ist 120000. schema legt die Tabellen in ein eigenes Schema, wie in Stores.postgres(url, schema="analytics"). Statt einer URL kannst du eine psycopg-Verbindung übergeben, die deine App schon hat. Runlight nutzt sie, wie sie ist, und schließt sie nie.
MySQL und MariaDB
python
Stores.mysql("mysql://runlight:password@127.0.0.1:3306/runlight")Dieser Store funktioniert mit MySQL 8.0.20 oder neuer und MariaDB 11.4 oder neuer. Eine mariadb://-URL funktioniert auch. Er nimmt statement_timeout wie Postgres, was MySQL auf Lesezugriffe anwendet und MariaDB auf jede Anweisung, wie unter Konfiguration.
Stores.url() wählt den Store anhand einer DATABASE_URL, die mit postgres://, mysql://, mariadb:// oder sqlite: beginnt.
Django
Füge Runlights App und seine Middleware zu deinen Einstellungen hinzu und nenne die Instanz in RUNLIGHT. Die Middleware kommt an erster Stelle, damit keine andere Middleware Runlights Anfragen oder Antworten verändert.
settings.py
INSTALLED_APPS = [
# ...
"runlight.django",
]
MIDDLEWARE = [
"runlight.django.RunlightMiddleware",
# ...
]
RUNLIGHT = "myproject.analytics.rl"RUNLIGHT enthält die Instanz selbst oder den gepunkteten Pfad zu ihr, und ein Pfad zu einer Funktion, die eine erstellt, funktioniert auch. RUNLIGHT_ROUTES übergibt Optionen an die Routen, mit ihren TypeScript-Namen, etwa {"accounts": True}. Die Middleware liefert das Dashboard und die API unter /runlight aus, Kurzlinks unter /go und jede Link-Domain, die du unter Einstellungen hinzufügst, noch vor den eigenen URLs von Django. Jede Link-Domain muss außerdem in ALLOWED_HOSTS stehen.
Führe den stündlichen Check mit dem Management-Befehl über Cron aus.
bash
*/5 * * * * cd /srv/myproject && python manage.py runlight_checkFlask
Übergib die App und die Instanz an init_app, das Runlight vor die eigenen Views der App setzt. Es antwortet unter /runlight, unter /go und auf jeder Link-Domain, die du unter Einstellungen hinzufügst, und gibt alles andere an deine App weiter.
app.py
from flask import Flask
import runlight.flask
from myproject.analytics import rl
app = Flask(__name__)
runlight.flask.init_app(app, rl)Optionen für die Routen übergibst du als Keyword-Argumente, etwa runlight.flask.init_app(app, rl, accounts=True). init_app fügt außerdem einen Befehl hinzu, der den stündlichen Check ausführt, für Cron.
bash
*/5 * * * * cd /srv/myproject && flask --app app runlight checkFastAPI
init_app fügt Runlight als Middleware vor den Routen der App ein, sodass es unter /runlight, unter /go und auf jeder Link-Domain antwortet, die du unter Einstellungen hinzufügst. Es funktioniert auch für Starlette-Apps.
main.py
from fastapi import FastAPI
import runlight.fastapi
from myproject.analytics import rl
app = FastAPI()
runlight.fastapi.init_app(app, rl)Runlights eigene Arbeit läuft in Worker-Threads, sie hält also nie den Event Loop auf. Für den stündlichen Check rufst du rl.check() aus einem Skript auf, das Cron ausführt, oder du setzt CRON_SECRET und rufst den Check über HTTP auf.
Jede WSGI- oder ASGI-App
Bottle, Pyramid und jede andere WSGI-App nehmen die WSGI-Middleware.
python
from runlight.wsgi import RunlightMiddleware
application = RunlightMiddleware(application, rl)Quart, Django unter ASGI und jede andere ASGI-App nehmen die ASGI-Middleware, die die Instanz als runlight= entgegennimmt.
python
from runlight.asgi import RunlightMiddleware
application = RunlightMiddleware(application, runlight=rl)Beide antworten unter /runlight, unter /go und auf jeder Link-Domain und geben alles andere an deine App weiter. Optionen für die Routen übergibst du als Keyword-Argumente, wie bei Flask. Um Runlight als eigene App zu betreiben, ohne eine von deinen dahinter, nimm runlight.wsgi.app(rl) mit einem WSGI-Server wie gunicorn oder runlight.asgi.app(rl) mit einem ASGI-Server wie uvicorn. Pfade außerhalb von Runlight antworten dann mit 404.
Ein Kurzlink unter /go/<name>, zu dem es keinen Link dieses Namens gibt, geht weiter an deine App, sodass deine eigene 404-Seite antwortet, ob die von Django, Flask, FastAPI oder deiner WSGI- oder ASGI-App. runlight.wsgi.app() und runlight.asgi.app() haben keine App dahinter und antworten selbst mit einem 404. Auf einer Link-Domain ist /go/<name> ein 404 wie jeder andere Pfad, der keiner der Links dieser Domain ist.
Egal, wie du es hinzufügst, setze das Skript in jede Seite, 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.
KI-Agenten
Die Middleware zeichnet die Abrufe von KI-Agenten auf, während deine App ihre Seiten ausliefert, und lässt jede andere Anfrage unverändert durch. Übergib observe=False an init_app oder an die Middleware, um das auszuschalten, oder setze in Django "observe": False in RUNLIGHT_ROUTES. Ohne Middleware rufst du für jede Seitenanfrage rl.observe(request) mit einem runlight.http.Request auf.
Der stündliche Check
Der stündliche Check 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. Führe ihn alle paar Minuten aus, mit python manage.py runlight_check in Django, flask runlight check in Flask oder rl.check() aus einem beliebigen Skript. Wenn nichts einen Befehl nach Zeitplan ausführen kann, setze CRON_SECRET und lass einen Scheduler damit /runlight/api/check aufrufen, wie Stündlicher Check zeigt.
Der eigenständige Server
Das Paket enthält den eigenständigen Server, der Runlight auf einer eigenen Domain betreibt, etwa stats.example.com, mit dem Dashboard an der Wurzel, im Dashboard hinzugefügten Websites, Konten mit Anmeldung und Kurzlinks auf jeder Domain, die du darauf zeigen lässt. Installiere das Paket und führe den Befehl runlight aus.
bash
pip install runlight
runlightDer Server lauscht auf Port 3000 und legt seine Daten in einem Ordner namens runlight-data in dem Verzeichnis ab, aus dem du ihn startest. Beim ersten Start gibt er einen Einrichtungslink mit einem Einmalcode aus. Öffne diesen Link und leg dein Konto an, wie es Dein Konto anlegen beschreibt. Der Server führt den stündlichen Check alle fünf Minuten selbst aus und lädt die Ortsdaten des Monats herunter, wie es der Node-Server tut. Betreibe ihn hinter einem Proxy, der HTTPS ergänzt, wie unter Ins Internet stellen.
Einstellungen
Der Server liest seine Einstellungen aus Umgebungsvariablen.
| Variable | Was sie tut |
|---|---|
PORT |
Der Port, auf dem er lauscht. Standard ist 3000. |
HOST |
Die Adresse, auf der er lauscht. Standard ist 0.0.0.0. |
DATA_DIR |
Der Ordner für die SQLite-Datei, das Geheimnis und die Ortsdaten. Standard ist ./runlight-data. |
DATABASE_URL |
Eine postgres://-Adresse speichert die Daten in Postgres statt in SQLite, und eine mysql://- oder mariadb://-Adresse speichert sie in MySQL oder MariaDB. |
RUNLIGHT_SECRET |
Der Schlüssel, der Anmeldungen signiert und gespeicherte Schlüssel verschlüsselt. Ohne ihn erzeugt der Server selbst einen und legt ihn in DATA_DIR ab, lesbar nur für den Benutzer, der ihn ausführt. |
RUNLIGHT_URL |
Die öffentliche Adresse des Servers, etwa https://stats.example.com, die nie zu einer Link-Domain werden kann. E-Mails mit Einladungen und Berichten verlinken darauf. |
RUNLIGHT_TOKEN |
Ein Token, das Skripte als Bearer senden können, zusätzlich zu den im Dashboard erstellten API-Tokens. |
RUNLIGHT_OBSERVE_KEY |
Ein einzelner Schlüssel für die KI-Agenten-Meldungen aller Websites. Der eigene Agenten-Schlüssel jeder Website, angezeigt unter Einstellungen, Einbinden, ist die bessere Wahl. |
TRUST_PROXY |
Auf false setzen, wenn kein Proxy vor dem Server steht, oder auf x-real-ip oder cf-connecting-ip, wenn dieser Header die Adresse des Besuchers enthält. |
RUNLIGHT_GEO |
Woher Orte kommen, wenn kein Header sie liefert, wie unter Orte. Standard ist city, möglich sind auch country, off oder der Pfad zu einer MMDB-Datei. |
CRON_SECRET |
Erlaubt einem Scheduler, den Check unter POST /api/check auszuführen. |
Befehle
runlight check führt den stündlichen Check einmal aus und holt die Ortsdaten des Monats. runlight migrate legt die Tabellen sofort an oder aktualisiert sie. Die Tabellen aktualisieren sich auch beim Start des Servers selbst, für ein Upgrade brauchst du also nur pip install --upgrade runlight und einen Neustart. Sichere den Datenordner, denn ohne das Geheimnis darin lassen sich gespeicherte Schlüssel nicht lesen.
Vergessene Passwörter
Führe dort, wo der Server läuft, den Befehl password aus, um einem Konto ein neues Passwort zu geben, das er ausgibt. Er schaltet außerdem die Zwei-Faktor-Anmeldung für dieses Konto aus und legt das Konto an, wenn es noch keines gibt, als Eigentümer auf einem Server ohne Personen und sonst als Admin.
bash
runlight password someone@example.comKI-Agenten aus einem Log
KI-Agenten wie GPTBot und ClaudeBot rufen Seiten ab, ohne JavaScript auszuführen, deshalb sieht das Script-Tag sie nie. Eine Python-App mit Runlight darin zählt sie dort, wo die Seite ausgeliefert wird, wie KI-Agenten zeigt. Für jede andere Website, die du selbst hostest, stehen sie im Zugriffslog des Webservers, und der Befehl agents liest es.
bash
runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --followDer Schlüssel ist der Agenten-Schlüssel der Website, der in Runlight unter Einstellungen, Einbinden angezeigt wird, und er kann nur Abrufe für diese Website melden. Fehlt --to oder --key, liest der Befehl RUNLIGHT_URL oder RUNLIGHT_OBSERVE_KEY aus der Umgebung. Der Befehl funktioniert wie beim eigenständigen Server, mit --follow, um weiterzulaufen, und --state, um dort weiterzumachen, wo der letzte Lauf aufgehört hat. Nur erfolgreiche Seitenabrufe bekannter KI-Agenten verlassen die Maschine, und die Adressen der Besucher bleiben, wo sie sind.
Was anders ist als bei der TypeScript-Bibliothek
Das Python-Paket besteht dieselben Konformitätstests wie die TypeScript-Bibliothek, auf SQLite, Postgres, MySQL und MariaDB. Die Unterschiede kommen daher, wie Python läuft.
- Runlights Arbeit ist synchron und läuft in Threads. Die ASGI-Middleware übergibt jede ihrer Anfragen an einen Worker-Thread, sodass ein Event Loop für den Rest deiner App frei bleibt.
- SQLite hält eine Verbindung, die sich alle Threads unter einer Sperre teilen. Postgres und MySQL öffnen eine Verbindung pro Thread, ein Server mit sechzehn Threads hält also bis zu sechzehn Verbindungen.
- Was die TypeScript-Bibliothek im Hintergrund erledigt, etwa das Löschen von Besuchen, nachdem eine kürzere Aufbewahrungsdauer gewählt wurde, läuft, sobald die Antwort gesendet ist.
- Das Rate-Limit des Trackers und die zwischengespeicherten Listen von Link-Domains und Website-Symbolen liegen im Arbeitsspeicher, jeder Prozess hat also seine eigenen. Ein Server mit mehreren Worker-Prozessen gewährt jeder Adresse das Limit einmal pro Prozess. Die Limits für falsche Passwörter liegen in der Datenbank, deshalb teilen sich alle Prozesse sie.
- Passwörter werden mit scrypt über Pythons
hashlibgehasht. Die Hashes stimmen mit denen der TypeScript-Bibliothek überein, Konten lassen sich also übernehmen.