Gesamte Doku

Start

  1. 01Erste Schritte
  2. 02Installation
  3. 03Tracking

Funktionen

  1. 04Das Dashboard
  2. 05Ziele
  3. 06Kurzlinks
  4. 07KI-Quellen und Agenten
  5. 08E-Mail-Berichte
  6. 09Stündlicher Check
  7. 10Frag deine KI

Referenz

  1. 11Konfiguration
  2. 12HTTP-API

Plattformen

  1. 13Eigenständiger Server
  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

Vertrauen

  1. 26Datenschutz

Plattformen

Go

Runlight läuft mit Go 1.25 oder neuer, in jeder net/http-App, mit chi oder Echo, oder als eigenständiger Server.

Das Go-Modul ist Runlight, noch einmal in Go 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 Go 1.25 oder neuer, und das Modul selbst setzt kein anderes Modul voraus.

Installieren

Füge das Modul mit go get hinzu, zusammen mit dem Treiber für deine Datenbank.

bash

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

Runlight spricht über database/sql mit seiner Datenbank, deshalb funktioniert jeder Treiber. Die Tests laufen mit modernc.org/sqlite für SQLite, github.com/jackc/pgx/v5/stdlib für Postgres und github.com/go-sql-driver/mysql für MySQL und MariaDB.

Die Instanz erstellen

Öffne die Datenbank, verpacke sie in einen Store und erstelle eine Instanz für die ganze App.

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"},
	})
}

Die Optionen heißen genauso wie unter Konfiguration, als Go-Felder geschrieben, etwa Sites, IgnoreProxy, RateLimit, Mail und ManagedSites. Jede Methode, die liest oder schreibt, nimmt als Erstes einen context.Context, und die Tabellen werden bei der ersten Anfrage angelegt.

Stores

Owned übergibt die Verbindung an Runlight, das sie schließt, wenn der Store geschlossen wird. Lass es weg, um eine *sql.DB zu teilen, die deine App schon hat. Runlight schließt sie dann nie.

Runlights Tabellen beginnen mit rl_. runlight.NewStoreWithPrefix(runlight.SQLite(db).Owned(0), "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 store.Tables() listet die Tabellen des Stores mit Namen auf, für eine App, die Runlight entfernt.

SQLite

go

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

Runlight hält eine SQLite-Verbindung und schaltet sie in den WAL-Modus mit einem Busy-Timeout. 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.

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

Importiere github.com/jackc/pgx/v5/stdlib für den Treiber pgx. Der Parameter statement_timeout bricht jede einzelne Anweisung nach so vielen Millisekunden ab, so wie es die TypeScript-Bibliothek nach 120000 tut. Um die Tabellen in einem eigenen Schema zu halten, füge der URL search_path=analytics hinzu. Der Pool braucht mindestens zwei Verbindungen.

MySQL und 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))

Dieser Store funktioniert mit MySQL 8.0.20 oder neuer und MariaDB 11.4 oder neuer. Die Zahl, die du Owned übergibst, ist das Statement-Timeout jeder Sitzung in Millisekunden, und 0 schaltet es aus. Der Pool braucht mindestens zwei Verbindungen.

net/http

Die Routen sind ein http.Handler. Häng sie unter ihrem Basispfad ein, standardmäßig /runlight, und füge die Kurzlinks unter /go/{slug} hinzu.

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 zeichnet die Abrufe von KI-Agenten auf, während deine App ihre Seiten ausliefert, und reicht jede Anfrage weiter. rl.LinkDomains beantwortet Kurzlinks auf deinen Link-Domains, den eigenen Namen, die du auf die App zeigen lässt, bevor der Mux die Anfrage sieht. Jeden anderen Host reicht es unverändert weiter, deine eigenen Seiten sind also nicht betroffen. In einer App, die jede Anfrage durch einen einzigen Handler schickt, beantwortet routes.Middleware(next) Link-Domains und Runlights eigene Pfade und gibt den Rest an next weiter, ohne deren Body zu lesen. Es beantwortet auch die OAuth-Dokumente, die MCP-Clients an der Wurzel der Website suchen.

rl.LinksHTTP() beantwortet einen Kurzlink, den es nicht gibt, mit einem schlichten 404. Um dort deine eigene 404-Seite zu zeigen, nimm rl.LinksHTTPOr(notFound), das solche Anfragen an deinen Handler notFound gibt. Auf einer Link-Domain ist /go/{slug} ein 404 wie jeder andere Pfad ohne Link.

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. RoutesOptions nimmt dieselben Einstellungen wie routes() unter Konfiguration, deshalb funktionieren Token, Authorize, BasePath und Origin so wie dort. Mit Accounts: true und einem RUNLIGHT_SECRET melden sich Leute mit ihrer eigenen E-Mail-Adresse und ihrem Passwort an, wie unter Konten.

chi

Der Adapter für chi ist ein eigenes Modul, chi wird also nie zur Voraussetzung für das Kernmodul.

bash

go get runlight.sh/go/chi

go

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

Mount fügt die Routen unter ihrem Basispfad, die OAuth-Dokumente und die Kurzlinks hinzu, und es beantwortet Link-Domains vor deinen eigenen Routen. Ruf es auf dem obersten Router vor deinen eigenen Routen auf, weil die Routen den vollständigen Pfad jeder Anfrage lesen und chi Middleware nur bis zur ersten Route annimmt. Nach einer Route aufgerufen, löst es mit diesem Hinweis eine Panic aus, weil eine Link-Domain sonst jeden Kurzlink verlöre, dessen Pfad eine deiner Routen trifft. Wenn deine Routen zuerst kommen müssen, leg rl.LinkDomains um den ganzen Router und ruf runlightchi.MountRoutes auf, das den Rest hinzufügt. Ein Kurzlink, den es nicht gibt, geht an den NotFound-Handler des Routers, sodass deine eigene 404-Seite antwortet.

Echo

Auch der Adapter für Echo ist ein eigenes Modul, und er funktioniert mit Echo v5.

bash

go get runlight.sh/go/echo

go

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

Register fügt die Routen, die OAuth-Dokumente und die Kurzlinks hinzu, so wie Mount es für chi tut. Es beantwortet Link-Domains, bevor Echo eine Anfrage routet, egal wo in der App es aufgerufen wird. Ein Kurzlink, den es nicht gibt, gibt echo.ErrNotFound zurück, sodass dein Fehler-Handler ihn so beantwortet wie jede Seite, die die App nicht hat.

Andere Router

Jeder Router, der einen http.Handler annimmt, kann Runlight genauso ausliefern wie http.ServeMux, für Link-Domains umhüllt von rl.LinkDomains. Um eine Anfrage selbst zu beantworten, wandle sie mit runlight.FromHTTP in eine von Runlight um, übergib sie an routes.Handle und schick die Antwort mit runlight.WriteHTTP zurück. FromHTTP liest den Body mit den Limits, die der Node-Adapter verwendet, 16 KB für den Tracker und 10 MB für alles andere, und behält die Adresse, von der die Verbindung kam. Weitergeleitete Header liest Runlight selbst, außer IgnoreProxy ist gesetzt.

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. Ruf rl.Check(ctx) alle paar Minuten aus einer Goroutine mit einem time.Ticker auf. Wenn in der App nichts nach Zeitplan läuft, setze CRON_SECRET und lass einen Scheduler damit /runlight/api/check aufrufen, wie Stündlicher Check zeigt.

Der eigenständige Server

Der Befehl runlight ist der eigenständige Server in Go. Er betreibt Runlight auf einer eigenen Domain, 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 ihn und führe ihn aus.

bash

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

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

Um den Server in einem eigenen Programm zu betreiben, stellt runlight.sh/go/server ihn als http.Handler bereit, wobei server.New dieselben Einstellungen als Felder nimmt.

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.

Die Tabellen aktualisieren sich beim Start des Servers selbst, für ein Upgrade brauchst du also nur einen neuen Build 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.com

KI-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 Go-App mit Runlight darin zählt sie dort, wo die Seite ausgeliefert wird, mit rl.Observer. 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 --follow

Der 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 Go-Modul besteht dieselben Konformitätstests wie die TypeScript-Bibliothek, auf SQLite, Postgres, MySQL und MariaDB. Die Unterschiede kommen daher, wie Go läuft.

  • Anfragen laufen in Goroutinen, und die Instanz verträgt viele gleichzeitig. SQLite hält eine Verbindung, die sich alle Anfragen unter einer Sperre teilen, und Postgres und MySQL nutzen den Pool, den database/sql verwaltet.
  • Was die TypeScript-Bibliothek im Hintergrund erledigt, etwa das Löschen von Besuchen, nachdem eine kürzere Aufbewahrungsdauer gewählt wurde, läuft in einer Goroutine neben der Antwort. rl.Idle() wartet darauf, was ein Programm vor dem Beenden aufrufen kann.
  • Zeitzonen kommen aus Gos eigener Zeitzonendatenbank. Ein Programm, das für ein System ohne eine solche gebaut wird, importiert time/tzdata.
  • Text wird mit Tabellen, die aus denen von Node erstellt sind, in Groß- oder Kleinbuchstaben umgewandelt, sodass Namen in beiden gleich verglichen werden. Websites werden nach Namen mit den Hauptregeln der Standardsortierung von Unicode aufgelistet, deshalb landet ab und zu ein Name in einer selteneren Schrift einen Platz neben der Stelle, an die Node ihn setzt.
  • Das Rate-Limit des Trackers und die zwischengespeicherten Listen von Link-Domains und Website-Symbolen liegen im Arbeitsspeicher, jeder Prozess hat also seine eigenen. Die Limits für falsche Passwörter liegen in der Datenbank, deshalb teilen sich alle Prozesse sie.
  • Passwörter werden in Go mit scrypt gehasht. Die Hashes stimmen mit denen der TypeScript-Bibliothek überein, Konten lassen sich also übernehmen.