Skip to content

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

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:

  1. CLI flags (highest priority)
  2. Environment variables
  3. YAML config file
  4. 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

  1. 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"))
}
  1. Register the route:
go
func main() {
    // Existing routes...
    http.HandleFunc("/new-endpoint", handleNewEndpoint)
    
    // Start server...
}
  1. 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)
    }
}
  1. 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:

  1. 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
}
  1. Update Docker configuration:
dockerfile
# Add new environment variables
ENV PORT=8080
ENV MAX_CONNECTIONS=1000
  1. 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:

  1. 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

  1. Fork the repository
  2. Clone your fork:
    bash
    git clone https://gitlab.bjoernbartels.earth/devops/dummy-responder.git
    cd dummy-responder
  3. Create a feature branch:
    bash
    git checkout -b feature/your-feature-name
  4. Make your changes and add tests
  5. Run the test suite:
    bash
    make test
    make docs
  6. Commit your changes:
    bash
    git commit -am "Add your feature description"
  7. Push to your fork:
    bash
    git push origin feature/your-feature-name
  8. 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

  1. Update version in relevant files
  2. Create release notes with feature highlights
  3. Tag the release:
    bash
    git tag -a v1.2.0 -m "Release version 1.2.0"
    git push origin v1.2.0
  4. Build and publish Docker images
  5. Update documentation and examples

Resources

Documentation

Tools

Go Libraries Used

  • net/http - HTTP server and client
  • golang.org/x/net/websocket - WebSocket support
  • github.com/swaggo/swag - Swagger documentation
  • github.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.

Released under the MIT License... (see LICENSE.md)