├── graphing ├── data │ └── influxdb │ │ └── .keep ├── Dockerfile ├── requirements.txt ├── docker-compose.yml ├── locustfile.py ├── reporter.py └── dashboard.json ├── rabbitmq ├── src │ ├── __init__.py │ ├── locustfile.py │ └── rabbitmq.py ├── Dockerfile ├── requirements.txt ├── docker-compose.yml └── readme.md ├── distributed ├── Dockerfile ├── requirements.txt ├── locustfile.py ├── docker-compose.yml └── readme.md ├── quickstart ├── Dockerfile ├── requirements.txt ├── docker-compose.yml └── locustfile.py ├── event-trapping ├── Dockerfile ├── requirements.txt ├── docker-compose.yml └── locustfile.py ├── .gitignore └── readme.md /graphing/data/influxdb/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rabbitmq/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /distributed/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11-onbuild 2 | -------------------------------------------------------------------------------- /graphing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11-onbuild 2 | -------------------------------------------------------------------------------- /quickstart/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11-onbuild 2 | -------------------------------------------------------------------------------- /rabbitmq/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11-onbuild 2 | -------------------------------------------------------------------------------- /event-trapping/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.11-onbuild 2 | -------------------------------------------------------------------------------- /rabbitmq/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio==0.7.5 2 | pika==0.10.0 3 | -------------------------------------------------------------------------------- /distributed/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio==0.7.5 2 | pika==0.10.0 3 | -------------------------------------------------------------------------------- /event-trapping/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio==0.7.5 2 | pika==0.10.0 3 | -------------------------------------------------------------------------------- /quickstart/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio==0.7.5 2 | pika==0.10.0 3 | -------------------------------------------------------------------------------- /graphing/requirements.txt: -------------------------------------------------------------------------------- 1 | locustio==0.7.5 2 | influxdb==3.0.0 3 | pika==0.10.0 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | data/influxdb/wal 3 | data/influxdb/meta 4 | data/influxdb/data 5 | -------------------------------------------------------------------------------- /quickstart/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | grafana: 4 | image: grafana/grafana:3.1.1 5 | ports: 6 | - "3000:3000" 7 | locust: 8 | build: . 9 | ports: 10 | - "8089:8089" 11 | volumes: 12 | - ".:/srv/app" 13 | command: "locust -f /srv/app/locustfile.py --host http://grafana:3000" 14 | 15 | -------------------------------------------------------------------------------- /event-trapping/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | grafana: 4 | image: grafana/grafana:3.1.1 5 | ports: 6 | - "3000:3000" 7 | locust: 8 | build: . 9 | ports: 10 | - "8089:8089" 11 | volumes: 12 | - ".:/srv/app" 13 | command: "locust -f /srv/app/locustfile.py --host http://grafana:3000" 14 | 15 | -------------------------------------------------------------------------------- /rabbitmq/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | rabbitmq: 4 | image: rabbitmq:3.6-management 5 | ports: 6 | - "5672:5672" 7 | - "15672:15672" 8 | locust: 9 | build: . 10 | ports: 11 | - "8089:8089" 12 | volumes: 13 | - "./src:/srv/app" 14 | command: "locust -f /srv/app/locustfile.py" 15 | environment: 16 | RABBITMQ_CONNECTION: amqp://guest:guest@rabbitmq:5672 17 | -------------------------------------------------------------------------------- /rabbitmq/src/locustfile.py: -------------------------------------------------------------------------------- 1 | from rabbitmq import get_client 2 | 3 | from locust import Locust, TaskSet, task, events 4 | 5 | class RabbitTaskSet(TaskSet): 6 | @task 7 | def publish(self): 8 | get_client().publish() 9 | 10 | class MyLocust(Locust): 11 | task_set = RabbitTaskSet 12 | min_wait = 1000 13 | max_wait = 1000 14 | 15 | def on_locust_stop_hatching(): 16 | get_client().disconnect() 17 | 18 | events.locust_stop_hatching += on_locust_stop_hatching 19 | -------------------------------------------------------------------------------- /distributed/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpLocust, TaskSet 2 | 3 | def login(l): 4 | l.client.post("/login", {"username":"admin", "password":"admin"}) 5 | 6 | def index(l): 7 | l.client.get("/") 8 | 9 | def profile(l): 10 | l.client.get("/profile") 11 | 12 | class UserBehavior(TaskSet): 13 | tasks = {index:2, profile:1} 14 | 15 | def on_start(self): 16 | login(self) 17 | 18 | class WebsiteUser(HttpLocust): 19 | task_set = UserBehavior 20 | min_wait=1000 21 | max_wait=1000 22 | -------------------------------------------------------------------------------- /graphing/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | influxdb: 4 | image: influxdb:1.0.1-alpine 5 | ports: 6 | - "8083:8083" 7 | - "8086:8086" 8 | volumes: 9 | - "./data/influxdb:/var/lib/influxdb" 10 | grafana: 11 | image: grafana/grafana:3.1.1 12 | ports: 13 | - "3000:3000" 14 | locust: 15 | build: . 16 | ports: 17 | - "8089:8089" 18 | volumes: 19 | - ".:/srv/app" 20 | command: "locust -f /srv/app/locustfile.py --host http://grafana:3000" 21 | 22 | -------------------------------------------------------------------------------- /distributed/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | grafana: 4 | image: grafana/grafana:3.1.1 5 | ports: 6 | - "3000:3000" 7 | overlord: 8 | build: . 9 | ports: 10 | - "8089:8089" 11 | volumes: 12 | - ".:/srv/app" 13 | command: "locust -f /srv/app/locustfile.py --master --master-bind-port=5557 --host http://grafana:3000" 14 | minion: 15 | build: . 16 | volumes: 17 | - ".:/srv/app" 18 | command: "locust -f /srv/app/locustfile.py --slave --master-host=overlord --master-port=5557" 19 | 20 | -------------------------------------------------------------------------------- /rabbitmq/readme.md: -------------------------------------------------------------------------------- 1 | # Load Testing RabbitMQ with Locust 2 | This directory contains a demo of using locust to load test rabbitmq. It 3 | uses a custom runner that blasts `basic_publish` calls to rabbitmq. 4 | 5 | ## Running It 6 | 1. `docker-compose up -d` 7 | 2. navigate to `http://dockerhost:15672` and login with `guest`/`guest` 8 | 3. Create an exchange named `test.exchange` and bind a queue to it. 9 | 4. Navigate to `http://dockerhost:8089` and launch a swarm 10 | 5. In the rabbitmq management panel, watch the publish rates hit the 11 | ceiling! 12 | 13 | -------------------------------------------------------------------------------- /quickstart/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpLocust, TaskSet, task 2 | 3 | 4 | class UserBehavior(TaskSet): 5 | def on_start(self): 6 | self.login() 7 | 8 | def login(self): 9 | # this is just using requests to post the auth details 10 | self.client.post("/login", {"username":"admin", "password":"admin"}) 11 | 12 | @task(2) 13 | def index(self): 14 | self.client.get("/") 15 | 16 | @task(1) 17 | def profile(self): 18 | self.client.get("/profile") 19 | 20 | class WebsiteUser(HttpLocust): 21 | task_set = UserBehavior 22 | min_wait = 1000 23 | max_wait = 1000 24 | -------------------------------------------------------------------------------- /distributed/readme.md: -------------------------------------------------------------------------------- 1 | # Distributed Example 2 | This example takes the quickstart example and makes it distributed. When 3 | `docker-compose up -d` is ran only the target and the locust master will 4 | come up, the slave will die right away (because by the time it tries to 5 | come up, master is not available). 6 | 7 | If we wanted to run 4 locust minions to distribute tasks amongst we'd 8 | run `docker-compose scale minion=4` to launch 4 containers. If we log 9 | into the locust interface at http://dockerhost:8089 we should see 4 10 | slaves connected. 11 | 12 | ## Things of Note 13 | * Requests per second will be distributed amongst the available slaves. 14 | So for example if you're aiming for 10 req/s and 5 slaves, each slave 15 | will run 2 req/s. 16 | 17 | -------------------------------------------------------------------------------- /graphing/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpLocust, TaskSet, task, events 2 | import logging 3 | from reporter import Reporter 4 | 5 | LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) ' 6 | '-35s %(lineno) -5d: %(message)s') 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | 10 | class UserBehavior(TaskSet): 11 | def on_start(self): 12 | self.login() 13 | 14 | def login(self): 15 | # this is just using requests to post the auth details 16 | self.client.post("/login", {"username":"admin", "password":"admin"}) 17 | 18 | @task 19 | def index(self): 20 | self.client.get("/") 21 | 22 | class WebsiteUser(HttpLocust): 23 | task_set = UserBehavior 24 | min_wait = 1000 25 | max_wait = 1000 26 | 27 | 28 | 29 | """ 30 | Here we trap some events 31 | """ 32 | reporter = Reporter() 33 | events.request_success += reporter.request_success 34 | events.hatch_complete += reporter.hatch_complete 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Locust.IO Demos 2 | This project has a few demos of how to use locust.io to load test 3 | various services. Given as part of [a presentation on locust.io](https://speakerdeck.com/jamescarr/load-testing-with-locust-dot-io#)! 4 | 5 | 6 | 7 | * `quickstart` - The locust.io quickstart tutorial that logs in and load 8 | tests pages on a grafana installation 9 | * `distributed` - the same thing as Quickstart but done with multiple 10 | workers 11 | * `Events` - A quick tour of events 12 | * Graphing - An example of using events to tap response times in 13 | influxdb + grafana 14 | * `RabbitMQ` - A final demo of using locust.io to load test something 15 | non-web based... by blasting AMQP packets to a rabbitMQ host 16 | 17 | 18 | ## Running the Samples 19 | All of these samples run locally using docker-compose. Simply switch to 20 | the demo directory, type `docker-compose up -d`. 21 | 22 | ### URLs 23 | 24 | * http://dockerhost:8086 - Locust.io dashboard 25 | * http://dockerhost:3000 - grafana installation, login is admin/admin 26 | * http://dockerhost:15672 - rabbitmq installation, login is guest/guest 27 | -------------------------------------------------------------------------------- /graphing/reporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from influxdb import InfluxDBClient 3 | from datetime import datetime 4 | 5 | class Reporter(object): 6 | 7 | def __init__(self): 8 | self._client = InfluxDBClient('influxdb', 8086, 'root', 'root', 'example') 9 | self._client.create_database('example') 10 | self._user_count = 0 11 | 12 | 13 | def hatch_complete(self, user_count): 14 | self._user_count = user_count 15 | 16 | 17 | def request_success(self, request_type, name, 18 | response_time, response_length): 19 | points = [{ 20 | "measurement": "request_success_duration", 21 | "tags": { 22 | "request_type": request_type, 23 | "name": name 24 | }, 25 | "time": datetime.now().isoformat(), 26 | "fields": { 27 | "value": response_time 28 | } 29 | }, 30 | { 31 | "measurement": "user_count", 32 | "time": datetime.now().isoformat(), 33 | "fields": { 34 | "value": self._user_count 35 | } 36 | }] 37 | self._client.write_points(points) 38 | -------------------------------------------------------------------------------- /event-trapping/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpLocust, TaskSet, task, events 2 | import logging 3 | LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) ' 4 | '-35s %(lineno) -5d: %(message)s') 5 | LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | class UserBehavior(TaskSet): 9 | def on_start(self): 10 | self.login() 11 | 12 | def login(self): 13 | # this is just using requests to post the auth details 14 | self.client.post("/login", {"username":"admin", "password":"admin"}) 15 | 16 | @task(2) 17 | def index(self): 18 | self.client.get("/") 19 | 20 | @task(1) 21 | def profile(self): 22 | self.client.get("/profiles") 23 | 24 | class WebsiteUser(HttpLocust): 25 | task_set = UserBehavior 26 | min_wait = 1000 27 | max_wait = 1000 28 | 29 | 30 | 31 | """ 32 | Here we trap some events 33 | """ 34 | def request_failure(request_type, name, response_time, exception): 35 | LOGGER.info('request_failure fired!') 36 | 37 | def request_success(request_type, name, response_time, response_length): 38 | LOGGER.info('request_success fired!') 39 | 40 | def locust_start_hatching(): 41 | LOGGER.info('locust_start_hatching fired!') 42 | 43 | def locust_stop_hatching(): 44 | LOGGER.info('locust_stop_hatching fired!') 45 | 46 | def hatch_complete(user_count): 47 | LOGGER.info('hatch_complete fired!') 48 | 49 | def quitting(): 50 | LOGGER.info('quitting fired!') 51 | 52 | 53 | events.locust_start_hatching += locust_start_hatching 54 | events.locust_stop_hatching += locust_stop_hatching 55 | events.hatch_complete += hatch_complete 56 | events.quitting += quitting 57 | events.request_success += request_success 58 | events.request_failure += request_failure 59 | -------------------------------------------------------------------------------- /rabbitmq/src/rabbitmq.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | import multiprocessing 4 | import os 5 | import threading 6 | 7 | from locust import events 8 | import pika 9 | from pika.exceptions import AMQPError 10 | 11 | 12 | LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) ' 13 | '-35s %(lineno) -5d: %(message)s') 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | class RabbitMQClient(object): 18 | _connected = False 19 | 20 | def __init__(self): 21 | self._process_name = multiprocessing.current_process().name 22 | self._thread_name = threading.current_thread().name 23 | 24 | def connect(self): 25 | params = pika.URLParameters(os.environ['RABBITMQ_CONNECTION']) 26 | self._connection = pika.BlockingConnection(params) 27 | self._channel = self._connection.channel() 28 | self._connected = True 29 | 30 | 31 | def publish(self): 32 | """ 33 | Constructs and publishes a simple message 34 | via amqp.basic_publish 35 | 36 | """ 37 | if not self._connected: 38 | self.connect() 39 | 40 | message = "Process: {}, Thread: {}".format(self._process_name, 41 | self._thread_name) 42 | 43 | try: 44 | watch = StopWatch() 45 | watch.start() 46 | self._channel.basic_publish('test.exchange', 'test.message', message) 47 | watch.stop() 48 | except AMQPError as e: 49 | watch.stop() 50 | events.request_failure.fire( 51 | request_type="BASIC_PUBLISH", 52 | name="test.message", 53 | response_time=watch.elapsed_time(), 54 | exception=e, 55 | ) 56 | else: 57 | events.request_success.fire( 58 | request_type="BASIC_PUBLISH", 59 | name="test.message", 60 | response_time=watch.elapsed_time(), 61 | response_length=0 62 | ) 63 | 64 | def close_channel(self): 65 | if self._channel is not None: 66 | LOGGER.info('Closing the channel') 67 | self._channel.close() 68 | 69 | def close_connection(self): 70 | if self._connection is not None: 71 | LOGGER.info('Closing connection') 72 | self._connection.close() 73 | 74 | def disconnect(self): 75 | self.close_channel() 76 | self.close_connection() 77 | self._connected = False 78 | 79 | 80 | class StopWatch(): 81 | def start(self): 82 | self._start = datetime.now() 83 | 84 | def stop(self): 85 | self._end = datetime.now() 86 | 87 | def elapsed_time(self): 88 | timedelta = self._end - self._start 89 | return timedelta.total_seconds() * 1000 90 | 91 | client = None 92 | def get_client(): 93 | global client 94 | if client is None: 95 | client = RabbitMQClient() 96 | return client 97 | -------------------------------------------------------------------------------- /graphing/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "title": "OurApp Load Test", 4 | "tags": [], 5 | "style": "dark", 6 | "timezone": "browser", 7 | "editable": true, 8 | "hideControls": false, 9 | "sharedCrosshair": false, 10 | "rows": [ 11 | { 12 | "collapse": false, 13 | "editable": true, 14 | "height": "250px", 15 | "panels": [ 16 | { 17 | "aliasColors": {}, 18 | "bars": false, 19 | "datasource": null, 20 | "editable": true, 21 | "error": false, 22 | "fill": 1, 23 | "grid": { 24 | "threshold1": null, 25 | "threshold1Color": "rgba(216, 200, 27, 0.27)", 26 | "threshold2": null, 27 | "threshold2Color": "rgba(234, 112, 112, 0.22)" 28 | }, 29 | "id": 1, 30 | "isNew": true, 31 | "legend": { 32 | "avg": false, 33 | "current": false, 34 | "max": false, 35 | "min": false, 36 | "show": true, 37 | "total": false, 38 | "values": false 39 | }, 40 | "lines": true, 41 | "linewidth": 2, 42 | "links": [], 43 | "nullPointMode": "connected", 44 | "percentage": false, 45 | "pointradius": 5, 46 | "points": false, 47 | "renderer": "flot", 48 | "seriesOverrides": [], 49 | "span": 6, 50 | "stack": false, 51 | "steppedLine": false, 52 | "targets": [ 53 | { 54 | "alias": "Response Time", 55 | "dsType": "influxdb", 56 | "groupBy": [ 57 | { 58 | "params": [ 59 | "10s" 60 | ], 61 | "type": "time" 62 | }, 63 | { 64 | "params": [ 65 | "0" 66 | ], 67 | "type": "fill" 68 | } 69 | ], 70 | "measurement": "request_success_duration", 71 | "policy": "default", 72 | "refId": "A", 73 | "resultFormat": "time_series", 74 | "select": [ 75 | [ 76 | { 77 | "params": [ 78 | "value" 79 | ], 80 | "type": "field" 81 | }, 82 | { 83 | "params": [], 84 | "type": "mean" 85 | } 86 | ] 87 | ], 88 | "tags": [] 89 | } 90 | ], 91 | "timeFrom": null, 92 | "timeShift": null, 93 | "title": "Request Duration", 94 | "tooltip": { 95 | "msResolution": true, 96 | "shared": true, 97 | "sort": 0, 98 | "value_type": "cumulative" 99 | }, 100 | "type": "graph", 101 | "xaxis": { 102 | "show": true 103 | }, 104 | "yaxes": [ 105 | { 106 | "format": "ms", 107 | "label": null, 108 | "logBase": 1, 109 | "max": null, 110 | "min": null, 111 | "show": true 112 | }, 113 | { 114 | "format": "short", 115 | "label": null, 116 | "logBase": 1, 117 | "max": null, 118 | "min": null, 119 | "show": true 120 | } 121 | ] 122 | }, 123 | { 124 | "aliasColors": {}, 125 | "bars": false, 126 | "datasource": null, 127 | "editable": true, 128 | "error": false, 129 | "fill": 1, 130 | "grid": { 131 | "threshold1": null, 132 | "threshold1Color": "rgba(216, 200, 27, 0.27)", 133 | "threshold2": null, 134 | "threshold2Color": "rgba(234, 112, 112, 0.22)" 135 | }, 136 | "id": 2, 137 | "isNew": true, 138 | "legend": { 139 | "avg": false, 140 | "current": false, 141 | "max": false, 142 | "min": false, 143 | "show": true, 144 | "total": false, 145 | "values": false 146 | }, 147 | "lines": true, 148 | "linewidth": 2, 149 | "links": [], 150 | "nullPointMode": "connected", 151 | "percentage": false, 152 | "pointradius": 5, 153 | "points": false, 154 | "renderer": "flot", 155 | "seriesOverrides": [], 156 | "span": 6, 157 | "stack": false, 158 | "steppedLine": false, 159 | "targets": [ 160 | { 161 | "dsType": "influxdb", 162 | "groupBy": [ 163 | { 164 | "params": [ 165 | "$interval" 166 | ], 167 | "type": "time" 168 | }, 169 | { 170 | "params": [ 171 | "0" 172 | ], 173 | "type": "fill" 174 | } 175 | ], 176 | "measurement": "user_count", 177 | "policy": "default", 178 | "refId": "A", 179 | "resultFormat": "time_series", 180 | "select": [ 181 | [ 182 | { 183 | "params": [ 184 | "value" 185 | ], 186 | "type": "field" 187 | }, 188 | { 189 | "params": [], 190 | "type": "mean" 191 | } 192 | ] 193 | ], 194 | "tags": [] 195 | } 196 | ], 197 | "timeFrom": null, 198 | "timeShift": null, 199 | "title": "Number of Users", 200 | "tooltip": { 201 | "msResolution": true, 202 | "shared": true, 203 | "sort": 0, 204 | "value_type": "cumulative" 205 | }, 206 | "type": "graph", 207 | "xaxis": { 208 | "show": true 209 | }, 210 | "yaxes": [ 211 | { 212 | "format": "short", 213 | "label": null, 214 | "logBase": 1, 215 | "max": null, 216 | "min": null, 217 | "show": true 218 | }, 219 | { 220 | "format": "short", 221 | "label": null, 222 | "logBase": 1, 223 | "max": null, 224 | "min": null, 225 | "show": true 226 | } 227 | ] 228 | } 229 | ], 230 | "title": "Row" 231 | } 232 | ], 233 | "time": { 234 | "from": "2016-10-04T15:37:31.762Z", 235 | "to": "2016-10-04T15:41:30.981Z" 236 | }, 237 | "timepicker": { 238 | "refresh_intervals": [ 239 | "5s", 240 | "10s", 241 | "30s", 242 | "1m", 243 | "5m", 244 | "15m", 245 | "30m", 246 | "1h", 247 | "2h", 248 | "1d" 249 | ], 250 | "time_options": [ 251 | "5m", 252 | "15m", 253 | "1h", 254 | "6h", 255 | "12h", 256 | "24h", 257 | "2d", 258 | "7d", 259 | "30d" 260 | ] 261 | }, 262 | "templating": { 263 | "list": [] 264 | }, 265 | "annotations": { 266 | "list": [] 267 | }, 268 | "refresh": false, 269 | "schemaVersion": 12, 270 | "version": 1, 271 | "links": [], 272 | "gnetId": null 273 | } 274 | --------------------------------------------------------------------------------