@raystack/chronicle 0.5.1 → 0.5.3
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/README.md +98 -0
- package/dist/cli/index.js +12 -7
- package/package.json +6 -1
- package/src/cli/commands/dev.ts +2 -1
- package/src/cli/commands/serve.ts +2 -1
- package/src/cli/commands/start.ts +2 -1
- package/src/components/mdx/link.tsx +34 -13
- package/src/pages/DocsPage.tsx +1 -1
- package/src/server/api/metrics.ts +23 -0
- package/src/server/api/page/index.ts +1 -0
- package/src/server/entry-server.tsx +6 -0
- package/src/server/plugins/telemetry.ts +21 -0
- package/src/server/routes/[...slug].md.ts +45 -0
- package/src/server/routes/llms.txt.ts +2 -1
- package/src/server/telemetry.ts +49 -0
- package/src/types/config.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Chronicle
|
|
2
|
+
|
|
3
|
+
Config-driven documentation framework built on Vite, Nitro, and Apsara UI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Config-driven** — Single `chronicle.yaml` for all site configuration
|
|
8
|
+
- **Themeable** — Built-in themes: `default` (sidebar + TOC) and `paper` (book-style)
|
|
9
|
+
- **MDX** — Write docs in MDX with callouts, tabs, mermaid diagrams, and syntax highlighting
|
|
10
|
+
- **API docs** — Interactive OpenAPI documentation with "Try it out" panel
|
|
11
|
+
- **LLMs** — Auto-generate `/llms.txt` and `/llms-full.txt` for AI consumption
|
|
12
|
+
- **CLI** — `init`, `dev`, `build`, `start`, `serve` commands
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @raystack/chronicle
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Initialize
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
chronicle init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Creates a `chronicle.yaml` and sample `index.mdx`.
|
|
29
|
+
|
|
30
|
+
### Develop
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
chronicle dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
37
|
+
|
|
38
|
+
### Build for production
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
chronicle build
|
|
42
|
+
chronicle start
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Contributing
|
|
46
|
+
|
|
47
|
+
We welcome contributions! Here's how to get started:
|
|
48
|
+
|
|
49
|
+
### Prerequisites
|
|
50
|
+
|
|
51
|
+
- [Node.js](https://nodejs.org/) >= 22
|
|
52
|
+
- [Bun](https://bun.sh/) >= 1.3
|
|
53
|
+
|
|
54
|
+
### Running Locally
|
|
55
|
+
|
|
56
|
+
1. Fork and clone the repository
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/<your-username>/chronicle.git
|
|
60
|
+
cd chronicle
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. Install dependencies
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bun install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. Build the CLI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bun run build:cli
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
4. Run the docs site locally
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bun run dev:docs
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Open [http://localhost:3000](http://localhost:3000) to see the docs site.
|
|
82
|
+
|
|
83
|
+
You can also run the CLI directly:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
./packages/chronicle/bin/chronicle.js dev --config docs/chronicle.yaml
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Making Changes
|
|
90
|
+
|
|
91
|
+
1. Create a branch from `main`
|
|
92
|
+
2. Make your changes
|
|
93
|
+
3. Test locally from the `docs/` directory
|
|
94
|
+
4. Open a pull request
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[Apache-2.0](LICENSE)
|
package/dist/cli/index.js
CHANGED
|
@@ -254,6 +254,10 @@ var analyticsSchema = z.object({
|
|
|
254
254
|
enabled: z.boolean().optional(),
|
|
255
255
|
googleAnalytics: googleAnalyticsSchema.optional()
|
|
256
256
|
});
|
|
257
|
+
var telemetrySchema = z.object({
|
|
258
|
+
enabled: z.boolean().optional(),
|
|
259
|
+
serviceName: z.string().optional()
|
|
260
|
+
});
|
|
257
261
|
var chronicleConfigSchema = z.object({
|
|
258
262
|
title: z.string(),
|
|
259
263
|
description: z.string().optional(),
|
|
@@ -267,7 +271,8 @@ var chronicleConfigSchema = z.object({
|
|
|
267
271
|
footer: footerSchema.optional(),
|
|
268
272
|
api: z.array(apiSchema).optional(),
|
|
269
273
|
llms: llmsSchema.optional(),
|
|
270
|
-
analytics: analyticsSchema.optional()
|
|
274
|
+
analytics: analyticsSchema.optional(),
|
|
275
|
+
telemetry: telemetrySchema.optional()
|
|
271
276
|
});
|
|
272
277
|
// src/cli/utils/config.ts
|
|
273
278
|
function resolveConfigPath(configPath) {
|
|
@@ -365,7 +370,7 @@ var buildCommand = new Command("build").description("Build for production").opti
|
|
|
365
370
|
// src/cli/commands/dev.ts
|
|
366
371
|
import chalk3 from "chalk";
|
|
367
372
|
import { Command as Command2 } from "commander";
|
|
368
|
-
var devCommand = new Command2("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").action(async (options) => {
|
|
373
|
+
var devCommand = new Command2("dev").description("Start development server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").action(async (options) => {
|
|
369
374
|
const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
|
|
370
375
|
const port = parseInt(options.port, 10);
|
|
371
376
|
await linkContent(contentDir);
|
|
@@ -375,7 +380,7 @@ var devCommand = new Command2("dev").description("Start development server").opt
|
|
|
375
380
|
const config2 = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
|
|
376
381
|
const server = await createServer({
|
|
377
382
|
...config2,
|
|
378
|
-
server: { ...config2.server, port }
|
|
383
|
+
server: { ...config2.server, port, host: options.host }
|
|
379
384
|
});
|
|
380
385
|
await server.listen();
|
|
381
386
|
server.printUrls();
|
|
@@ -450,7 +455,7 @@ Run`, chalk4.cyan("chronicle dev"), "to start development server");
|
|
|
450
455
|
// src/cli/commands/serve.ts
|
|
451
456
|
import chalk5 from "chalk";
|
|
452
457
|
import { Command as Command4 } from "commander";
|
|
453
|
-
var serveCommand = new Command4("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
|
|
458
|
+
var serveCommand = new Command4("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--config <path>", "Path to chronicle.yaml").option("--host <host>", "Host address", "localhost").option("--preset <preset>", "Deploy preset (vercel, cloudflare, node-server)").action(async (options) => {
|
|
454
459
|
const { contentDir, configPath, preset } = await loadCLIConfig(options.config, {
|
|
455
460
|
content: options.content,
|
|
456
461
|
preset: options.preset
|
|
@@ -471,7 +476,7 @@ var serveCommand = new Command4("serve").description("Build and start production
|
|
|
471
476
|
console.log(chalk5.cyan("Starting production server..."));
|
|
472
477
|
const server = await preview({
|
|
473
478
|
...config2,
|
|
474
|
-
preview: { port }
|
|
479
|
+
preview: { port, host: options.host }
|
|
475
480
|
});
|
|
476
481
|
server.printUrls();
|
|
477
482
|
});
|
|
@@ -479,7 +484,7 @@ var serveCommand = new Command4("serve").description("Build and start production
|
|
|
479
484
|
// src/cli/commands/start.ts
|
|
480
485
|
import chalk6 from "chalk";
|
|
481
486
|
import { Command as Command5 } from "commander";
|
|
482
|
-
var startCommand = new Command5("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").action(async (options) => {
|
|
487
|
+
var startCommand = new Command5("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("--content <path>", "Content directory").option("--host <host>", "Host address", "localhost").action(async (options) => {
|
|
483
488
|
const { contentDir, configPath } = await loadCLIConfig(undefined, { content: options.content });
|
|
484
489
|
const port = parseInt(options.port, 10);
|
|
485
490
|
await linkContent(contentDir);
|
|
@@ -489,7 +494,7 @@ var startCommand = new Command5("start").description("Start production server").
|
|
|
489
494
|
const config2 = await createViteConfig2({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
|
|
490
495
|
const server = await preview({
|
|
491
496
|
...config2,
|
|
492
|
-
preview: { port }
|
|
497
|
+
preview: { port, host: options.host }
|
|
493
498
|
});
|
|
494
499
|
server.printUrls();
|
|
495
500
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raystack/chronicle",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Config-driven documentation framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
37
37
|
"@codemirror/view": "^6.39.14",
|
|
38
38
|
"@heroicons/react": "^2.2.0",
|
|
39
|
+
"@opentelemetry/api": "^1.9.1",
|
|
40
|
+
"@opentelemetry/exporter-prometheus": "^0.214.0",
|
|
41
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
42
|
+
"@opentelemetry/sdk-metrics": "^2.6.1",
|
|
43
|
+
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
39
44
|
"@raystack/apsara": "0.55.1",
|
|
40
45
|
"@shikijs/rehype": "^4.0.2",
|
|
41
46
|
"@vitejs/plugin-react": "^6.0.1",
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -9,6 +9,7 @@ export const devCommand = new Command('dev')
|
|
|
9
9
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
10
10
|
.option('--content <path>', 'Content directory')
|
|
11
11
|
.option('--config <path>', 'Path to chronicle.yaml')
|
|
12
|
+
.option('--host <host>', 'Host address', 'localhost')
|
|
12
13
|
.action(async options => {
|
|
13
14
|
const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
|
|
14
15
|
const port = parseInt(options.port, 10);
|
|
@@ -23,7 +24,7 @@ export const devCommand = new Command('dev')
|
|
|
23
24
|
const config = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
|
|
24
25
|
const server = await createServer({
|
|
25
26
|
...config,
|
|
26
|
-
server: { ...config.server, port }
|
|
27
|
+
server: { ...config.server, port, host: options.host }
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
await server.listen();
|
|
@@ -9,6 +9,7 @@ export const serveCommand = new Command('serve')
|
|
|
9
9
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
10
10
|
.option('--content <path>', 'Content directory')
|
|
11
11
|
.option('--config <path>', 'Path to chronicle.yaml')
|
|
12
|
+
.option('--host <host>', 'Host address', 'localhost')
|
|
12
13
|
.option(
|
|
13
14
|
'--preset <preset>',
|
|
14
15
|
'Deploy preset (vercel, cloudflare, node-server)'
|
|
@@ -38,7 +39,7 @@ export const serveCommand = new Command('serve')
|
|
|
38
39
|
console.log(chalk.cyan('Starting production server...'));
|
|
39
40
|
const server = await preview({
|
|
40
41
|
...config,
|
|
41
|
-
preview: { port }
|
|
42
|
+
preview: { port, host: options.host }
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
server.printUrls();
|
|
@@ -8,6 +8,7 @@ export const startCommand = new Command('start')
|
|
|
8
8
|
.description('Start production server')
|
|
9
9
|
.option('-p, --port <port>', 'Port number', '3000')
|
|
10
10
|
.option('--content <path>', 'Content directory')
|
|
11
|
+
.option('--host <host>', 'Host address', 'localhost')
|
|
11
12
|
.action(async options => {
|
|
12
13
|
const { contentDir, configPath } = await loadCLIConfig(undefined, { content: options.content });
|
|
13
14
|
const port = parseInt(options.port, 10);
|
|
@@ -21,7 +22,7 @@ export const startCommand = new Command('start')
|
|
|
21
22
|
const config = await createViteConfig({ packageRoot: PACKAGE_ROOT, projectRoot: process.cwd(), contentDir, configPath });
|
|
22
23
|
const server = await preview({
|
|
23
24
|
...config,
|
|
24
|
-
preview: { port }
|
|
25
|
+
preview: { port, host: options.host }
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
server.printUrls();
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Link as ApsaraLink } from '@raystack/apsara';
|
|
2
|
-
import type { ComponentProps } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import type { ComponentProps, MouseEvent } from 'react';
|
|
3
|
+
import { useNavigate } from 'react-router';
|
|
4
4
|
|
|
5
5
|
type LinkProps = ComponentProps<'a'>;
|
|
6
6
|
|
|
7
|
-
export function Link({ href, children, ...props }: LinkProps) {
|
|
7
|
+
export function Link({ href, children, onClick: onClickProp, ...props }: LinkProps) {
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
8
10
|
if (!href) {
|
|
9
11
|
return <span {...props}>{children}</span>;
|
|
10
12
|
}
|
|
@@ -12,14 +14,6 @@ export function Link({ href, children, ...props }: LinkProps) {
|
|
|
12
14
|
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
13
15
|
const isAnchor = href.startsWith('#');
|
|
14
16
|
|
|
15
|
-
if (isAnchor) {
|
|
16
|
-
return (
|
|
17
|
-
<ApsaraLink href={href} {...props}>
|
|
18
|
-
{children}
|
|
19
|
-
</ApsaraLink>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
17
|
if (isExternal) {
|
|
24
18
|
return (
|
|
25
19
|
<ApsaraLink
|
|
@@ -33,9 +27,36 @@ export function Link({ href, children, ...props }: LinkProps) {
|
|
|
33
27
|
);
|
|
34
28
|
}
|
|
35
29
|
|
|
30
|
+
if (isAnchor) {
|
|
31
|
+
return (
|
|
32
|
+
<ApsaraLink href={href} {...props}>
|
|
33
|
+
{children}
|
|
34
|
+
</ApsaraLink>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
39
|
+
if (
|
|
40
|
+
e.defaultPrevented ||
|
|
41
|
+
e.button !== 0 ||
|
|
42
|
+
e.metaKey ||
|
|
43
|
+
e.ctrlKey ||
|
|
44
|
+
e.shiftKey ||
|
|
45
|
+
e.altKey
|
|
46
|
+
) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onClickProp?.(e);
|
|
51
|
+
if (e.defaultPrevented) return;
|
|
52
|
+
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
navigate(href);
|
|
55
|
+
};
|
|
56
|
+
|
|
36
57
|
return (
|
|
37
|
-
<
|
|
58
|
+
<ApsaraLink href={href} {...props} onClick={onClick}>
|
|
38
59
|
{children}
|
|
39
|
-
</
|
|
60
|
+
</ApsaraLink>
|
|
40
61
|
);
|
|
41
62
|
}
|
package/src/pages/DocsPage.tsx
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './[...slug]';
|
|
@@ -10,6 +10,7 @@ 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
12
|
import { App } from './App';
|
|
13
|
+
import { recordSSRRender } from './telemetry';
|
|
13
14
|
|
|
14
15
|
import clientAssets from './entry-client?assets=client';
|
|
15
16
|
import serverAssets from './entry-server?assets=ssr';
|
|
@@ -57,6 +58,7 @@ export default {
|
|
|
57
58
|
|
|
58
59
|
const assets = clientAssets.merge(serverAssets);
|
|
59
60
|
|
|
61
|
+
const renderStart = performance.now();
|
|
60
62
|
const stream = await renderToReadableStream(
|
|
61
63
|
<html lang="en">
|
|
62
64
|
<head>
|
|
@@ -91,9 +93,13 @@ export default {
|
|
|
91
93
|
</html>,
|
|
92
94
|
);
|
|
93
95
|
|
|
96
|
+
const renderDuration = performance.now() - renderStart;
|
|
97
|
+
|
|
94
98
|
const isApiRoute = pathname.startsWith('/apis');
|
|
95
99
|
const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
|
|
96
100
|
|
|
101
|
+
recordSSRRender(pathname, status, renderDuration);
|
|
102
|
+
|
|
97
103
|
return new Response(stream, {
|
|
98
104
|
status,
|
|
99
105
|
headers: { 'Content-Type': 'text/html;charset=utf-8' },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { definePlugin } from 'nitro'
|
|
2
|
+
import { loadConfig } from '@/lib/config'
|
|
3
|
+
import { initTelemetry, recordRequest } from '../telemetry'
|
|
4
|
+
|
|
5
|
+
export default definePlugin((nitroApp) => {
|
|
6
|
+
const config = loadConfig()
|
|
7
|
+
if (!config.telemetry?.enabled) return
|
|
8
|
+
|
|
9
|
+
initTelemetry(config)
|
|
10
|
+
|
|
11
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
12
|
+
if (event.path === '/api/metrics') return
|
|
13
|
+
event.context._requestStart = performance.now()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
nitroApp.hooks.hook('response', (res, event) => {
|
|
17
|
+
if (!event.context._requestStart) return
|
|
18
|
+
const duration = performance.now() - event.context._requestStart
|
|
19
|
+
recordRequest(event.method, event.path, res.status, duration)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
4
|
+
import { loadConfig } from '@/lib/config';
|
|
5
|
+
import { getPage, getOriginalPath } from '@/lib/source';
|
|
6
|
+
import { safePath } from '@/server/utils/safe-path';
|
|
7
|
+
|
|
8
|
+
export default defineHandler(async event => {
|
|
9
|
+
const pathname = event.path || event.req.url?.split('?')[0] || '';
|
|
10
|
+
if (!pathname.endsWith('.md')) return;
|
|
11
|
+
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
if (!config.llms?.enabled) {
|
|
14
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stripped = pathname.replace(/\.md$/, '');
|
|
18
|
+
const parts = stripped === '/index' || stripped === '/'
|
|
19
|
+
? []
|
|
20
|
+
: stripped.slice(1).split('/').filter(Boolean);
|
|
21
|
+
const page = await getPage(parts);
|
|
22
|
+
|
|
23
|
+
if (!page) {
|
|
24
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const originalPath = getOriginalPath(page);
|
|
28
|
+
if (!originalPath) {
|
|
29
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const contentDir = __CHRONICLE_CONTENT_DIR__;
|
|
33
|
+
const filePath = safePath(contentDir, '/' + originalPath);
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const raw = await fs.readFile(filePath, 'utf-8').catch(() => null);
|
|
39
|
+
if (!raw) {
|
|
40
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
event.res.headers.set('Content-Type', 'text/markdown; charset=utf-8');
|
|
44
|
+
return matter(raw).content;
|
|
45
|
+
});
|
|
@@ -12,7 +12,8 @@ export default defineHandler(async event => {
|
|
|
12
12
|
const pages = await getPages();
|
|
13
13
|
const index = pages.map(p => {
|
|
14
14
|
const fm = extractFrontmatter(p);
|
|
15
|
-
|
|
15
|
+
const mdUrl = p.url === '/' ? '/index.md' : `${p.url}.md`;
|
|
16
|
+
return `- [${fm.title}](${mdUrl})`;
|
|
16
17
|
}).join('\n');
|
|
17
18
|
const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
|
|
18
19
|
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -67,6 +67,11 @@ const analyticsSchema = z.object({
|
|
|
67
67
|
googleAnalytics: googleAnalyticsSchema.optional(),
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
const telemetrySchema = z.object({
|
|
71
|
+
enabled: z.boolean().optional(),
|
|
72
|
+
serviceName: z.string().optional(),
|
|
73
|
+
})
|
|
74
|
+
|
|
70
75
|
export const chronicleConfigSchema = z.object({
|
|
71
76
|
title: z.string(),
|
|
72
77
|
description: z.string().optional(),
|
|
@@ -81,6 +86,7 @@ export const chronicleConfigSchema = z.object({
|
|
|
81
86
|
api: z.array(apiSchema).optional(),
|
|
82
87
|
llms: llmsSchema.optional(),
|
|
83
88
|
analytics: analyticsSchema.optional(),
|
|
89
|
+
telemetry: telemetrySchema.optional(),
|
|
84
90
|
})
|
|
85
91
|
|
|
86
92
|
export type ChronicleConfig = z.infer<typeof chronicleConfigSchema>
|
|
@@ -97,3 +103,4 @@ export type FooterConfig = z.infer<typeof footerSchema>
|
|
|
97
103
|
export type LlmsConfig = z.infer<typeof llmsSchema>
|
|
98
104
|
export type AnalyticsConfig = z.infer<typeof analyticsSchema>
|
|
99
105
|
export type GoogleAnalyticsConfig = z.infer<typeof googleAnalyticsSchema>
|
|
106
|
+
export type TelemetryConfig = z.infer<typeof telemetrySchema>
|