@raystack/chronicle 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2 -1
- package/package.json +2 -2
- package/src/lib/page-context.tsx +35 -7
- package/src/pages/DocsPage.tsx +4 -1
- package/src/pages/NotFound.module.css +3 -0
- package/src/pages/NotFound.tsx +7 -12
- package/src/server/api/{page/[...slug].ts → page.ts} +2 -2
- package/src/server/entry-server.tsx +2 -2
- package/src/server/plugins/telemetry.ts +47 -7
- package/src/types/config.ts +1 -0
- package/src/server/api/metrics.ts +0 -23
- package/src/server/api/page/index.ts +0 -1
- package/src/server/telemetry.ts +0 -49
package/dist/cli/index.js
CHANGED
|
@@ -256,7 +256,8 @@ var analyticsSchema = z.object({
|
|
|
256
256
|
});
|
|
257
257
|
var telemetrySchema = z.object({
|
|
258
258
|
enabled: z.boolean().optional(),
|
|
259
|
-
serviceName: z.string().optional()
|
|
259
|
+
serviceName: z.string().optional(),
|
|
260
|
+
port: z.number().int().min(1).max(65535).default(9090)
|
|
260
261
|
});
|
|
261
262
|
var chronicleConfigSchema = z.object({
|
|
262
263
|
title: z.string(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raystack/chronicle",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Config-driven documentation framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"codemirror": "^6.0.2",
|
|
50
50
|
"commander": "^14.0.2",
|
|
51
51
|
"fumadocs-core": "16.6.15",
|
|
52
|
-
"fumadocs-mdx": "
|
|
52
|
+
"fumadocs-mdx": "14.2.6",
|
|
53
53
|
"glob": "^11.0.0",
|
|
54
54
|
"gray-matter": "^4.0.3",
|
|
55
55
|
"h3": "^2.0.1-rc.16",
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -22,6 +22,7 @@ interface PageContextValue {
|
|
|
22
22
|
config: ChronicleConfig;
|
|
23
23
|
tree: Root;
|
|
24
24
|
page: PageData | null;
|
|
25
|
+
errorStatus: number | null;
|
|
25
26
|
apiSpecs: ApiSpec[];
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -35,6 +36,7 @@ export function usePageContext(): PageContextValue {
|
|
|
35
36
|
config: { title: 'Documentation' },
|
|
36
37
|
tree: { name: 'root', children: [] } as Root,
|
|
37
38
|
page: null,
|
|
39
|
+
errorStatus: null,
|
|
38
40
|
apiSpecs: []
|
|
39
41
|
};
|
|
40
42
|
}
|
|
@@ -50,6 +52,16 @@ interface PageProviderProps {
|
|
|
50
52
|
children: ReactNode;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
function isApisRoute(pathname: string): boolean {
|
|
56
|
+
return pathname === '/apis' || pathname.startsWith('/apis/');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getInitialErrorStatus(page: PageData | null, pathname: string): number | null {
|
|
60
|
+
if (page) return null;
|
|
61
|
+
if (pathname === '/' || isApisRoute(pathname)) return null;
|
|
62
|
+
return 404;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
export function PageProvider({
|
|
54
66
|
initialConfig,
|
|
55
67
|
initialTree,
|
|
@@ -61,6 +73,7 @@ export function PageProvider({
|
|
|
61
73
|
const { pathname } = useLocation();
|
|
62
74
|
const [tree] = useState<Root>(initialTree);
|
|
63
75
|
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
76
|
+
const [errorStatus, setErrorStatus] = useState<number | null>(getInitialErrorStatus(initialPage, pathname));
|
|
64
77
|
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
65
78
|
const [currentPath, setCurrentPath] = useState(pathname);
|
|
66
79
|
|
|
@@ -70,7 +83,7 @@ export function PageProvider({
|
|
|
70
83
|
|
|
71
84
|
const cancelled = { current: false };
|
|
72
85
|
|
|
73
|
-
if (pathname
|
|
86
|
+
if (isApisRoute(pathname)) {
|
|
74
87
|
if (apiSpecs.length === 0) {
|
|
75
88
|
fetch('/api/specs')
|
|
76
89
|
.then(res => res.json())
|
|
@@ -86,24 +99,39 @@ export function PageProvider({
|
|
|
86
99
|
? []
|
|
87
100
|
: pathname.slice(1).split('/').filter(Boolean);
|
|
88
101
|
|
|
89
|
-
const apiPath = slug.length === 0 ? '/api/page
|
|
102
|
+
const apiPath = slug.length === 0 ? '/api/page' : `/api/page?slug=${slug.join(',')}`;
|
|
90
103
|
|
|
91
104
|
fetch(apiPath)
|
|
92
|
-
.then(res =>
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
.then(res => {
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
if (!cancelled.current) {
|
|
108
|
+
setPage(null);
|
|
109
|
+
setErrorStatus(res.status);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
return res.json();
|
|
114
|
+
})
|
|
115
|
+
.then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string } | undefined) => {
|
|
116
|
+
if (cancelled.current || !data) return;
|
|
95
117
|
const { content, toc } = await loadMdx(data.originalPath || data.relativePath);
|
|
96
118
|
if (cancelled.current) return;
|
|
119
|
+
setErrorStatus(null);
|
|
97
120
|
setPage({ slug, frontmatter: data.frontmatter, content, toc });
|
|
98
121
|
})
|
|
99
|
-
.catch(() => {
|
|
122
|
+
.catch(() => {
|
|
123
|
+
if (!cancelled.current) {
|
|
124
|
+
setPage(null);
|
|
125
|
+
setErrorStatus(500);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
100
128
|
|
|
101
129
|
return () => { cancelled.current = true; };
|
|
102
130
|
}, [pathname]);
|
|
103
131
|
|
|
104
132
|
return (
|
|
105
133
|
<PageContext.Provider
|
|
106
|
-
value={{ config: initialConfig, tree, page, apiSpecs }}
|
|
134
|
+
value={{ config: initialConfig, tree, page, errorStatus, apiSpecs }}
|
|
107
135
|
>
|
|
108
136
|
{children}
|
|
109
137
|
</PageContext.Provider>
|
package/src/pages/DocsPage.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Head } from '@/lib/head';
|
|
2
2
|
import { usePageContext } from '@/lib/page-context';
|
|
3
|
+
import { NotFound } from '@/pages/NotFound';
|
|
3
4
|
import { getTheme } from '@/themes/registry';
|
|
4
5
|
|
|
5
6
|
interface DocsPageProps {
|
|
@@ -7,8 +8,10 @@ interface DocsPageProps {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function DocsPage({ slug }: DocsPageProps) {
|
|
10
|
-
const { config, tree, page } = usePageContext();
|
|
11
|
+
const { config, tree, page, errorStatus } = usePageContext();
|
|
11
12
|
|
|
13
|
+
if (errorStatus === 404) return <NotFound />;
|
|
14
|
+
if (errorStatus) return <NotFound />;
|
|
12
15
|
if (!page) return null;
|
|
13
16
|
|
|
14
17
|
const { Page } = getTheme(config.theme?.name);
|
package/src/pages/NotFound.tsx
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EmptyState } from '@raystack/apsara';
|
|
2
|
+
import styles from './NotFound.module.css';
|
|
2
3
|
|
|
3
4
|
export function NotFound() {
|
|
4
5
|
return (
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
>
|
|
11
|
-
<Headline size='large' as='h1'>
|
|
12
|
-
404
|
|
13
|
-
</Headline>
|
|
14
|
-
<Text size={3}>Page not found</Text>
|
|
15
|
-
</Flex>
|
|
6
|
+
<EmptyState
|
|
7
|
+
heading="404"
|
|
8
|
+
subHeading="Page not found"
|
|
9
|
+
classNames={{ container: styles.emptyState }}
|
|
10
|
+
/>
|
|
16
11
|
);
|
|
17
12
|
}
|
|
@@ -2,8 +2,8 @@ import { defineHandler, HTTPError } from 'nitro';
|
|
|
2
2
|
import { getPage, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
3
3
|
|
|
4
4
|
export default defineHandler(async event => {
|
|
5
|
-
const slugParam = event.
|
|
6
|
-
const slug = slugParam ? slugParam.split('
|
|
5
|
+
const slugParam = event.url.searchParams.get('slug') ?? '';
|
|
6
|
+
const slug = slugParam ? slugParam.split(',').filter(Boolean) : [];
|
|
7
7
|
const page = await getPage(slug);
|
|
8
8
|
|
|
9
9
|
if (!page) {
|
|
@@ -9,8 +9,8 @@ import { loadConfig } from '@/lib/config';
|
|
|
9
9
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
10
|
import { PageProvider } from '@/lib/page-context';
|
|
11
11
|
import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
|
|
12
|
+
import { useNitroApp } from 'nitro/app';
|
|
12
13
|
import { App } from './App';
|
|
13
|
-
import { recordSSRRender } from './telemetry';
|
|
14
14
|
|
|
15
15
|
import clientAssets from './entry-client?assets=client';
|
|
16
16
|
import serverAssets from './entry-server?assets=ssr';
|
|
@@ -98,7 +98,7 @@ export default {
|
|
|
98
98
|
const isApiRoute = pathname.startsWith('/apis');
|
|
99
99
|
const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
useNitroApp().hooks.callHook('chronicle:ssr-rendered', pathname, status, renderDuration);
|
|
102
102
|
|
|
103
103
|
return new Response(stream, {
|
|
104
104
|
status,
|
|
@@ -1,21 +1,61 @@
|
|
|
1
|
+
import type { Counter, Histogram } from '@opentelemetry/api'
|
|
2
|
+
import { MeterProvider } from '@opentelemetry/sdk-metrics'
|
|
3
|
+
import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
|
|
4
|
+
import { resourceFromAttributes } from '@opentelemetry/resources'
|
|
5
|
+
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
|
|
6
|
+
import type { H3Event } from 'h3'
|
|
1
7
|
import { definePlugin } from 'nitro'
|
|
2
8
|
import { loadConfig } from '@/lib/config'
|
|
3
|
-
|
|
9
|
+
|
|
10
|
+
declare module 'nitro/types' {
|
|
11
|
+
interface NitroRuntimeHooks {
|
|
12
|
+
'chronicle:ssr-rendered': (route: string, status: number, durationMs: number) => void
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
export default definePlugin((nitroApp) => {
|
|
6
17
|
const config = loadConfig()
|
|
7
18
|
if (!config.telemetry?.enabled) return
|
|
8
19
|
|
|
9
|
-
|
|
20
|
+
const resource = resourceFromAttributes({
|
|
21
|
+
[ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const port = config.telemetry?.port ?? 9090
|
|
25
|
+
const exporter = new PrometheusExporter({ port })
|
|
26
|
+
const provider = new MeterProvider({ resource, readers: [exporter] })
|
|
27
|
+
const meter = provider.getMeter('chronicle')
|
|
28
|
+
|
|
29
|
+
const requestCounter: Counter = meter.createCounter('http_server_request_total', {
|
|
30
|
+
description: 'Total HTTP requests',
|
|
31
|
+
})
|
|
32
|
+
const requestDuration: Histogram = meter.createHistogram('http_server_request_duration_ms', {
|
|
33
|
+
description: 'HTTP request duration in ms',
|
|
34
|
+
})
|
|
35
|
+
const ssrRenderDuration: Histogram = meter.createHistogram('http_server_ssr_render_duration_ms', {
|
|
36
|
+
description: 'SSR render duration in ms',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
nitroApp.hooks.hook('close', async () => {
|
|
40
|
+
await provider.shutdown()
|
|
41
|
+
await exporter.shutdown()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
nitroApp.hooks.hook('chronicle:ssr-rendered', (route, status, durationMs) => {
|
|
45
|
+
ssrRenderDuration.record(durationMs, { route, status })
|
|
46
|
+
})
|
|
10
47
|
|
|
11
48
|
nitroApp.hooks.hook('request', (event) => {
|
|
12
|
-
|
|
13
|
-
event.context._requestStart = performance.now()
|
|
49
|
+
(event as H3Event).context._requestStart = performance.now()
|
|
14
50
|
})
|
|
15
51
|
|
|
16
52
|
nitroApp.hooks.hook('response', (res, event) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
53
|
+
const start = (event as H3Event).context._requestStart as number | undefined
|
|
54
|
+
if (start === undefined) return
|
|
55
|
+
const duration = performance.now() - start
|
|
56
|
+
const method = event.req.method
|
|
57
|
+
const route = new URL(event.req.url).pathname
|
|
58
|
+
requestCounter.add(1, { method, route, status: res.status })
|
|
59
|
+
requestDuration.record(duration, { method, route, status: res.status })
|
|
20
60
|
})
|
|
21
61
|
})
|
package/src/types/config.ts
CHANGED
|
@@ -70,6 +70,7 @@ const analyticsSchema = z.object({
|
|
|
70
70
|
const telemetrySchema = z.object({
|
|
71
71
|
enabled: z.boolean().optional(),
|
|
72
72
|
serviceName: z.string().optional(),
|
|
73
|
+
port: z.number().int().min(1).max(65535).default(9090),
|
|
73
74
|
})
|
|
74
75
|
|
|
75
76
|
export const chronicleConfigSchema = z.object({
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { defineHandler } from 'nitro'
|
|
3
|
-
import { getExporter } from '../telemetry'
|
|
4
|
-
|
|
5
|
-
export default defineHandler(async () => {
|
|
6
|
-
const exporter = getExporter()
|
|
7
|
-
if (!exporter) {
|
|
8
|
-
return new Response('Telemetry not enabled', { status: 404 })
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const metricsString = await new Promise<string>((resolve) => {
|
|
12
|
-
const mockRes = {
|
|
13
|
-
setHeader: () => mockRes,
|
|
14
|
-
end: (data: string) => resolve(data),
|
|
15
|
-
} as unknown as ServerResponse
|
|
16
|
-
|
|
17
|
-
exporter.getMetricsRequestHandler({} as unknown as IncomingMessage, mockRes)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
return new Response(metricsString, {
|
|
21
|
-
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
22
|
-
})
|
|
23
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from './[...slug]';
|
package/src/server/telemetry.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { Counter, Histogram } from '@opentelemetry/api'
|
|
2
|
-
import sdkMetrics from '@opentelemetry/sdk-metrics'
|
|
3
|
-
import prometheusExporter from '@opentelemetry/exporter-prometheus'
|
|
4
|
-
import resources from '@opentelemetry/resources'
|
|
5
|
-
import semconv from '@opentelemetry/semantic-conventions'
|
|
6
|
-
import type { ChronicleConfig } from '@/types/config'
|
|
7
|
-
|
|
8
|
-
const { MeterProvider } = sdkMetrics
|
|
9
|
-
const { PrometheusExporter } = prometheusExporter
|
|
10
|
-
const { resourceFromAttributes } = resources
|
|
11
|
-
const { ATTR_SERVICE_NAME } = semconv
|
|
12
|
-
|
|
13
|
-
let exporter: PrometheusExporter
|
|
14
|
-
let requestCounter: Counter
|
|
15
|
-
let requestDuration: Histogram
|
|
16
|
-
let ssrRenderDuration: Histogram
|
|
17
|
-
|
|
18
|
-
export function initTelemetry(config: ChronicleConfig) {
|
|
19
|
-
const resource = resourceFromAttributes({
|
|
20
|
-
[ATTR_SERVICE_NAME]: config.telemetry?.serviceName ?? 'chronicle',
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
exporter = new PrometheusExporter({ preventServerStart: true })
|
|
24
|
-
const provider = new MeterProvider({ resource, readers: [exporter] })
|
|
25
|
-
const meter = provider.getMeter('chronicle')
|
|
26
|
-
|
|
27
|
-
requestCounter = meter.createCounter('http_server_request_total', {
|
|
28
|
-
description: 'Total HTTP requests',
|
|
29
|
-
})
|
|
30
|
-
requestDuration = meter.createHistogram('http_server_request_duration_ms', {
|
|
31
|
-
description: 'HTTP request duration in ms',
|
|
32
|
-
})
|
|
33
|
-
ssrRenderDuration = meter.createHistogram('http_server_ssr_render_duration_ms', {
|
|
34
|
-
description: 'SSR render duration in ms',
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function getExporter() {
|
|
39
|
-
return exporter
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function recordRequest(method: string, route: string, status: number, durationMs: number) {
|
|
43
|
-
requestCounter?.add(1, { method, route, status })
|
|
44
|
-
requestDuration?.record(durationMs, { method, route, status })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function recordSSRRender(route: string, status: number, durationMs: number) {
|
|
48
|
-
ssrRenderDuration?.record(durationMs, { route, status })
|
|
49
|
-
}
|