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

Rust

Runlight läuft in einem Rust-Dienst auf tokio, hinter axum, hyper oder jedem Server, der einen Tower-Service annimmt, mit seinen Tabellen in der eigenen Datenbank der App über sqlx.

Das Rust-Crate ist Runlight, noch einmal in Rust 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 Rust 1.88 oder neuer und läuft auf tokio. Der Store kommt aus einem zweiten Crate, runlight-sqlx, das für sqlx 0.9 Rust 1.94 braucht.

Installieren

Füge beide Crates hinzu, mit der Datenbank, die du nutzt, und dem Framework-Adapter, den du möchtest.

toml

[dependencies]
runlight = { version = "0.1", features = ["axum"] }
runlight-sqlx = { version = "0.1", features = ["sqlite"] }

Das Feature transport des Crates runlight ist standardmäßig eingeschaltet und gibt ihm einen Client für die Mail-Dienste, verbundene Installationen, Website-Symbole und den KI-Assistenten. Das Feature tower macht die Routen zu einem Tower-Service, und axum fügt die Router unten hinzu. Die Features von runlight-sqlx sind sqlite, postgres und mysql.

Die Instanz erstellen

Erstelle ein Runlight, wenn dein Dienst startet, und behalte es in deinem State. Es lässt sich günstig klonen.

rust

use runlight::{Runlight, RunlightOptions, SiteOptions};

let store = runlight_sqlx::connect("sqlite:data/runlight.db").await?;
let mut options = RunlightOptions::new(store);
options.site = Some(SiteOptions {
    name: Some("example.com".into()),
    hostnames: Some(vec!["example.com".into()]),
    timezone: Some("Europe/London".into()),
    ..SiteOptions::default()
});
let rl = Runlight::new(options)?;

Die Optionen heißen genauso wie unter Konfiguration, in Snake Case, deshalb funktionieren sites, trust_proxy, rate_limit, mail und managed_sites alle so wie dort. Die Tabellen werden bei der ersten Anfrage angelegt.

Stores

runlight_sqlx::connect öffnet anhand einer URL einen eigenen Pool. Um den Pool zu nutzen, den deine App schon hat, übergib ihn, dann legt Runlight seine Tabellen neben deine.

Runlights Tabellen beginnen mit rl_. sqlite_with_prefix, postgres_with_prefix, mysql_with_prefix und connect_with_prefix nehmen ein Präfix, das stattdessen verwendet wird, etwa runlight_sqlx::sqlite_with_prefix(pool, "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

rust

let store = runlight_sqlx::sqlite(pool); // a sqlx::SqlitePool

Der Store hält für seine Anweisungen eine Verbindung des Pools, eine nach der anderen, im WAL-Modus mit einem Busy-Timeout von fünf Sekunden. Das Feature sqlite baut SQLite aus dem Quellcode, dein Build braucht also einen C-Compiler.

MySQL und MariaDB

rust

let store = runlight_sqlx::mysql(pool); // a sqlx::MySqlPool

Dieser Store funktioniert mit MySQL 8.0.20 oder neuer und MariaDB 11.4 oder neuer, und der Pool braucht mindestens zwei Verbindungen. Die Standardanmeldung von MySQL 8 über eine Verbindung ohne TLS braucht das sqlx-Feature mysql-rsa, schalte es also in deiner eigenen Cargo.toml ein oder verbinde dich über TLS.

Postgres

rust

let store = runlight_sqlx::postgres(pool); // a sqlx::PgPool

Der Pool braucht mindestens zwei Verbindungen. Jede Anweisung läuft für sich, deshalb schließen sich Runlights Schreibvorgänge nie einer Transaktion an, die deine App offen hat.

axum

Führe die Router mit deiner App zusammen. Das Dashboard und die API antworten unter /runlight, Kurzlinks unter /go/{slug}.

rust

use runlight::{RoutesOptions, TokenOption};

let routes = rl.routes(RoutesOptions {
    token: TokenOption::Given(std::env::var("RUNLIGHT_TOKEN")?),
    ..RoutesOptions::default()
})?;
let app = axum::Router::new()
    .merge(routes.router())
    .merge(rl.link_router())
    .layer(axum::middleware::from_fn_with_state(rl.clone(), runlight::adapters::axum_support::middleware));

Die Middleware beantwortet eine Anfrage auf einer Link-Domain mit ihrem Kurzlink, bevor deine eigenen Routen sie sehen, und sie zeichnet Abrufe deiner Seiten durch KI-Agenten auf. Eine Anfrage für jeden anderen Host geht unverändert an deine App weiter. Behalte den Layer, denn ein axum-Router sieht nur die Pfade, die er bekommen hat, und die Kurzlinks einer Link-Domain liegen an der Wurzel ihres eigenen Namens. Liefere die App mit into_make_service_with_connect_info::<SocketAddr>() aus, damit Runlight die Adresse der Verbindung sieht, wenn kein Proxy-Header den Besucher nennt. Füge dann 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 die Einstellungen, die routes() unter Konfiguration nimmt, in Snake Case, deshalb funktionieren base_path, accounts und origin so wie dort.

link_router() beantwortet jede Anfrage, die axum an /go/{slug} routet. Eine Anfrage auf einer Link-Domain kommt zuerst dran, deshalb ist /go/<name> dort ein 404 wie jeder andere Pfad, der keiner der Links dieser Domain ist. Auf deinem eigenen Host leitet es weiter, wenn es den Link gibt, und ein Name ohne Link bekommt Runlights schlichtes 404, weil axum die Route dann schon zugeordnet hat und die Anfrage nicht mehr an den Fallback deiner App geben kann. rl.link_service() antwortet genauso.

Andere Server

Routes ist ein tower::Service für jeden http::Request, deshalb können hyper und alles andere, was auf tower aufbaut, es direkt ausliefern. Jede Anfrage, die es auf einer Link-Domain bekommt, beantwortet es mit dem Kurzlink, und der Kurzlink-Service aus rl.link_service() macht dasselbe. Ohne das Feature tower baust du selbst einen runlight::http::Request und rufst routes.handle(request).await auf, was mit einer runlight::http::Response antwortet.

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().await alle paar Minuten aus einem eigenen Task auf. Wenn im Dienst nichts nach Zeitplan läuft, setze CRON_SECRET und lass einen Scheduler damit /runlight/api/check aufrufen, wie Stündlicher Check zeigt.

rust

let checker = rl.clone();
tokio::spawn(async move {
    loop {
        if let Err(error) = checker.check().await {
            eprintln!("Runlight check failed: {error}");
        }
        tokio::time::sleep(std::time::Duration::from_secs(300)).await;
    }
});

Ort

Runlight liest Land, Region und Stadt aus den Headern, die Vercel, Cloudflare und Netlify hinzufügen. Hinter allem anderen gibst du ihm über runlight::mmdb::Mmdb eine Städte-Datenbank von MaxMind oder DB-IP, die die Datei seitenweise liest, so wie Abfragen es brauchen.

rust

options.geo = Some(std::sync::Arc::new(runlight::mmdb::Mmdb::open("data/dbip-city-lite.mmdb")?));

Was anders ist als bei der TypeScript-Bibliothek

Das Rust-Crate besteht dieselben Konformitätstests wie die TypeScript-Bibliothek, auf SQLite, Postgres, MySQL und MariaDB. Die Unterschiede kommen daher, wie Rust läuft.

  • Das Crate schickt seine ausgehenden Anfragen, für Mail-Dienste, verbundene Installationen, Website-Symbole und den KI-Assistenten, über einen Fetcher, den du ersetzen kannst, sodass Tests und abgeschottete Netze ihren eigenen übergeben können.
  • Es liest Zeitzonen aus der Datenbank des Systems, deshalb folgt eine Zone, deren Regeln sich nach der Kopie der TypeScript-Bibliothek geändert haben, den neueren Regeln.
  • Einen eigenständigen Server gibt es in Rust nicht. Für ein Dashboard über mehrere Websites betreibst du den Node-Server oder den Server einer anderen Sprache und verbindest die Website dieses Dienstes damit.