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

.NET

Runlight läuft mit .NET 10 oder neuer, in einer ASP.NET-Core-App oder als eigenständiger Server auf einer eigenen Domain.

Das .NET-Paket ist Runlight, noch einmal in C# 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 .NET 10 oder neuer und den ADO.NET-Treiber für deine Datenbank, den deine App schon hat, wenn sie diese Datenbank nutzt.

Installieren

Füge das Paket für ASP.NET Core und den Treiber für deine Datenbank hinzu. Das Paket bringt das Kernpaket Runlight mit.

bash

dotnet add package Runlight.AspNetCore
dotnet add package Microsoft.Data.Sqlite

Das Kernpaket hat keine eigenen Abhängigkeiten. Es erreicht die Datenbank über den Treiber, den du ihm übergibst, und teilt sich dessen Connection-Pool.

Die Instanz erstellen

Erstelle beim Start der App eine Instanz und registriere sie als Singleton.

Program.cs

using Microsoft.Data.Sqlite;
using Runlight;
using Runlight.Store;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(new Runlight.Runlight(new RunlightOptions
{
    Store = Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db"),
    Site = new SiteOptions { Name = "example.com", Hostnames = ["example.com"], Timezone = "Europe/London" },
}));

Die Optionen tragen die Namen aus Konfiguration, in PascalCase geschrieben, deshalb funktionieren Sites, TrustProxy, RateLimit, Mail und ManagedSites alle so wie dort. Die Tabellen werden bei der ersten Anfrage angelegt.

Stores

Runlights Tabellen beginnen mit rl_. Jeder Store nimmt ein Argument prefix, das stattdessen verwendet wird, etwa Stores.Sqlite(SqliteFactory.Instance, 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

csharp

Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db");

SQLite braucht Microsoft.Data.Sqlite. Leg die Datei in einen Ordner, in den die App schreiben darf, weil SQLite daneben ein Journal schreibt. Fehlt der Ordner, wird er angelegt.

Postgres

csharp

Stores.Postgres(NpgsqlDataSource.Create("Host=127.0.0.1;Username=runlight;Password=password;Database=runlight"));

Dieser Store braucht Npgsql. Übergib die eigene NpgsqlDataSource der App, um ihren Pool zu teilen. Ein zweites Argument wie 30000 bricht jede einzelne Abfrage nach so vielen Millisekunden ab, und Search Path=analytics im Connection-String legt die Tabellen in ein eigenes Schema.

MySQL und MariaDB

csharp

Stores.MySql(new MySqlDataSource("Server=127.0.0.1;User ID=runlight;Password=password;Database=runlight"));

Dieser Store braucht MySqlConnector und funktioniert mit MySQL 8.0.20 oder neuer und MariaDB 11.4 oder neuer. Er nimmt dasselbe zweite Argument wie der Store für Postgres.

ASP.NET Core

Binde Runlight ein, nachdem die App gebaut ist.

Program.cs

var app = builder.Build();

app.MapRunlight();

app.Run();

MapRunlight() liefert das Dashboard, die API und das Skript unter /runlight aus und die Kurzlinks unter /go. Es beantwortet außerdem jede Link-Domain, die du unter Einstellungen hinzufügst, und zeichnet die Abrufe von KI-Agenten auf, während deine Seiten ausgeliefert werden, was für alle anderen keine Zeit kostet. Runlight prüft seine Anfragen selbst, deshalb überspringen seine Endpunkte die Antiforgery-Prüfung von ASP.NET Core, die den Tracker abweisen würde. Sie erlauben anonyme Anfragen, sodass auch eine App, deren Fallback-Policy einen angemeldeten Benutzer verlangt, ihre Besucher zählt. 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. Die Optionen für die Routen kommen in ein RoutesOptions, mit den Namen aus Konfiguration.

csharp

app.MapRunlight(new RoutesOptions { BasePath = "/stats", Accounts = true, Origin = "https://example.com" });

Mit Accounts = true und einem RUNLIGHT_SECRET melden sich Leute mit ihrer eigenen E-Mail-Adresse und ihrem Passwort an, wie unter Konten. Mit Token = null haben die Routen keine eigene Anmeldung, und ihre Endpunkte folgen dann der Autorisierungs-Policy der App.

app.UseRunlight() erledigt dieselbe Arbeit als Middleware, für eine App, die Runlight vor ihrer übrigen Middleware haben möchte oder kein Endpoint-Routing nutzt. Anfragen, die nicht zu Runlight gehören, gehen weiter an die App. Middleware hat keine Endpunkt-Autorisierung, ruf also UseAuthentication() und UseAuthorization() davor auf, wenn das Dashboard auf die Anmeldung der App angewiesen ist.

MapRunlight() funktioniert auch innerhalb von UseEndpoints und beantwortet Link-Domains dort direkt vor deinen Endpunkten. In einer Routengruppe aus MapGroup verweigert es den Start, weil die Routen ihren vollständigen Pfad brauchen, also mappe es auf der App und setz BasePath auf den vollständigen Pfad, etwa /admin/runlight. Ein Kurzlink, den es nicht gibt, bleibt deiner App überlassen. MapRunlight() beantwortet ihn mit einem nackten 404, so wie ASP.NET Core jeden Pfad ohne Endpunkt beantwortet, sodass UseStatusCodePages dort deine eigene Seite zeigt, und UseRunlight() reicht die Anfrage an die App weiter.

Eine Link-Domain ist ein eigener Host, füge sie also zu AllowedHosts hinzu, wenn die App diese Liste setzt, sonst weist ASP.NET Core die Anfrage vorher ab.

Andere .NET-Apps

Jede .NET-App kann Runlight eine Anfrage übergeben und seine Antwort zurückschicken. Baue aus der absoluten URL, der Methode, den Headern, dem Body und der Adresse der Verbindung einen Runlight.Http.Request und übergib ihn an FrontController.AnswerAsync().

csharp

using Runlight.Http;
using Runlight.Server;

var request = new Request(url, method, headers, body, remoteAddress);
Response answer = await FrontController.AnswerAsync(rl, routes, request);
await rl.IdleAsync();

answer.Status, answer.HeaderLines() und answer.WriteToAsync() liefern, was zu senden ist. IdleAsync() erledigt die Arbeit, die die TypeScript-Bibliothek nach dem Antworten erledigt, ruf es also auf, sobald die Antwort raus ist. In einem eigenen Endpunkt oder einer eigenen Middleware in ASP.NET Core erledigt RunlightHttp.ServeAsync(context, rl, routes) all das für einen HttpContext.

Der stündliche Check

Der stündliche Check kann innerhalb der App als Hosted Service laufen.

RunlightCheck.cs

public sealed class RunlightCheck(Runlight.Runlight rl) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
        do
        {
            await rl.CheckAsync(stoppingToken);
            await rl.IdleAsync();
        }
        while (await timer.WaitForNextTickAsync(stoppingToken));
    }
}

Registriere ihn mit builder.Services.AddHostedService<RunlightCheck>(). Eine App, die in mehreren Instanzen läuft, kann das weglassen und CRON_SECRET setzen und stattdessen einen einzigen Scheduler /runlight/api/check aufrufen lassen.

Der eigenständige Server

Das Tool runlight betreibt Runlight auf einer eigenen Domain, etwa stats.example.com, so wie der eigenständige Server. Es liefert das Dashboard an der Wurzel aus, verwaltet die Websites im Dashboard und bietet Konten mit Anmeldung und Kurzlinks auf jeder Domain, die du darauf zeigen lässt.

bash

dotnet tool install --global Runlight.Server
mkdir stats && cd stats
runlight serve

Der Server lauscht auf Port 3000 oder auf PORT, mit Kestrel. Die Daten liegen in runlight-data in dem Ordner, aus dem du ihn startest. Beim ersten Start gibt er einen Link mit einem Einmalcode aus, der das erste Konto anlegt, und schreibt den Link in setup.txt im Datenordner, und runlight setup gibt ihn noch einmal aus. Der Link funktioniert nur, solange es kein Konto gibt. Rollen, Einladungen, Zwei-Faktor-Anmeldung und das Hinzufügen von Websites funktionieren wie beim eigenständigen Server.

Einstellungen

Die Einstellungen kommen aus Umgebungsvariablen oder aus einer config.json in dem Ordner, aus dem du den Server startest, mit denselben Namen. Sind beide gesetzt, gewinnen die Umgebungsvariablen.

config.json

{
  "RUNLIGHT_URL": "https://stats.example.com",
  "DATABASE_URL": "postgres://runlight:password@localhost:5432/runlight"
}
Einstellung Was sie tut
PORT Der Port, auf dem der Server lauscht, standardmäßig 3000.
HOST Die Adresse, auf der er lauscht, standardmäßig 0.0.0.0.
RUNLIGHT_URL Die öffentliche Adresse des Dashboards, etwa https://stats.example.com. Sie kann nie zu einer Link-Domain werden, und E-Mails mit Einladungen und Berichten verlinken darauf.
DATABASE_URL Eine postgres://-, mysql://- oder mariadb://-Adresse speichert die Daten in dieser Datenbank statt in SQLite.
DATA_DIR Der Datenordner, standardmäßig runlight-data. Ein relativer Pfad wird von dem Ordner aus gelesen, in dem der Server startet.
RUNLIGHT_SECRET Der Schlüssel, der Anmeldungen signiert und gespeicherte Schlüssel verschlüsselt. Ohne ihn erzeugt der Server selbst einen und legt ihn im Datenordner ab.
RUNLIGHT_TOKEN Ein Bearer-Token für Skripte, mit dem auch statt über den Einrichtungslink das erste Konto angelegt wird.
TRUST_PROXY Auf false setzen, wenn nichts 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, wie beim eigenständigen Server. 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.
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.
RUNLIGHT_CONFIG Der Pfad einer Konfigurationsdatei, die woanders liegt als in dem Ordner, in dem der Server startet.

Der Server führt den stündlichen Check alle fünf Minuten aus und lädt einmal im Monat die neuen Ortsdaten herunter. Für einen Server, der nicht ständig läuft, erledigt runlight cron dieselbe Arbeit einmal. Er gibt nichts aus, wenn alles in Ordnung ist, abgesehen von einer Zeile, wenn die Ortsdaten eines neuen Monats bereitliegen.

bash

*/5 * * * * cd /srv/stats && runlight cron

Ein systemd-Dienst oder ein beliebiger Prozessmanager hält den Server am Laufen. Er hält bei SIGTERM an und gibt den Antworten, die er begonnen hat, fünf Sekunden, um fertig zu werden.

Vergessene Passwörter

Führe im Ordner des Servers 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.

bash

runlight password someone@example.com

Upgrades und Backups

Führe dotnet tool update --global Runlight.Server aus und starte den Server neu. Die Tabellen aktualisieren sich beim nächsten Start selbst, oder du führst runlight migrate aus, um sie sofort zu aktualisieren. Sichere den Datenordner und config.json, denn ohne das Geheimnis lassen sich gespeicherte Schlüssel nicht lesen.

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 ASP.NET-Core-App mit Runlight darin zählt sie, während sie ihre Seiten ausliefert. 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 oder aus config.json. --site ist die Adresse der Website, für Logs im üblichen Format von nginx oder Apache, die den Host weglassen. Die JSON-Logs von Caddy enthalten den Host, dort ist es also nicht nötig.

Mit --follow läuft der Befehl weiter, sendet Abrufe, sobald sie passieren, und macht weiter, wenn das Log rotiert wird. Füge --state agents.json hinzu, damit er nach einem Neustart dort weitermacht, wo er aufgehört hat. Ohne --follow liest er das Log einmal und hört auf, und mit --state beginnt der nächste Lauf dort, wo der letzte endete, was gut zu Cron passt. Nur ein Lauf zur Zeit kann eine Zustandsdatei benutzen, und eine Sperre, die ein abgestürzter Lauf hinterlassen hat, wird übernommen. Nur erfolgreiche Seitenabrufe bekannter KI-Agenten verlassen die Maschine, jeweils mit der Adresse der Seite, dem User-Agent des Agenten und dem Zeitpunkt der Auslieferung.

Was anders ist als bei der TypeScript-Bibliothek

Das .NET-Paket besteht dieselben Konformitätstests wie die TypeScript-Bibliothek, auf SQLite, Postgres, MySQL und MariaDB. Die Unterschiede kommen daher, wie .NET-Apps laufen.

  • Der Connection-Pool ist der des Treibers. Postgres und MySQL nehmen für jede Abfrage eine Verbindung aus dem Pool der Datenquelle, und SQLite hält eine Verbindung offen und führt eine Anweisung nach der anderen aus.
  • Was die TypeScript-Bibliothek im Hintergrund erledigt, etwa das Löschen von Besuchen, nachdem eine kürzere Aufbewahrungsdauer gewählt wurde, läuft, sobald ASP.NET Core die Antwort gesendet hat.
  • Passwörter werden mit scrypt gehasht, in C# geschrieben, weil .NET keines eingebaut hat. Die Hashes stimmen mit denen der TypeScript-Bibliothek überein, Konten lassen sich also übernehmen.
  • Das Rate-Limit des Trackers liegt im Arbeitsspeicher, jede Instanz der App zählt also für sich. Die Limits für falsche Passwörter liegen in der Datenbank, deshalb teilen sich alle Instanzen sie.
  • E-Mail-Berichte formatieren Zahlen, Datumsangaben, Geldbeträge und Ländernamen anhand einer Kopie der ICU-Daten, die Node nutzt, damit sie auf jedem System gleich aussehen.