feat: add Grafana Faro RUM, fix dashboards, add Grafana to admin nav
All checks were successful
Release / Test backend (push) Successful in 40s
Release / Check ui (push) Successful in 1m5s
Release / Docker / caddy (push) Successful in 1m9s
Release / Docker / backend (push) Successful in 3m14s
Release / Docker / runner (push) Successful in 4m7s
Release / Upload source maps (push) Successful in 2m11s
Release / Docker / ui (push) Successful in 2m23s
Release / Gitea Release (push) Successful in 40s

- Add @grafana/faro-web-sdk to UI; wire initializeFaro in hooks.client.ts
  gated on PUBLIC_FARO_COLLECTOR_URL (no-op in dev)
- Add Grafana Alloy service (faro.receiver) to homelab compose;
  Faro endpoint → alloy:12347 (faro.libnovel.cc via cloudflared)
- Add PUBLIC_FARO_COLLECTOR_URL env var to docker-compose.yml UI service
- Add Web Vitals dashboard (web-vitals.json): LCP/INP/CLS/TTFB/FCP p75
  stats + LCP/TTFB time-series + Faro exception logs from Loki
- Fix runner.json: strip libnovel_ prefix from all metric names
- Fix backend.json: replace 5 dead http_client_* panels with
  spanmetrics-based equivalents (Request Rate by Span Name + Latency
  by Span Name p95)
- Fix OTel collector: add service.telemetry.metrics.address: 0.0.0.0:8888
  so Prometheus can scrape collector self-metrics
- Add Grafana link to admin nav external tools; add admin_nav_grafana
  message key to all 5 locale files; recompile paraglide
This commit is contained in:
Admin
2026-04-05 12:58:16 +05:00
parent eb8a92f0c1
commit 4d3b91af30
18 changed files with 593 additions and 62 deletions

View File

@@ -410,6 +410,7 @@
"admin_nav_uptime": "Uptime",
"admin_nav_push": "Push",
"admin_nav_gitea": "Gitea",
"admin_nav_grafana": "Grafana",
"admin_scrape_status_idle": "Idle",
"admin_scrape_status_running": "Running",

View File

@@ -471,5 +471,6 @@
"feed_chapters_label": "{n} chapitres",
"feed_browse_cta": "Parcourir le catalogue",
"feed_find_users_cta": "Trouver des lecteurs",
"admin_nav_gitea": "Gitea"
"admin_nav_gitea": "Gitea",
"admin_nav_grafana": "Grafana"
}

View File

@@ -471,5 +471,6 @@
"feed_chapters_label": "{n} bab",
"feed_browse_cta": "Jelajahi katalog",
"feed_find_users_cta": "Temukan pembaca",
"admin_nav_gitea": "Gitea"
"admin_nav_gitea": "Gitea",
"admin_nav_grafana": "Grafana"
}

View File

@@ -471,5 +471,6 @@
"feed_chapters_label": "{n} capítulos",
"feed_browse_cta": "Ver catálogo",
"feed_find_users_cta": "Encontrar leitores",
"admin_nav_gitea": "Gitea"
"admin_nav_gitea": "Gitea",
"admin_nav_grafana": "Grafana"
}

View File

@@ -471,5 +471,6 @@
"feed_chapters_label": "{n} глав",
"feed_browse_cta": "Каталог",
"feed_find_users_cta": "Найти читателей",
"admin_nav_gitea": "Gitea"
"admin_nav_gitea": "Gitea",
"admin_nav_grafana": "Grafana"
}

142
ui/package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.1005.0",
"@aws-sdk/s3-request-presigner": "^3.1005.0",
"@grafana/faro-web-sdk": "^2.3.1",
"@inlang/paraglide-js": "^2.15.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
@@ -1689,6 +1690,115 @@
"module-details-from-path": "^1.0.4"
}
},
"node_modules/@grafana/faro-core": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@grafana/faro-core/-/faro-core-2.3.1.tgz",
"integrity": "sha512-htDKO0YFKr0tfntrPoM151vOPSZzmP6oE0+0MDvbI1WDaBW4erXmYi3feGJLWDXt5/vZBg9iQRmZoRzTLTTcOA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/otlp-transformer": "^0.213.0"
}
},
"node_modules/@grafana/faro-core/node_modules/@opentelemetry/otlp-transformer": {
"version": "0.213.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.213.0.tgz",
"integrity": "sha512-RSuAlxFFPjeK4d5Y6ps8L2WhaQI6CXWllIjvo5nkAlBpmq2XdYWEBGiAbOF4nDs8CX4QblJDv5BbMUft3sEfDw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.213.0",
"@opentelemetry/core": "2.6.0",
"@opentelemetry/resources": "2.6.0",
"@opentelemetry/sdk-logs": "0.213.0",
"@opentelemetry/sdk-metrics": "2.6.0",
"@opentelemetry/sdk-trace-base": "2.6.0",
"protobufjs": "^7.0.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@grafana/faro-core/node_modules/@opentelemetry/resources": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz",
"integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.6.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@grafana/faro-core/node_modules/@opentelemetry/sdk-logs": {
"version": "0.213.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.213.0.tgz",
"integrity": "sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.213.0",
"@opentelemetry/core": "2.6.0",
"@opentelemetry/resources": "2.6.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"node_modules/@grafana/faro-core/node_modules/@opentelemetry/sdk-metrics": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.0.tgz",
"integrity": "sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.6.0",
"@opentelemetry/resources": "2.6.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <1.10.0"
}
},
"node_modules/@grafana/faro-core/node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz",
"integrity": "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.6.0",
"@opentelemetry/resources": "2.6.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@grafana/faro-web-sdk": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@grafana/faro-web-sdk/-/faro-web-sdk-2.3.1.tgz",
"integrity": "sha512-WMfErl2YSP+CcfcobMpCdK6apX86hc8bymMXsvYLQpBBkQ0KJjIilEQS/YXd+g/cg6F1kwbeweisBKluNNy5sA==",
"license": "Apache-2.0",
"dependencies": {
"@grafana/faro-core": "^2.3.1",
"ua-parser-js": "1.0.41",
"web-vitals": "^5.1.0"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
@@ -7377,6 +7487,32 @@
"node": ">=14.17"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
"integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"license": "MIT",
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
@@ -7540,6 +7676,12 @@
}
}
},
"node_modules/web-vitals": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz",
"integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View File

@@ -31,6 +31,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.1005.0",
"@aws-sdk/s3-request-presigner": "^3.1005.0",
"@grafana/faro-web-sdk": "^2.3.1",
"@inlang/paraglide-js": "^2.15.1",
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",

View File

@@ -1,5 +1,6 @@
import * as Sentry from '@sentry/sveltekit';
import { env } from '$env/dynamic/public';
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
// Sentry / GlitchTip client-side error tracking.
// No-op when PUBLIC_GLITCHTIP_DSN is unset (e.g. local dev).
@@ -13,4 +14,21 @@ if (env.PUBLIC_GLITCHTIP_DSN) {
});
}
// Grafana Faro RUM — browser performance monitoring (Web Vitals, traces, errors).
// No-op when PUBLIC_FARO_COLLECTOR_URL is unset (e.g. local dev).
if (env.PUBLIC_FARO_COLLECTOR_URL) {
initializeFaro({
url: env.PUBLIC_FARO_COLLECTOR_URL,
app: {
name: 'libnovel-ui',
version: env.PUBLIC_BUILD_VERSION || 'dev',
environment: 'production'
},
instrumentations: [
// Core Web Vitals (LCP, CLS, INP, TTFB, FCP) + JS errors + console
...getWebInstrumentations({ captureConsole: false })
]
});
}
export const handleError = Sentry.handleErrorWithSentry();

View File

@@ -381,6 +381,7 @@ export * from './admin_nav_logs.js'
export * from './admin_nav_uptime.js'
export * from './admin_nav_push.js'
export * from './admin_nav_gitea.js'
export * from './admin_nav_grafana.js'
export * from './admin_scrape_status_idle.js'
export * from './admin_scrape_full_catalogue.js'
export * from './admin_scrape_single_book.js'

View File

@@ -0,0 +1,44 @@
/* eslint-disable */
import { getLocale, experimentalStaticLocale } from '../runtime.js';
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
/** @typedef {{}} Admin_Nav_GrafanaInputs */
const en_admin_nav_grafana = /** @type {(inputs: Admin_Nav_GrafanaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Grafana`)
};
const ru_admin_nav_grafana = /** @type {(inputs: Admin_Nav_GrafanaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Grafana`)
};
const id_admin_nav_grafana = /** @type {(inputs: Admin_Nav_GrafanaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Grafana`)
};
const pt_admin_nav_grafana = /** @type {(inputs: Admin_Nav_GrafanaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Grafana`)
};
const fr_admin_nav_grafana = /** @type {(inputs: Admin_Nav_GrafanaInputs) => LocalizedString} */ () => {
return /** @type {LocalizedString} */ (`Grafana`)
};
/**
* | output |
* | --- |
* | "Grafana" |
*
* @param {Admin_Nav_GrafanaInputs} inputs
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
* @returns {LocalizedString}
*/
export const admin_nav_grafana = /** @type {((inputs?: Admin_Nav_GrafanaInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Admin_Nav_GrafanaInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
if (locale === "en") return en_admin_nav_grafana(inputs)
if (locale === "ru") return ru_admin_nav_grafana(inputs)
if (locale === "id") return id_admin_nav_grafana(inputs)
if (locale === "pt") return pt_admin_nav_grafana(inputs)
return fr_admin_nav_grafana(inputs)
});

View File

@@ -19,6 +19,7 @@
{ href: 'https://logs.libnovel.cc', label: () => m.admin_nav_logs() },
{ href: 'https://uptime.libnovel.cc', label: () => m.admin_nav_uptime() },
{ href: 'https://push.libnovel.cc', label: () => m.admin_nav_push() },
{ href: 'https://grafana.libnovel.cc', label: () => m.admin_nav_grafana() },
{ href: 'https://gitea.kalekber.cc/kamil/libnovel', label: () => m.admin_nav_gitea() }
];