Yang_yang
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Complete Grafana k6 Load Testing Tutorial > **Project Repository**: [https://github.com/Yang92047111/Grafana_k6_Load_Testing_Quick_Start](https://github.com/Yang92047111/Grafana_k6_Load_Testing_Quick_Start) This tutorial provides a comprehensive guide to setting up and using Grafana, Prometheus, and k6 for monitoring and load testing cloud-native applications. ## Table of Contents 1. [Introduction to k6](#Introduction-to-k6) 2. [How k6 Works](#How-k6-Works) 3. [Project Structure](#Project-Structure) 4. [Quick Start Guide](#Quick-Start-Guide) 5. [Setting Up the Environment](#Setting-Up-the-Environment) 6. [Creating a Go Backend Service](#Creating-a-Go-Backend-Service) 7. [Dockerizing the Application](#Dockerizing-the-Application) 8. [Writing k6 Load Tests](#Writing-k6-Load-Tests) 9. [Running k6 Tests](#Running-k6-Tests) 10. [Monitoring and Analysis](#Monitoring-and-Analysis) 11. [CI/CD Pipeline](#CI/CD-Pipeline) 12. [Troubleshooting](#Troubleshooting) ## Introduction to k6 k6 is a developer-centric, free and open-source load testing tool built for making performance testing a productive and enjoyable experience. It's designed to test the reliability and performance of systems under load. ### Key Features: - **JavaScript-based**: Write tests in modern JavaScript (ES6+) - **Developer-friendly**: CLI tool with great developer experience - **Cloud and on-premise**: Run locally or in the cloud - **Rich metrics**: Built-in metrics and custom metrics support - **Integrations**: Works with CI/CD pipelines and monitoring systems ## How k6 Works k6 follows a straightforward architecture for load testing: ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ k6 CLI Tool │ │ Test Scripts │ │ Target API │ │ │ │ (JavaScript) │ │ (Go Service) │ │ ┌─────────────┐ │ │ │ │ │ │ │Load Testing │ │───▶│ ┌──────────────┐ │───▶│ ┌─────────────┐ │ │ │ Engine │ │ │ │HTTP Requests │ │ │ │Endpoints │ │ │ └─────────────┘ │ │ │Checks & Thresholds│ │ │Handlers │ │ │ │ │ │Virtual Users │ │ │ │ │ │ │ ┌─────────────┐ │ │ └──────────────┘ │ │ └─────────────┘ │ │ │ Metrics │ │ │ │ │ │ │ │ Collection │ │ └──────────────────┘ └─────────────────┘ │ └─────────────┘ │ │ │ └─────────────────┘ │ │ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Results │ │ Monitoring │ │ Logs & │ │ & Reports │ │ (Grafana) │ │ Responses │ └─────────────────┘ └──────────────────┘ └─────────────────┘ ``` ### k6 Test Lifecycle: 1. **Init**: Code runs once per VU (Virtual User) to set up test data 2. **Setup**: Runs once before the test starts 3. **VU Code**: Main test function that runs for each iteration 4. **Teardown**: Runs once after the test completes ## Project Structure This tutorial implements a complete cloud-native load testing solution with the following structure: ``` grafana-k6-quick-start/ ├── .dockerignore # Docker build optimization ├── .github/workflows/ # CI/CD pipeline (GitHub Actions) ├── backend/ # Go REST API service │ ├── handlers/ # HTTP request handlers + tests │ ├── middleware/ # HTTP middleware + tests │ ├── models/ # Data models and structures │ ├── main.go # Application entry point │ ├── Dockerfile # Container configuration │ └── go.mod/go.sum # Go module dependencies ├── monitoring/ # Observability stack │ ├── grafana/ # Dashboards and provisioning │ └── prometheus/ # Prometheus configuration ├── tests/ # k6 load testing scripts │ ├── smoke-test.js # Basic functionality validation │ ├── load-test.js # Standard load testing │ ├── stress-test.js # High load stress testing │ ├── spike-test.js # Sudden load spike testing │ └── comprehensive-test.js # Full CRUD testing ├── docker-compose.yml # Local development stack ├── Makefile # Build and deployment commands └── README.md # Project documentation ``` ## Quick Start Guide ### Option A: Local Development (API only) ```bash # Clone the repository (if needed) git clone <repository-url> cd grafana-k6-quick-start # Quick setup - downloads dependencies make dev-setup # Start the API server make run # In another terminal, test the API curl http://localhost:8080/health # Run a quick smoke test make k6-smoke ``` ### Option B: Full Setup with Monitoring ```bash # Complete development environment with Docker make dev-setup-full # Or step by step: make deps tidy # Setup Go dependencies make docker-build # Build Docker image make compose-up # Start monitoring stack # Access services: # API: http://localhost:8080 # Grafana: http://localhost:3000 (admin/admin) # Prometheus: http://localhost:9090 ``` ### Available Make Commands ```bash make help # Show all available commands make test # Run unit tests make build # Build Go application make docker-build # Build Docker image make k6-smoke # Run smoke test make k6-load # Run load test make monitoring-up # Start monitoring stack make dev-teardown # Clean up environment ``` ## Setting Up the Environment ### Prerequisites: - Docker and Docker Compose - Go 1.21+ - k6 ### Installation Commands: ```bash # Install k6 (macOS) brew install k6 # Install k6 (Linux) sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update sudo apt-get install k6 # Verify installation k6 version ``` ## Creating a Go Backend Service Let's create a simple REST API service in Go that we'll use as our testing target. ### Project Structure: ``` backend/ ├── main.go ├── handlers/ │ └── handlers.go ├── models/ │ └── models.go ├── go.mod └── Dockerfile ``` ### main.go ```go package main import ( "log" "net/http" "os" "time" "github.com/gorilla/mux" "your-project/handlers" ) func main() { r := mux.NewRouter() // Routes r.HandleFunc("/health", handlers.HealthCheck).Methods("GET") r.HandleFunc("/api/users", handlers.GetUsers).Methods("GET") r.HandleFunc("/api/users", handlers.CreateUser).Methods("POST") r.HandleFunc("/api/users/{id}", handlers.GetUser).Methods("GET") r.HandleFunc("/api/users/{id}", handlers.UpdateUser).Methods("PUT") r.HandleFunc("/api/users/{id}", handlers.DeleteUser).Methods("DELETE") // Middleware r.Use(handlers.LoggingMiddleware) r.Use(handlers.CORSMiddleware) port := os.Getenv("PORT") if port == "" { port = "8080" } srv := &http.Server{ Handler: r, Addr: ":" + port, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Printf("Server starting on port %s", port) log.Fatal(srv.ListenAndServe()) } ``` ### handlers/handlers.go ```go package handlers import ( "encoding/json" "fmt" "log" "net/http" "strconv" "time" "github.com/gorilla/mux" "your-project/models" ) var users = make(map[int]models.User) var userID = 1 func HealthCheck(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "status": "healthy", "timestamp": time.Now().Unix(), "service": "user-api", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func GetUsers(w http.ResponseWriter, r *http.Request) { // Simulate some processing time time.Sleep(50 * time.Millisecond) userList := make([]models.User, 0, len(users)) for _, user := range users { userList = append(userList, user) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(userList) } func CreateUser(w http.ResponseWriter, r *http.Request) { var user models.User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Simulate processing time time.Sleep(100 * time.Millisecond) user.ID = userID user.CreatedAt = time.Now() users[userID] = user userID++ w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) } func GetUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } user, exists := users[id] if !exists { http.Error(w, "User not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } func UpdateUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } existingUser, exists := users[id] if !exists { http.Error(w, "User not found", http.StatusNotFound) return } var updatedUser models.User if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Simulate processing time time.Sleep(75 * time.Millisecond) updatedUser.ID = id updatedUser.CreatedAt = existingUser.CreatedAt updatedUser.UpdatedAt = time.Now() users[id] = updatedUser w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updatedUser) } func DeleteUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } if _, exists := users[id]; !exists { http.Error(w, "User not found", http.StatusNotFound) return } delete(users, id) w.WriteHeader(http.StatusNoContent) } // Middleware func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) } func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } ``` ### models/models.go ```go package models import "time" type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` } ``` ### go.mod ```go module your-project go 1.21 require github.com/gorilla/mux v1.8.0 ``` ## Dockerizing the Application ### Dockerfile ```dockerfile # Build stage FROM golang:1.21-alpine AS builder WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Build the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # Final stage FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ # Copy the binary from builder stage COPY --from=builder /app/main . # Expose port EXPOSE 8080 # Command to run CMD ["./main"] ``` ### docker-compose.yml ```yaml version: '3.8' services: user-api: build: ./backend ports: - "8080:8080" environment: - PORT=8080 healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped ``` ## Writing k6 Load Tests ### Basic Load Test (load-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('error_rate'); const customTrend = new Trend('custom_duration'); const httpReqFailed = new Counter('http_req_failed'); // Test configuration export const options = { stages: [ { duration: '30s', target: 10 }, // Ramp up to 10 users { duration: '1m', target: 10 }, // Stay at 10 users { duration: '30s', target: 50 }, // Ramp up to 50 users { duration: '2m', target: 50 }, // Stay at 50 users { duration: '30s', target: 0 }, // Ramp down to 0 users ], thresholds: { http_req_duration: ['p(95)<500'], // 95% of requests must be below 500ms http_req_failed: ['rate<0.05'], // Error rate must be below 5% error_rate: ['rate<0.1'], // Custom error rate }, }; // Base URL - adjust based on your setup const BASE_URL = 'http://localhost:8080'; // const BASE_URL = 'http://user-api.local'; // For ingress // Test data const users = [ { name: 'John Doe', email: 'john@example.com', age: 30 }, { name: 'Jane Smith', email: 'jane@example.com', age: 25 }, { name: 'Bob Johnson', email: 'bob@example.com', age: 35 }, ]; export function setup() { // This runs once before all VUs start console.log('Starting load test setup...'); // Health check const healthResponse = http.get(`${BASE_URL}/health`); if (healthResponse.status !== 200) { throw new Error('Health check failed'); } return { message: 'Setup complete' }; } export default function(data) { // Main test function - runs for each VU iteration // Health check let response = http.get(`${BASE_URL}/health`); const healthCheckPassed = check(response, { 'health check status is 200': (r) => r.status === 200, 'health check has status field': (r) => JSON.parse(r.body).status === 'healthy', }); if (!healthCheckPassed) { errorRate.add(1); httpReqFailed.add(1); } // Get all users response = http.get(`${BASE_URL}/api/users`); check(response, { 'get users status is 200': (r) => r.status === 200, 'get users response time < 500ms': (r) => r.timings.duration < 500, }); // Create a new user const userData = users[Math.floor(Math.random() * users.length)]; userData.email = `${userData.name.replace(' ', '').toLowerCase()}-${Date.now()}@example.com`; const createUserParams = { headers: { 'Content-Type': 'application/json', }, }; response = http.post(`${BASE_URL}/api/users`, JSON.stringify(userData), createUserParams); const createUserPassed = check(response, { 'create user status is 201': (r) => r.status === 201, 'create user has id': (r) => JSON.parse(r.body).id > 0, 'create user response time < 1000ms': (r) => r.timings.duration < 1000, }); if (createUserPassed && response.status === 201) { const createdUser = JSON.parse(response.body); // Get the created user response = http.get(`${BASE_URL}/api/users/${createdUser.id}`); check(response, { 'get user by id status is 200': (r) => r.status === 200, 'get user by id returns correct user': (r) => JSON.parse(r.body).id === createdUser.id, }); // Update the user const updatedUserData = { ...userData, age: userData.age + 1, }; response = http.put(`${BASE_URL}/api/users/${createdUser.id}`, JSON.stringify(updatedUserData), createUserParams); check(response, { 'update user status is 200': (r) => r.status === 200, 'update user age changed': (r) => JSON.parse(r.body).age === updatedUserData.age, }); // Delete the user response = http.del(`${BASE_URL}/api/users/${createdUser.id}`); check(response, { 'delete user status is 204': (r) => r.status === 204, }); } else { errorRate.add(1); } // Record custom metrics customTrend.add(response.timings.duration); // Sleep between 1-3 seconds sleep(Math.random() * 2 + 1); } export function teardown(data) { // This runs once after all VUs have finished console.log('Load test teardown complete'); } ``` ### Spike Test (spike-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '10s', target: 10 }, // Below normal load { duration: '1m', target: 10 }, // Normal load { duration: '10s', target: 100 }, // Around the breaking point { duration: '3m', target: 100 }, // Beyond the breaking point { duration: '10s', target: 10 }, // Scale down. Recovery stage. { duration: '3m', target: 10 }, // Recovery stage. { duration: '10s', target: 0 }, // Scale down to 0 users. ], thresholds: { http_req_duration: ['p(99)<1500'], // 99% of requests must be below 1.5s http_req_failed: ['rate<0.1'], // Error rate must be below 10% }, }; const BASE_URL = 'http://localhost:8080'; export default function() { const response = http.get(`${BASE_URL}/health`); check(response, { 'spike test - status is 200': (r) => r.status === 200, 'spike test - response time OK': (r) => r.timings.duration < 2000, }); sleep(1); } ``` ### Stress Test (stress-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '2m', target: 100 }, // Ramp up to 100 users { duration: '5m', target: 100 }, // Stay at 100 users { duration: '2m', target: 200 }, // Ramp up to 200 users { duration: '5m', target: 200 }, // Stay at 200 users { duration: '2m', target: 300 }, // Ramp up to 300 users { duration: '5m', target: 300 }, // Stay at 300 users { duration: '2m', target: 400 }, // Ramp up to 400 users { duration: '5m', target: 400 }, // Stay at 400 users { duration: '10m', target: 0 }, // Ramp down to 0 users ], thresholds: { http_req_duration: ['p(99)<2000'], // 99% of requests must be below 2s http_req_failed: ['rate<0.1'], // Error rate must be below 10% }, }; const BASE_URL = 'http://localhost:8080'; export default function() { const responses = http.batch([ ['GET', `${BASE_URL}/health`], ['GET', `${BASE_URL}/api/users`], ]); check(responses[0], { 'stress test - health check status is 200': (r) => r.status === 200, }); check(responses[1], { 'stress test - users endpoint status is 200': (r) => r.status === 200, }); sleep(1); } ``` ### Smoke Test (smoke-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { vus: 1, // 1 user looping for 1 minute duration: '1m', thresholds: { http_req_duration: ['p(99)<1500'], // 99% of requests must be below 1.5s http_req_failed: ['rate<0.01'], // Error rate must be below 1% }, }; const BASE_URL = 'http://localhost:8080'; export default function() { const response = http.get(`${BASE_URL}/health`); check(response, { 'smoke test - status is 200': (r) => r.status === 200, 'smoke test - service is healthy': (r) => JSON.parse(r.body).status === 'healthy', }); sleep(1); } ``` ## Running k6 Tests ### Test Execution Commands: ```bash # Run smoke test (quick validation) k6 run smoke-test.js # Run basic load test k6 run load-test.js # Run stress test k6 run stress-test.js # Run spike test k6 run spike-test.js # Run test with custom VUs and duration k6 run --vus 50 --duration 30s load-test.js # Run test and output results to file k6 run --out json=results.json load-test.js # Run test with custom thresholds k6 run --threshold http_req_duration=p(95)<200 load-test.js ``` ### k6 in Docker: ```bash # Run k6 tests in Docker docker run --rm -i grafana/k6:latest run - <load-test.js # Run with volume mount for test files docker run --rm -v "$PWD:/tests" grafana/k6:latest run /tests/load-test.js # Run with network access to local services docker run --rm --network=host -v "$PWD:/tests" grafana/k6:latest run /tests/load-test.js ``` ### k6 in Kubernetes: #### k6-job.yaml ```yaml apiVersion: batch/v1 kind: Job metadata: name: k6-load-test spec: template: spec: containers: - name: k6 image: grafana/k6:latest command: ["k6", "run", "--vus", "50", "--duration", "5m", "/scripts/load-test.js"] volumeMounts: - name: k6-scripts mountPath: /scripts env: - name: K6_OUT value: "json=/tmp/results.json" volumes: - name: k6-scripts configMap: name: k6-scripts restartPolicy: Never backoffLimit: 1 ``` #### Create ConfigMap for test scripts: ```bash kubectl create configmap k6-scripts --from-file=load-test.js kubectl apply -f k6-job.yaml # Check job status kubectl get jobs kubectl logs job/k6-load-test ``` ## Monitoring and Analysis ### Setting up Grafana and Prometheus with Docker Compose: The monitoring stack is already configured in the `docker-compose.yml` file with Grafana and Prometheus services. ### Start the monitoring stack: ```bash # Start all services including monitoring docker compose up -d # Access the services: # Grafana: http://localhost:3000 (admin/admin) # Prometheus: http://localhost:9090 ``` ### Run k6 with Prometheus output: ```bash # Run k6 with Prometheus remote write output (when using docker-compose) k6 run --out experimental-prometheus-rw=http://localhost:9090/api/v1/write load-test.js # Alternative: Run k6 in Docker with Prometheus output docker compose exec k6 k6 run --out experimental-prometheus-rw=http://prometheus:9090/api/v1/write /tests/load-test.js ``` ### k6 Grafana Dashboard Configuration: 1. **Access Grafana**: http://localhost:3000 (admin/admin) 2. **Add Prometheus Data Source**: - URL: http://localhost:9090 - Access: Server (default) - HTTP Method: GET 3. **Import k6 Dashboard**: - Go to + → Import - Use dashboard ID: 19665 (k6 Prometheus dashboard) - Or create custom dashboard with these queries: #### Key Grafana Queries (PromQL): ```promql # HTTP Request Duration (95th percentile) histogram_quantile(0.95, rate(k6_http_req_duration_bucket[5m])) # HTTP Request Rate rate(k6_http_reqs_total[5m]) # Error Rate (percentage) rate(k6_http_req_failed_total[5m]) / rate(k6_http_reqs_total[5m]) * 100 # Virtual Users k6_vus # Data Received Rate rate(k6_data_received_total[5m]) # Data Sent Rate rate(k6_data_sent_total[5m]) # HTTP Request Duration Average rate(k6_http_req_duration_sum[5m]) / rate(k6_http_req_duration_count[5m]) # Checks Pass Rate rate(k6_checks_passed_total[5m]) / (rate(k6_checks_passed_total[5m]) + rate(k6_checks_failed_total[5m])) * 100 ``` ## Advanced k6 Testing Patterns ### Data-Driven Testing: #### users-data.json ```json [ {"name": "Alice Johnson", "email": "alice@example.com", "age": 28}, {"name": "Bob Smith", "email": "bob@example.com", "age": 34}, {"name": "Charlie Brown", "email": "charlie@example.com", "age": 22}, {"name": "Diana Prince", "email": "diana@example.com", "age": 31}, {"name": "Eve Davis", "email": "eve@example.com", "age": 26} ] ``` #### data-driven-test.js ```javascript import http from 'k6/http'; import { check } from 'k6'; const userData = JSON.parse(open('./users-data.json')); export const options = { vus: 10, duration: '2m', }; const BASE_URL = 'http://localhost:8080'; export default function() { // Pick a random user from the dataset const user = userData[Math.floor(Math.random() * userData.length)]; // Modify email to make it unique user.email = `${user.name.replace(' ', '').toLowerCase()}-${__ITER}@example.com`; const response = http.post(`${BASE_URL}/api/users`, JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }); check(response, { 'user created successfully': (r) => r.status === 201, 'user has correct name': (r) => JSON.parse(r.body).name === user.name, }); } ``` ### Session-Based Testing: #### session-test.js ```javascript import http from 'k6/http'; import { check, group, sleep } from 'k6'; export const options = { vus: 20, duration: '5m', }; const BASE_URL = 'http://localhost:8080'; export default function() { // Simulate user session group('User Registration Flow', () => { const userData = { name: `User ${__VU}-${__ITER}`, email: `user-${__VU}-${__ITER}@example.com`, age: Math.floor(Math.random() * 50) + 18, }; // Create user let response = http.post(`${BASE_URL}/api/users`, JSON.stringify(userData), { headers: { 'Content-Type': 'application/json' }, }); const userId = check(response, { 'user created': (r) => r.status === 201, }) ? JSON.parse(response.body).id : null; if (userId) { sleep(1); // Get user profile group('Profile Operations', () => { response = http.get(`${BASE_URL}/api/users/${userId}`); check(response, { 'profile retrieved': (r) => r.status === 200, }); sleep(2); // Update user profile userData.age += 1; response = http.put(`${BASE_URL}/api/users/${userId}`, JSON.stringify(userData), { headers: { 'Content-Type': 'application/json' }, }); check(response, { 'profile updated': (r) => r.status === 200, }); }); sleep(1); // Clean up - delete user response = http.del(`${BASE_URL}/api/users/${userId}`); check(response, { 'user deleted': (r) => r.status === 204, }); } }); sleep(Math.random() * 5 + 1); } ``` ### Performance Testing Best Practices: #### comprehensive-test.js ```javascript import http from 'k6/http'; import { check, group, sleep, fail } from 'k6'; import { Rate, Trend, Counter } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('errors'); const responseTimeTrend = new Trend('response_time'); const customCounter = new Counter('custom_counter'); export const options = { stages: [ { duration: '1m', target: 20 }, { duration: '3m', target: 20 }, { duration: '1m', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.05'], errors: ['rate<0.1'], response_time: ['p(95)<600'], }, ext: { loadimpact: { name: 'Comprehensive API Test', projectID: 123456, }, }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; export function setup() { // Setup phase - run once console.log(`Testing API at: ${BASE_URL}`); const response = http.get(`${BASE_URL}/health`); if (response.status !== 200) { fail('API is not healthy'); } return { baseUrl: BASE_URL, testData: generateTestData(100), }; } export default function(data) { const testUser = data.testData[Math.floor(Math.random() * data.testData.length)]; group('API Health Check', () => { const response = http.get(`${data.baseUrl}/health`); const success = check(response, { 'health check passes': (r) => r.status === 200, 'has healthy status': (r) => JSON.parse(r.body).status === 'healthy', }); if (!success) { errorRate.add(1); } responseTimeTrend.add(response.timings.duration); }); group('User CRUD Operations', () => { // Create let response = http.post(`${data.baseUrl}/api/users`, JSON.stringify(testUser), { headers: { 'Content-Type': 'application/json' }, }); const createSuccess = check(response, { 'user created successfully': (r) => r.status === 201, 'response has user id': (r) => JSON.parse(r.body).id > 0, }); if (!createSuccess) { errorRate.add(1); return; // Skip remaining operations if creation fails } const userId = JSON.parse(response.body).id; customCounter.add(1); sleep(0.5); // Read response = http.get(`${data.baseUrl}/api/users/${userId}`); check(response, { 'user retrieved successfully': (r) => r.status === 200, 'correct user returned': (r) => JSON.parse(r.body).id === userId, }) || errorRate.add(1); sleep(0.5); // Update testUser.age += 1; response = http.put(`${data.baseUrl}/api/users/${userId}`, JSON.stringify(testUser), { headers: { 'Content-Type': 'application/json' }, }); check(response, { 'user updated successfully': (r) => r.status === 200, 'age was updated': (r) => JSON.parse(r.body).age === testUser.age, }) || errorRate.add(1); sleep(0.5); // Delete response = http.del(`${data.baseUrl}/api/users/${userId}`); check(response, { 'user deleted successfully': (r) => r.status === 204, }) || errorRate.add(1); }); sleep(Math.random() * 2 + 1); } export function teardown(data) { console.log('Test completed'); console.log(`Base URL used: ${data.baseUrl}`); } function generateTestData(count) { const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry']; const domains = ['example.com', 'test.com', 'demo.com']; const users = []; for (let i = 0; i < count; i++) { users.push({ name: `${names[i % names.length]} ${i}`, email: `user${i}@${domains[i % domains.length]}`, age: Math.floor(Math.random() * 50) + 18, }); } return users; } ``` ## Continuous Integration Integration ### GitHub Actions Workflow: #### .github/workflows/load-test.yml ```yaml name: Load Testing with k6 on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 2 * * *' # Run daily at 2 AM jobs: load-test: runs-on: ubuntu-latest services: api: image: user-api:latest ports: - 8080:8080 env: PORT: 8080 options: >- --health-cmd "wget --quiet --tries=1 --spider http://localhost:8080/health" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Build Docker image run: | cd backend docker build -t user-api:latest . - name: Install k6 run: | sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update sudo apt-get install k6 - name: Wait for API to be ready run: | timeout 300 bash -c 'until curl -f http://localhost:8080/health; do sleep 5; done' - name: Run smoke test run: k6 run tests/smoke-test.js - name: Run load test run: k6 run tests/load-test.js --out json=results.json - name: Upload test results uses: actions/upload-artifact@v3 if: always() with: name: k6-test-results path: results.json - name: Comment PR with results if: github.event_name == 'pull_request' uses: actions/github-script@v6 with: script: | const fs = require('fs'); try { const results = fs.readFileSync('results.json', 'utf8'); const metrics = JSON.parse(results); // Process and comment results... } catch (error) { console.log('No results file found'); } ``` ## CI/CD Pipeline The project includes a comprehensive GitHub Actions pipeline that automates testing, building, and deployment. Here's what the pipeline includes: ### Pipeline Overview ```yaml # .github/workflows/ci-cd.yml name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: # Unit tests with coverage build: # Docker image build and push load-test: # Automated k6 testing helm-test: # Helm chart validation security-scan: # Security vulnerability scanning deploy-staging: # Deploy to staging environment deploy-production: # Deploy to production post-deploy-tests: # Production smoke tests ``` ### Pipeline Features - **Unit Testing**: Go test suite with coverage reporting - **Security Scanning**: Gosec (static analysis) + Trivy (image scanning) - **Load Testing**: Automated k6 tests against built containers - **Helm Validation**: Chart linting and templating verification - **Multi-stage Deployment**: Staging → Production with approvals - **Post-deployment Testing**: Smoke tests in production ### Using the Pipeline The pipeline automatically triggers on: - **Pull Requests**: Runs tests and validation only - **Develop Branch**: Deploys to staging environment - **Main Branch**: Deploys to production environment ## Troubleshooting ### Common Issues and Solutions #### Docker Build Problems ```bash # Test Docker connectivity make docker-test # Clean Docker cache make docker-clean # Test network connectivity make network-test # If Docker issues persist, use local development make dev-setup make run ``` #### API Not Starting ```bash # Check if port is in use lsof -i :8080 # Check API logs make compose-logs # Test API directly go run backend/main.go ``` #### k6 Tests Failing ```bash # Verify API is healthy curl http://localhost:8080/health # Test with verbose output k6 run --http-debug tests/smoke-test.js # Check if base URL is correct export BASE_URL=http://localhost:8080 k6 run tests/smoke-test.js ``` #### Kubernetes Deployment Issues ```bash # Check pod status kubectl get pods kubectl describe pod <pod-name> # Check service connectivity kubectl get svc kubectl port-forward service/user-api 8080:80 # Check Helm deployment helm status user-api helm list ``` #### Monitoring Stack Issues ```bash # Check Prometheus connection kubectl port-forward -n monitoring service/prometheus 9090:9090 curl http://localhost:9090/-/healthy # Verify Grafana datasource # Login to Grafana → Configuration → Data Sources # Check k6 output format k6 run --out experimental-prometheus-rw=http://localhost:9090/api/v1/write tests/smoke-test.js ``` ### Make Commands for Troubleshooting ```bash make help # Show all available commands make docker-test # Test Docker connectivity make network-test # Test network connectivity make test-coverage # Run tests with coverage report make k8s-status # Show Kubernetes resource status make monitoring-up # Restart monitoring stack ``` ## Conclusion and Next Steps This comprehensive tutorial has covered: 1. **k6 Fundamentals**: Understanding how k6 works and its architecture 2. **Environment Setup**: Local development with Docker 3. **Backend Service**: Go-based REST API with proper endpoints 4. **Container Orchestration**: Docker containerization 5. **Load Testing Scenarios**: Various test types (smoke, load, stress, spike) 6. **Monitoring Integration**: Grafana and Prometheus for metrics visualization 7. **Advanced Patterns**: Data-driven and session-based testing 8. **CI/CD Integration**: Automated testing in GitHub Actions ### Next Steps: 1. **Scale Testing**: Experiment with distributed k6 testing across multiple nodes 2. **Advanced Monitoring**: Integrate with Prometheus, Jaeger for distributed tracing 3. **Security Testing**: Add authentication and authorization testing scenarios 4. **Database Integration**: Include database load testing scenarios 5. **Cloud Deployment**: Deploy to cloud providers (AWS EKS, GCP GKE, Azure AKS) 6. **Performance Tuning**: Use k6 results to optimize application performance 7. **Custom Extensions**: Develop k6 extensions for specific testing needs ### Key Takeaways: - **Start Small**: Begin with smoke tests before running intensive load tests - **Monitor Everything**: Always collect metrics and monitor system behavior - **Test Early**: Integrate performance testing into your development workflow - **Realistic Scenarios**: Design tests that mirror real user behavior - **Infrastructure Matters**: Ensure your test environment resembles production - **Continuous Improvement**: Use test results to drive performance optimizations This setup provides a solid foundation for comprehensive load testing with k6, enabling you to ensure your applications can handle production traffic effectively.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully