Appearance
Developer Guide
Getting Started
Welcome to the [dummy-responder] developer guide! This guide will help you understand the codebase, contribute to the project, and integrate the service into your own applications.
Prerequisites
Required Tools
- Go 1.21+: Download Go
- Docker: Install Docker
- Make: Usually pre-installed on Unix systems
- Git: For version control
Optional Tools
- kubectl: For Kubernetes deployment testing
- k6: For load testing
- wscat: For WebSocket testing (
npm install -g wscat
)
Project Structure
dummy-responder/
├── cmd/
│ └── dummy-responder/ # Main application entry point
│ └── main.go # Application bootstrap and routing
├── internal/ # Private application packages
│ ├── config/ # Configuration management
│ │ └── config.go # Config loading, validation, defaults
│ ├── handlers/ # HTTP request handlers
│ │ ├── http.go # Main HTTP endpoints (/health, /config, /)
│ │ └── test.go # Interactive test page handler
│ ├── sse/ # Server-Sent Events implementation
│ │ └── sse.go # SSE endpoint and streaming logic
│ ├── types/ # Shared type definitions
│ │ ├── constants.go # Application constants
│ │ └── types.go # Request/response types
│ ├── utils/ # Utility functions
│ │ └── utils.go # CORS, delays, random, IP utilities
│ └── websocket/ # WebSocket implementation
│ └── websocket.go # WebSocket endpoint and messaging
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── main.go.backup # Original monolithic implementation (backup)
├── STRUCTURE.md # Detailed project structure documentation
├── Makefile # Build and development tasks
├── Dockerfile # Container build instructions
├── docker-compose.yml # Multi-container development setup
├── k8s-example.yaml # Kubernetes deployment manifest
├── README.md # Project overview and quick start
├── LICENSE.md # MIT license
├── CODE-OF-CONDUCT.md # Community guidelines
├── CONTRIBUTING.md # Contribution guidelines
├── docs/ # Auto-generated Swagger documentation
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── examples/ # Usage examples and test scripts
│ ├── realtime-test.html # Combined WebSocket and SSE test page
│ ├── test-scenarios.sh # HTTP endpoint testing
│ ├── test-realtime.sh # WebSocket and SSE testing
│ ├── test-configuration.sh # Configuration testing
│ ├── client-test.go # Go client example
│ ├── load-testing.md # Load testing guide
│ └── ci-cd-integration.md # CI/CD integration guide
└── website/ # VitePress documentation site
├── package.json
├── docs/
│ ├── .vitepress/
│ ├── index.md
│ ├── arc42.md
│ ├── prd.md
│ ├── developer-guide.md
│ └── user-guide.md
└── node_modules/
Core Architecture
Configuration System
The service uses a layered configuration system with this priority order:
- CLI flags (highest priority)
- Environment variables
- YAML config file
- Built-in defaults (lowest priority)
📖 For complete configuration details, see the Configuration Reference.
Logging Implementation
The current implementation uses minimal console output for essential information only. Extensive structured logging is not implemented. For production environments requiring detailed logging, consider integrating a logging framework.
Main Components
The service is built with a modular architecture:
Handler Functions
Root HTTP Handler (handleRoot
)
go
func handleRoot(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
status := parseIntParam(r, "status", 200)
delay := parseDurationParam(r, "delay", 0)
contentType := r.URL.Query().Get("content-type")
failureRate := parseIntParam(r, "failure-rate", 0)
// Simulate failure if configured
if failureRate > 0 && shouldSimulateFailure(failureRate) {
http.Error(w, "Simulated failure", 500)
return
}
// Apply delay if configured
if delay > 0 {
time.Sleep(delay)
}
// Set response headers and send response
// ...
}
WebSocket Handler (handleWebSocket
)
go
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Upgrade(w, r, nil, 1024, 1024)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
defer conn.Close()
// Send welcome message
// Handle incoming messages in a loop
// Echo messages back to client
// Broadcast to other connected clients
}
SSE Handler (handleSSE
)
go
func handleSSE(w http.ResponseWriter, r *http.Request) {
// Set SSE headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// Send periodic events
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Send event data
case <-r.Context().Done():
return
}
}
}
Development Workflow
1. Local Development Setup
bash
# Clone the repository
git clone <repository-url>
cd dummy-responder
# Install dependencies
go mod download
# Run the service locally (default port 8080)
make run
# Start with custom port
go run ./cmd/dummy-responder -port 9090
# Use environment variable
PORT=3000 go run ./cmd/dummy-responder
# Show help
go run ./cmd/dummy-responder -help
# Using Makefile shortcuts
make run-custom-port # Runs on port 9090
make run-env-port # Runs on port 3000
make help # Shows CLI help
2. Building the Project
bash
# Build binary
make build
# Build for different platforms
GOOS=linux GOARCH=amd64 make build
GOOS=windows GOARCH=amd64 make build
# Clean build artifacts
make clean
3. Testing
bash
# Run all tests
make test
# Run tests with coverage
go test -cover ./...
# Test specific functionality
curl "http://localhost:8080/?status=201&delay=1s"
4. Documentation Generation
bash
# Generate Swagger docs
make docs
# Serve docs locally
make serve-docs
# View Swagger UI
open http://localhost:8080/swagger/
Adding New Features
Adding a New HTTP Endpoint
- Define the handler function:
go
func handleNewEndpoint(w http.ResponseWriter, r *http.Request) {
// Add Swagger annotations
// @Summary New endpoint description
// @Description Detailed description
// @Tags endpoints
// @Accept json
// @Produce json
// @Param param query string false "Parameter description"
// @Success 200 {string} string "Success response"
// @Router /new-endpoint [get]
// Implementation
w.WriteHeader(http.StatusOK)
w.Write([]byte("New endpoint response"))
}
- Register the route:
go
func main() {
// Existing routes...
http.HandleFunc("/new-endpoint", handleNewEndpoint)
// Start server...
}
- Add tests:
go
func TestNewEndpoint(t *testing.T) {
req, err := http.NewRequest("GET", "/new-endpoint", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handleNewEndpoint)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Expected status %v, got %v", http.StatusOK, status)
}
}
- Update documentation:
bash
# Regenerate Swagger docs
make docs
# Update README if needed
Adding Configuration Options
The service supports configuration via CLI flags and environment variables. Here's how to add new options:
- Define CLI flags and environment variables:
go
import "flag"
// Helper function for env vars with defaults
func getEnvOrDefault(envVar, defaultValue string) string {
if value := os.Getenv(envVar); value != "" {
return value
}
return defaultValue
}
func main() {
// Define CLI flags with env var fallback
var (
port = flag.String("port", getEnvOrDefault("PORT", "8080"), "Port to listen on")
maxConnections = flag.Int("max-connections", getEnvInt("MAX_CONNECTIONS", 1000), "Max connections")
)
// Parse flags
flag.Parse()
// Use the values
fmt.Printf("Starting on port %s\n", *port)
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
- Update Docker configuration:
dockerfile
# Add new environment variables
ENV PORT=8080
ENV MAX_CONNECTIONS=1000
- Update Kubernetes manifest:
yaml
spec:
containers:
- name: dummy-responder
env:
value: "DEBUG"
- name: MAX_CONNECTIONS
value: "2000"
Testing Strategies
Configuration Testing
Test the layered configuration system to ensure proper priority and loading:
bash
#!/bin/bash
# test-configuration.sh
echo "🧪 Testing configuration priority and loading..."
# Test 1: Generate and test config file
echo "📄 Testing config file generation..."
./dummy-responder -gen-config -config test-config.yml
if [ -f "test-config.yml" ]; then
echo "✅ Config file generated successfully"
else
echo "❌ Config file generation failed"
exit 1
fi
# Test 2: Test config file loading
echo "📄 Testing config file loading..."
timeout 3s ./dummy-responder -config test-config.yml &
PID=$!
sleep 1
if curl -s http://localhost:8080/config | jq .server.port | grep -q "8080"; then
echo "✅ Config file loaded correctly"
else
echo "❌ Config file loading failed"
fi
kill $PID 2>/dev/null || true
# Test 3: Test environment variable override
echo "🌍 Testing environment variable override..."
PORT=9090 timeout 3s ./dummy-responder -config test-config.yml &
PID=$!
sleep 1
if curl -s http://localhost:9090/config | jq .server.port | grep -q "9090"; then
echo "✅ Environment variable override works"
else
echo "❌ Environment variable override failed"
fi
kill $PID 2>/dev/null || true
# Test 4: Test CLI flag override (highest priority)
echo "🚩 Testing CLI flag override..."
PORT=9090 timeout 3s ./dummy-responder -config test-config.yml -port 8080 &
PID=$!
sleep 1
if curl -s http://localhost:8080/config | jq .server.port | grep -q "8080"; then
echo "✅ CLI flag override works (highest priority)"
else
echo "❌ CLI flag override failed"
fi
kill $PID 2>/dev/null || true
# Test 5: Test config endpoint structure
echo "🔍 Testing config endpoint structure..."
timeout 3s ./dummy-responder &
PID=$!
sleep 1
CONFIG_RESPONSE=$(curl -s http://localhost:8080/config)
if echo "$CONFIG_RESPONSE" | jq -e '.server.port and .defaults.responseCode and .websocket.enabled and .sse.enabled' > /dev/null; then
echo "✅ Config endpoint returns complete structure"
else
echo "❌ Config endpoint missing required fields"
echo "Response: $CONFIG_RESPONSE"
fi
kill $PID 2>/dev/null || true
echo "🧪 Configuration tests completed!"
cleanup: rm -f test-config.yml
Unit Testing
go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestHandleRootDefaultResponse(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handleRoot)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Expected status %v, got %v", http.StatusOK, status)
}
expected := "Hello, World!"
if rr.Body.String() != expected {
t.Errorf("Expected body %v, got %v", expected, rr.Body.String())
}
}
func TestHandleRootWithCustomStatus(t *testing.T) {
req, err := http.NewRequest("GET", "/?status=201", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handleRoot)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusCreated {
t.Errorf("Expected status %v, got %v", http.StatusCreated, status)
}
}
func TestHandleRootWithDelay(t *testing.T) {
req, err := http.NewRequest("GET", "/?delay=100ms", nil)
if err != nil {
t.Fatal(err)
}
start := time.Now()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handleRoot)
handler.ServeHTTP(rr, req)
duration := time.Since(start)
if duration < 100*time.Millisecond {
t.Errorf("Expected delay of at least 100ms, got %v", duration)
}
}
Integration Testing
bash
#!/bin/bash
# integration-test.sh
set -e
echo "Starting integration tests..."
# Start the service in background
make build && ./dummy-responder &
SERVER_PID=$!
# Wait for server to start
sleep 2
# Test basic functionality
echo "Testing basic HTTP response..."
curl -f http://localhost:8080/ || exit 1
echo "Testing custom status code..."
curl -f -o /dev/null -s -w "%{http_code}" http://localhost:8080/?status=201 | grep 201 || exit 1
echo "Testing JSON response..."
curl -H "Accept: application/json" http://localhost:8080/?content-type=json | jq . || exit 1
echo "Testing WebSocket..."
# Use wscat to test WebSocket (if available)
if command -v wscat &> /dev/null; then
echo "test" | wscat -c ws://localhost:8080/ws -x || exit 1
fi
# Clean up
kill $SERVER_PID
echo "All integration tests passed!"
Load Testing
javascript
// load-test.js (k6 script)
import http from 'k6/http';
import ws from 'k6/ws';
import { check } from 'k6';
export let options = {
stages: [
{ duration: '30s', target: 100 },
{ duration: '1m', target: 500 },
{ duration: '30s', target: 0 },
],
};
export default function () {
// HTTP load test
let response = http.get('http://localhost:8080/?status=200');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
// WebSocket test (every 10th iteration)
if (__ITER % 10 === 0) {
let url = 'ws://localhost:8080/ws';
let res = ws.connect(url, function (socket) {
socket.on('open', function open() {
socket.send('test message');
});
socket.on('message', function (message) {
console.log('Received:', message);
});
socket.setTimeout(function () {
socket.close();
}, 1000);
});
}
}
Docker Development
Building Images
bash
# Build development image
docker build -t dummy-responder:dev .
# Build production image with specific tag
docker build -t dummy-responder:1.0.0 .
# Multi-platform build
docker buildx build --platform linux/amd64,linux/arm64 -t dummy-responder:latest .
Development with Docker Compose
yaml
# docker-compose.dev.yml
version: '3.8'
services:
dummy-responder:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
volumes:
- .:/app
working_dir: /app
command: ["./dummy-responder"]
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- dummy-responder
Kubernetes Development
Local Testing with Kind
bash
# Create local cluster
kind create cluster --name dummy-responder-test
# Build and load image
docker build -t dummy-responder:test .
kind load docker-image dummy-responder:test --name dummy-responder-test
# Deploy to cluster
kubectl apply -f k8s-example.yaml
# Port forward for testing
kubectl port-forward service/dummy-responder 8080:80
# Test the deployment
curl http://localhost:8080/health
Helm Chart Development
yaml
# charts/dummy-responder/values.yaml
replicaCount: 3
image:
repository: dummy-responder
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: true
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
hosts:
- host: dummy-responder.local
paths: ["/"]
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
CI/CD Integration
GitHub Actions Workflow
yaml
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Run tests
run: |
go test -v ./...
go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t dummy-responder:${{ github.sha }} .
- name: Run integration tests
run: |
docker run -d -p 8080:8080 --name test-container dummy-responder:${{ github.sha }}
sleep 5
curl -f http://localhost:8080/health
docker stop test-container
deploy:
runs-on: ubuntu-latest
needs: [test, build]
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to staging
run: echo "Deploy to staging environment"
GitLab CI Configuration
yaml
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
test:
stage: test
image: golang:1.21
script:
- go test -v ./...
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
coverage: '/coverage: \d+\.\d+% of statements/'
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
deploy:staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/dummy-responder dummy-responder=$DOCKER_IMAGE
environment:
name: staging
only:
- main
Performance Optimization
Profiling
go
import (
_ "net/http/pprof"
"net/http"
)
func main() {
// Enable pprof endpoint
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Your main application...
}
Access profiling data:
bash
# CPU profiling
go tool pprof http://localhost:6060/debug/pprof/profile
# Memory profiling
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine profiling
go tool pprof http://localhost:6060/debug/pprof/goroutine
Benchmarking
go
func BenchmarkHandleRoot(b *testing.B) {
req, _ := http.NewRequest("GET", "/", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handleRoot)
handler.ServeHTTP(rr, req)
}
}
func BenchmarkHandleRootWithDelay(b *testing.B) {
req, _ := http.NewRequest("GET", "/?delay=1ms", nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handleRoot)
handler.ServeHTTP(rr, req)
}
}
Run benchmarks:
bash
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkHandleRoot
# With memory allocation stats
go test -bench=. -benchmem
Troubleshooting
Common Issues
1. Port Already in Use
bash
# Find process using port 8080
lsof -i :8080
# Kill process
kill -9 <PID>
# Or use different port
# Development mode
PORT=8090 go run ./cmd/dummy-responder
2. WebSocket Connection Fails
bash
# Check if WebSocket endpoint is accessible
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: test" -H "Sec-WebSocket-Version: 13" http://localhost:8080/ws
3. SSE Connection Issues
bash
# Test SSE endpoint
curl -N -H "Accept: text/event-stream" http://localhost:8080/events
4. Docker Build Fails
bash
# Build with no cache
docker build --no-cache -t dummy-responder .
# Check Docker logs
docker logs <container-id>
Debugging
Limited Logging
The current implementation provides minimal console output. For detailed debugging, consider adding custom logging statements or integrating a structured logging library.
Debug Tips:
- Add Debug Output - Use
fmt.Printf
for temporary debugging:
go
// In handlers, add debug prints
func HandleRoot(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Received request: %s %s from %s\n", r.Method, r.URL.Path, r.RemoteAddr)
fmt.Printf("Query parameters: %v\n", r.URL.Query())
// ... rest of handler
fmt.Printf("Sending response: %d\n", status)
}
}
2. Use Binary for Testing:
bash
make build && ./dummy-responder
Contributing
Development Setup
- Fork the repository
- Clone your fork:bash
git clone https://gitlab.bjoernbartels.earth/devops/dummy-responder.git cd dummy-responder
- Create a feature branch:bash
git checkout -b feature/your-feature-name
- Make your changes and add tests
- Run the test suite:bash
make test make docs
- Commit your changes:bash
git commit -am "Add your feature description"
- Push to your fork:bash
git push origin feature/your-feature-name
- Create a Pull Request on GitHub
Code Style Guidelines
- Follow Go's official style guide and use
gofmt
- Add comprehensive tests for new features
- Include Swagger annotations for new endpoints
- Update documentation for new features
- Use conventional commit messages
Release Process
- Update version in relevant files
- Create release notes with feature highlights
- Tag the release:bash
git tag -a v1.2.0 -m "Release version 1.2.0" git push origin v1.2.0
- Build and publish Docker images
- Update documentation and examples
Resources
Documentation
Tools
Go Libraries Used
net/http
- HTTP server and clientgolang.org/x/net/websocket
- WebSocket supportgithub.com/swaggo/swag
- Swagger documentationgithub.com/swaggo/http-swagger
- Swagger UI
Happy coding! If you have questions or need help, please check the GitHub issues or create a new issue.