Plattformen
Java
Runlight läuft mit Java 21 oder neuer, auf dem eigenen HTTP-Server des JDK, in einem Servlet-Container oder in einer Spring-Boot-App, oder als eigenständiger Server.
Die Java-Bibliothek ist Runlight, noch einmal in Java geschrieben. Sie liefert dasselbe Dashboard und dieselbe API aus und gibt auf jede Anfrage die Antwort, die auch die TypeScript-Bibliothek gibt. Ihre Zahlen landen in denselben Tabellen, deshalb kann jede der beiden eine Datenbank lesen, die die andere geschrieben hat. Sie braucht Java 21 oder neuer, und der Kern hat keine eigenen Abhängigkeiten.
Installieren
Füge den Kern zu deinem Build hinzu.
pom.xml
<dependency>
<groupId>sh.runlight</groupId>
<artifactId>runlight</artifactId>
<version>0.1.1</version>
</dependency>build.gradle.kts
implementation("sh.runlight:runlight:0.1.1")Runlight erreicht deine Datenbank über JDBC, den Treiber bringt also deine App mit. Das ist org.xerial:sqlite-jdbc für SQLite, org.postgresql:postgresql für Postgres und com.mysql:mysql-connector-j oder org.mariadb.jdbc:mariadb-java-client für MySQL und MariaDB. Zwei weitere Artefakte binden Runlight in ein Framework ein. sh.runlight:runlight-servlet liefert es in Tomcat 10.1, Jetty 12 oder jedem anderen Container für Jakarta Servlet 6 aus, und sh.runlight:runlight-spring-boot-starter richtet es in Spring Boot 3.5 oder 4 ein.
Die Instanz erstellen
Erstelle beim Start deiner App eine Instanz und behalte sie, solange die App läuft.
Analytics.java
import java.util.List;
import java.util.Map;
import sh.runlight.Runlight;
import sh.runlight.store.Stores;
Runlight rl = new Runlight(new Runlight.Options()
.store(Stores.sqlite("data/runlight.db"))
.site(Map.of("name", "example.com", "hostnames", List.of("example.com"), "timezone", "Europe/London")));Die Optionen heißen genauso wie unter Konfiguration, als Setter auf Runlight.Options, deshalb funktionieren sites, trustProxy, rateLimit, mail und managedSites alle so wie dort. Eine Instanz bedient alle Threads deiner App gleichzeitig. Die Tabellen werden bei der ersten Anfrage angelegt.
Stores
Runlights Tabellen beginnen mit rl_. Jede Store-Methode hat eine Variante, die als letztes Argument ein Präfix nimmt, das stattdessen verwendet wird, etwa Stores.sqlite(path, "wp_rl_") oder Stores.dataSource(dataSource, dialect, "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
java
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
java
Stores.postgres("postgres://runlight:password@127.0.0.1:5432/runlight");Jede einzelne Anweisung wird nach zwei Minuten abgebrochen. Stores.postgres(url, 30_000, "analytics") setzt ein anderes Limit in Millisekunden und legt die Tabellen in ein eigenes Schema.
MySQL und MariaDB
java
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. Stores.mysql(url, 30_000) setzt das Statement-Timeout, das MySQL auf Lesezugriffe anwendet und MariaDB auf jede Anweisung, wie unter Konfiguration.
Die eigene DataSource deiner App
java
Stores.dataSource(dataSource);Runlight leiht sich für jede Anweisung eine Verbindung aus dem Pool deiner App und gibt sie zurück, und seine Tabellen beginnen alle mit rl_, sodass sie in der eigenen Datenbank deiner App liegen können. Stores.url() wählt den Store anhand einer DATABASE_URL, die mit postgres://, mysql://, mariadb:// oder sqlite: beginnt.
Der eigene HTTP-Server des JDK
Eine App auf com.sun.net.httpserver.HttpServer hängt das Dashboard, die API und den Tracker unter /runlight ein.
Main.java
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import sh.runlight.Routes;
import sh.runlight.server.JdkServer;
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
var routes = rl.routes(new Routes.Options().basePath("/runlight"));
var app = JdkServer.mount(server, rl, routes, yourHandler);
app.getFilters().add(JdkServer.observer(rl));
server.start();Gib dem Server einen Executor, weil die Routen die Datenbank lesen und der Standard des JDK jede Anfrage auf einem einzigen Thread ausführt. mount beantwortet außerdem Kurzlinks unter /go/ und liefert deinen Handler unter / aus, hinter einem Filter, der unter Einstellungen hinzugefügte Link-Domains beantwortet, sodass eine Anfrage auf jedem anderen Host deine App unverändert erreicht. Der Filter observer zeichnet die Abrufe von KI-Agenten auf, während deine App ihre Seiten ausliefert.
Servlet-Container
Füge sh.runlight:runlight-servlet hinzu und registriere dann das Servlet unter /runlight/* und den Filter auf jedem Pfad, in einem ServletContainerInitializer oder einem Listener.
java
import sh.runlight.servlet.RunlightFilter;
import sh.runlight.servlet.RunlightServlet;
context.addServlet("runlight", new RunlightServlet(rl)).addMapping("/runlight/*");
context.addFilter("runlight-links", new RunlightFilter(rl)).addMappingForUrlPatterns(null, false, "/*");Das Servlet übernimmt seinen Basispfad aus seinem Mapping. Der Filter beantwortet Kurzlinks und Link-Domains und zeichnet Abrufe von KI-Agenten auf, und er liest nie einen Request-Body, der bleibt deiner App. Link-Domains brauchen die App im Root-Kontext, weil ihre Kürzel aus dem ganzen Pfad gelesen werden.
Spring Boot
Füge sh.runlight:runlight-spring-boot-starter hinzu und richte Runlight in application.properties ein.
application.properties
runlight.site.name=example.com
runlight.site.hostnames=example.com
runlight.site.timezone=Europe/London
runlight.token=${RUNLIGHT_TOKEN}Der Starter legt seine Tabellen in die eigene DataSource deiner App oder in die Datenbank, die runlight.database-url nennt. Er liefert die Routen unter runlight.base-path (/runlight) und Kurzlinks unter runlight.link-path (/go) aus, er beantwortet unter Einstellungen hinzugefügte Link-Domains vor dem eigenen Routing deiner App, und er führt den stündlichen Check jede Minute aus. Seine Filter laufen vor denen von Spring Security. Setze runlight.open=true, um das Dashboard ohne Token auszuliefern, hinter der eigenen Anmeldung deiner App, dann rückt sein Filter hinter Spring Security, damit deine Regeln ihn schützen. Eine eigene Bean vom Typ Runlight oder Routes tritt an die Stelle der vom Starter erstellten. Die übrigen Einstellungen sind runlight.sites[0].*, managed-sites, secret, trust-proxy, rate-limit, origin, accounts, cron-secret, observe-key, observe, check.every und web.enabled.
Der Filter und der Starter beantworten einen Kurzlink unter /go/<name> nur, wenn es den Link gibt. Ein Name ohne Link geht weiter an deine App, sodass deine eigene 404-Seite antwortet, oder die Fehlerseite von Spring Boot, wenn du keine hast. Auf dem Server des JDK gibt mount den Link-Pfad an Runlight, das diesen Namen selbst mit einem 404 beantwortet, so wie der eigenständige Server. Auf einer Link-Domain ist /go/<name> ein 404 wie jeder andere Pfad, der keiner der Links dieser Domain ist.
Das Skript einbinden
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) in Routes.Options und einem RUNLIGHT_SECRET melden sich Leute mit ihrer eigenen E-Mail-Adresse und ihrem Passwort an, wie unter Konten.
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. Der Spring-Boot-Starter führt ihn für dich aus. Überall sonst rufst du rl.check() alle paar Minuten aus einem ScheduledExecutorService auf, oder du setzt CRON_SECRET und lässt einen Scheduler damit /runlight/api/check aufrufen, wie Stündlicher Check zeigt.
Der eigenständige Server
Das Kern-Jar 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. Lade das Jar runlight und den JDBC-Treiber deiner Datenbank von Maven Central herunter, leg sie nebeneinander und starte ihn.
bash
java -cp runlight.jar:sqlite-jdbc.jar sh.runlight.server.CliUnter Windows trennst du die Jars mit ;. 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.
Einstellungen
Der Server liest seine Einstellungen aus Umgebungsvariablen oder aus Zeilen wie PORT=8080 in runlight.properties in dem Verzeichnis, aus dem du ihn startest. Die Umgebung hat Vorrang vor der Datei, und --config nennt eine andere Datei.
| 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. Leg den Treiber dieser Datenbank in den Classpath. |
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
Jeder Befehl steht hinter sh.runlight.server.Cli. cron führt den stündlichen Check einmal aus und holt die Ortsdaten des Monats, und 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 das neue Jar 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
java -cp runlight.jar:sqlite-jdbc.jar sh.runlight.server.Cli 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 Java-App mit Runlight darin zählt sie dort, wo die Seite ausgeliefert wird, so wie die Filter oben. Für jede andere Website, die du selbst hostest, stehen sie im Zugriffslog des Webservers, und der Befehl agents liest es. Er braucht keinen Datenbanktreiber.
bash
java -jar runlight.jar 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
Die Java-Bibliothek besteht dieselben Konformitätstests wie die TypeScript-Bibliothek, auf SQLite, Postgres, MySQL und MariaDB. Die Unterschiede kommen daher, wie Java läuft.
- Runlights Arbeit ist synchron und läuft auf den Threads deines Servers, ob Plattform-Threads oder virtuelle. Eine Instanz lässt sich gefahrlos zwischen ihnen teilen.
- SQLite hält eine Verbindung, die alle Threads nacheinander nutzen. Postgres und MySQL halten einen kleinen eigenen Pool oder leihen sich Verbindungen aus der
DataSourcedeiner App. - 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. Die Adapter rufen dafür
rl.idle()auf, und eine App, die Anfragen auf eigene Weise beantwortet, sollte das auch tun. - 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 mit scrypt gehasht, in Java geschrieben, weil das JDK keines mitbringt. Die Hashes stimmen mit denen der TypeScript-Bibliothek überein, Konten lassen sich also übernehmen.