Artillery Plugins: Metrics, Assertions, and Observability Integrations

Artillery Plugins: Metrics, Assertions, and Observability Integrations

Out of the box, Artillery gives you latency percentiles and status code counts. That's a starting point, but for serious load testing you need custom assertions, metrics that go to your observability stack, and hooks to run custom logic during tests. Artillery's plugin system covers all of this.

This post covers the expect plugin for assertions, custom processors for scripting, and the major observability integrations: Datadog, CloudWatch, and Prometheus.

The Expect Plugin

The expect plugin adds response assertions to your scenarios. Without it, Artillery records HTTP errors (5xx, timeouts) but won't fail a test because a 200 response had the wrong body.

Install it:

npm install -g artillery-plugin-expect

Enable it in your config:

config:
  target: "https://api.example.com"
  plugins:
    expect: {}
  phases:
    - duration: 60
      arrivalRate: 10

Now add assertions to your flow steps:

scenarios:
  - name: "User API"
    flow:
      - get:
          url: "/users/1"
          expect:
            - statusCode: 200
            - contentType: json
            - hasProperty: "id"
            - hasProperty: "email"
            - equals:
                - "{{ response.body.active }}"
                - true

      - post:
          url: "/users"
          json:
            name: "Test User"
            email: "test{{ $randomNumber(1,9999) }}@example.com"
          expect:
            - statusCode: 201
            - hasProperty: "id"
            - matchesRegexp:
                - "{{ response.body.email }}"
                - "^test[0-9]+@example\\.com$"

Available assertions:

Assertion What it checks
statusCode: 200 HTTP status code equals value
statusCode: [200, 201] Status code is one of the values
contentType: json Content-Type header contains "json"
hasProperty: "field" Response body has the field (nested: "a.b.c")
notHasProperty: "field" Response body does not have the field
equals Two values are strictly equal
notEquals Two values are not equal
greaterThan Numeric comparison
lessThan Numeric comparison
matchesRegexp Value matches regex pattern

Expectation failures show in the summary report under "Errors" with a description of which assertion failed and on which URL. They don't cause the VU to abort — the scenario continues to the next step. If you need to abort on failure, use a processor (covered below).

Custom Processors

Processors are JavaScript modules that hook into the Artillery execution lifecycle. You get full Node.js — you can call databases, generate complex data, log to external systems, or implement custom assertion logic.

config:
  target: "https://api.example.com"
  processor: "./processor.js"
  phases:
    - duration: 60
      arrivalRate: 10

scenarios:
  - name: "Custom flow"
    flow:
      - function: "generatePayload"
      - post:
          url: "/api/items"
          json: "{{ payload }}"
          afterResponse: "validateResponse"
// processor.js

// Called as a flow step — sets variables for the VU
function generatePayload(context, events, done) {
  const items = ['A', 'B', 'C'];
  const item = items[Math.floor(Math.random() * items.length)];
  
  context.vars.payload = {
    itemType: item,
    quantity: Math.floor(Math.random() * 10) + 1,
    timestamp: Date.now()
  };
  
  return done();
}

// Called after a response is received
function validateResponse(requestParams, response, context, events, done) {
  if (response.statusCode === 200) {
    const body = JSON.parse(response.body);
    
    if (!body.id) {
      events.emit('counter', 'missing_id_in_response', 1);
    }
    
    if (body.processingTime > 500) {
      events.emit('counter', 'slow_processing', 1);
    }
    
    // Store for use in later steps
    context.vars.itemId = body.id;
  }
  
  return done();
}

module.exports = { generatePayload, validateResponse };

Lifecycle Hooks

Processors can hook into the full test lifecycle:

// processor.js

// Runs once before the test starts
function beforeScenario(context, events, done) {
  // Set up per-VU state
  context.vars.requestCount = 0;
  return done();
}

// Runs once after each scenario completes
function afterScenario(context, events, done) {
  console.log(`VU completed ${context.vars.requestCount} requests`);
  return done();
}

// Runs before each request
function beforeRequest(requestParams, context, events, done) {
  context.vars.requestCount = (context.vars.requestCount || 0) + 1;
  
  // Add a request signature header
  requestParams.headers['X-Request-Nonce'] = generateNonce();
  
  return done();
}

// Runs after each response
function afterResponse(requestParams, response, context, events, done) {
  // Emit a custom metric
  const latency = response.timings.phases.total;
  events.emit('histogram', 'custom.latency', latency);
  
  return done();
}

function generateNonce() {
  return Math.random().toString(36).substring(2, 15);
}

module.exports = {
  beforeScenario,
  afterScenario,
  beforeRequest,
  afterResponse
};

Reference these in config:

config:
  processor: "./processor.js"
  hooks:
    beforeScenario:
      - beforeScenario
    afterScenario:
      - afterScenario
    beforeRequest:
      - beforeRequest
    afterResponse:
      - afterResponse

Custom Metrics

Processors emit custom metrics via the events object:

// Counter — increments a named counter
events.emit('counter', 'checkout.success', 1);
events.emit('counter', 'checkout.failure', 1);

// Histogram — records a value distribution (latencies, sizes, etc.)
events.emit('histogram', 'order.item_count', itemCount);
events.emit('histogram', 'response.body_size', response.body.length);

// Rate — events per second
events.emit('rate', 'cache.hit');

These appear in Artillery's summary output and are forwarded to any configured integrations (Datadog, CloudWatch, etc.).

Datadog Integration

Send metrics to Datadog during the test run:

npm install -g artillery-plugin-publish-metrics
config:
  target: "https://api.example.com"
  plugins:
    publish-metrics:
      - type: datadog
        apiKey: "{{ $processEnvironment.DATADOG_API_KEY }}"
        region: "us1"
        prefix: "artillery."
        tags:
          - "env:staging"
          - "service:api"
          - "test:load"
        event:
          send: true
          title: "Artillery Load Test"
          text: "Load test started/stopped"
          priority: "normal"
          alertType: "info"
  phases:
    - duration: 300
      arrivalRate: 50

With this config, Artillery streams metrics to Datadog in real time. You can build dashboards showing:

  • artillery.latency.p95 — p95 response time
  • artillery.latency.p99 — p99 response time
  • artillery.http.codes.200 — count of 200 responses
  • artillery.http.codes.500 — count of 500 responses
  • Any custom metrics you emit from processors

The event.send: true option creates Datadog events when the test starts and stops — useful for annotating your existing service dashboards so you can correlate load test runs with changes in system metrics (CPU, memory, database query times).

You can cross-reference Artillery's latency metrics against your application's own Datadog metrics. If Artillery shows p95 latency spiking at the same time your database metrics show connection pool exhaustion, you've found your bottleneck.

CloudWatch Integration

For AWS environments:

config:
  plugins:
    publish-metrics:
      - type: cloudwatch
        region: "us-east-1"
        namespace: "LoadTests"
        dimensions:
          - name: "Environment"
            value: "staging"
          - name: "Service"
            value: "api"

This uses the AWS SDK under the hood. Set credentials via environment variables:

AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... artillery run test.yaml

Or if running on EC2/ECS with an IAM role, credentials are picked up automatically.

CloudWatch metrics appear under the namespace you specify. Create CloudWatch alarms on these metrics to alert if load tests push latency above thresholds.

Prometheus / StatsD Integration

For self-hosted observability stacks:

config:
  plugins:
    publish-metrics:
      - type: statsd
        host: "localhost"
        port: 8125
        prefix: "artillery."
        tags:
          plain: ["env:staging"]

StatsD metrics are compatible with most metrics collectors — Prometheus via statsd_exporter, InfluxDB, Graphite. If you're already running a StatsD pipeline, plug Artillery into it.

The CloudWatch Embedded Metrics Plugin

If you're running Artillery in Lambda or want structured CloudWatch Logs rather than CloudWatch Metrics API calls:

config:
  plugins:
    publish-metrics:
      - type: cloudwatch
        region: "us-east-1"
        namespace: "ArtilleryTests"
        sendInterval: 60

The sendInterval controls how often metrics are flushed. Lower values give you finer granularity in CloudWatch but increase API call volume.

Writing a Custom Plugin

If the built-in integrations don't cover your stack, write a plugin:

// artillery-plugin-custom-metrics.js

'use strict';

class CustomMetricsPlugin {
  constructor(config, events) {
    this.config = config;
    
    // Hook into the stats event emitted after each reporting period
    events.on('stats', (stats) => {
      this.publishStats(stats);
    });
    
    // Hook into test completion
    events.on('done', (stats) => {
      this.publishFinalStats(stats);
      this.cleanup();
    });
  }
  
  publishStats(stats) {
    const p95 = stats.latency?.p95;
    const p99 = stats.latency?.p99;
    const rps = stats.rps?.mean;
    
    // Push to your metrics backend
    console.log(JSON.stringify({
      timestamp: Date.now(),
      p95_latency: p95,
      p99_latency: p99,
      requests_per_second: rps,
      error_count: stats.errors || 0
    }));
  }
  
  publishFinalStats(stats) {
    // Final summary
  }
  
  cleanup() {
    // Close connections, flush buffers
  }
}

module.exports = {
  Plugin: CustomMetricsPlugin
};
config:
  plugins:
    custom-metrics:
      someConfig: "value"

Artillery instantiates the plugin class with (config, events). The events object is an EventEmitter. Subscribe to stats for periodic metrics and done for the final summary.

Combining Plugins

Plugins compose — you can run multiple simultaneously:

config:
  plugins:
    expect: {}
    publish-metrics:
      - type: datadog
        apiKey: "{{ $processEnvironment.DATADOG_API_KEY }}"
        prefix: "artillery."
        tags:
          - "env:staging"
      - type: statsd
        host: "metrics.internal"
        port: 8125

In this config, assertions via expect run in-process, and metrics go to both Datadog and StatsD simultaneously.

Installed Plugins vs. Package.json

For team projects, manage plugins in package.json:

{
  "devDependencies": {
    "artillery": "^2.0.0",
    "artillery-plugin-expect": "^2.0.0",
    "artillery-plugin-publish-metrics": "^2.0.0"
  }
}
npm install
npx artillery run test.yaml

This ensures everyone on the team and CI uses the same plugin versions. Global installs (npm install -g) lead to version drift and "works on my machine" debugging sessions.

Useful Processor Patterns

Rate limiting your own test — sometimes you want to slow down a specific step:

function rateLimitedRequest(context, events, done) {
  setTimeout(() => done(), 100); // add 100ms delay
}

Aborting a scenario on failure:

function checkCriticalResponse(requestParams, response, context, events, done) {
  if (response.statusCode !== 200) {
    // Signal Artillery to abandon this VU's scenario
    context.vars.__abort = true;
  }
  return done();
}

Logging slow requests:

function logSlowRequests(requestParams, response, context, events, done) {
  const duration = response.timings.phases.total;
  if (duration > 1000) {
    console.warn(`Slow request: ${requestParams.method} ${requestParams.url} took ${duration}ms`);
    events.emit('counter', 'slow_requests', 1);
  }
  return done();
}

These patterns give you observability beyond what Artillery's built-in reporting provides, and they integrate naturally with your existing monitoring infrastructure.

Read more

Start now free