refactor(docs): reimplement documentation page with Pigsty-style layout
- Replaced docs landing page with a direct redirect to content - Implemented 3-column layout: Sticky Sidebar, Main Content, Right Metadata - added DocsSidebar with collapsible categories - Added Feedback component for user sentiment - Updated sync script to pull from knowledge/docs - Refactored doc version page styles
This commit is contained in:
parent
75af007c12
commit
4623324622
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ models/
|
||||
.DS_Store
|
||||
ui/.DS_Store
|
||||
ui/*/.DS_Store
|
||||
src/content/
|
||||
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
|
||||
63
docs/DESIGN_DOCS_REFACTOR.md
Normal file
63
docs/DESIGN_DOCS_REFACTOR.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Documentation Page Redesign Specification
|
||||
|
||||
## 1. Overview
|
||||
Refactor the `/docs` section of `console.svc.plus` to strictly load content from `cloud-neutral-workshop/knowledge/docs`. The UI will be redesigned to match the [Pigsty Documentation](https://pigsty.cc/docs/) layout while maintaining the existing `console.svc.plus` visual identity (Tailwind tokens, fonts, and colors).
|
||||
|
||||
## 2. Layout Structure
|
||||
|
||||
The layout will consist of a fixed header and a three-column content area (on desktop).
|
||||
|
||||
### 2.1. Global Header (Modified)
|
||||
- **Content**: Logo, Main Nav (Home, Docs, Blog, etc.), Search Bar, GitHub Link, User Profile.
|
||||
- **Additions**:
|
||||
- **Version Selector**: Dropdown to switch between documentation versions (if available).
|
||||
- **Search**: Integrated Algolia/Command-K search bar.
|
||||
|
||||
### 2.2. Left Sidebar (Navigation)
|
||||
- **Behavior**: Sticky, independently scrollable.
|
||||
- **Content**:
|
||||
- Tree-view navigation structure matching the directory structure of `knowledge/docs`.
|
||||
- **Expandable/Collapsible**: Categories should be collapsible folders.
|
||||
- **Active State**: clear visual indicator for current page.
|
||||
|
||||
### 2.3. Main Content Area (Center)
|
||||
- **Typography**: Optimized for long-form reading (prose, decent line-height).
|
||||
- **Elements**:
|
||||
- Breadcrumbs at the top.
|
||||
- H1 Title.
|
||||
- Last updated timestamp.
|
||||
- Content rendered via MDX/Markdown.
|
||||
- **Feedback Section (Footer)**: "Is this page helpful?" (Yes/No buttons) similar to Pigsty.
|
||||
- Prev/Next page navigation links.
|
||||
|
||||
### 2.4. Right Sidebar (Table of Contents & Meta)
|
||||
- **Behavior**: Sticky, hidden on mobile.
|
||||
- **Content**:
|
||||
- **On this Page**: Auto-generated TOC from H2/H3 headers.
|
||||
- **Metadata**:
|
||||
- "Module": Tags or categorization pills.
|
||||
- "Edit this page": Link to GitHub source.
|
||||
- "Contributors": List of contributors (optional).
|
||||
|
||||
## 3. Visual Style & Theming
|
||||
- **Colors**: Use existing `brand-*` and `surface-*` tokens from `console.svc.plus`.
|
||||
- Sidebar Background: `bg-surface-muted` or `bg-background` with right border.
|
||||
- Active Link: `text-primary` with `bg-primary/10` background.
|
||||
- **Responsiveness**:
|
||||
- **Mobile**: Hambergur menu to open Sidebar. TOC hidden or moved to top of content (Accordion).
|
||||
|
||||
## 4. Implementation Plan
|
||||
|
||||
### 4.1. Data Source
|
||||
- Ensure `scripts/sync-doc-content.sh` pulls specifically from `knowledge/docs`.
|
||||
- Update `contentlayer` or `next-mdx-remote` configuration to handle the nested structure of `docs/`.
|
||||
|
||||
### 4.2. Components
|
||||
1. **`DocsLayout`**: Wrapper for the 3-column grid.
|
||||
2. **`SidebarTree`**: Recursive component for navigation.
|
||||
3. **`TOC`**: Component to parse headings and display right sidebar.
|
||||
4. **`FeedbackWidget`**: Simple interactivity for user sentiment.
|
||||
|
||||
## 5. Reference
|
||||
- **Inspiration**: [Pigsty Docs](https://pigsty.cc/docs/)
|
||||
- **Theme**: Cloud-Neutral Toolkit (Dark/Light mode support).
|
||||
39
scripts/sync-doc-content.sh
Executable file
39
scripts/sync-doc-content.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTENT_DIR="src/content/doc"
|
||||
REPO_URL="https://github.com/cloud-neutral-workshop/knowledge.git"
|
||||
SOURCE_PATH="docs"
|
||||
|
||||
# Ensure we're in the project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ -d "${CONTENT_DIR}/.git" ]; then
|
||||
echo "Updating existing git repo in ${CONTENT_DIR}..."
|
||||
git -C "${CONTENT_DIR}" fetch --depth=1 origin main
|
||||
git -C "${CONTENT_DIR}" reset --hard origin/main
|
||||
git -C "${CONTENT_DIR}" clean -fdx
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Syncing docs content from ${REPO_URL}/${SOURCE_PATH} to ${CONTENT_DIR}..."
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||
|
||||
git clone --depth=1 "${REPO_URL}" "${TMP_DIR}/repo"
|
||||
|
||||
mkdir -p "${CONTENT_DIR}"
|
||||
|
||||
# Remove existing content but keep the directory
|
||||
# Find and delete all files and directories inside CONTENT_DIR, but ignore errors if empty
|
||||
find "${CONTENT_DIR}" -mindepth 1 -delete 2>/dev/null || true
|
||||
|
||||
# Copy only the docs/ directory from repo to content dir, excluding .git
|
||||
if [ -d "${TMP_DIR}/repo/${SOURCE_PATH}" ]; then
|
||||
tar -C "${TMP_DIR}/repo/${SOURCE_PATH}" --exclude='.git' -cf - . | tar -C "${CONTENT_DIR}" -xf -
|
||||
echo "Docs content synced successfully from ${SOURCE_PATH}/"
|
||||
else
|
||||
echo "Warning: ${SOURCE_PATH}/ directory not found in repository"
|
||||
exit 1
|
||||
fi
|
||||
74
src/app/docs/DocsSidebar.tsx
Normal file
74
src/app/docs/DocsSidebar.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { ChevronRight, ChevronDown, FileText } from 'lucide-react'
|
||||
import type { DocCollection } from './types'
|
||||
|
||||
interface DocsSidebarProps {
|
||||
collections: DocCollection[]
|
||||
}
|
||||
|
||||
export default function DocsSidebar({ collections }: DocsSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Sort collections by title or defined order if any
|
||||
const sortedCollections = [...collections].sort((a, b) => a.title.localeCompare(b.title))
|
||||
|
||||
return (
|
||||
<aside className="sticky top-[64px] hidden h-[calc(100vh-64px)] w-64 shrink-0 overflow-y-auto border-r border-surface-border bg-background py-6 pl-8 pr-4 lg:block">
|
||||
<nav className="space-y-6">
|
||||
{sortedCollections.map((collection) => (
|
||||
<SidebarGroup
|
||||
key={collection.slug}
|
||||
collection={collection}
|
||||
activePath={pathname}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ collection, activePath }: { collection: DocCollection; activePath: string }) {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
// Check if any child is active to auto-expand (optional, defaulted to true for now)
|
||||
const isActive = collection.versions.some(v => activePath === `/docs/${collection.slug}/${v.slug}`)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex w-full items-center justify-between text-sm font-semibold text-heading transistion hover:text-primary"
|
||||
>
|
||||
<span>{collection.title}</span>
|
||||
{isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<ul className="space-y-1 border-l border-surface-border pl-4">
|
||||
{collection.versions.map((version) => {
|
||||
const href = `/docs/${collection.slug}/${version.slug}`
|
||||
const isPageActive = activePath === href
|
||||
|
||||
return (
|
||||
<li key={version.slug}>
|
||||
<Link
|
||||
href={href}
|
||||
className={`block rounded-md px-2 py-1.5 text-sm transition-colors ${isPageActive
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-text-muted hover:text-heading hover:bg-surface-muted'
|
||||
}`}
|
||||
>
|
||||
{version.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
src/app/docs/Feedback.tsx
Normal file
36
src/app/docs/Feedback.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ThumbsUp, ThumbsDown } from 'lucide-react'
|
||||
|
||||
export default function Feedback() {
|
||||
const [voted, setVoted] = useState<'yes' | 'no' | null>(null)
|
||||
|
||||
return (
|
||||
<div className="mt-16 border-t border-surface-border pt-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-semibold text-heading">Is this page helpful?</h3>
|
||||
{voted === null ? (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setVoted('yes')}
|
||||
className="flex items-center gap-2 rounded-md border border-surface-border bg-surface px-4 py-2 text-sm font-medium text-text transition hover:border-primary hover:text-primary"
|
||||
>
|
||||
<ThumbsUp className="h-4 w-4" />
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setVoted('no')}
|
||||
className="flex items-center gap-2 rounded-md border border-surface-border bg-surface px-4 py-2 text-sm font-medium text-text transition hover:border-danger hover:text-danger"
|
||||
>
|
||||
<ThumbsDown className="h-4 w-4" />
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">Thanks for your feedback!</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -4,27 +4,31 @@ export const revalidate = false
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import Breadcrumbs, { type Crumb } from '../../../../components/download/Breadcrumbs'
|
||||
import DocArticle from '@/components/doc/DocArticle'
|
||||
import DocMetaPanel from '@/components/doc/DocMetaPanel'
|
||||
import DocVersionSwitcher from '@/components/doc/DocVersionSwitcher'
|
||||
import Feedback from '../../Feedback'
|
||||
import { getDocVersionParams, getDocVersion } from '../../resources.server'
|
||||
import { isFeatureEnabled } from '@lib/featureToggles'
|
||||
import Link from 'next/link'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
|
||||
function buildBreadcrumbs(
|
||||
slug: string,
|
||||
docTitle: string,
|
||||
version?: { label: string; slug: string },
|
||||
): Crumb[] {
|
||||
const crumbs: Crumb[] = [
|
||||
{ label: 'Docs', href: '/docs' },
|
||||
{ label: docTitle, href: `/docs/${slug}` },
|
||||
]
|
||||
if (version) {
|
||||
const versionSlug = version.slug
|
||||
crumbs.push({ label: version.label, href: `/docs/${slug}/${versionSlug}` })
|
||||
}
|
||||
return crumbs
|
||||
// Simple Breadcrumbs Component inline (or could be separate)
|
||||
function DocsBreadcrumbs({ items }: { items: { label: string; href: string }[] }) {
|
||||
return (
|
||||
<nav className="flex items-center gap-2 text-sm text-text-muted mb-6">
|
||||
{items.map((item, index) => (
|
||||
<div key={item.href} className="flex items-center gap-2">
|
||||
{index > 0 && <ChevronRight className="h-4 w-4" />}
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`transition hover:text-primary ${index === items.length - 1 ? 'font-medium text-text' : ''}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
@ -37,8 +41,13 @@ export const generateStaticParams = async () => {
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Documentation',
|
||||
export async function generateMetadata({ params }: { params: { collection: string; version: string } }): Promise<Metadata> {
|
||||
const doc = await getDocVersion(params.collection, params.version)
|
||||
if (!doc) return {}
|
||||
return {
|
||||
title: `${doc.version.title} - ${doc.collection.title} | Documentation`,
|
||||
description: doc.version.description,
|
||||
}
|
||||
}
|
||||
|
||||
export default async function DocVersionPage({
|
||||
@ -56,39 +65,52 @@ export default async function DocVersionPage({
|
||||
}
|
||||
|
||||
const { collection, version } = doc
|
||||
const breadcrumbs = buildBreadcrumbs(collection.slug, collection.title, version)
|
||||
|
||||
const breadcrumbs = [
|
||||
{ label: 'Documentation', href: '/docs' },
|
||||
{ label: collection.title, href: `/docs/${collection.slug}` },
|
||||
{ label: version.title, href: `/docs/${collection.slug}/${version.slug}` },
|
||||
]
|
||||
|
||||
return (
|
||||
<main className="px-4 py-8 md:px-8">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-6">
|
||||
<Breadcrumbs items={breadcrumbs} />
|
||||
<section className="rounded-3xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-purple-600">{collection.title}</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 md:text-4xl">{version.title}</h1>
|
||||
<p className="mt-2 text-sm text-gray-600">{collection.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-3 text-sm text-gray-500 md:items-end">
|
||||
<DocVersionSwitcher
|
||||
collectionSlug={collection.slug}
|
||||
versions={collection.versions.map((item) => ({ slug: item.slug, label: item.label }))}
|
||||
activeSlug={version.slug}
|
||||
/>
|
||||
{version.updatedAt && <span suppressHydrationWarning>Updated {version.updatedAt}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex gap-12 xl:gap-16">
|
||||
{/* Center Content */}
|
||||
<article className="min-w-0 flex-1">
|
||||
<DocsBreadcrumbs items={breadcrumbs} />
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,240px)_1fr]">
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<DocMetaPanel description={version.description} updatedAt={version.updatedAt} tags={version.tags} />
|
||||
</div>
|
||||
<div className="rounded-3xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<DocArticle content={version.content} />
|
||||
</div>
|
||||
<header className="mb-10 border-b border-surface-border pb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-heading sm:text-4xl">{version.title}</h1>
|
||||
{version.description && <p className="mt-4 text-lg text-text-muted">{version.description}</p>}
|
||||
</header>
|
||||
|
||||
<div className="prose prose-slate max-w-none dark:prose-invert prose-headings:scroll-mt-20 prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline">
|
||||
<DocArticle content={version.content} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Feedback />
|
||||
</article>
|
||||
|
||||
{/* Right Sidebar */}
|
||||
<aside className="hidden w-64 shrink-0 lg:block xl:w-72">
|
||||
<div className="sticky top-[100px] space-y-8 border-l border-surface-border pl-6">
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-text-subtle">Metadata</h3>
|
||||
<DocMetaPanel
|
||||
description={undefined} // Description already shown in header
|
||||
updatedAt={version.updatedAt}
|
||||
tags={version.tags}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* We could add TOC here later */}
|
||||
{/*
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-text-subtle">On This Page</h3>
|
||||
<TOC content={version.content} />
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
21
src/app/docs/layout.tsx
Normal file
21
src/app/docs/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { getDocCollections } from './resources.server'
|
||||
import DocsSidebar from './DocsSidebar'
|
||||
import Navbar from '@components/Navbar'
|
||||
import Footer from '@components/Footer'
|
||||
|
||||
export default async function DocsLayout({ children }: { children: React.ReactNode }) {
|
||||
const collections = await getDocCollections()
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background text-text">
|
||||
<Navbar />
|
||||
<div className="mx-auto flex w-full max-w-[1536px] items-start">
|
||||
<DocsSidebar collections={collections} />
|
||||
<main className="min-h-[calc(100vh-64px)] flex-1 overflow-x-hidden py-8 px-4 sm:px-8 lg:px-10">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,63 +1,37 @@
|
||||
export const dynamic = 'error'
|
||||
export const revalidate = false
|
||||
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import type { DocCollection } from './types'
|
||||
import { getDocResources } from './resources.server'
|
||||
import { isFeatureEnabled } from '@lib/featureToggles'
|
||||
import DocCollectionCard from './DocCollectionCard'
|
||||
|
||||
function formatMeta(resource: DocCollection) {
|
||||
const parts: string[] = []
|
||||
if (resource.updatedAt) {
|
||||
parts.push('Updated')
|
||||
}
|
||||
if (resource.versions.length > 1) {
|
||||
parts.push(`${resource.versions.length} versions`)
|
||||
}
|
||||
return parts.join(' • ')
|
||||
}
|
||||
import { redirect, notFound } from 'next/navigation'
|
||||
import { getDocCollections } from './resources.server'
|
||||
|
||||
export default async function DocsHome() {
|
||||
if (!isFeatureEnabled('appModules', '/docs')) {
|
||||
notFound()
|
||||
const collections = await getDocCollections()
|
||||
|
||||
if (collections.length === 0) {
|
||||
return (
|
||||
<div className="flex h-64 flex-col items-center justify-center rounded-lg border border-dashed border-surface-border bg-surface p-8 text-center">
|
||||
<h3 className="text-lg font-semibold text-heading">No Documentation Found</h3>
|
||||
<p className="max-w-md text-sm text-text-muted mt-2">
|
||||
We couldn't find any documentation files. Please ensure content is synced to <code>src/content/doc</code>.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const manifest = await getDocResources()
|
||||
const resources = [...manifest].sort((a, b) => {
|
||||
const aTime = a.updatedAt ? Date.parse(a.updatedAt) : 0
|
||||
const bTime = b.updatedAt ? Date.parse(b.updatedAt) : 0
|
||||
return bTime - aTime
|
||||
// Try to find a collection named 'index', 'intro', 'home', 'docs' or similar to prioritize
|
||||
const priorityKeys = ['index', 'intro', 'introduction', 'home', 'docs', 'overview']
|
||||
const sorted = [...collections].sort((a, b) => {
|
||||
const aIndex = priorityKeys.indexOf(a.slug.toLowerCase())
|
||||
const bIndex = priorityKeys.indexOf(b.slug.toLowerCase())
|
||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex
|
||||
if (aIndex !== -1) return -1
|
||||
if (bIndex !== -1) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return (
|
||||
<main className="bg-brand-surface px-6 py-12 sm:px-8">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-10">
|
||||
<header className="space-y-4 text-brand-heading">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-brand">Knowledge Base</p>
|
||||
<h1 className="text-[32px] font-bold text-brand md:text-[36px]">Documentation Library</h1>
|
||||
<p className="max-w-3xl text-sm text-brand-heading/80 md:text-base">
|
||||
Browse curated implementation guides, architecture notes, and runbooks from dl.svc.plus. Select a resource to open
|
||||
the focused reading workspace.
|
||||
</p>
|
||||
</header>
|
||||
const firstCollection = sorted[0]
|
||||
const firstVersion = firstCollection.versions[0]
|
||||
|
||||
<section>
|
||||
{resources.length === 0 ? (
|
||||
<div className="rounded-2xl border border-dashed border-brand-border bg-white p-10 text-center text-sm text-brand-heading/70">
|
||||
Documentation resources are not available at the moment. Please check back later.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{resources.map((resource) => {
|
||||
const meta = formatMeta(resource)
|
||||
return <DocCollectionCard key={resource.slug} collection={resource} meta={meta} />
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
if (firstCollection && firstVersion) {
|
||||
redirect(`/docs/${firstCollection.slug}/${firstVersion.slug}`)
|
||||
}
|
||||
|
||||
notFound()
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
---
|
||||
title: Observability Baseline
|
||||
description: Establish a consistent telemetry surface before onboarding workloads.
|
||||
updatedAt: 2024-11-05
|
||||
tags:
|
||||
- tracing
|
||||
- metrics
|
||||
- dashboards
|
||||
collection: observability
|
||||
collectionLabel: Observability
|
||||
version: "2024 Q4"
|
||||
versionSlug: overview
|
||||
---
|
||||
|
||||
## Why this matters
|
||||
|
||||
Reliable dashboards and alerts depend on predictable signals. This baseline locks in a minimal telemetry contract so new services inherit the same trace attributes, metric names, and log keys.
|
||||
|
||||
### Core checklist
|
||||
|
||||
- Emit request, dependency, and queue spans with shared trace IDs.
|
||||
- Forward deployment, region, and tenant labels with every metric.
|
||||
- Normalize structured logs with `severity`, `service`, and `component` fields.
|
||||
|
||||
### Rollout tips
|
||||
|
||||
1. Start with staging namespaces and mirror traffic where possible.
|
||||
2. Validate alerts on canary services before expanding coverage.
|
||||
3. Keep a changelog in the runbook so teams can replay the rollout.
|
||||
@ -1,25 +0,0 @@
|
||||
---
|
||||
title: Zero Downtime Dashboards
|
||||
description: Use shadow pipelines to publish dashboards without interrupting operators.
|
||||
updatedAt: 2024-12-12
|
||||
tags:
|
||||
- dashboards
|
||||
- releases
|
||||
collection: observability
|
||||
collectionLabel: Observability
|
||||
version: Preview
|
||||
versionSlug: zero-downtime
|
||||
format: mdx
|
||||
---
|
||||
|
||||
<Callout title="Guardrails" tone="warning">
|
||||
Always keep the stable dashboard folder intact. Publish experimental panels into a shadow folder first.
|
||||
</Callout>
|
||||
|
||||
<Steps title="Release path">
|
||||
<li>Create a new folder such as `dashboards/shadow` and sync it to staging only.</li>
|
||||
<li>Attach the same data sources as production but pin to canary namespaces.</li>
|
||||
<li>After validation, promote the folder and archive the previous release.</li>
|
||||
</Steps>
|
||||
|
||||
Teams can safely add complex visualizations without losing historical parity. The same pattern works for alert rule experiments.
|
||||
Loading…
Reference in New Issue
Block a user