Command Palette

Search for a command to run...

0
Blog
Previous

Implementing Grafana k6 Load Testing in GRIT Framework

A comprehensive guide to setting up and running performance tests with Grafana k6 in a social media management platform

Implementing Grafana k6 Load Testing in GRIT Framework

k6 Load Testing Dashboard

Introduction

Performance testing is crucial for any production application, especially for platforms handling concurrent users and real-time operations. In this guide, I'll walk you through implementing Grafana k6 load testing in the GRIT social media management framework - a full-stack application built with Go, Next.js, PostgreSQL, and Redis.

What is Grafana k6?

k6 is an open-source load testing tool built for developers. It allows you to write test scripts in JavaScript and provides detailed performance metrics. Unlike traditional load testing tools, k6 is:

  • Developer-friendly: Write tests in JavaScript
  • CLI-first: Perfect for CI/CD integration
  • Performant: Written in Go, handles thousands of VUs (Virtual Users)
  • Cloud-ready: Integrates with Grafana Cloud for advanced analytics

Prerequisites

Before we begin, ensure you have:

  • Docker and Docker Compose installed
  • Node.js and pnpm (for the frontend)
  • Go 1.21+ (for the backend)
  • Basic understanding of HTTP APIs and JavaScript

Project Architecture Overview

Our GRIT framework consists of:

social-media-manager/
├── apps/
│   ├── api/          # Go backend (Fiber framework)
│   ├── admin/        # Next.js admin dashboard
│   └── web/          # Next.js public website
├── k6/               # Load testing scripts
├── docker-compose.yml
└── .env

Step 1: Install Grafana k6

macOS (Homebrew)

brew install k6

Linux (Debian/Ubuntu)

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

Windows (Chocolatey)

choco install k6

Docker (Alternative)

docker pull grafana/k6:latest

Verify installation:

k6 version

Step 2: Project Setup

1. Create k6 Directory Structure

mkdir -p k6
cd k6

2. Create Base Configuration

Create k6/config.js:

export const BASE_URL = __ENV.BASE_URL || "http://localhost:8080"
export const FRONTEND_URL = __ENV.FRONTEND_URL || "http://localhost:3000"
 
export const defaultOptions = {
  stages: [
    { duration: "30s", target: 10 }, // Ramp up to 10 users
    { duration: "1m", target: 10 }, // Stay at 10 users
    { duration: "30s", target: 0 }, // Ramp down to 0 users
  ],
  thresholds: {
    http_req_duration: ["p(95)<500"], // 95% of requests under 500ms
    http_req_failed: ["rate<0.01"], // Less than 1% errors
  },
}

Step 3: Create Authentication Test

Create k6/auth-test.js:

import http from "k6/http"
import { check, sleep } from "k6"
import { Rate } from "k6/metrics"
import { BASE_URL, defaultOptions } from "./config.js"
 
export const options = defaultOptions
 
const errorRate = new Rate("errors")
 
export function setup() {
  // Create test user
  const signupPayload = JSON.stringify({
    email: `test-${Date.now()}@example.com`,
    password: "Test123!@#",
    name: "Test User",
  })
 
  const signupRes = http.post(`${BASE_URL}/api/auth/signup`, signupPayload, {
    headers: { "Content-Type": "application/json" },
  })
 
  check(signupRes, {
    "signup successful": (r) => r.status === 201,
  })
 
  return {
    email: JSON.parse(signupPayload).email,
    password: "Test123!@#",
  }
}
 
export default function (data) {
  // Login
  const loginPayload = JSON.stringify({
    email: data.email,
    password: data.password,
  })
 
  const loginRes = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
    headers: { "Content-Type": "application/json" },
  })
 
  const loginSuccess = check(loginRes, {
    "login status is 200": (r) => r.status === 200,
    "has access token": (r) => r.json("access_token") !== undefined,
  })
 
  errorRate.add(!loginSuccess)
 
  if (loginSuccess) {
    const token = loginRes.json("access_token")
 
    // Get user profile
    const profileRes = http.get(`${BASE_URL}/api/auth/me`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    })
 
    check(profileRes, {
      "profile status is 200": (r) => r.status === 200,
      "has user data": (r) => r.json("email") !== undefined,
    })
  }
 
  sleep(1)
}
 
export function teardown(data) {
  console.log("Test completed")
}

Step 4: Create API Load Test

Create k6/api-test.js:

import http from "k6/http"
import { check, group, sleep } from "k6"
import { Counter, Trend } from "k6/metrics"
import { BASE_URL } from "./config.js"
 
export const options = {
  stages: [
    { duration: "1m", target: 50 }, // Ramp up to 50 users
    { duration: "3m", target: 50 }, // Stay at 50 users
    { duration: "1m", target: 100 }, // Spike to 100 users
    { duration: "2m", target: 100 }, // Stay at 100 users
    { duration: "1m", target: 0 }, // Ramp down
  ],
  thresholds: {
    http_req_duration: ["p(95)<1000", "p(99)<2000"],
    http_req_failed: ["rate<0.05"],
    "group_duration{group:::API Health}": ["avg<200"],
    "group_duration{group:::Posts API}": ["avg<500"],
  },
}
 
const apiErrors = new Counter("api_errors")
const apiDuration = new Trend("api_duration")
 
let authToken
 
export function setup() {
  // Authenticate once for all VUs
  const loginPayload = JSON.stringify({
    email: "admin@example.com",
    password: "admin123",
  })
 
  const loginRes = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
    headers: { "Content-Type": "application/json" },
  })
 
  if (loginRes.status === 200) {
    return { token: loginRes.json("access_token") }
  }
 
  throw new Error("Authentication failed in setup")
}
 
export default function (data) {
  const headers = {
    Authorization: `Bearer ${data.token}`,
    "Content-Type": "application/json",
  }
 
  group("API Health", () => {
    const healthRes = http.get(`${BASE_URL}/health`)
    check(healthRes, {
      "health check is 200": (r) => r.status === 200,
    })
  })
 
  group("Posts API", () => {
    // List posts
    const listRes = http.get(`${BASE_URL}/api/posts`, { headers })
    const listSuccess = check(listRes, {
      "list posts is 200": (r) => r.status === 200,
      "has posts array": (r) => Array.isArray(r.json("data")),
    })
 
    if (!listSuccess) apiErrors.add(1)
    apiDuration.add(listRes.timings.duration)
 
    sleep(1)
 
    // Create post
    const createPayload = JSON.stringify({
      content: `Test post ${Date.now()}`,
      platforms: ["twitter", "linkedin"],
      scheduled_at: new Date(Date.now() + 3600000).toISOString(),
    })
 
    const createRes = http.post(`${BASE_URL}/api/posts`, createPayload, {
      headers,
    })
    check(createRes, {
      "create post is 201": (r) => r.status === 201,
      "has post id": (r) => r.json("id") !== undefined,
    })
  })
 
  group("Media API", () => {
    const mediaRes = http.get(`${BASE_URL}/api/media`, { headers })
    check(mediaRes, {
      "list media is 200": (r) => r.status === 200,
    })
  })
 
  group("Dashboard API", () => {
    const dashboardRes = http.get(`${BASE_URL}/api/dashboard/stats`, {
      headers,
    })
    check(dashboardRes, {
      "dashboard stats is 200": (r) => r.status === 200,
    })
  })
 
  sleep(2)
}

Step 5: Create Stress Test

Create k6/stress-test.js:

import http from "k6/http"
import { check, sleep } from "k6"
import { BASE_URL } from "./config.js"
 
export const options = {
  stages: [
    { duration: "2m", target: 100 }, // Ramp up to 100 users
    { duration: "5m", target: 100 }, // Stay at 100
    { duration: "2m", target: 200 }, // Spike to 200
    { duration: "5m", target: 200 }, // Stay at 200
    { duration: "2m", target: 300 }, // Push to 300
    { duration: "5m", target: 300 }, // Stay at 300
    { duration: "5m", target: 0 }, // Ramp down
  ],
  thresholds: {
    http_req_duration: ["p(99)<3000"],
    http_req_failed: ["rate<0.1"],
  },
}
 
export function setup() {
  const loginRes = http.post(
    `${BASE_URL}/api/auth/login`,
    JSON.stringify({
      email: "admin@example.com",
      password: "admin123",
    }),
    { headers: { "Content-Type": "application/json" } }
  )
 
  return { token: loginRes.json("access_token") }
}
 
export default function (data) {
  const headers = {
    Authorization: `Bearer ${data.token}`,
  }
 
  // Simulate realistic user behavior
  const endpoints = [
    "/api/posts",
    "/api/media",
    "/api/dashboard/stats",
    "/api/social-accounts",
    "/api/analytics",
  ]
 
  const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)]
  const res = http.get(`${BASE_URL}${endpoint}`, { headers })
 
  check(res, {
    "status is 200": (r) => r.status === 200,
    "response time OK": (r) => r.timings.duration < 3000,
  })
 
  sleep(Math.random() * 3 + 1) // Random sleep 1-4 seconds
}

Step 6: Create 200 Users Test

Create k6/200-users-test.js:

import http from "k6/http"
import { check, group, sleep } from "k6"
import { SharedArray } from "k6/data"
import { BASE_URL } from "./config.js"
 
// Generate test users
const users = new SharedArray("users", function () {
  const userList = []
  for (let i = 0; i < 200; i++) {
    userList.push({
      email: `user${i}@test.com`,
      password: "Test123!@#",
      name: `Test User ${i}`,
    })
  }
  return userList
})
 
export const options = {
  scenarios: {
    concurrent_users: {
      executor: "per-vu-iterations",
      vus: 200,
      iterations: 1,
      maxDuration: "10m",
    },
  },
  thresholds: {
    http_req_duration: ["p(95)<2000"],
    http_req_failed: ["rate<0.05"],
  },
}
 
export default function () {
  const user = users[__VU - 1] // Each VU gets a unique user
 
  group("User Registration and Activity", () => {
    // Register
    const signupRes = http.post(
      `${BASE_URL}/api/auth/signup`,
      JSON.stringify(user),
      { headers: { "Content-Type": "application/json" } }
    )
 
    const signupSuccess = check(signupRes, {
      "signup successful": (r) => r.status === 201,
    })
 
    if (!signupSuccess) {
      console.error(`Signup failed for ${user.email}`)
      return
    }
 
    sleep(1)
 
    // Login
    const loginRes = http.post(
      `${BASE_URL}/api/auth/login`,
      JSON.stringify({
        email: user.email,
        password: user.password,
      }),
      { headers: { "Content-Type": "application/json" } }
    )
 
    const loginSuccess = check(loginRes, {
      "login successful": (r) => r.status === 200,
    })
 
    if (!loginSuccess) {
      console.error(`Login failed for ${user.email}`)
      return
    }
 
    const token = loginRes.json("access_token")
    const headers = {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    }
 
    sleep(2)
 
    // Perform various actions
    http.get(`${BASE_URL}/api/posts`, { headers })
    sleep(1)
 
    http.get(`${BASE_URL}/api/dashboard/stats`, { headers })
    sleep(1)
 
    http.get(`${BASE_URL}/api/social-accounts`, { headers })
    sleep(1)
 
    // Create a post
    http.post(
      `${BASE_URL}/api/posts`,
      JSON.stringify({
        content: `Post from ${user.name}`,
        platforms: ["twitter"],
      }),
      { headers }
    )
 
    sleep(2)
  })
}

Step 7: Create Test Runner Script

Create scripts/run-k6-tests.sh:

#!/bin/bash
 
set -e
 
echo "🚀 Starting k6 Load Tests for GRIT Framework"
echo "=============================================="
 
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
 
# Check if k6 is installed
if ! command -v k6 &> /dev/null; then
    echo -e "${RED}❌ k6 is not installed${NC}"
    echo "Install it from: https://k6.io/docs/getting-started/installation/"
    exit 1
fi
 
# Check if services are running
echo -e "${YELLOW}📋 Checking if services are running...${NC}"
if ! curl -s http://localhost:8080/health > /dev/null; then
    echo -e "${RED}❌ API is not running on port 8080${NC}"
    echo "Start it with: docker-compose up -d"
    exit 1
fi
 
echo -e "${GREEN}✅ Services are running${NC}"
echo ""
 
# Function to run a test
run_test() {
    local test_name=$1
    local test_file=$2
 
    echo -e "${YELLOW}🧪 Running ${test_name}...${NC}"
    k6 run --out json=k6/results/${test_name}-$(date +%Y%m%d-%H%M%S).json ${test_file}
 
    if [ $? -eq 0 ]; then
        echo -e "${GREEN}✅ ${test_name} completed successfully${NC}"
    else
        echo -e "${RED}❌ ${test_name} failed${NC}"
    fi
    echo ""
}
 
# Create results directory
mkdir -p k6/results
 
# Run tests based on argument
case "${1:-all}" in
    auth)
        run_test "Authentication Test" "k6/auth-test.js"
        ;;
    api)
        run_test "API Load Test" "k6/api-test.js"
        ;;
    stress)
        run_test "Stress Test" "k6/stress-test.js"
        ;;
    200users)
        run_test "200 Users Test" "k6/200-users-test.js"
        ;;
    all)
        run_test "Authentication Test" "k6/auth-test.js"
        sleep 5
        run_test "API Load Test" "k6/api-test.js"
        sleep 5
        run_test "Stress Test" "k6/stress-test.js"
        ;;
    *)
        echo "Usage: $0 {auth|api|stress|200users|all}"
        exit 1
        ;;
esac
 
echo -e "${GREEN}🎉 All tests completed!${NC}"
echo "Results saved in k6/results/"

Make it executable:

chmod +x scripts/run-k6-tests.sh

Step 8: Running the Tests

Start Your Services

# Start all services
docker-compose up -d
 
# Verify services are running
docker-compose ps

Run Individual Tests

# Authentication test
./scripts/run-k6-tests.sh auth
 
# API load test
./scripts/run-k6-tests.sh api
 
# Stress test
./scripts/run-k6-tests.sh stress
 
# 200 concurrent users test
./scripts/run-k6-tests.sh 200users

Run All Tests

./scripts/run-k6-tests.sh all

Run with Custom Options

# Override base URL
k6 run -e BASE_URL=https://api.production.com k6/api-test.js
 
# Run with more VUs
k6 run --vus 500 --duration 5m k6/stress-test.js
 
# Output to InfluxDB
k6 run --out influxdb=http://localhost:8086/k6 k6/api-test.js

Step 9: Integrate with Grafana Cloud

1. Sign Up for Grafana Cloud

Visit grafana.com/products/cloud and create a free account.

2. Get Your k6 Cloud Token

# Login to k6 cloud
k6 login cloud
 
# Or set token directly
export K6_CLOUD_TOKEN=your_token_here

3. Run Tests with Cloud Output

k6 run --out cloud k6/api-test.js

4. View Results in Grafana

Your test results will be available at: https://app.k6.io/runs/YOUR_RUN_ID

Step 10: CI/CD Integration

GitHub Actions Example

Create .github/workflows/k6-tests.yml:

name: k6 Load Tests
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 2 * * *" # Run daily at 2 AM
 
jobs:
  k6-tests:
    runs-on: ubuntu-latest
 
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: grit
          POSTGRES_PASSWORD: grit
          POSTGRES_DB: social-media-manager
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
 
      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
 
    steps:
      - uses: actions/checkout@v3
 
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: "1.21"
 
      - 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: Start API Server
        run: |
          cd apps/api
          go run cmd/server/main.go &
          sleep 10
        env:
          DATABASE_URL: postgres://grit:grit@localhost:5432/social-media-manager?sslmode=disable
          REDIS_URL: redis://localhost:6379
 
      - name: Run k6 Tests
        run: |
          k6 run --out json=results.json k6/api-test.js
        env:
          BASE_URL: http://localhost:8080
 
      - name: Upload Results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: k6-results
          path: results.json

Understanding k6 Metrics

Key Metrics

  • http_req_duration: Total time for the request (sending, waiting, receiving)
  • http_req_blocked: Time spent blocked before initiating request
  • http_req_connecting: Time spent establishing TCP connection
  • http_req_sending: Time spent sending data
  • http_req_waiting: Time spent waiting for response (TTFB)
  • http_req_receiving: Time spent receiving response data
  • http_req_failed: Rate of failed requests
  • iterations: Number of times VU executed the default function
  • vus: Current number of active virtual users

Thresholds

Thresholds define pass/fail criteria:

thresholds: {
  // 95% of requests must complete within 500ms
  'http_req_duration': ['p(95)<500'],
 
  // 99% of requests must complete within 2s
  'http_req_duration': ['p(99)<2000'],
 
  // Error rate must be below 1%
  'http_req_failed': ['rate<0.01'],
 
  // Average request duration must be below 300ms
  'http_req_duration': ['avg<300'],
}

Best Practices

1. Start Small

Begin with low VU counts and gradually increase to find your breaking point.

2. Use Realistic Scenarios

Model actual user behavior with appropriate think times (sleep).

3. Monitor System Resources

Watch CPU, memory, and database connections during tests.

4. Test in Isolation

Run tests in a dedicated environment to avoid interference.

Compare results over time to catch performance regressions.

6. Set Meaningful Thresholds

Base thresholds on your SLAs and user expectations.

Troubleshooting

High Error Rates

# Check API logs
docker-compose logs api
 
# Check database connections
docker-compose exec postgres psql -U grit -d social-media-manager -c "SELECT count(*) FROM pg_stat_activity;"

Slow Response Times

# Check Redis performance
docker-compose exec redis redis-cli INFO stats
 
# Monitor database queries
docker-compose logs postgres | grep "duration:"

Connection Timeouts

Increase timeouts in k6:

export const options = {
  httpDebug: "full",
  timeout: "60s",
}

Conclusion

You now have a complete k6 load testing setup for your GRIT framework. This implementation allows you to:

  • Test authentication flows
  • Validate API performance under load
  • Identify bottlenecks before production
  • Integrate performance testing into CI/CD
  • Monitor trends with Grafana Cloud

Regular load testing ensures your application can handle real-world traffic and provides confidence when scaling.

Resources


Found this helpful? Check out more articles on my portfolio at yourportfolio.com