-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add 404 page and serve metrics on separate port #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3784e52
2547c36
030e738
53574b9
bb734e1
ef09ec4
2e02f18
3f5caaa
004da0f
a00168e
adb4046
e422e7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .emptyState { | ||
| justify-content: center; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,12 @@ | ||
| import { Flex, Headline, Text } from '@raystack/apsara'; | ||
| import { EmptyState } from '@raystack/apsara'; | ||
| import styles from './NotFound.module.css'; | ||
|
|
||
| export function NotFound() { | ||
| return ( | ||
| <Flex | ||
| direction='column' | ||
| align='center' | ||
| justify='center' | ||
| style={{ minHeight: '60vh' }} | ||
| > | ||
| <Headline size='large' as='h1'> | ||
| 404 | ||
| </Headline> | ||
| <Text size={3}>Page not found</Text> | ||
| </Flex> | ||
| <EmptyState | ||
| heading="404" | ||
| subHeading="Page not found" | ||
| classNames={{ container: styles.emptyState }} | ||
| /> | ||
| ); | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,8 +9,8 @@ import { loadConfig } from '@/lib/config'; | |||||
| import { loadApiSpecs } from '@/lib/openapi'; | ||||||
| import { PageProvider } from '@/lib/page-context'; | ||||||
| import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; | ||||||
| import { useNitroApp } from 'nitro/app'; | ||||||
| import { App } from './App'; | ||||||
| import { recordSSRRender } from './telemetry'; | ||||||
|
|
||||||
| import clientAssets from './entry-client?assets=client'; | ||||||
| import serverAssets from './entry-server?assets=ssr'; | ||||||
|
|
@@ -98,7 +98,7 @@ export default { | |||||
| const isApiRoute = pathname.startsWith('/apis'); | ||||||
| const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200; | ||||||
|
|
||||||
| recordSSRRender(pathname, status, renderDuration); | ||||||
| useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, in Nitro/Hookable, hooks.callHook(...) returns a Promise that should be awaited or otherwise handled to avoid unhandled rejections if any hook callbacks reject. Citations:
🏁 Script executed: # Find and examine the entry-server.tsx file
fd -t f entry-server.tsxRepository: raystack/chronicle Length of output: 108 🏁 Script executed: # Read the file around line 101 to understand context
head -n 120 packages/chronicle/src/server/entry-server.tsx | tail -n 40Repository: raystack/chronicle Length of output: 1369 🏁 Script executed: # Get more context - full function containing line 101
sed -n '80,120p' packages/chronicle/src/server/entry-server.tsxRepository: raystack/chronicle Length of output: 929 🏁 Script executed: # Get the full function signature and start
sed -n '50,110p' packages/chronicle/src/server/entry-server.tsxRepository: raystack/chronicle Length of output: 2003 🏁 Script executed: # Check if the handler function is async
sed -n '40,105p' packages/chronicle/src/server/entry-server.tsxRepository: raystack/chronicle Length of output: 2306 🏁 Script executed: # Get the complete handler function definition from start to line 110
sed -n '1,110p' packages/chronicle/src/server/entry-server.tsx | head -n 50Repository: raystack/chronicle Length of output: 1897 🏁 Script executed: # Search for the handler function signature
rg -A 80 "export default" packages/chronicle/src/server/entry-server.tsx | head -n 100Repository: raystack/chronicle Length of output: 2745 🏁 Script executed: # Check the telemetry hook registration to understand if it's meant to be fire-and-forget
cat packages/chronicle/src/server/plugins/telemetry.tsRepository: raystack/chronicle Length of output: 2247 🏁 Script executed: # Look for other usages of callHook in the codebase to see if there's a pattern
rg "callHook" packages/chronicle/src/server/Repository: raystack/chronicle Length of output: 202 Handle the Nitro hook promise explicitly.
Proposed fix- useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration);
+ await useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| return new Response(stream, { | ||||||
| status, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,21 +1,61 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import type { Counter, Histogram } from '@opentelemetry/api' | ||||||||||||||||||||||||||||||||||||||||||||
| import { MeterProvider } from '@opentelemetry/sdk-metrics' | ||||||||||||||||||||||||||||||||||||||||||||
| import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' | ||||||||||||||||||||||||||||||||||||||||||||
| import { resourceFromAttributes } from '@opentelemetry/resources' | ||||||||||||||||||||||||||||||||||||||||||||
| import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions' | ||||||||||||||||||||||||||||||||||||||||||||
| import type { H3Event } from 'h3' | ||||||||||||||||||||||||||||||||||||||||||||
| import { definePlugin } from 'nitro' | ||||||||||||||||||||||||||||||||||||||||||||
| import { loadConfig } from '@/lib/config' | ||||||||||||||||||||||||||||||||||||||||||||
| import { initTelemetry, recordRequest } from '../telemetry' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| declare module 'nitro/types' { | ||||||||||||||||||||||||||||||||||||||||||||
| interface NitroRuntimeHooks { | ||||||||||||||||||||||||||||||||||||||||||||
| 'chronicle:ssr-rendered': (route: string, status: number, durationMs: number) => void | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export default definePlugin((nitroApp) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const config = loadConfig() | ||||||||||||||||||||||||||||||||||||||||||||
| if (!config.telemetry?.enabled) return | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| initTelemetry(config) | ||||||||||||||||||||||||||||||||||||||||||||
| const resource = resourceFromAttributes({ | ||||||||||||||||||||||||||||||||||||||||||||
| [ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle', | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const port = config.telemetry?.port ?? 9090 | ||||||||||||||||||||||||||||||||||||||||||||
| const exporter = new PrometheusExporter({ port }) | ||||||||||||||||||||||||||||||||||||||||||||
| const provider = new MeterProvider({ resource, readers: [exporter] }) | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, Citations:
🏁 Script executed: cat -n packages/chronicle/src/server/plugins/telemetry.ts | head -50Repository: raystack/chronicle Length of output: 2230 🏁 Script executed: rg -i "preset|vercel|cloudflare|node-server" packages/chronicle --type ts --type js | head -20Repository: raystack/chronicle Length of output: 1920 🏁 Script executed: cat -n packages/chronicle/src/lib/config.ts | head -100Repository: raystack/chronicle Length of output: 1222 🏁 Script executed: cat -n packages/chronicle/src/types/config.tsRepository: raystack/chronicle Length of output: 3802 Guard this exporter to supported runtimes or gate telemetry to
Without a guard, enabling 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| const meter = provider.getMeter('chronicle') | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const requestCounter: Counter = meter.createCounter('http_server_request_total', { | ||||||||||||||||||||||||||||||||||||||||||||
| description: 'Total HTTP requests', | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| const requestDuration: Histogram = meter.createHistogram('http_server_request_duration_ms', { | ||||||||||||||||||||||||||||||||||||||||||||
| description: 'HTTP request duration in ms', | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| const ssrRenderDuration: Histogram = meter.createHistogram('http_server_ssr_render_duration_ms', { | ||||||||||||||||||||||||||||||||||||||||||||
| description: 'SSR render duration in ms', | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| nitroApp.hooks.hook('close', async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| await provider.shutdown() | ||||||||||||||||||||||||||||||||||||||||||||
| await exporter.shutdown() | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, MeterProvider.shutdown shuts down registered readers such as PrometheusExporter, making an extra exporter.shutdown redundant. Citations:
🏁 Script executed: cat -n packages/chronicle/src/server/plugins/telemetry.tsRepository: raystack/chronicle Length of output: 2646 Remove the redundant The Proposed fix nitroApp.hooks.hook('close', async () => {
await provider.shutdown()
- await exporter.shutdown()
})🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| nitroApp.hooks.hook('chronicle:ssr-rendered', (route, status, durationMs) => { | ||||||||||||||||||||||||||||||||||||||||||||
| ssrRenderDuration.record(durationMs, { route, status }) | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| nitroApp.hooks.hook('request', (event) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (event.path === '/api/metrics') return | ||||||||||||||||||||||||||||||||||||||||||||
| event.context._requestStart = performance.now() | ||||||||||||||||||||||||||||||||||||||||||||
| (event as H3Event).context._requestStart = performance.now() | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| nitroApp.hooks.hook('response', (res, event) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (!event.context._requestStart) return | ||||||||||||||||||||||||||||||||||||||||||||
| const duration = performance.now() - event.context._requestStart | ||||||||||||||||||||||||||||||||||||||||||||
| recordRequest(event.method, event.path, res.status, duration) | ||||||||||||||||||||||||||||||||||||||||||||
| const start = (event as H3Event).context._requestStart as number | undefined | ||||||||||||||||||||||||||||||||||||||||||||
| if (start === undefined) return | ||||||||||||||||||||||||||||||||||||||||||||
| const duration = performance.now() - start | ||||||||||||||||||||||||||||||||||||||||||||
| const method = event.req.method | ||||||||||||||||||||||||||||||||||||||||||||
| const route = new URL(event.req.url).pathname | ||||||||||||||||||||||||||||||||||||||||||||
| requestCounter.add(1, { method, route, status: res.status }) | ||||||||||||||||||||||||||||||||||||||||||||
| requestDuration.record(duration, { method, route, status: res.status }) | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
52
to
60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify that event.req.url in Nitro/H3 is a relative path
# Search for how other Nitro plugins or H3 middleware handle URL parsing
rg -n -C3 'event\.req\.url|getRequestURL' --type ts packages/chronicle/src/serverRepository: raystack/chronicle Length of output: 1387 🏁 Script executed: #!/bin/bash
# Search for usage of event.path or other URL handling patterns in H3/Nitro plugins
rg -n 'event\.path|getRequestURL|event\.node\.req' --type ts packages/chronicle/src/server/pluginsRepository: raystack/chronicle Length of output: 44 🏁 Script executed: #!/bin/bash
# Check what properties are available on H3Event by looking at imports and type usage
rg -n 'H3Event|defineEventHandler|import.*h3' --type ts packages/chronicle/src/server | head -40Repository: raystack/chronicle Length of output: 402 🌐 Web query:
💡 Result: In H3 (the high-performance HTTP framework used by Nuxt/Nitro), an H3Event object is created for each incoming HTTP request. The event.req property provides access to the incoming HTTP request information, modeled after the native Web API Request with runtime addons. event.req.url is a string containing the absolute URL of the request (full URL including protocol, host, port, pathname, search, and hash if present). This matches the standard behavior of the Fetch API's Request.url property, which always returns the absolute URL. However, in server environments like Node.js, H3 constructs event.req such that event.req.url typically holds the relative path (pathname + search) from the request URL, without the origin (e.g., "/api/users?id=123" for a request to "https://example.com/api/users?id=123"). This is consistent with traditional Node.js http.IncomingMessage.url and common server framework conventions, allowing logging like [GET] /api/users. To get the relative path (just pathname + search): - Use event.req.url directly (e.g., "/api/users?id=123") - Or prefer event.path, which is explicitly the request path (same value, recommended over parsing event.req.url) To get the absolute pathname (full URL as string): - Use getRequestURL(event) utility (e.g., "https://example.com/api/users?id=123") Examples from official docs: app.get("/", async (event) => { console.log(event.req.url); // "/" const fullUrl = getRequestURL(event); // "https://example.com/" }); event.path // "/test?test=123" (relative path example) event.url provides a parsed URL object for the full request URL. Sources confirm event.req is Web Request-based but adapted for server use, with event.path preferred for relative paths post-refactors. Citations:
🏁 Script executed: #!/bin/bash
# Check if there are tests for telemetry plugin
fd -e test.ts -e spec.ts -e test.js packages/chronicle | grep -i telemetry
# Also check test directories
find packages/chronicle -type f -name "*test*" -o -name "*spec*" | grep -i telemetryRepository: raystack/chronicle Length of output: 439 🏁 Script executed: #!/bin/bash
# Check if event.path is used elsewhere in the codebase for URL parsing
rg -n 'event\.path' --type ts packages/chronicle/src/serverRepository: raystack/chronicle Length of output: 185
In H3/Nitro, Use nitroApp.hooks.hook('response', (res, event) => {
const start = (event as H3Event).context._requestStart as number | undefined
if (start === undefined) return
const duration = performance.now() - start
const method = event.req.method
- const route = new URL(event.req.url).pathname
+ const route = event.path.split('?')[0]
requestCounter.add(1, { method, route, status: res.status })
requestDuration.record(duration, { method, route, status: res.status })
})Alternatively, if you need to construct a URL object, use a base: 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document the validated
telemetry.portconstraint.The table says
number, but the implementation validates an integer in1-65535. Please mirror that here so users do not hit startup validation errors after copying the example.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents