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/sqliteRunlight 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/chigo
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/echogo
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
runlightEl 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.comAgentes 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 --followLa 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.