feat: production readiness - metrics, health, CI/CD, Docker, Helm (#1)

* docs: add specification for production readiness & website launch

Covers DevOps hooks, CI/CD, Prometheus observability, Docker/Helm
deployment, and synapbus.dev website with documentation and blog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add implementation plan and research for production readiness

Covers 5 workstreams: git hooks, CI/CD, observability, deployment
artifacts, and website. All constitution gates pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add pre-commit and pre-push git hooks

Pre-commit runs go vet, golangci-lint (optional), and fast tests.
Pre-push runs full test suite and build verification.
Installable via `make hooks`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add GitHub Actions for PR checks and releases

ci.yml: lint, test, build on PRs to main.
release.yml: multi-platform binaries + Docker image on version tags.
Targets: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64.
Docker pushed to ghcr.io/smart-mcp-proxy/synapbus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Dockerfile, docker-compose, and Helm chart

Multi-stage Docker build (node + golang + scratch), ~30MB image.
docker-compose.yml for local development with volume persistence.
Helm chart with configurable Deployment, Service, PVC, Ingress,
and Prometheus ServiceMonitor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Prometheus metrics and Kubernetes health endpoints

Add internal/metrics package with Prometheus collectors (HTTP requests,
duration, messages, agents, connections) and chi-compatible middleware.
Add internal/health package with /healthz (liveness) and /readyz
(readiness with DB ping) endpoints. Wire into main.go with promhttp
handler at /metrics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update Go version to 1.25, fix Docker build issues

- Update Dockerfile golang image from 1.23 to 1.25 (matches go.mod)
- Add tzdata package for timezone support in scratch image
- Use npm install --legacy-peer-deps for web frontend build
- Update CI/release workflows to use Go 1.25

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve CI failures - golangci-lint v2 and npm peer deps

- Upgrade golangci-lint-action to v7 with v2.1 (supports Go 1.25)
- Use npm install --legacy-peer-deps instead of npm ci for web builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace golangci-lint with go vet (golangci-lint doesn't support Go 1.25 yet)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dumbris
2026-03-13 21:03:47 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 1cabbae6b5
commit ff81f2f218
28 changed files with 1452 additions and 12 deletions
+12
View File
@@ -0,0 +1,12 @@
.git
bin/
data/
web/node_modules/
web/.svelte-kit/
*.test
*.out
.env
.env.*
.claude/
.specify/
__pycache__/
+60
View File
@@ -0,0 +1,60 @@
name: CI
on:
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Run go vet
run: CGO_ENABLED=0 go vet ./...
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build web frontend
run: |
cd web && npm install --legacy-peer-deps && npm run build
- name: Copy web assets
run: |
rm -rf internal/web/dist
cp -r web/build internal/web/dist
- name: Run tests
run: CGO_ENABLED=0 go test -count=1 -timeout 120s ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Verify binary builds
run: CGO_ENABLED=0 go build -o /dev/null ./cmd/synapbus/
+136
View File
@@ -0,0 +1,136 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
name: Build & Release
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
include:
- goos: linux
goarch: amd64
suffix: ""
- goos: linux
goarch: arm64
suffix: ""
- goos: darwin
goarch: amd64
suffix: ""
- goos: darwin
goarch: arm64
suffix: ""
- goos: windows
goarch: amd64
suffix: ".exe"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build web frontend
run: |
cd web && npm install --legacy-peer-deps && npm run build
- name: Copy web assets
run: |
rm -rf internal/web/dist
cp -r web/build internal/web/dist
- name: Build binary
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go build -ldflags "-s -w" -o synapbus-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }} ./cmd/synapbus/
- uses: actions/upload-artifact@v4
with:
name: synapbus-${{ matrix.goos }}-${{ matrix.goarch }}
path: synapbus-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }}
publish:
name: Publish Release
needs: release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: artifacts/*
docker:
name: Docker
needs: release
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build web frontend
run: |
cd web && npm install --legacy-peer-deps && npm run build
- name: Copy web assets
run: |
rm -rf internal/web/dist
cp -r web/build internal/web/dist
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version tag
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/smart-mcp-proxy/synapbus
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
- uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+27
View File
@@ -0,0 +1,27 @@
# Stage 1: Build web frontend
FROM node:20-alpine AS web-builder
WORKDIR /app/web
COPY web/package*.json ./
RUN npm install --legacy-peer-deps
COPY web/ ./
RUN npm run build
# Stage 2: Build Go binary
FROM golang:1.25-alpine AS go-builder
RUN apk add --no-cache tzdata
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
COPY --from=web-builder /app/web/build internal/web/dist/
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /synapbus ./cmd/synapbus/
# Stage 3: Runtime
FROM scratch
COPY --from=go-builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=go-builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=go-builder /synapbus /synapbus
EXPOSE 8080
VOLUME ["/data"]
ENTRYPOINT ["/synapbus"]
CMD ["serve", "--host", "0.0.0.0", "--port", "8080", "--data", "/data"]
+9 -1
View File
@@ -1,4 +1,4 @@
.PHONY: build test dev web clean lint
.PHONY: build test dev web clean lint hooks
BINARY := synapbus
MODULE := github.com/smart-mcp-proxy/synapbus
@@ -29,4 +29,12 @@ clean:
lint:
golangci-lint run ./...
hooks:
@echo "Installing git hooks..."
@mkdir -p scripts/hooks
@chmod +x scripts/hooks/pre-commit scripts/hooks/pre-push
@ln -sf ../../scripts/hooks/pre-commit .git/hooks/pre-commit
@ln -sf ../../scripts/hooks/pre-push .git/hooks/pre-push
@echo "✅ Git hooks installed"
.DEFAULT_GOAL := build
+21
View File
@@ -16,6 +16,7 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
"github.com/smart-mcp-proxy/synapbus/internal/admin"
@@ -25,8 +26,10 @@ import (
"github.com/smart-mcp-proxy/synapbus/internal/attachments"
"github.com/smart-mcp-proxy/synapbus/internal/auth"
"github.com/smart-mcp-proxy/synapbus/internal/channels"
"github.com/smart-mcp-proxy/synapbus/internal/health"
mcpserver "github.com/smart-mcp-proxy/synapbus/internal/mcp"
"github.com/smart-mcp-proxy/synapbus/internal/messaging"
prommetrics "github.com/smart-mcp-proxy/synapbus/internal/metrics"
"github.com/smart-mcp-proxy/synapbus/internal/search"
"github.com/smart-mcp-proxy/synapbus/internal/search/embedding"
"github.com/smart-mcp-proxy/synapbus/internal/storage"
@@ -338,12 +341,30 @@ func runServe(cmd *cobra.Command, args []string) error {
expiryWorker.Start()
slog.Info("task expiry worker started")
// Create health checker
healthChecker := health.NewChecker(db.DB, "0.1.0")
// Set up chi router
r := chi.NewRouter()
// Add metrics middleware when enabled
if metricsEnabled {
r.Use(prommetrics.Middleware)
slog.Info("prometheus HTTP metrics middleware enabled")
}
// Health endpoint (no auth)
r.Get("/health", mcpserver.NewHealthHandler(mcpSrv.ConnectionManager(), "0.1.0", startTime))
// Kubernetes-style health probes (no auth, always registered)
r.Get("/healthz", healthChecker.Healthz)
r.Get("/readyz", healthChecker.Readyz)
// Prometheus metrics endpoint (only when enabled)
if metricsEnabled {
r.Handle("/metrics", promhttp.Handler())
}
// Auth endpoints (public)
r.Post("/auth/register", authHandlers.HandleRegister)
r.Post("/auth/login", authHandlers.HandleLogin)
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: synapbus
description: Agent-to-agent messaging for AI swarms
type: application
version: 0.1.0
appVersion: "0.1.0"
@@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "synapbus.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "synapbus.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "synapbus.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "synapbus.labels" -}}
helm.sh/chart: {{ include "synapbus.chart" . }}
{{ include "synapbus.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "synapbus.selectorLabels" -}}
app.kubernetes.io/name: {{ include "synapbus.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -0,0 +1,78 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "synapbus.fullname" . }}
labels:
{{- include "synapbus.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "synapbus.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "synapbus.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- serve
- --host
- "0.0.0.0"
- --port
- "8080"
- --data
- /data
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 3
periodSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "synapbus.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -0,0 +1,41 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "synapbus.fullname" . }}
labels:
{{- include "synapbus.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "synapbus.fullname" $ }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}
+17
View File
@@ -0,0 +1,17 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "synapbus.fullname" . }}
labels:
{{- include "synapbus.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "synapbus.fullname" . }}
labels:
{{- include "synapbus.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "synapbus.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,20 @@
{{- if .Values.metrics.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "synapbus.fullname" . }}
labels:
{{- include "synapbus.labels" . | nindent 4 }}
{{- with .Values.metrics.serviceMonitor.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "synapbus.selectorLabels" . | nindent 6 }}
endpoints:
- port: http
path: /metrics
interval: {{ .Values.metrics.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
{{- end }}
+66
View File
@@ -0,0 +1,66 @@
replicaCount: 1
image:
repository: ghcr.io/smart-mcp-proxy/synapbus
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: false
name: ""
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: synapbus.local
paths:
- path: /
pathType: Prefix
tls: []
# - secretName: synapbus-tls
# hosts:
# - synapbus.local
persistence:
enabled: true
storageClass: ""
accessModes:
- ReadWriteOnce
size: 1Gi
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
env:
SYNAPBUS_LOG_LEVEL: info
SYNAPBUS_METRICS: "true"
metrics:
enabled: true
serviceMonitor:
interval: 30s
scrapeTimeout: 10s
additionalLabels: {}
nodeSelector: {}
tolerations: []
affinity: {}
+18
View File
@@ -0,0 +1,18 @@
services:
synapbus:
build: .
ports:
- "8080:8080"
volumes:
- synapbus-data:/data
environment:
- SYNAPBUS_LOG_LEVEL=info
- SYNAPBUS_METRICS=true
restart: unless-stopped
# Note: healthcheck is omitted because the runtime image uses scratch,
# which has no shell, wget, or curl. Use an external health checker
# (e.g., Docker healthcheck with a sidecar, or orchestrator-level probes)
# to poll GET /healthz and GET /readyz on port 8080.
volumes:
synapbus-data:
+10 -3
View File
@@ -8,6 +8,8 @@ require (
github.com/google/uuid v1.6.0
github.com/mark3labs/mcp-go v0.45.0
github.com/ory/fosite v0.49.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.49.0
modernc.org/sqlite v1.46.1
@@ -16,6 +18,7 @@ require (
require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -46,6 +49,7 @@ require (
github.com/mattn/goveralls v0.0.12 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/openzipkin/zipkin-go v0.4.2 // indirect
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe // indirect
@@ -54,6 +58,8 @@ require (
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@@ -62,7 +68,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/viterin/partial v1.1.0 // indirect
github.com/viterin/vek v0.4.2 // indirect
@@ -82,10 +88,11 @@ require (
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
go.opentelemetry.io/otel/trace v1.31.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
@@ -93,7 +100,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.1 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.67.6 // indirect
+28 -8
View File
@@ -46,6 +46,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -175,8 +177,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -274,6 +276,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU=
@@ -293,6 +297,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -330,6 +336,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes=
@@ -359,7 +367,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -413,8 +429,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
@@ -482,6 +498,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -489,6 +507,8 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@@ -598,8 +618,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -859,8 +879,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+35
View File
@@ -0,0 +1,35 @@
package health
import (
"database/sql"
"encoding/json"
"net/http"
)
// Checker provides Kubernetes-style health check endpoints.
type Checker struct {
db *sql.DB
version string
}
// NewChecker creates a new health Checker with the given database and version.
func NewChecker(db *sql.DB, version string) *Checker {
return &Checker{db: db, version: version}
}
// Healthz is the liveness probe - returns 200 if process is alive.
func (c *Checker) Healthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// Readyz is the readiness probe - returns 200 only if DB is accessible.
func (c *Checker) Readyz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := c.db.PingContext(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(map[string]string{"status": "not ready", "error": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ready", "version": c.version})
}
+99
View File
@@ -0,0 +1,99 @@
package health
import (
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
_ "modernc.org/sqlite"
)
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("open test db: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestHealthz(t *testing.T) {
db := setupTestDB(t)
checker := NewChecker(db, "1.0.0-test")
req := httptest.NewRequest("GET", "/healthz", nil)
rec := httptest.NewRecorder()
checker.Healthz(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
var resp map[string]string
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp["status"] != "ok" {
t.Errorf("expected status=ok, got %q", resp["status"])
}
}
func TestReadyz_Healthy(t *testing.T) {
db := setupTestDB(t)
checker := NewChecker(db, "1.0.0-test")
req := httptest.NewRequest("GET", "/readyz", nil)
rec := httptest.NewRecorder()
checker.Readyz(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
var resp map[string]string
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp["status"] != "ready" {
t.Errorf("expected status=ready, got %q", resp["status"])
}
if resp["version"] != "1.0.0-test" {
t.Errorf("expected version=1.0.0-test, got %q", resp["version"])
}
}
func TestReadyz_Unhealthy(t *testing.T) {
db := setupTestDB(t)
checker := NewChecker(db, "1.0.0-test")
// Close the DB to simulate an unavailable database.
db.Close()
req := httptest.NewRequest("GET", "/readyz", nil)
rec := httptest.NewRecorder()
checker.Readyz(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Errorf("expected status 503, got %d", rec.Code)
}
var resp map[string]string
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp["status"] != "not ready" {
t.Errorf("expected status='not ready', got %q", resp["status"])
}
if resp["error"] == "" {
t.Error("expected non-empty error field")
}
}
+45
View File
@@ -0,0 +1,45 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
HTTPRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: "synapbus",
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
HTTPRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "synapbus",
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
MessagesTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "synapbus",
Name: "messages_total",
Help: "Total number of messages sent",
})
AgentsActive = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "synapbus",
Name: "agents_active",
Help: "Number of active registered agents",
})
ActiveConnections = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "synapbus",
Name: "active_connections",
Help: "Number of active connections",
})
)
+147
View File
@@ -0,0 +1,147 @@
package metrics
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
func TestHTTPRequestsTotal(t *testing.T) {
// Increment counter
HTTPRequestsTotal.WithLabelValues("GET", "/test", "200").Inc()
// Verify it was registered and can be collected
gathered, err := prometheus.DefaultGatherer.Gather()
if err != nil {
t.Fatalf("gather metrics: %v", err)
}
found := false
for _, mf := range gathered {
if mf.GetName() == "synapbus_http_requests_total" {
found = true
break
}
}
if !found {
t.Error("synapbus_http_requests_total metric not found in gathered metrics")
}
}
func TestHTTPRequestDuration(t *testing.T) {
HTTPRequestDuration.WithLabelValues("GET", "/test").Observe(0.5)
gathered, err := prometheus.DefaultGatherer.Gather()
if err != nil {
t.Fatalf("gather metrics: %v", err)
}
found := false
for _, mf := range gathered {
if mf.GetName() == "synapbus_http_request_duration_seconds" {
found = true
break
}
}
if !found {
t.Error("synapbus_http_request_duration_seconds metric not found in gathered metrics")
}
}
func TestMessagesTotal(t *testing.T) {
MessagesTotal.Inc()
var m dto.Metric
if err := MessagesTotal.Write(&m); err != nil {
t.Fatalf("write metric: %v", err)
}
if m.GetCounter().GetValue() < 1 {
t.Error("expected messages_total counter >= 1")
}
}
func TestAgentsActive(t *testing.T) {
AgentsActive.Set(5)
var m dto.Metric
if err := AgentsActive.Write(&m); err != nil {
t.Fatalf("write metric: %v", err)
}
if m.GetGauge().GetValue() != 5 {
t.Errorf("expected agents_active = 5, got %v", m.GetGauge().GetValue())
}
}
func TestActiveConnections(t *testing.T) {
ActiveConnections.Set(10)
var m dto.Metric
if err := ActiveConnections.Write(&m); err != nil {
t.Fatalf("write metric: %v", err)
}
if m.GetGauge().GetValue() != 10 {
t.Errorf("expected active_connections = 10, got %v", m.GetGauge().GetValue())
}
}
func TestMiddleware(t *testing.T) {
handler := Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
w.Write([]byte("ok"))
}))
req := httptest.NewRequest("POST", "/api/messages", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d", rec.Code)
}
// Verify the counter was incremented
gathered, err := prometheus.DefaultGatherer.Gather()
if err != nil {
t.Fatalf("gather metrics: %v", err)
}
found := false
for _, mf := range gathered {
if mf.GetName() == "synapbus_http_requests_total" {
for _, m := range mf.GetMetric() {
for _, l := range m.GetLabel() {
if l.GetName() == "status" && l.GetValue() == "201" {
found = true
}
}
}
}
}
if !found {
t.Error("expected http_requests_total with status=201 after middleware")
}
}
func TestResponseWriterUnwrap(t *testing.T) {
rec := httptest.NewRecorder()
rw := newResponseWriter(rec)
if rw.Unwrap() != rec {
t.Error("Unwrap should return the underlying ResponseWriter")
}
}
func TestResponseWriterDefaultStatus(t *testing.T) {
rec := httptest.NewRecorder()
rw := newResponseWriter(rec)
// Write without calling WriteHeader - should default to 200
rw.Write([]byte("hello"))
if rw.statusCode != http.StatusOK {
t.Errorf("expected default status 200, got %d", rw.statusCode)
}
}
+54
View File
@@ -0,0 +1,54 @@
package metrics
import (
"net/http"
"strconv"
"time"
)
// responseWriter wraps http.ResponseWriter to capture the status code.
type responseWriter struct {
http.ResponseWriter
statusCode int
written bool
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
}
func (rw *responseWriter) WriteHeader(code int) {
if !rw.written {
rw.statusCode = code
rw.written = true
}
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
if !rw.written {
rw.written = true
}
return rw.ResponseWriter.Write(b)
}
// Unwrap returns the underlying ResponseWriter, supporting http.Flusher etc.
func (rw *responseWriter) Unwrap() http.ResponseWriter {
return rw.ResponseWriter
}
// Middleware records HTTP request count and duration for every request.
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := newResponseWriter(w)
next.ServeHTTP(wrapped, r)
duration := time.Since(start).Seconds()
status := strconv.Itoa(wrapped.statusCode)
HTTPRequestsTotal.WithLabelValues(r.Method, r.URL.Path, status).Inc()
HTTPRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
})
}
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Pre-commit hook: fast quality checks on staged Go files
set -e
echo "🔍 Pre-commit checks..."
# Check if any Go files are staged
STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$' || true)
if [ -z "$STAGED_GO_FILES" ]; then
echo "✅ No Go files staged, skipping checks"
exit 0
fi
# 1. go vet
echo " Running go vet..."
CGO_ENABLED=0 go vet ./... || { echo "❌ go vet failed"; exit 1; }
# 2. golangci-lint (skip if not installed)
if command -v golangci-lint &> /dev/null; then
echo " Running golangci-lint..."
golangci-lint run --new-from-rev=HEAD~1 ./... || { echo "❌ golangci-lint failed"; exit 1; }
else
echo " ⚠️ golangci-lint not found, skipping (install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest)"
fi
# 3. Fast unit tests (short mode)
echo " Running fast tests..."
CGO_ENABLED=0 go test -short -count=1 ./... || { echo "❌ tests failed"; exit 1; }
echo "✅ All pre-commit checks passed"
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Pre-push hook: full validation before pushing
set -e
echo "🔍 Pre-push checks..."
# 1. Full test suite
echo " Running full test suite..."
CGO_ENABLED=0 go test -count=1 -timeout 120s ./... || { echo "❌ tests failed"; exit 1; }
# 2. Build verification
echo " Verifying build..."
CGO_ENABLED=0 go build -o /dev/null ./cmd/synapbus/ || { echo "❌ build failed"; exit 1; }
echo "✅ All pre-push checks passed"
@@ -0,0 +1,35 @@
# Specification Quality Checklist: SynapBus Production Readiness & Website Launch
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-13
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass validation. Spec is ready for planning phase.
- Validated 2026-03-13 — iteration 1 (all pass).
+137
View File
@@ -0,0 +1,137 @@
# Implementation Plan: SynapBus Production Readiness & Website Launch
**Branch**: `001-production-readiness` | **Date**: 2026-03-13 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/001-production-readiness/spec.md`
## Summary
Transform SynapBus from MVP to production-ready by adding: (1) git pre-commit/pre-push hooks for quality gates, (2) GitHub Actions CI for PR validation and release builds, (3) Prometheus metrics and Kubernetes health endpoints, (4) Dockerfile + docker-compose + Helm chart, (5) public website at synapbus.dev with documentation and blog.
## Technical Context
**Language/Version**: Go 1.23+ (server), SvelteKit 2 / Svelte 5 + Tailwind (website)
**Primary Dependencies**: prometheus/client_golang, chi/v5, cobra, modernc.org/sqlite
**Storage**: SQLite (embedded, pure Go) — unchanged
**Testing**: `go test ./...` (CGO_ENABLED=0), Python E2E tests
**Target Platform**: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
**Project Type**: Multi-deliverable: Go service + deployment artifacts + static website
**Performance Goals**: Health endpoints <10ms, metrics scrape <100ms, website LCP <2s
**Constraints**: Zero CGO, single binary, all deps pure Go
**Scale/Scope**: 5 workstreams, ~25 files to create/modify
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Single Binary | PASS | Prometheus is compiled into binary. Docker/Helm are deployment artifacts. |
| II. MCP-Native | PASS | No changes to MCP interface. |
| III. Pure Go, Zero CGO | PASS | prometheus/client_golang is pure Go. No CGO required. |
| IV. Multi-Tenant | PASS | No changes to tenant model. |
| V. Embedded OAuth | PASS | No changes to auth. |
| VI. Semantic Storage | PASS | No storage changes. |
| VII. Swarm Patterns | PASS | No changes to swarm. |
| VIII. Observable | PASS | Prometheus enhances observability. |
| IX. Progressive | PASS | Metrics are opt-in via --metrics flag. |
| X. Web UI | PASS | Website is separate; embedded UI unchanged. |
All gates pass. No violations.
## Project Structure
### Documentation (this feature)
```text
specs/001-production-readiness/
├── plan.md # This file
├── research.md # Phase 0 output
├── spec.md # Feature specification
└── checklists/
└── requirements.md # Validation checklist
```
### Source Code (repository root)
```text
# Workstream 1: DevOps & Hooks
scripts/
├── hooks/
│ ├── pre-commit # go vet + golangci-lint + fast tests
│ └── pre-push # full tests + build verify
# Workstream 2: CI/CD
.github/workflows/
├── ci.yml # PR checks (lint, test, build)
└── release.yml # Tag-triggered release (binaries + Docker)
# Workstream 3: Observability
internal/
├── metrics/
│ ├── metrics.go # Prometheus collector registration
│ ├── middleware.go # HTTP metrics middleware
│ └── metrics_test.go # Tests
└── health/
├── health.go # /healthz, /readyz handlers
└── health_test.go # Tests
# Workstream 4: Deployment
Dockerfile # Multi-stage build
docker-compose.yml # Local dev stack
deploy/helm/synapbus/
├── Chart.yaml
├── values.yaml
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── pvc.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ └── _helpers.tpl
# Workstream 5: Website (separate repo)
~/repos/synapbus-website/ # SvelteKit on Cloudflare Pages
├── src/routes/
│ ├── +page.svelte # Landing page
│ ├── docs/ # Documentation
│ ├── blog/ # Blog posts
│ └── install/ # Installation guide
├── static/ # Generated images
└── wrangler.toml # Cloudflare config
```
**Structure Decision**: Existing Go project structure is preserved. New packages added under `internal/` for metrics and health. Deployment artifacts at repo root. Website in separate repo.
## Implementation Workstreams
### WS-1: Git Hooks (P1, ~30 min)
- Create `scripts/hooks/pre-commit` and `scripts/hooks/pre-push`
- Add `make hooks` target to install them
- Test with intentional lint error
### WS-2: CI/CD Workflows (P1, ~45 min)
- Create `.github/workflows/ci.yml` for PR checks
- Create `.github/workflows/release.yml` for tag releases
- Test by pushing branch
### WS-3: Observability (P1, ~1 hour)
- Add `internal/metrics/` package with Prometheus collectors
- Add `internal/health/` package with /healthz and /readyz
- Wire into main.go router
- Add middleware for HTTP request metrics
- Write unit tests
### WS-4: Deployment Artifacts (P2, ~1 hour)
- Create Dockerfile (multi-stage: builder + scratch)
- Create docker-compose.yml
- Create Helm chart in deploy/helm/synapbus/
- Test Docker build locally
### WS-5: Website (P2, ~2 hours)
- Create synapbus-website repo
- Scaffold SvelteKit + Tailwind project
- Build landing page with AI-generated hero images
- Create documentation pages
- Create 2 blog posts with diagrams
- Deploy to Cloudflare Pages
- Configure DNS for synapbus.dev
@@ -0,0 +1,49 @@
# Research: SynapBus Production Readiness
## R1: Prometheus Client for Zero-CGO Go
**Decision**: Use `github.com/prometheus/client_golang` v1.20+
**Rationale**: Pure Go library, no CGO required. Industry standard for Go services. Provides promhttp handler, collectors, and middleware patterns.
**Alternatives considered**: VictoriaMetrics client (lighter but less ecosystem support), OpenTelemetry (heavier, would add complexity)
## R2: Health Check Patterns
**Decision**: Implement `/healthz` (liveness) and `/readyz` (readiness) following Kubernetes probe conventions.
**Rationale**: Standard K8s pattern. Liveness = "is the process alive" (always 200 unless deadlocked). Readiness = "can it serve traffic" (checks DB connectivity).
**Alternatives considered**: Single `/health` endpoint (already exists but doesn't distinguish liveness from readiness)
## R3: Multi-Stage Docker Build
**Decision**: Two-stage build: `golang:1.23-alpine` builder → `scratch` runtime with ca-certificates and tzdata.
**Rationale**: Scratch produces smallest possible image. CGO_ENABLED=0 ensures static binary. Alpine builder provides build tools without bloating runtime.
**Alternatives considered**: Alpine runtime (adds ~5MB but has shell for debugging), distroless (Google's option, slightly larger)
## R4: Helm Chart Structure
**Decision**: Standard Helm 3 chart with Deployment, Service, PVC, optional Ingress. Single values.yaml.
**Rationale**: Follows Helm best practices. PVC for data persistence. Ingress optional for cloud deployments.
**Alternatives considered**: Kustomize (less user-friendly), raw K8s manifests (no templating)
## R5: GitHub Actions CI Strategy
**Decision**: Two workflows: `ci.yml` (on PR) and `release.yml` (on tag push v*). Use `actions/setup-go@v5` and `actions/setup-node@v4`.
**Rationale**: Separating PR checks from releases keeps CI fast. Tag-based releases are idiomatic for Go projects.
**Alternatives considered**: GoReleaser (adds dependency), single workflow with conditionals (harder to maintain)
## R6: Website Technology
**Decision**: SvelteKit 2 + Svelte 5 + Tailwind in separate private repo, deployed to Cloudflare Pages.
**Rationale**: Matches user's tech preferences (see CLAUDE.md). Cloudflare Pages provides free hosting with edge CDN. Separate repo avoids bloating the Go project.
**Alternatives considered**: Astro (used for mcpproxy.app), Hugo (Go-native but less flexible for interactive pages)
## R7: Pre-commit/Pre-push Hooks
**Decision**: Shell scripts in `scripts/hooks/`, installed via `make hooks` symlink.
**Rationale**: Go projects don't use npm (no husky). Shell scripts are universal, zero dependencies. Graceful degradation if golangci-lint not installed.
**Alternatives considered**: lefthook (Go-based, good but adds dependency), pre-commit framework (Python dependency)
## R8: Docker Image Registry
**Decision**: ghcr.io/smart-mcp-proxy/synapbus
**Rationale**: GitHub Container Registry is free for public repos, integrates with GitHub Actions natively (GITHUB_TOKEN auth).
**Alternatives considered**: Docker Hub (rate limits), ECR (AWS-specific)
+190
View File
@@ -0,0 +1,190 @@
# Feature Specification: SynapBus Production Readiness & Website Launch
**Feature Branch**: `001-production-readiness`
**Created**: 2026-03-13
**Status**: Draft
**Input**: Transform SynapBus from MVP to production-ready service with DevOps, observability, deployment artifacts, and public website.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Developer Catches Issues Before Push (Priority: P1)
A developer working on SynapBus runs `git commit` and `git push`. Pre-commit hooks automatically run fast checks (formatting, vet, lint). Pre-push hooks run the full test suite and build verification. The developer receives immediate feedback on code quality issues before code reaches the remote repository.
**Why this priority**: Prevents broken code from entering the repository, saving team time and maintaining code quality. Foundation for all other quality processes.
**Independent Test**: Can be tested by making a commit with a lint error and verifying it is rejected, then fixing it and verifying the commit succeeds.
**Acceptance Scenarios**:
1. **Given** a developer stages Go files with lint warnings, **When** they run `git commit`, **Then** the commit is rejected with clear error messages identifying the issues.
2. **Given** a developer has clean code staged, **When** they run `git commit`, **Then** hooks pass and the commit succeeds within 15 seconds.
3. **Given** a developer pushes code that breaks tests, **When** they run `git push`, **Then** the push is rejected with test failure output.
4. **Given** a developer pushes code that passes all tests, **When** they run `git push`, **Then** the push succeeds.
---
### User Story 2 - CI Validates Pull Requests Automatically (Priority: P1)
A contributor opens a pull request on GitHub. The CI pipeline automatically runs linting, tests, and multi-platform build verification. The PR status checks prevent merging broken code. On merge to main, release artifacts are built automatically.
**Why this priority**: Automated quality gates are essential for any production service. Enables safe collaboration.
**Independent Test**: Can be tested by opening a PR with a failing test and verifying the check fails, then fixing and verifying it passes.
**Acceptance Scenarios**:
1. **Given** a PR is opened, **When** CI runs, **Then** it executes lint, test, and build steps and reports status.
2. **Given** CI detects test failures, **When** the PR status is updated, **Then** the merge button is blocked.
3. **Given** a version tag is pushed, **When** the release workflow runs, **Then** binaries for 5 platform/arch combinations are published as GitHub Release assets.
4. **Given** a version tag is pushed, **When** the release workflow runs, **Then** a Docker image is built and pushed to the container registry.
---
### User Story 3 - Operator Monitors SynapBus Health (Priority: P1)
An operator deploys SynapBus and configures monitoring to scrape its metrics endpoint. They can monitor request rates, latencies, message volumes, and agent counts. Liveness and readiness probes work correctly for container orchestration.
**Why this priority**: Observability is non-negotiable for production services. Without it, operators are blind to issues.
**Independent Test**: Can be tested by starting SynapBus, querying the metrics endpoint, and verifying properly formatted output.
**Acceptance Scenarios**:
1. **Given** SynapBus is running with metrics enabled, **When** an operator queries the metrics endpoint, **Then** they receive properly formatted metrics including request counts, latencies, and business metrics.
2. **Given** SynapBus is healthy, **When** a liveness probe hits the health endpoint, **Then** it returns success status.
3. **Given** SynapBus is ready to serve traffic, **When** a readiness probe checks, **Then** it returns success status.
4. **Given** the database is inaccessible, **When** a readiness probe checks, **Then** it returns failure status.
---
### User Story 4 - Operator Deploys SynapBus in Containers (Priority: P2)
An operator deploys SynapBus using containers (standalone or compose) or container orchestration (via configuration templates). Data persists across restarts. Configuration is managed via environment variables or template values.
**Why this priority**: Containerized deployment is the standard for modern services. Templates enable repeatable deployments.
**Independent Test**: Can be tested by building and running the container image, sending a message, restarting, and verifying the message persists.
**Acceptance Scenarios**:
1. **Given** a container build file exists, **When** an operator builds the image, **Then** a minimal image is produced (under 50MB).
2. **Given** a compose file exists, **When** an operator starts the stack, **Then** SynapBus starts and is accessible on the configured port.
3. **Given** a deployment template exists, **When** an operator installs it, **Then** SynapBus deploys with correct service, workload, and persistence configuration.
4. **Given** SynapBus is running with persistent storage, **When** the container restarts, **Then** all data persists.
---
### User Story 5 - Visitor Discovers SynapBus Online (Priority: P2)
A developer or technical decision-maker visits synapbus.dev. They see a visually compelling landing page that explains what SynapBus does, how to install it, and why they should use it. They can browse documentation and read blog posts about agent messaging patterns.
**Why this priority**: A public website is essential for adoption. Developers need documentation and clear value propositions.
**Independent Test**: Can be tested by navigating to the website and verifying all pages load correctly on desktop and mobile.
**Acceptance Scenarios**:
1. **Given** a visitor navigates to synapbus.dev, **When** the page loads, **Then** they see an attractive landing page with clear product description and call-to-action.
2. **Given** a visitor navigates to documentation, **When** the docs page loads, **Then** they see installation instructions, configuration reference, and feature guides.
3. **Given** a visitor navigates to the blog, **When** the blog page loads, **Then** they see at least 2 posts with hero images and diagrams.
4. **Given** a visitor uses a mobile device, **When** they browse the site, **Then** all pages are fully responsive and usable.
---
### User Story 6 - Developer Downloads and Installs SynapBus (Priority: P3)
A developer visits the installation page, sees download options for their platform, and can install SynapBus with a single command or download.
**Why this priority**: Easy installation drives adoption. Multiple distribution channels serve different user preferences.
**Independent Test**: Can be tested by following the installation instructions on a clean machine.
**Acceptance Scenarios**:
1. **Given** a developer is on the installation page, **When** they select their platform, **Then** they see a direct download link and command-line install command.
2. **Given** release artifacts exist, **When** a developer downloads one, **Then** it is a self-contained binary that runs without dependencies.
---
### Edge Cases
- What happens when git hooks are installed but lint tools are not available? Hooks should warn and skip lint (not block).
- What happens when the readiness endpoint is called during database migration? Should return failure until migrations complete.
- What happens when metrics scraping is disabled? The metrics endpoint should not be registered on the router.
- What happens when container build runs on ARM64? Multi-architecture build must work for both amd64 and arm64.
- What happens when deployment template is installed without persistence? Should work with ephemeral storage (with a warning in values).
## Requirements *(mandatory)*
### Functional Requirements
**DevOps & Hooks**:
- **FR-001**: Repository MUST include a pre-commit hook script that runs code analysis and fast tests on staged files.
- **FR-002**: Repository MUST include a pre-push hook script that runs the full test suite and verifies the binary builds successfully.
- **FR-003**: Hooks MUST be installable via a single make target and MUST gracefully degrade if optional tools are missing.
**CI/CD**:
- **FR-004**: Repository MUST include an automated workflow for PR checks that runs lint, tests, and build verification.
- **FR-005**: Repository MUST include an automated workflow for releases triggered by version tags that builds binaries for 5 platform/architecture combinations.
- **FR-006**: Release workflow MUST build and push a multi-architecture container image to the project's container registry.
- **FR-007**: PR workflow MUST build the web frontend before the main build to ensure embedded assets are current.
**Observability**:
- **FR-008**: System MUST expose a metrics endpoint in standard monitoring format when metrics are enabled.
- **FR-009**: System MUST expose liveness and readiness health check endpoints that return appropriate status codes.
- **FR-010**: Metrics MUST include: request counts (by method, path, status), request duration distribution, message totals, active agent count, and active connection count.
- **FR-011**: Readiness probe MUST verify database connectivity before reporting ready.
**Deployment**:
- **FR-012**: Repository MUST include a multi-stage container build file that produces a minimal runtime image.
- **FR-013**: Repository MUST include a compose file for local development with volume persistence.
- **FR-014**: Repository MUST include a deployment template with configurable values for image, replicas, resources, persistence, ingress, and environment variables.
- **FR-015**: Deployment template MUST include liveness and readiness probes using the health check endpoints.
**Website**:
- **FR-016**: A separate repository MUST host the synapbus.dev website.
- **FR-017**: Website MUST include: landing page, installation guide, configuration reference, features overview, use cases, and blog.
- **FR-018**: Website MUST be deployed to a CDN with DNS configured for synapbus.dev.
- **FR-019**: Website MUST include at least 2 blog posts with generated hero images and technical diagrams.
- **FR-020**: Website MUST be fully responsive on mobile devices (320px to 1920px+ viewports).
### Key Entities
- **Git Hook**: Script installed in the repository that runs quality checks at commit/push time.
- **CI Workflow**: Automated pipeline definition for build, test, and release processes.
- **Metric**: Named measurement exposed for monitoring systems (counters, gauges, distributions).
- **Health Probe**: Endpoint returning status codes for orchestrator health checking.
- **Container Image**: Packaged application image built from multi-stage build, pushed to registry.
- **Deployment Template**: Configuration template with customizable values for container orchestration.
- **Website Page**: Static page served from CDN edge locations.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Pre-commit hooks complete in under 15 seconds for a typical change.
- **SC-002**: PR validation pipeline completes in under 5 minutes.
- **SC-003**: Release workflow produces verified binaries for all 5 target platforms.
- **SC-004**: Container image size is under 50MB compressed.
- **SC-005**: Health check endpoints respond in under 10ms.
- **SC-006**: Metrics endpoint returns valid, parseable monitoring data.
- **SC-007**: Deployment template deploys successfully on a standard container orchestration cluster.
- **SC-008**: Website pages load in under 2 seconds on a standard connection.
- **SC-009**: Website scores 90+ on mobile performance audits.
- **SC-010**: All blog posts include hero images and at least one technical diagram.
## Assumptions
- Website will be in a separate private repository following established project patterns.
- Container images are pushed to GitHub Container Registry (ghcr.io).
- Deployment templates follow standard container orchestration conventions.
- Pre-commit/pre-push hooks are shell scripts (this is a Go project).
- Metrics use a standard Go metrics library compatible with the project's zero-CGO constraint.
- CI uses GitHub Actions with Go 1.23+ and Node.js 20+ for web builds.
- Image generation API is used for hero graphics on blog posts.
- Website uses dark theme, modern developer-focused aesthetic.
- Blog topics: (1) Agent-to-agent messaging patterns, (2) Building AI swarms with MCP.
- CDN deployment uses the platform's CLI tooling.
- Binary releases use a build matrix approach (not external release tooling).
- The existing `--metrics` flag in the server startup is currently unused and will be wired to the new metrics integration.