Spaces:
Runtime error
Runtime error
feat: allow disabling metrics server, consolidate exit handlers (#1201)
Browse files* feat: allow disabling metrics server, consolidate exit handlers
* Make server off by default, enable in prods, update docs accordingly
---------
Co-authored-by: Nathan Sarrazin <sarrazin.nathan@gmail.com>
- .env +2 -1
- chart/env/prod.yaml +1 -0
- docs/source/_toctree.yml +2 -0
- docs/source/configuration/metrics.md +9 -0
- src/hooks.server.ts +3 -0
- src/lib/server/abortedGenerations.ts +2 -4
- src/lib/server/database.ts +3 -9
- src/lib/server/exitHandler.ts +41 -0
- src/lib/server/metrics.ts +21 -12
- src/lib/server/websearch/scrape/playwright.ts +2 -1
.env
CHANGED
|
@@ -159,6 +159,7 @@ ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to
|
|
| 159 |
USAGE_LIMITS=`{}`
|
| 160 |
|
| 161 |
ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls
|
| 162 |
-
|
|
|
|
| 163 |
LOG_LEVEL=info
|
| 164 |
BODY_SIZE_LIMIT=15728640
|
|
|
|
| 159 |
USAGE_LIMITS=`{}`
|
| 160 |
|
| 161 |
ALLOW_INSECURE_COOKIES=false # recommended to keep this to false but set to true if you need to run over http without tls
|
| 162 |
+
METRICS_ENABLED=false
|
| 163 |
+
METRICS_PORT=5565
|
| 164 |
LOG_LEVEL=info
|
| 165 |
BODY_SIZE_LIMIT=15728640
|
chart/env/prod.yaml
CHANGED
|
@@ -35,6 +35,7 @@ envVars:
|
|
| 35 |
EXPOSE_API: "true"
|
| 36 |
METRICS_PORT: 5565
|
| 37 |
LOG_LEVEL: "debug"
|
|
|
|
| 38 |
MODELS: >
|
| 39 |
[
|
| 40 |
{
|
|
|
|
| 35 |
EXPOSE_API: "true"
|
| 36 |
METRICS_PORT: 5565
|
| 37 |
LOG_LEVEL: "debug"
|
| 38 |
+
METRICS_ENABLED: "true"
|
| 39 |
MODELS: >
|
| 40 |
[
|
| 41 |
{
|
docs/source/_toctree.yml
CHANGED
|
@@ -20,6 +20,8 @@
|
|
| 20 |
title: OpenID
|
| 21 |
- local: configuration/web-search
|
| 22 |
title: Web Search
|
|
|
|
|
|
|
| 23 |
- local: configuration/embeddings
|
| 24 |
title: Text Embedding Models
|
| 25 |
- title: Models
|
|
|
|
| 20 |
title: OpenID
|
| 21 |
- local: configuration/web-search
|
| 22 |
title: Web Search
|
| 23 |
+
- local: configuration/metrics
|
| 24 |
+
title: Metrics
|
| 25 |
- local: configuration/embeddings
|
| 26 |
title: Text Embedding Models
|
| 27 |
- title: Models
|
docs/source/configuration/metrics.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Metrics
|
| 2 |
+
|
| 3 |
+
The server can expose prometheus metrics on port `5565` but is off by default. You may enable the metrics server with `METRICS_ENABLED=true` and change the port with `METRICS_PORT=1234`.
|
| 4 |
+
|
| 5 |
+
<Tip>
|
| 6 |
+
|
| 7 |
+
In development with `npm run dev`, the metrics server does not shutdown gracefully due to Sveltekit not providing hooks for restart. It's recommended to disable the metrics server in this case.
|
| 8 |
+
|
| 9 |
+
</Tip>
|
src/hooks.server.ts
CHANGED
|
@@ -13,10 +13,13 @@ import { refreshAssistantsCounts } from "$lib/assistantStats/refresh-assistants-
|
|
| 13 |
import { logger } from "$lib/server/logger";
|
| 14 |
import { AbortedGenerations } from "$lib/server/abortedGenerations";
|
| 15 |
import { MetricsServer } from "$lib/server/metrics";
|
|
|
|
| 16 |
import { ObjectId } from "mongodb";
|
| 17 |
|
| 18 |
// TODO: move this code on a started server hook, instead of using a "building" flag
|
| 19 |
if (!building) {
|
|
|
|
|
|
|
| 20 |
await checkAndRunMigrations();
|
| 21 |
if (env.ENABLE_ASSISTANTS) {
|
| 22 |
refreshAssistantsCounts();
|
|
|
|
| 13 |
import { logger } from "$lib/server/logger";
|
| 14 |
import { AbortedGenerations } from "$lib/server/abortedGenerations";
|
| 15 |
import { MetricsServer } from "$lib/server/metrics";
|
| 16 |
+
import { initExitHandler } from "$lib/server/exitHandler";
|
| 17 |
import { ObjectId } from "mongodb";
|
| 18 |
|
| 19 |
// TODO: move this code on a started server hook, instead of using a "building" flag
|
| 20 |
if (!building) {
|
| 21 |
+
initExitHandler();
|
| 22 |
+
|
| 23 |
await checkAndRunMigrations();
|
| 24 |
if (env.ENABLE_ASSISTANTS) {
|
| 25 |
refreshAssistantsCounts();
|
src/lib/server/abortedGenerations.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
import { collections } from "$lib/server/database";
|
|
|
|
| 5 |
|
| 6 |
export class AbortedGenerations {
|
| 7 |
private static instance: AbortedGenerations;
|
|
@@ -10,10 +11,7 @@ export class AbortedGenerations {
|
|
| 10 |
|
| 11 |
private constructor() {
|
| 12 |
const interval = setInterval(this.updateList, 1000);
|
| 13 |
-
|
| 14 |
-
process.on("SIGINT", () => {
|
| 15 |
-
clearInterval(interval);
|
| 16 |
-
});
|
| 17 |
}
|
| 18 |
|
| 19 |
public static getInstance(): AbortedGenerations {
|
|
|
|
| 2 |
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
import { collections } from "$lib/server/database";
|
| 5 |
+
import { onExit } from "./exitHandler";
|
| 6 |
|
| 7 |
export class AbortedGenerations {
|
| 8 |
private static instance: AbortedGenerations;
|
|
|
|
| 11 |
|
| 12 |
private constructor() {
|
| 13 |
const interval = setInterval(this.updateList, 1000);
|
| 14 |
+
onExit(() => clearInterval(interval));
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
public static getInstance(): AbortedGenerations {
|
src/lib/server/database.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Semaphore } from "$lib/types/Semaphore";
|
|
| 15 |
import type { AssistantStats } from "$lib/types/AssistantStats";
|
| 16 |
import { logger } from "$lib/server/logger";
|
| 17 |
import { building } from "$app/environment";
|
|
|
|
| 18 |
|
| 19 |
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
|
| 20 |
|
|
@@ -41,15 +42,8 @@ export class Database {
|
|
| 41 |
this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
|
| 42 |
this.client.on("open", () => this.initDatabase());
|
| 43 |
|
| 44 |
-
// Disconnect DB on
|
| 45 |
-
|
| 46 |
-
await this.client.close(true);
|
| 47 |
-
|
| 48 |
-
// https://github.com/sveltejs/kit/issues/9540
|
| 49 |
-
setTimeout(() => {
|
| 50 |
-
process.exit(0);
|
| 51 |
-
}, 100);
|
| 52 |
-
});
|
| 53 |
}
|
| 54 |
|
| 55 |
public static getInstance(): Database {
|
|
|
|
| 15 |
import type { AssistantStats } from "$lib/types/AssistantStats";
|
| 16 |
import { logger } from "$lib/server/logger";
|
| 17 |
import { building } from "$app/environment";
|
| 18 |
+
import { onExit } from "./exitHandler";
|
| 19 |
|
| 20 |
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
|
| 21 |
|
|
|
|
| 42 |
this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
|
| 43 |
this.client.on("open", () => this.initDatabase());
|
| 44 |
|
| 45 |
+
// Disconnect DB on exit
|
| 46 |
+
onExit(() => this.client.close(true));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
public static getInstance(): Database {
|
src/lib/server/exitHandler.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomUUID } from "$lib/utils/randomUuid";
|
| 2 |
+
import { timeout } from "$lib/utils/timeout";
|
| 3 |
+
import { logger } from "./logger";
|
| 4 |
+
|
| 5 |
+
type ExitHandler = () => void | Promise<void>;
|
| 6 |
+
type ExitHandlerUnsubscribe = () => void;
|
| 7 |
+
|
| 8 |
+
const listeners = new Map<string, ExitHandler>();
|
| 9 |
+
|
| 10 |
+
export function onExit(cb: ExitHandler): ExitHandlerUnsubscribe {
|
| 11 |
+
const uuid = randomUUID();
|
| 12 |
+
listeners.set(uuid, cb);
|
| 13 |
+
return () => {
|
| 14 |
+
listeners.delete(uuid);
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
async function runExitHandler(handler: ExitHandler): Promise<void> {
|
| 19 |
+
return timeout(Promise.resolve().then(handler), 30_000).catch((err) => {
|
| 20 |
+
logger.error("Exit handler failed to run", err);
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function initExitHandler() {
|
| 25 |
+
let signalCount = 0;
|
| 26 |
+
const exitHandler = async () => {
|
| 27 |
+
signalCount++;
|
| 28 |
+
if (signalCount === 1) {
|
| 29 |
+
logger.info("Received signal... Exiting");
|
| 30 |
+
await Promise.all(Array.from(listeners.values()).map(runExitHandler));
|
| 31 |
+
logger.info("All exit handlers ran... Waiting for svelte server to exit");
|
| 32 |
+
}
|
| 33 |
+
if (signalCount === 3) {
|
| 34 |
+
logger.warn("Received 3 signals... Exiting immediately");
|
| 35 |
+
process.exit(1);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
process.on("SIGINT", exitHandler);
|
| 40 |
+
process.on("SIGTERM", exitHandler);
|
| 41 |
+
}
|
src/lib/server/metrics.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { logger } from "$lib/server/logger";
|
|
| 4 |
import { env } from "$env/dynamic/private";
|
| 5 |
import type { Model } from "$lib/types/Model";
|
| 6 |
import type { Tool } from "$lib/types/Tool";
|
|
|
|
|
|
|
| 7 |
|
| 8 |
interface Metrics {
|
| 9 |
model: {
|
|
@@ -37,11 +39,26 @@ export class MetricsServer {
|
|
| 37 |
|
| 38 |
private constructor() {
|
| 39 |
const app = express();
|
| 40 |
-
const port = env.METRICS_PORT || "5565";
|
| 41 |
|
| 42 |
-
const
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
const register = new Registry();
|
| 47 |
collectDefaultMetrics({ register });
|
|
@@ -160,14 +177,6 @@ export class MetricsServer {
|
|
| 160 |
res.send(metrics);
|
| 161 |
});
|
| 162 |
});
|
| 163 |
-
|
| 164 |
-
process.on("SIGINT", async () => {
|
| 165 |
-
logger.info("Sigint received, disconnect metrics server ...");
|
| 166 |
-
server.close(() => {
|
| 167 |
-
logger.info("Server stopped ...");
|
| 168 |
-
});
|
| 169 |
-
process.exit();
|
| 170 |
-
});
|
| 171 |
}
|
| 172 |
|
| 173 |
public static getInstance(): MetricsServer {
|
|
|
|
| 4 |
import { env } from "$env/dynamic/private";
|
| 5 |
import type { Model } from "$lib/types/Model";
|
| 6 |
import type { Tool } from "$lib/types/Tool";
|
| 7 |
+
import { onExit } from "./exitHandler";
|
| 8 |
+
import { promisify } from "util";
|
| 9 |
|
| 10 |
interface Metrics {
|
| 11 |
model: {
|
|
|
|
| 39 |
|
| 40 |
private constructor() {
|
| 41 |
const app = express();
|
|
|
|
| 42 |
|
| 43 |
+
const port = Number(env.METRICS_PORT || "5565");
|
| 44 |
+
if (isNaN(port) || port < 0 || port > 65535) {
|
| 45 |
+
logger.warn(`Invalid value for METRICS_PORT: ${env.METRICS_PORT}`);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if (env.METRICS_ENABLED !== "false" && env.METRICS_ENABLED !== "true") {
|
| 49 |
+
logger.warn(`Invalid value for METRICS_ENABLED: ${env.METRICS_ENABLED}`);
|
| 50 |
+
}
|
| 51 |
+
if (env.METRICS_ENABLED === "true") {
|
| 52 |
+
const server = app.listen(port, () => {
|
| 53 |
+
logger.info(`Metrics server listening on port ${port}`);
|
| 54 |
+
});
|
| 55 |
+
const closeServer = promisify(server.close);
|
| 56 |
+
onExit(async () => {
|
| 57 |
+
logger.info("Disconnecting metrics server ...");
|
| 58 |
+
await closeServer();
|
| 59 |
+
logger.info("Server stopped ...");
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
|
| 63 |
const register = new Registry();
|
| 64 |
collectDefaultMetrics({ register });
|
|
|
|
| 177 |
res.send(metrics);
|
| 178 |
});
|
| 179 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
|
| 182 |
public static getInstance(): MetricsServer {
|
src/lib/server/websearch/scrape/playwright.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
| 9 |
import { PlaywrightBlocker } from "@cliqz/adblocker-playwright";
|
| 10 |
import { env } from "$env/dynamic/private";
|
| 11 |
import { logger } from "$lib/server/logger";
|
|
|
|
| 12 |
|
| 13 |
const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch)
|
| 14 |
.then((blker) => {
|
|
@@ -24,7 +25,7 @@ const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch)
|
|
| 24 |
let browserSingleton: Promise<Browser> | undefined;
|
| 25 |
async function getBrowser() {
|
| 26 |
const browser = await chromium.launch({ headless: true });
|
| 27 |
-
|
| 28 |
browser.on("disconnected", () => {
|
| 29 |
logger.warn("Browser closed");
|
| 30 |
browserSingleton = undefined;
|
|
|
|
| 9 |
import { PlaywrightBlocker } from "@cliqz/adblocker-playwright";
|
| 10 |
import { env } from "$env/dynamic/private";
|
| 11 |
import { logger } from "$lib/server/logger";
|
| 12 |
+
import { onExit } from "$lib/server/exitHandler";
|
| 13 |
|
| 14 |
const blocker = await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch)
|
| 15 |
.then((blker) => {
|
|
|
|
| 25 |
let browserSingleton: Promise<Browser> | undefined;
|
| 26 |
async function getBrowser() {
|
| 27 |
const browser = await chromium.launch({ headless: true });
|
| 28 |
+
onExit(() => browser.close());
|
| 29 |
browser.on("disconnected", () => {
|
| 30 |
logger.warn("Browser closed");
|
| 31 |
browserSingleton = undefined;
|