diff --git a/api-client/deno.json b/api-client/deno.json index 7ea3508..a94cac4 100644 --- a/api-client/deno.json +++ b/api-client/deno.json @@ -3,7 +3,7 @@ "@preact/signals": "npm:@preact/signals@^2.9.0" }, "name": "@01edu/api-client", - "version": "0.2.3", + "version": "0.2.4", "license": "MIT", "exports": { ".": "./mod.ts" }, "compilerOptions": { diff --git a/api/deno.json b/api/deno.json index 274cb9e..342aa92 100644 --- a/api/deno.json +++ b/api/deno.json @@ -4,7 +4,7 @@ "@std/http": "jsr:@std/http@^1.0.25" }, "name": "@01edu/api", - "version": "0.2.3", + "version": "0.2.4", "license": "MIT", "exports": { "./context": "./context.ts", diff --git a/api/dev.ts b/api/dev.ts index ecb26ab..38444df 100644 --- a/api/dev.ts +++ b/api/dev.ts @@ -3,7 +3,8 @@ import { respond } from './response.ts' import type { RequestContext } from '@01edu/types/context' import { route } from './router.ts' import { ARR, NUM, OBJ, optional, STR } from './validator.ts' -import type { Metric, Sql } from '@01edu/types/db' +import type { Metric as DbMetric, Sql } from '@01edu/types/db' +import { routeMetrics } from './route-metrics.ts' /** * Authorizes access to developer routes. @@ -64,7 +65,7 @@ export const createSqlDevRoute = (sql?: Sql) => { * * @returns A route handler configuration. */ -export const createQueryMetricsDevRoute = (metrics: Metric[]) => +export const createQueryMetricsDevRoute = (metrics: DbMetric[]) => route({ authorize: authorizeDevAccess, fn: () => metrics, @@ -91,9 +92,31 @@ export const createQueryMetricsDevRoute = (metrics: Metric[]) => run: NUM('Number of statement runs'), filterHit: NUM('Bloom filter bypass hits'), filterMiss: NUM('Bloom filter misses'), + memused: NUM('Approximate memory used by the statement'), }, 'SQLite sqlite3_stmt_status counters'), }), 'Collected query metrics', ), description: 'List collected SQL query metrics', }) + +export const createRouterMetricsDevRoute = () => + route({ + authorize: authorizeDevAccess, + fn: () => Object.values(routeMetrics), + output: ARR( + OBJ({ + key: STR('Route key (method:path) allow to identify which route'), + duration: NUM( + 'Total time the route handler take to respond, in milliseconds', + ), + count: NUM('How many times the route was called'), + error: NUM('Number of time it responded with a status 400 or above'), + success: NUM( + 'Number of time it responded with a status under 399 (Success, Redirect and Info)', + ), + }, 'Route metrics'), + 'Collected route metrics', + ), + description: 'List collected route metrics', + }) diff --git a/api/route-metrics.ts b/api/route-metrics.ts new file mode 100644 index 0000000..17ae2df --- /dev/null +++ b/api/route-metrics.ts @@ -0,0 +1,10 @@ +/* Imported by both the server and dev routes. + * Defined in a distinct module to avoid import loops + */ +export const routeMetrics: Record = {} diff --git a/api/router.ts b/api/router.ts index 35711be..ccdd4cb 100644 --- a/api/router.ts +++ b/api/router.ts @@ -23,7 +23,11 @@ import type { import type { Log } from './log.ts' import { respond, ResponseError } from './response.ts' import type { Metric, Sql } from '@01edu/types/db' -import { createQueryMetricsDevRoute, createSqlDevRoute } from './dev.ts' +import { + createQueryMetricsDevRoute, + createRouterMetricsDevRoute, + createSqlDevRoute, +} from './dev.ts' import { createDocRoute } from './doc.ts' import { createHealthRoute } from './health.ts' @@ -129,6 +133,10 @@ export const makeRouter = ( defs['GET/api/sql/metrics'] = createQueryMetricsDevRoute(metrics) } + if (!defs['GET/api/router/metrics']) { + defs['GET/api/router/metrics'] = createRouterMetricsDevRoute() + } + if (!defs['GET/api/doc']) { defs['GET/api/doc'] = createDocRoute(defs) } diff --git a/api/server.ts b/api/server.ts index 78860a8..132939c 100644 --- a/api/server.ts +++ b/api/server.ts @@ -41,6 +41,7 @@ import { respond, ResponseError } from './response.ts' import { now } from '@01edu/time' import type { Awaitable } from '@01edu/types' import { BASE_URL } from './env.ts' +import { routeMetrics as metrics } from './route-metrics.ts' type Handler = (ctx: RequestContext) => Awaitable /** @@ -56,7 +57,8 @@ export const server = ( ): (req: Request, url?: URL) => Promise => { const handleRequest = async (ctx: RequestContext) => { const logProps: Record = {} - logProps.path = `${ctx.req.method}:${ctx.url.pathname}` + const key = `${ctx.req.method}:${ctx.url.pathname}` + logProps.path = key log.info('in', logProps) try { const res = await routeHandler(ctx) @@ -78,6 +80,13 @@ export const server = ( logProps.duration = now() - ctx.span! log.error('out', logProps) return response + } finally { + const metric = metrics[key] || + (metrics[key] = { key, duration: 0, count: 0, success: 0, error: 0 }) + metric.duration += (logProps.duration as number) || 0 + const status = (logProps.status as number) || 0 + metric[status > 399 ? 'error' : 'success']++ + metric.count++ } } diff --git a/db/deno.json b/db/deno.json index e6911ea..331295c 100644 --- a/db/deno.json +++ b/db/deno.json @@ -4,7 +4,7 @@ "@std/assert": "jsr:@std/assert@^1.0.19" }, "name": "@01edu/db", - "version": "0.2.3", + "version": "0.2.4", "license": "MIT", "exports": { ".": "./mod.ts", diff --git a/db/mod.ts b/db/mod.ts index 4f50fc9..eea0801 100644 --- a/db/mod.ts +++ b/db/mod.ts @@ -7,7 +7,7 @@ */ import { assertEquals } from '@std/assert/equals' -import { Database, type BindParameters, type BindValue } from '@db/sqlite' +import { type BindParameters, type BindValue, Database } from '@db/sqlite' import type { Expand, MatchKeys, UnionToIntersection } from '@01edu/types' import type { ExplainRow, Metric, Sql } from '@01edu/types/db' import { respond } from '@01edu/api/response' @@ -518,4 +518,3 @@ export const sqlCheck = ( const { value } = sql`SELECT EXISTS(SELECT 1 ${String.raw(query, ...args)})` return ((params: T) => value(params)?.[0] === 1) } - diff --git a/signal-router/deno.json b/signal-router/deno.json index 5a43319..e46ae5b 100644 --- a/signal-router/deno.json +++ b/signal-router/deno.json @@ -4,7 +4,7 @@ "preact": "npm:preact@^10.29.0" }, "name": "@01edu/signal-router", - "version": "0.2.1", + "version": "0.2.2", "license": "MIT", "exports": { ".": "./mod.tsx" }, "compilerOptions": { diff --git a/signal-router/mod.tsx b/signal-router/mod.tsx index 4fcb726..55775b6 100644 --- a/signal-router/mod.tsx +++ b/signal-router/mod.tsx @@ -35,7 +35,7 @@ import type { TargetedMouseEvent, TargetedPointerEvent, } from 'preact' -import { computed, Signal } from '@preact/signals' +import { computed, Signal, untracked } from '@preact/signals' const isCurrentURL = (alt: URL) => { const url = urlSignal.value @@ -138,7 +138,7 @@ const getUrl = ({ href, hash, params }: GetUrlProps): URL => { * ``` */ export const navigate = (props: GetUrlProps & { replace?: boolean }): void => - navigateUrl(getUrl(props).href, props.replace) + untracked(() => navigateUrl(getUrl(props).href, props.replace)) /** * Props for the `` component. diff --git a/types/deno.json b/types/deno.json index cdf9c21..dc2b055 100644 --- a/types/deno.json +++ b/types/deno.json @@ -3,7 +3,7 @@ "@db/sqlite": "jsr:@cd/sqlite@^0.13.1" }, "name": "@01edu/types", - "version": "0.2.3", + "version": "0.2.4", "license": "MIT", "exports": { ".": "./mod.d.ts", diff --git a/types/router.d.ts b/types/router.d.ts index 0ee2fa4..dfa75f0 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -79,6 +79,12 @@ type ReservedRoutes = { * @deprecated */ 'GET/api/sql/metrics'?: Handler + + /** + * ⚠️ WARNING: You are overriding the system router metrics route. + * @deprecated + */ + 'GET/api/router/metrics'?: Handler } // deno-lint-ignore no-explicit-any