Toda la documentación

Empezar

  1. 01Primeros pasos
  2. 02Instalación
  3. 03Seguimiento

Funciones

  1. 04El panel
  2. 05Objetivos
  3. 06Enlaces cortos
  4. 07Fuentes y agentes de IA
  5. 08Informes por email
  6. 09Comprobación programada
  7. 10Pregunta a tu IA

Referencia

  1. 11Configuración
  2. 12API HTTP

Plataformas

  1. 13Servidor independiente
  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

Confianza

  1. 26Privacidad

Plataformas

Go

Runlight funciona con Go 1.25 o posterior, dentro de cualquier aplicación net/http, chi o Echo, o como servidor independiente.

El módulo de Go es Runlight escrito de nuevo en Go. Sirve el mismo panel y la misma API, y da a cada petición la misma respuesta que da la biblioteca de TypeScript. Sus cifras van en las mismas tablas, así que cualquiera de los dos puede leer una base de datos que haya escrito el otro. Necesita Go 1.25 o posterior, y el propio módulo no requiere ningún otro módulo.

Instalación

Añade el módulo con go get, junto con el driver de tu base de datos.

bash

go get runlight.sh/go
go get modernc.org/sqlite

Runlight habla con su base de datos a través de database/sql, así que sirve cualquier driver. Los tests se ejecutan con modernc.org/sqlite para SQLite, github.com/jackc/pgx/v5/stdlib para Postgres y github.com/go-sql-driver/mysql para MySQL y MariaDB.

Crea la instancia

Abre la base de datos, envuélvela en un almacén y crea una instancia para toda la aplicación.

analytics.go

package main

import (
	"database/sql"

	_ "modernc.org/sqlite"
	runlight "runlight.sh/go"
)

func newRunlight() (*runlight.Runlight, error) {
	db, err := sql.Open("sqlite", "data/runlight.db")
	if err != nil {
		return nil, err
	}
	return runlight.New(runlight.Options{
		Store: runlight.NewStore(runlight.SQLite(db).Owned(0)),
		Site:  &runlight.SiteOptions{Name: "example.com", Hostnames: []string{"example.com"}, Timezone: "Europe/London"},
	})
}

Las opciones tienen los mismos nombres que en Configuración, escritos como campos de Go, como Sites, IgnoreProxy, RateLimit, Mail y ManagedSites. Cada método que lee o escribe recibe primero un context.Context, y las tablas se crean en la primera petición.

Almacenes

Owned cede la conexión a Runlight, que la cierra cuando se cierra el almacén. Omítelo para compartir un *sql.DB que tu aplicación ya tenga, que Runlight entonces nunca cierra.

Las tablas de Runlight empiezan por rl_. runlight.NewStoreWithPrefix(runlight.SQLite(db).Owned(0), "wp_rl_") usa otro prefijo en su lugar, así que dos aplicaciones que comparten una base de datos mantienen cada una sus propias tablas. Un prefijo tiene como máximo 32 letras, dígitos y guiones bajos, y no empieza por un dígito, y store.Tables() lista las tablas del almacén por nombre, para una aplicación que quiera quitar Runlight.

SQLite

go

db, err := sql.Open("sqlite", "data/runlight.db")
store := runlight.NewStore(runlight.SQLite(db).Owned(0))

Runlight mantiene una sola conexión de SQLite y la pone en modo WAL con un busy timeout. Guarda el archivo fuera de cualquier carpeta que sirva tu servidor web, en una carpeta en la que la aplicación pueda escribir, ya que SQLite escribe un journal a su lado.

Postgres

go

db, err := sql.Open("pgx", "postgres://runlight:password@127.0.0.1:5432/runlight?statement_timeout=120000")
store := runlight.NewStore(runlight.Postgres(db).Owned(0))

Importa github.com/jackc/pgx/v5/stdlib para el driver pgx. El parámetro statement_timeout detiene cualquier sentencia pasados esos milisegundos, como hace la biblioteca de TypeScript a los 120000. Para guardar las tablas en un schema propio, añade search_path=analytics a la URL. El pool necesita al menos dos conexiones.

MySQL y MariaDB

go

db, err := sql.Open("mysql", "runlight:password@tcp(127.0.0.1:3306)/runlight?charset=utf8mb4&loc=UTC")
store := runlight.NewStore(runlight.MySQL(db).Owned(120000))

Este almacén funciona con MySQL 8.0.20 o posterior y MariaDB 11.4 o posterior. El número que se pasa a Owned es el timeout de sentencia de cada sesión en milisegundos, y 0 lo desactiva. El pool necesita al menos dos conexiones.

net/http

Las rutas son un http.Handler. Móntalas en su ruta base, /runlight por defecto, y añade los enlaces cortos en /go/{slug}.

main.go

rl, err := newRunlight()
if err != nil {
	log.Fatal(err)
}
routes, err := rl.Routes(runlight.RoutesOptions{})
if err != nil {
	log.Fatal(err)
}

mux := http.NewServeMux()
mux.Handle("/runlight", routes)
mux.Handle("/runlight/", routes)
mux.Handle("GET /go/{slug}", rl.LinksHTTP())
mux.Handle("/", yourApp)

log.Fatal(http.ListenAndServe(":8080", rl.LinkDomains(rl.Observer(mux))))

rl.Observer registra los accesos de los agentes de IA mientras tu aplicación sirve sus páginas, y deja pasar todas las peticiones. rl.LinkDomains responde a los enlaces cortos en tus dominios de enlaces, los nombres propios que apuntas a la aplicación, antes de que el mux vea la petición. Deja pasar intacto cualquier otro host, así que tus propias páginas no se ven afectadas. En una aplicación que envía todas las peticiones por un solo handler, routes.Middleware(next) responde a los dominios de enlaces y a las rutas propias de Runlight, y pasa el resto a next sin leer sus cuerpos. También responde a los documentos OAuth que los clientes de MCP buscan en la raíz del sitio.

rl.LinksHTTP() responde a un enlace corto que nadie tiene con un 404 simple. Para mostrar tu propia página 404 ahí, usa rl.LinksHTTPOr(notFound), que pasa esas peticiones a tu handler notFound. En un dominio de enlaces, /go/{slug} es un 404 como cualquier otra ruta sin enlace.

Añade el script a cada página, justo antes de </head>.

html

<script defer src="/runlight/s.js"></script>

Después define RUNLIGHT_TOKEN como una cadena larga y aleatoria y abre una vez /runlight/?token= seguido de esa cadena para iniciar sesión, como describe Primeros pasos. RoutesOptions acepta los mismos ajustes que routes() en Configuración, así que Token, Authorize, BasePath y Origin funcionan como allí. Con Accounts: true y un RUNLIGHT_SECRET, cada persona inicia sesión con su propio correo y contraseña, como en Cuentas.

chi

El adaptador de chi es un módulo aparte, así que chi nunca es un requisito del módulo principal.

bash

go get runlight.sh/go/chi

go

r := chi.NewRouter()
r.Use(rl.Observer)
runlightchi.Mount(r, rl, routes)

Mount añade las rutas bajo su ruta base, los documentos OAuth y los enlaces cortos, y responde a los dominios de enlaces antes que tus propias rutas. Llámalo en el router principal antes de tus propias rutas, ya que las rutas leen la ruta completa de cada petición y chi solo acepta middleware hasta la primera ruta. Si se llama después de una ruta, entra en pánico con ese consejo, porque si no un dominio de enlaces perdería cada enlace corto cuya ruta coincida con una de tus rutas. Cuando tus rutas tengan que ir primero, envuelve todo el router en rl.LinkDomains y llama a runlightchi.MountRoutes, que añade el resto. Un enlace corto que nadie tiene va al handler NotFound del router, así que tu propia página 404 le responde.

Echo

El adaptador de Echo también es un módulo aparte, y funciona con Echo v5.

bash

go get runlight.sh/go/echo

go

e := echo.New()
e.Use(runlightecho.Observer(rl))
runlightecho.Register(e, rl, routes)

Register añade las rutas, los documentos OAuth y los enlaces cortos, igual que Mount en chi. Responde a los dominios de enlaces antes de que Echo enrute cada petición, sea cual sea el punto de la aplicación donde se llame. Un enlace corto que nadie tiene devuelve echo.ErrNotFound, así que tu manejador de errores le responde como a cualquier página que la aplicación no tiene.

Otros routers

Cualquier router que acepte un http.Handler puede servir Runlight igual que http.ServeMux, envuelto en rl.LinkDomains para los dominios de enlaces. Para responder tú mismo a una petición, conviértela en una de Runlight con runlight.FromHTTP, pásala a routes.Handle y devuelve la respuesta con runlight.WriteHTTP. FromHTTP lee el cuerpo con los límites que usa el adaptador de Node, 16 KB para el tracker y 10 MB para todo lo demás, y conserva la dirección de la que llegó la conexión. Runlight lee él mismo las cabeceras reenviadas salvo que se defina IgnoreProxy.

La comprobación programada

La comprobación programada rota las sales diarias, envía los informes por email pendientes, aplica cuánto tiempo conserva cada sitio sus visitas y construye los resúmenes que mantienen rápidos los rangos largos. Llama a rl.Check(ctx) cada pocos minutos desde una goroutine con un time.Ticker. Cuando nada en la aplicación se ejecuta con un temporizador, define CRON_SECRET y haz que un programador de tareas llame a /runlight/api/check con él, como muestra Comprobación programada.

El servidor independiente

El comando runlight es el servidor independiente en Go. Ejecuta Runlight en un dominio propio, como stats.example.com, con el panel en la raíz, sitios añadidos desde el panel, cuentas con inicio de sesión y enlaces cortos en cualquier dominio que apuntes hacia él. Instálalo y ejecútalo.

bash

go install runlight.sh/go/cmd/runlight@latest
runlight

El servidor escucha en el puerto 3000 y guarda sus datos en una carpeta llamada runlight-data dentro del directorio desde el que lo arrancas. En su primer arranque muestra un enlace de configuración con un código de un solo uso. Abre ese enlace y crea tu cuenta, como describe Crea tu cuenta. El servidor ejecuta él mismo la comprobación programada cada cinco minutos, y descarga los datos de ubicación del mes igual que el servidor de Node. Ponlo detrás de un proxy que añada HTTPS, como en Publícalo en internet.

Para ejecutar el servidor dentro de un programa propio, runlight.sh/go/server lo ofrece como un http.Handler, con server.New aceptando los mismos ajustes como campos.

Ajustes

El servidor lee sus ajustes de variables de entorno.

Variable Qué hace
PORT El puerto en el que escucha. Por defecto, 3000.
HOST La dirección en la que escucha. Por defecto, 0.0.0.0.
DATA_DIR La carpeta para el archivo SQLite, el secreto y los datos de ubicación. Por defecto, ./runlight-data.
DATABASE_URL Una dirección postgres:// guarda los datos en Postgres en lugar de SQLite, y una dirección mysql:// o mariadb:// los guarda en MySQL o MariaDB.
RUNLIGHT_SECRET La clave que firma los inicios de sesión y cifra las claves guardadas. Sin ella, el servidor crea una y la guarda en DATA_DIR, legible solo por el usuario que lo ejecuta.
RUNLIGHT_URL La dirección pública del servidor, como https://stats.example.com, que nunca puede convertirse en dominio de enlaces. Los correos de invitación y de informes enlazan a ella.
RUNLIGHT_TOKEN Un token que los scripts pueden enviar como bearer, además de los tokens de API creados en el panel.
RUNLIGHT_OBSERVE_KEY Una sola clave para los informes de agentes de IA de todos los sitios. La clave de agentes propia de cada sitio, que aparece en Ajustes, Instalación, es la mejor opción.
TRUST_PROXY Defínela como false cuando no haya ningún proxy delante del servidor, o como x-real-ip o cf-connecting-ip cuando esa cabecera lleve la dirección del visitante.
RUNLIGHT_GEO De dónde salen las ubicaciones cuando ninguna cabecera las da, como en Ubicaciones. Por defecto es city, y puede ser country, off o la ruta a un archivo MMDB.

Las tablas se actualizan solas cuando arranca el servidor, así que para actualizar basta con compilar de nuevo y reiniciar. Haz copia de seguridad de la carpeta de datos, ya que las claves guardadas no se pueden leer sin el secreto que contiene.

Contraseñas olvidadas

Ejecuta el comando password donde se ejecuta el servidor para darle a una cuenta una contraseña nueva, que el comando muestra. También desactiva el inicio de sesión en dos pasos de esa cuenta, y crea la cuenta si no existe, como propietario en un servidor que aún no tiene a nadie y como administrador en los demás casos.

bash

runlight password someone@example.com

Agentes de IA desde un log

Los agentes de IA como GPTBot y ClaudeBot acceden a las páginas sin ejecutar JavaScript, así que la etiqueta de script nunca los ve. Una aplicación Go con Runlight dentro los cuenta donde se sirve la página, con rl.Observer. Para cualquier otro sitio que alojes tú, el log de acceso del servidor web los tiene, y el comando agents lo lee.

bash

runlight agents --log /var/log/nginx/access.log --to https://stats.example.com --key rlo_... --site https://example.com --follow

La clave es la clave de agentes del sitio, que aparece en Runlight en Ajustes, Instalación, y solo puede informar de accesos a ese sitio. Cuando falta --to o --key, el comando lee RUNLIGHT_URL o RUNLIGHT_OBSERVE_KEY del entorno. El comando funciona igual que en el servidor independiente, con --follow para seguir en marcha y --state para continuar donde se quedó la última ejecución. Solo salen de la máquina los accesos correctos a páginas de agentes de IA conocidos, y las direcciones de los visitantes se quedan donde están.

En qué se diferencia de la biblioteca de TypeScript

El módulo de Go pasa las pruebas de conformidad que se exigen a la biblioteca de TypeScript, en SQLite, Postgres, MySQL y MariaDB. Las diferencias vienen de cómo se ejecuta Go.

  • Las peticiones se ejecutan en goroutines, y la instancia es segura para muchas a la vez. SQLite mantiene una sola conexión, que todas las peticiones comparten con un bloqueo, y Postgres y MySQL usan el pool que mantiene database/sql.
  • Lo que la biblioteca de TypeScript hace en segundo plano, como borrar visitas después de elegir una retención más corta, se ejecuta en una goroutine junto a la respuesta. rl.Idle() espera a que termine, y un programa puede llamarlo antes de salir.
  • Las zonas horarias salen de la propia base de datos de zonas horarias de Go. Un programa compilado para un sistema sin ella importa time/tzdata.
  • El texto se pasa a mayúsculas o minúsculas con tablas generadas a partir de las de Node, así que los nombres coinciden de la misma forma en ambos. Los sitios se ordenan por nombre con las reglas principales del orden por defecto de Unicode, así que de vez en cuando un nombre en una escritura menos común queda un puesto más allá de donde lo pone Node.
  • El límite de peticiones del tracker y las listas en caché de dominios de enlaces y de iconos de sitios se guardan en memoria, así que cada proceso tiene las suyas. Los límites de contraseñas incorrectas se guardan en la base de datos, así que todos los procesos los comparten.
  • El hash de las contraseñas se calcula con scrypt en Go. Los hashes coinciden con los de la biblioteca de TypeScript, así que las cuentas se mantienen.