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:
co-authored by
Claude Opus 4.6
parent
1cabbae6b5
commit
ff81f2f218
@@ -0,0 +1,12 @@
|
||||
.git
|
||||
bin/
|
||||
data/
|
||||
web/node_modules/
|
||||
web/.svelte-kit/
|
||||
*.test
|
||||
*.out
|
||||
.env
|
||||
.env.*
|
||||
.claude/
|
||||
.specify/
|
||||
__pycache__/
|
||||
@@ -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/
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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: {}
|
||||
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Executable
+31
@@ -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"
|
||||
Executable
+15
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user