From 6b7af67e801ac5d408a724e27944bf33f60fd2e2 Mon Sep 17 00:00:00 2001 From: Arthur Silva Sens Date: Wed, 19 Aug 2026 11:01:42 -0300 Subject: [PATCH] Add postgres exporter docs to website Signed-off-by: Arthur Silva Sens --- docs-config.ts | 11 + docs/exporters/documenting-exporters.md | 62 +++ docs/exporters/index.md | 24 + docs/instrumenting/exporters.md | 2 +- scripts/fetch-repo-docs.ts | 409 +++++++++++++++--- src/app/docs/LeftNav.tsx | 13 +- .../docs/[...slug]/PrevNextEditButtons.tsx | 28 +- src/app/docs/[...slug]/page.tsx | 18 +- src/app/sitemap.ts | 3 + src/docs-collection-types.ts | 17 +- src/docs-config-types.ts | 18 +- 11 files changed, 523 insertions(+), 82 deletions(-) create mode 100644 docs/exporters/documenting-exporters.md create mode 100644 docs/exporters/index.md diff --git a/docs-config.ts b/docs-config.ts index fb065179a..b1af1f543 100644 --- a/docs-config.ts +++ b/docs-config.ts @@ -33,6 +33,7 @@ export default { repo: "prometheus", repoDocsDir: "docs", slugPrefix: "prometheus", + versioning: "release-branches", minNumVersions: 10, }, { @@ -40,8 +41,18 @@ export default { repo: "alertmanager", repoDocsDir: "docs", slugPrefix: "alerting", + versioning: "release-branches", minNumVersions: 8, }, + { + owner: "prometheus-community", + repo: "postgres_exporter", + repoDocsDir: "docs", + slugPrefix: "exporters/postgres", + versioning: "git-ref", + ref: "3d2ee4adba4ec98106b6ec45e59b12f5b49ef3a4", + allowMissingFrontmatter: true, + }, ], // Single pages to fetch from external repos (not versioned). diff --git a/docs/exporters/documenting-exporters.md b/docs/exporters/documenting-exporters.md new file mode 100644 index 000000000..e04c39028 --- /dev/null +++ b/docs/exporters/documenting-exporters.md @@ -0,0 +1,62 @@ +--- +title: Documenting exporters +sort_rank: 100 +--- + +Exporter documentation should live in the exporter's own repository. This keeps +the docs close to the code and release process, while still making them +available on prometheus.io. + +## Source layout + +Put user-facing documentation in a top-level `docs/` directory in the exporter +repository: + +```text +docs/ + README.md + getting-started.md + configuration.md +``` + +Use `docs/README.md` as the index page. GitHub renders this file when browsing +the `docs/` directory, and prometheus.io publishes it as the exporter landing +page. + +## Frontmatter + +Every Markdown page must include frontmatter with a `title` and `sort_rank`: + +```yaml +--- +title: Getting started +sort_rank: 1 +--- +``` + +Use `nav_title` when the sidebar label should be shorter than the page title. +Use `hide_in_nav: true` only for pages that should be addressable directly but +not shown in the sidebar. + +Keep the page's `# Heading` in the Markdown body. Unlike the Prometheus server +and Alertmanager docs, exporter docs are rendered on prometheus.io exactly as +they appear on GitHub, so the heading remains useful in both places. + +## Links and assets + +Use relative links with the `.md` extension when linking between pages in the +same exporter repository: + +```markdown +See [configuration](configuration.md) for all flags and options. +``` + +Store images and other documentation assets under `docs/` next to the Markdown +files that reference them. Relative image links are copied and rewritten by the +website build. + +## Releases + +The website pulls exporter docs from the latest stable GitHub release tag. Docs +merged into an exporter repository appear on prometheus.io after the exporter +cuts a release containing those files. diff --git a/docs/exporters/index.md b/docs/exporters/index.md new file mode 100644 index 000000000..dd8216b05 --- /dev/null +++ b/docs/exporters/index.md @@ -0,0 +1,24 @@ +--- +title: Exporters +sort_rank: 6 +nav_icon: package +--- + +Exporters expose metrics from systems that cannot be instrumented directly with +Prometheus client libraries. The exporter documentation in this section is +maintained in each exporter's own repository and pulled into this website from +the latest stable release. + +The broader catalog of available exporters and integrations is listed in +[Exporters and integrations](/docs/instrumenting/exporters/). + +## Documented exporters + +* [PostgreSQL exporter](/docs/exporters/postgres/) (coming from + [prometheus-community/postgres_exporter](https://github.com/prometheus-community/postgres_exporter)) + +## Adding exporter documentation + +Exporter maintainers can add website documentation by adding a `docs/` directory +to the exporter repository and following the +[exporter documentation contract](/docs/exporters/documenting-exporters/). diff --git a/docs/instrumenting/exporters.md b/docs/instrumenting/exporters.md index 1b78645a1..db73433f2 100644 --- a/docs/instrumenting/exporters.md +++ b/docs/instrumenting/exporters.md @@ -50,7 +50,7 @@ wide variety of JVM-based applications, for example [Kafka](http://kafka.apache. * [OpenTSDB Exporter](https://github.com/cloudflare/opentsdb_exporter) * [Oracle DB Exporter](https://github.com/iamseth/oracledb_exporter) * [PgBouncer exporter](https://github.com/prometheus-community/pgbouncer_exporter) - * [PostgreSQL exporter](https://github.com/prometheus-community/postgres_exporter) + * [PostgreSQL exporter](/docs/exporters/postgres/) ([source](https://github.com/prometheus-community/postgres_exporter)) * [Presto exporter](https://github.com/yahoojapan/presto_exporter) * [ProxySQL exporter](https://github.com/percona/proxysql_exporter) * [RavenDB exporter](https://github.com/marcinbudny/ravendb_exporter) diff --git a/scripts/fetch-repo-docs.ts b/scripts/fetch-repo-docs.ts index b8e98c2b6..8ebc8a081 100644 --- a/scripts/fetch-repo-docs.ts +++ b/scripts/fetch-repo-docs.ts @@ -20,6 +20,48 @@ const OUTDIR = "./generated"; const docsCollection: DocsCollection = {}; const allRepoVersions: AllRepoVersions = {}; +const normalizeMarkdownFilePath = (filePath: string) => + filePath === "README.md" + ? "index.md" + : filePath.replace(/\/README\.md$/, "/index.md"); + +const markdownSlugPath = (filePath: string) => + normalizeMarkdownFilePath(filePath).replace(/(^|\/)index\.md$/, "").replace(/\.md$/, ""); + +const preferredPreviewOrder = [ + "index", + "getting-started", + "connecting", + "database-permissions", + "configuration", + "secrets", + "docker", + "aws-rds", +]; + +const inferTitle = (markdown: string, filePath: string) => { + const heading = markdown + .split(/\r?\n/) + .find((line) => line.startsWith("# ")) + ?.replace(/^#\s+/, "") + .trim(); + + if (heading) { + return heading; + } + + return path + .basename(filePath, ".md") + .replace(/[-_]/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); +}; + +const inferSortRank = (filePath: string, index: number) => { + const slugPath = markdownSlugPath(filePath) || "index"; + const sortRank = preferredPreviewOrder.indexOf(slugPath); + return sortRank === -1 ? 1000 + index : sortRank + 1; +}; + // Find all files (.md and others) recursively in a directory. const findFiles = (dir: string): string[] => { let results: string[] = []; @@ -44,27 +86,28 @@ const syncRepo = (owner: string, repo: string, repoDir: string) => { `git clone --bare --filter=blob:none https://github.com/${owner}/${repo}.git ${repoDir}` ); execSync(`git -C ${repoDir} config core.sparseCheckout true`); - } else { - execSync(`git -C ${repoDir} fetch --prune --quiet`); } + execSync( + `git -C ${repoDir} fetch origin '+refs/heads/*:refs/remotes/origin/*' '+refs/tags/*:refs/tags/*' --prune --quiet` + ); }; const checkoutVersionDocs = ( owner: string, repo: string, repoDir: string, - version: string, + checkoutName: string, + gitRef: string, workingTreeBase: string, repoDocsDir: string ) => { const workingTree = path.resolve( - `${workingTreeBase}/${owner}/${repo}/${version}` + `${workingTreeBase}/${owner}/${repo}/${checkoutName}` ); - const checkoutConfig = `${repoDir}/worktrees/${version}/info/sparse-checkout`; - const branch = `release-${version}`; + const checkoutConfig = `${repoDir}/worktrees/${checkoutName}/info/sparse-checkout`; console.log( - `Checking out ${branch} of ${owner}/${repo} from ${repoDir} into ${workingTree}...` + `Checking out ${gitRef} of ${owner}/${repo} from ${repoDir} into ${workingTree}...` ); if (!fs.existsSync(checkoutConfig) || !fs.existsSync(workingTree)) { @@ -75,7 +118,7 @@ const checkoutVersionDocs = ( execSync(`rm -rf ${workingTree}`); } execSync( - `cd ${repoDir} && git worktree prune && git worktree add --no-checkout ${workingTree} ${branch}` + `cd ${repoDir} && git worktree prune && git worktree add --no-checkout ${workingTree} ${gitRef}` ); if (!fs.existsSync(path.dirname(checkoutConfig))) { fs.mkdirSync(path.dirname(checkoutConfig), { recursive: true }); @@ -89,13 +132,107 @@ const checkoutVersionDocs = ( execSync(`git -C ${workingTree} clean --force`); }; -const fetchRepoDocs = async ({ +type IntegrateRepoDocsOptions = { + owner: string; + repo: string; + docsDir: string; + repoDocsDir: string; + assetsRoot: string; + allowMissingFrontmatter?: boolean; + includeFile: (filePath: string) => boolean; + addMarkdownDoc: (args: { + file: string; + filePath: string; + slugPath: string; + title: string; + navTitle?: string; + sortRank: number; + hideInNav?: boolean; + }) => void; +}; + +const integrateRepoDocs = ({ + owner, + repo, + docsDir, + repoDocsDir, + assetsRoot, + allowMissingFrontmatter, + includeFile, + addMarkdownDoc, +}: IntegrateRepoDocsOptions) => { + const files = findFiles(docsDir).sort((a, b) => { + const aPath = path.relative(docsDir, a); + const bPath = path.relative(docsDir, b); + return markdownSlugPath(aPath).localeCompare(markdownSlugPath(bPath)); + }); + + for (const [index, file] of files.entries()) { + const filePath = path.relative(docsDir, file); + const normalizedFilePath = normalizeMarkdownFilePath(filePath); + + if (!includeFile(normalizedFilePath)) { + continue; + } + + if (file.endsWith(".md")) { + console.log("Found Markdown file:", filePath); + + const markdown = fs.readFileSync(file, "utf-8"); + const { + content, + data: { + title, + nav_title: navTitle, + sort_rank: sortRank, + hide_in_nav: hideInNav, + }, + } = matter(markdown); + + if (!title && !allowMissingFrontmatter) { + throw new Error(`Missing title in ${file}`); + } + if (sortRank == null && !allowMissingFrontmatter) { + // Docs in https://github.com/prometheus/prometheus/tree/main/docs/command-line + // are currently missing sort_rank 😤 + if (!filePath.includes("command-line")) { + throw new Error(`Missing sort_rank in ${file}`); + } + } + + const finalTitle = title ?? inferTitle(content, filePath); + const finalSortRank = sortRank ?? inferSortRank(filePath, index); + + addMarkdownDoc({ + file, + filePath: path.posix.join( + repoDocsDir, + filePath.split(path.sep).join(path.posix.sep) + ), + slugPath: markdownSlugPath(filePath), + title: finalTitle, + navTitle, + sortRank: finalSortRank, + hideInNav, + }); + } else { + console.log("Found non-Markdown asset file:", filePath); + const destDir = `${OUTDIR}/${assetsRoot}/${path.dirname(filePath)}`; + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + fs.copyFileSync(file, `${destDir}/${path.basename(filePath)}`); + } + } +}; + +const fetchVersionedRepoDocs = async ({ owner, repo, repoDocsDir, minNumVersions, slugPrefix, -}: GithubMarkdownSource) => { +}: Extract) => { console.log(`Fetching releases and repo docs for ${owner}/${repo}...`); // Clone a bare repo with sparse checkout so we can get the docs at specific @@ -184,6 +321,7 @@ const fetchRepoDocs = async ({ repo, repoCheckoutDir, version, + `origin/release-${version}`, `${OUTDIR}/repo-docs`, repoDocsDir ); @@ -192,47 +330,33 @@ const fetchRepoDocs = async ({ // Store metadata about Markdown page files, copy non-Markdown // assets to the docs assets directory. const assetsRoot = `/repo-docs-assets/${owner}/${repo}/${version}`; - for (const file of findFiles(docsDir)) { - const filePath = path.relative(docsDir, file); - - if ( - owner === "prometheus" && - ["prometheus", "alertmanager"].includes(repo) && - filePath === "index.md" - ) { - // Skip the index.md file in the external repo, as it is not a real or conformant page. - console.log("Skipping Prometheus index.md file:", filePath); - continue; - } - - if (file.endsWith(".md")) { - console.log("Found Markdown file:", filePath); - - const { - data: { - title, - nav_title: navTitle, - sort_rank: sortRank, - hide_in_nav: hideInNav, - }, - } = matter(fs.readFileSync(file, "utf-8")); - - if (!title) { - throw new Error(`Missing title in ${file}`); - } - if (!sortRank) { - // Docs in https://github.com/prometheus/prometheus/tree/main/docs/command-line - // are currently missing sort_rank 😤 - if (!filePath.includes("command-line")) { - throw new Error(`Missing sort_rank in ${file}`); - } + integrateRepoDocs({ + owner, + repo, + docsDir, + repoDocsDir, + assetsRoot, + includeFile: (filePath) => { + if ( + owner === "prometheus" && + ["prometheus", "alertmanager"].includes(repo) && + filePath === "index.md" + ) { + // Skip the index.md file in the external repo, as it is not a real or conformant page. + console.log("Skipping Prometheus index.md file:", filePath); + return false; } - - const slug = path.join( - slugPrefix, - version, - filePath.replace(/(\/index)*\.md$/, "") - ); + return true; + }, + addMarkdownDoc: ({ + file, + slugPath, + title, + navTitle, + sortRank, + hideInNav, + }) => { + const slug = path.join(slugPrefix, version, slugPath); const newDoc: DocMetadata = { type: "repo-doc", slug, @@ -254,25 +378,184 @@ const fetchRepoDocs = async ({ docsCollection[slug] = newDoc; if (version === latestVersion) { - const latestSlug = path.join( - slugPrefix, - "latest", - filePath.replace(/(\/index)*\.md$/, "") - ); + const latestSlug = path.join(slugPrefix, "latest", slugPath); // Also add the latest version to the collection with // "latest" as the version in the slug. docsCollection[latestSlug] = { ...newDoc, slug: latestSlug }; } - } else { - console.log("Found non-Markdown asset file:", filePath); - const destDir = `${OUTDIR}/${assetsRoot}/${path.dirname(filePath)}`; - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - fs.copyFileSync(file, `${destDir}/${path.basename(filePath)}`); - } + }, + }); + } +}; + +const getLatestReleaseTag = async (owner: string, repo: string) => { + const iterator = octokit.paginate.iterator(octokit.rest.repos.listReleases, { + owner, + repo, + per_page: 100, + }); + + for await (const { data: releases } of iterator) { + const latestRelease = releases.find( + (release) => !release.draft && !release.prerelease + ); + if (latestRelease) { + return latestRelease.tag_name; } } + + throw new Error(`No stable release found for ${owner}/${repo}.`); +}; + +const fetchLatestReleaseTagRepoDocs = async ({ + owner, + repo, + repoDocsDir, + slugPrefix, +}: Extract) => { + console.log(`Fetching latest release docs for ${owner}/${repo}...`); + + const repoCheckoutDir = `${OUTDIR}/repos/${owner}/${repo}.git`; + syncRepo(owner, repo, repoCheckoutDir); + + const gitRef = await getLatestReleaseTag(owner, repo); + checkoutVersionDocs( + owner, + repo, + repoCheckoutDir, + gitRef, + gitRef, + `${OUTDIR}/repo-docs`, + repoDocsDir + ); + + const docsDir = `${OUTDIR}/repo-docs/${owner}/${repo}/${gitRef}/${repoDocsDir}`; + const assetsRoot = `/repo-docs-assets/${owner}/${repo}/${gitRef}`; + + if (!fs.existsSync(docsDir)) { + console.warn( + `Skipping ${owner}/${repo}@${gitRef}: ${repoDocsDir} directory does not exist.` + ); + return; + } + + integrateRepoDocs({ + owner, + repo, + docsDir, + repoDocsDir, + assetsRoot, + includeFile: () => true, + addMarkdownDoc: ({ + file, + filePath, + slugPath, + title, + navTitle, + sortRank, + hideInNav, + }) => { + const slug = path.join(slugPrefix, slugPath); + + docsCollection[slug] = { + type: "unversioned-repo-doc", + slug, + filePath: file, + owner, + repo, + gitRef, + slugPrefix, + repoFilePath: filePath, + assetsRoot, + title, + navTitle, + sortRank, + hideInNav, + children: [], + }; + }, + }); +}; + +const fetchGitRefRepoDocs = async ({ + owner, + repo, + repoDocsDir, + slugPrefix, + ref, + allowMissingFrontmatter, +}: Extract) => { + console.log(`Fetching docs for ${owner}/${repo}@${ref}...`); + + const repoCheckoutDir = `${OUTDIR}/repos/${owner}/${repo}.git`; + syncRepo(owner, repo, repoCheckoutDir); + execSync(`git -C ${repoCheckoutDir} fetch origin ${ref} --quiet`); + + const checkoutName = ref.replace(/[^a-zA-Z0-9._-]/g, "-"); + checkoutVersionDocs( + owner, + repo, + repoCheckoutDir, + checkoutName, + ref, + `${OUTDIR}/repo-docs`, + repoDocsDir + ); + + const docsDir = `${OUTDIR}/repo-docs/${owner}/${repo}/${checkoutName}/${repoDocsDir}`; + const assetsRoot = `/repo-docs-assets/${owner}/${repo}/${checkoutName}`; + + integrateRepoDocs({ + owner, + repo, + docsDir, + repoDocsDir, + assetsRoot, + allowMissingFrontmatter, + includeFile: () => true, + addMarkdownDoc: ({ + file, + filePath, + slugPath, + title, + navTitle, + sortRank, + hideInNav, + }) => { + const slug = path.join(slugPrefix, slugPath); + + docsCollection[slug] = { + type: "unversioned-repo-doc", + slug, + filePath: file, + owner, + repo, + gitRef: ref, + slugPrefix, + repoFilePath: filePath, + assetsRoot, + title, + navTitle, + sortRank, + hideInNav, + children: [], + }; + }, + }); +}; + +const fetchRepoDocs = async (source: GithubMarkdownSource) => { + switch (source.versioning) { + case "release-branches": + await fetchVersionedRepoDocs(source); + break; + case "latest-release-tag": + await fetchLatestReleaseTagRepoDocs(source); + break; + case "git-ref": + await fetchGitRefRepoDocs(source); + break; + } }; for (const sourceConfig of docsConfig.githubMarkdownSources) { diff --git a/src/app/docs/LeftNav.tsx b/src/app/docs/LeftNav.tsx index 8ff800107..3c6f3c510 100644 --- a/src/app/docs/LeftNav.tsx +++ b/src/app/docs/LeftNav.tsx @@ -21,6 +21,7 @@ import { IconChartLine, IconMap, IconFileDescription, + IconPackage, IconProps, IconTag, } from "@tabler/icons-react"; @@ -37,6 +38,7 @@ const iconMap: Record> = { "chart-line": IconChartLine, map: IconMap, "file-description": IconFileDescription, + package: IconPackage, }; function NavIcon({ iconName, ...props }: { iconName: string } & IconProps) { @@ -86,6 +88,10 @@ function buildRecursiveNav( return true; } + if (child.type === "unversioned-repo-doc") { + return true; + } + // Always show latest version docs if we're not looking at a different version of the same repo. if ( !currentPageVersion && @@ -122,7 +128,12 @@ function buildRecursiveNav(