Plateformes
.NET
Runlight tourne sous .NET 10 ou plus récent, dans une application ASP.NET Core, ou sur son propre domaine en serveur autonome.
Le paquet .NET, c’est Runlight réécrit en C#. Il sert le même tableau de bord et la même API, et il donne à chaque requête la même réponse que la bibliothèque TypeScript. Ses chiffres vont dans les mêmes tables, donc chacun peut lire une base de données écrite par l’autre. Il lui faut .NET 10 ou plus récent et le pilote ADO.NET de votre base de données, que votre application a déjà si elle utilise cette base.
Installation
Ajoutez le paquet ASP.NET Core et le pilote de votre base de données. Le paquet apporte avec lui le paquet principal Runlight.
bash
dotnet add package Runlight.AspNetCore
dotnet add package Microsoft.Data.SqliteLe paquet principal n’a aucune dépendance propre. Il atteint la base de données par le pilote que vous lui donnez, et il partage le pool de connexions de ce pilote.
Créer l’instance
Créez une instance au démarrage de l’application et enregistrez-la comme 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" },
}));Les options portent les noms de Configuration, écrits en PascalCase, donc Sites, TrustProxy, RateLimit, Mail et ManagedSites fonctionnent tous comme là-bas. Les tables sont créées à la première requête.
Stockages
Les tables de Runlight commencent par rl_. Chaque stockage accepte un argument prefix à utiliser à la place, comme Stores.Sqlite(SqliteFactory.Instance, path, prefix: "wp_rl_"), pour que deux applications qui partagent une base gardent chacune leurs propres tables. Un préfixe compte jusqu’à 32 lettres, chiffres et tirets bas, et ne commence pas par un chiffre. store.Tables() liste les tables du stockage par leur nom, pour une application qui retire Runlight.
SQLite
csharp
Stores.Sqlite(SqliteFactory.Instance, "data/runlight.db");SQLite a besoin de Microsoft.Data.Sqlite. Gardez le fichier dans un dossier où l’application peut écrire, puisque SQLite écrit un journal à côté. Le dossier est créé s’il manque.
Postgres
csharp
Stores.Postgres(NpgsqlDataSource.Create("Host=127.0.0.1;Username=runlight;Password=password;Database=runlight"));Ce stockage a besoin de Npgsql. Passez la NpgsqlDataSource de l’application pour partager son pool. Un deuxième argument comme 30000 arrête toute requête SQL au bout de ce nombre de millisecondes, et Search Path=analytics dans la chaîne de connexion garde les tables dans leur propre schéma.
MySQL et MariaDB
csharp
Stores.MySql(new MySqlDataSource("Server=127.0.0.1;User ID=runlight;Password=password;Database=runlight"));Ce stockage a besoin de MySqlConnector et fonctionne avec MySQL 8.0.20 ou plus récent et MariaDB 11.4 ou plus récent. Il accepte le même deuxième argument que le stockage Postgres.
ASP.NET Core
Mappez Runlight une fois l’application construite.
Program.cs
var app = builder.Build();
app.MapRunlight();
app.Run();MapRunlight() sert le tableau de bord, l’API et le script sous /runlight, et les liens courts sur /go. Il répond aussi à tout domaine de liens que vous ajoutez dans Réglages, et il enregistre les requêtes des agents IA pendant que vos pages sont servies, ce qui ne prend aucun temps pour les autres visiteurs. Runlight vérifie lui-même ses requêtes, donc ses endpoints sautent la vérification antiforgery d’ASP.NET Core, qui refuserait le traceur. Ils acceptent les requêtes anonymes, donc une application dont la politique par défaut exige un utilisateur connecté compte quand même ses visiteurs. Ajoutez le script à chaque page, juste avant </head>.
html
<script defer src="/runlight/s.js"></script>Définissez ensuite RUNLIGHT_TOKEN avec une longue chaîne aléatoire et ouvrez une fois /runlight/?token= suivi de cette chaîne pour vous connecter, comme l’explique Pour commencer. Les options des routes vont dans un RoutesOptions, avec les noms de Configuration.
csharp
app.MapRunlight(new RoutesOptions { BasePath = "/stats", Accounts = true, Origin = "https://example.com" });Avec Accounts = true et un RUNLIGHT_SECRET, chacun se connecte avec son propre e-mail et son propre mot de passe, comme dans Comptes. Avec Token = null, les routes n’ont pas de connexion propre, et leurs endpoints suivent alors la politique d’autorisation de l’application.
app.UseRunlight() fait le même travail sous forme de middleware, pour une application qui veut Runlight avant ses autres middlewares ou qui n’utilise pas le routage par endpoints. Les requêtes qui ne sont pas pour Runlight continuent vers l’application. Un middleware ne porte aucune autorisation d’endpoint, donc appelez UseAuthentication() et UseAuthorization() avant lui quand le tableau de bord s’appuie sur la connexion de l’application.
MapRunlight() fonctionne aussi dans UseEndpoints, où il répond aux domaines de liens juste avant vos endpoints. Dans un groupe de routes créé par MapGroup, il refuse de démarrer, puisque les routes ont besoin de leur chemin complet, donc mappez-le sur l’application et donnez à BasePath le chemin complet, par exemple /admin/runlight. Un lien court que personne n’a est laissé à votre application. MapRunlight() lui répond avec un 404 nu, comme ASP.NET Core répond à tout chemin sans endpoint, donc UseStatusCodePages y affiche votre propre page, et UseRunlight() transmet la requête à l’application.
Un domaine de liens est un hôte à part, donc ajoutez-le à AllowedHosts quand l’application définit cette liste, sinon ASP.NET Core refuse la requête avant tout.
Autres applications .NET
N’importe quelle application .NET peut passer une requête à Runlight et renvoyer sa réponse. Construisez un Runlight.Http.Request à partir de l’URL absolue, de la méthode, des en-têtes, du corps et de l’adresse de la connexion, puis passez-le à 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() et answer.WriteToAsync() donnent ce qu’il faut envoyer. IdleAsync() exécute le travail que la bibliothèque TypeScript fait après avoir répondu, donc appelez-la une fois la réponse partie. Dans un endpoint ou un middleware ASP.NET Core à vous, RunlightHttp.ServeAsync(context, rl, routes) fait tout cela pour un HttpContext.
La vérification planifiée
La vérification planifiée peut tourner dans l’application sous forme de hosted service.
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));
}
}Enregistrez-le avec builder.Services.AddHostedService<RunlightCheck>(). Une application qui tourne en plusieurs copies peut s’en passer et définir CRON_SECRET, puis faire appeler /runlight/api/check par un seul planificateur.
Le serveur autonome
L’outil runlight fait tourner Runlight sur son propre domaine, comme stats.example.com, à la manière du serveur autonome. Il sert le tableau de bord à la racine, gère les sites depuis le tableau de bord, et propose des comptes de connexion et des liens courts sur n’importe quel domaine que vous faites pointer vers lui.
bash
dotnet tool install --global Runlight.Server
mkdir stats && cd stats
runlight serveLe serveur écoute sur le port 3000, ou sur PORT, avec Kestrel. Les données vivent dans runlight-data, dans le dossier depuis lequel vous le lancez. À son premier démarrage, il affiche un lien avec un code à usage unique qui crée le premier compte et écrit ce lien dans setup.txt, dans le dossier de données, et runlight setup l’affiche à nouveau. Le lien ne fonctionne que tant qu’il n’y a aucun compte. Les rôles, les invitations, la connexion à deux facteurs et l’ajout de sites fonctionnent comme sur le serveur autonome.
Réglages
Les réglages viennent des variables d’environnement, ou d’un config.json qui contient les mêmes noms, dans le dossier depuis lequel vous lancez le serveur. Les variables d’environnement l’emportent quand les deux sont définis.
config.json
{
"RUNLIGHT_URL": "https://stats.example.com",
"DATABASE_URL": "postgres://runlight:password@localhost:5432/runlight"
}| Réglage | Ce qu’il fait |
|---|---|
PORT |
Le port d’écoute du serveur, 3000 par défaut. |
HOST |
L’adresse d’écoute, 0.0.0.0 par défaut. |
RUNLIGHT_URL |
L’adresse publique du tableau de bord, par exemple https://stats.example.com. Elle ne peut jamais devenir un domaine de liens, et les e-mails d’invitation et de rapport pointent vers elle. |
DATABASE_URL |
Une adresse postgres://, mysql:// ou mariadb:// garde les données dans cette base au lieu de SQLite. |
DATA_DIR |
Le dossier de données, runlight-data par défaut. Un chemin relatif part du dossier où démarre le serveur. |
RUNLIGHT_SECRET |
La clé qui signe les connexions et chiffre les clés enregistrées. Sans elle, le serveur en crée une et la garde dans le dossier de données. |
RUNLIGHT_TOKEN |
Un jeton bearer pour les scripts, qui crée aussi le premier compte à la place du lien de configuration. |
TRUST_PROXY |
Mettez false quand rien ne se trouve devant le serveur, ou x-real-ip ou cf-connecting-ip quand c’est cet en-tête qui contient l’adresse du visiteur. |
RUNLIGHT_GEO |
D’où viennent les lieux, comme sur le serveur autonome. city par défaut, et peut valoir country, off ou le chemin d’un fichier MMDB. |
CRON_SECRET |
Permet à un planificateur de lancer la vérification sur POST /api/check. |
RUNLIGHT_OBSERVE_KEY |
Une seule clé pour les signalements d’agents IA de tous les sites. La clé des agents propre à chaque site, affichée dans Réglages, Installation, est le meilleur choix. |
RUNLIGHT_CONFIG |
Le chemin d’un fichier de configuration situé ailleurs que dans le dossier où démarre le serveur. |
Le serveur lance la vérification planifiée toutes les cinq minutes et télécharge les nouvelles données de localisation une fois par mois. Pour un serveur qui ne tourne pas en permanence, runlight cron fait le même travail une fois. La commande n’affiche rien quand tout va bien, sauf une ligne quand les données de localisation d’un nouveau mois sont prêtes.
bash
*/5 * * * * cd /srv/stats && runlight cronUn service systemd ou n’importe quel gestionnaire de processus maintient le serveur en marche. Il s’arrête sur SIGTERM et laisse cinq secondes aux réponses commencées pour se terminer.
Mots de passe oubliés
Lancez la commande password dans le dossier du serveur pour donner à un compte un nouveau mot de passe, qu’elle affiche. Elle désactive aussi la connexion à deux facteurs pour ce compte, et elle crée le compte s’il n’existe pas.
bash
runlight password someone@example.comMises à jour et sauvegardes
Lancez dotnet tool update --global Runlight.Server et redémarrez le serveur. Les tables se mettent à jour toutes seules au démarrage suivant, ou lancez runlight migrate pour les mettre à jour tout de suite. Sauvegardez le dossier de données et config.json, puisque les clés enregistrées ne peuvent pas être lues sans le secret.
Agents IA depuis un journal
Les agents IA comme GPTBot et ClaudeBot récupèrent les pages sans exécuter de JavaScript, donc la balise script ne les voit jamais. Une application ASP.NET Core qui contient Runlight les compte pendant qu’elle sert ses pages. Pour tout autre site que vous hébergez vous-même, le journal d’accès du serveur web les contient, et la commande agents le lit.
bash
runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --followLa clé est la clé des agents du site, affichée dans Runlight dans Réglages, Installation, et elle ne peut signaler que les requêtes de ce site. Quand --to ou --key est omis, la commande lit RUNLIGHT_URL ou RUNLIGHT_OBSERVE_KEY dans l’environnement ou dans config.json. --site est l’adresse du site, pour les journaux au format habituel de nginx ou d’Apache, qui omettent l’hôte. Les journaux JSON de Caddy contiennent l’hôte, donc il n’y est pas nécessaire.
Avec --follow, la commande continue de tourner, envoie les requêtes au fur et à mesure et continue quand le journal est renouvelé. Ajoutez --state agents.json pour qu’un redémarrage reprenne là où elle s’est arrêtée. Sans --follow, elle lit le journal une fois et s’arrête, et avec --state, l’exécution suivante commence là où la précédente a fini, ce qui convient à cron. Une seule exécution à la fois peut utiliser un fichier d’état, et un verrou laissé par une exécution qui a planté est repris. Seules les requêtes de pages réussies des agents IA connus quittent la machine, chacune avec l’adresse de la page, le user agent de l’agent et le moment où elle a été servie.
Différences avec la bibliothèque TypeScript
Le paquet .NET passe les tests de conformité imposés à la bibliothèque TypeScript, sur SQLite, Postgres, MySQL et MariaDB. Les différences viennent de la façon dont tournent les applications .NET.
- Le pool de connexions est celui du pilote. Postgres et MySQL prennent une connexion dans le pool de la source de données pour chaque requête, et SQLite garde une connexion ouverte et exécute une instruction à la fois.
- Ce que la bibliothèque TypeScript fait en arrière-plan, comme supprimer des visites après le choix d’une durée de conservation plus courte, s’exécute une fois qu’ASP.NET Core a envoyé la réponse.
- Les mots de passe sont hachés avec scrypt écrit en C#, puisque .NET n’en intègre pas. Les hachages sont identiques à celles de la bibliothèque TypeScript, donc les comptes se transfèrent.
- La limite de débit du traceur est gardée en mémoire, donc chaque copie de l’application compte de son côté. Les limites de mots de passe erronés sont gardées dans la base, donc toutes les copies les partagent.
- Les rapports par e-mail mettent en forme les nombres, les dates, les montants et les noms de pays à partir d’une copie des données ICU qu’utilise Node, donc ils se lisent de la même façon sur tous les systèmes.