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

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

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

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 DataSource deiner 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.