scripts/apm-server-load-test.js (308 lines of code) (raw):
/**
* MIT License
*
* Copyright (c) 2017-present, Elasticsearch BV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
const faker = require('faker')
const { merge } = require('lodash')
const crypto = require('crypto')
const fetch = require('node-fetch')
const { join } = require('path')
const { version } = require('../packages/rum/package.json')
const { writeFile } = require('fs').promises
const serverUrl = process.env.APM_SERVER_URL || 'http://localhost:8200'
const esUrl = process.env.ES_URL || 'http://localhost:9200'
const esAuth = process.env.ES_AUTH
const debug = process.env.DEBUG
/**
* To make the random id generation work.
*/
const destination = new Uint8Array(16)
global.crypto = {
getRandomValues: () => {
return crypto.randomFillSync(destination)
}
}
const {
generateRandomId
} = require('../packages/rum-core/dist/lib/common/utils')
const defaultMeta = {
metadata: {
service: {
name: 'apm-server-load-test',
agent: {
name: 'rum-js',
version
},
language: {
name: 'javascript'
}
}
}
}
const defaultTransaction = {
transaction: {
id: '73a0d4714e793b8a',
trace_id: '2b282fc14bd5a45bd69798c0c70fbe53',
name: 'test-transaction',
type: 'page-load',
duration: 3092,
context: {
page: {
referer: '',
url: 'https://www.elastic.co/'
},
response: {
transfer_size: 333,
encoded_body_size: 28019,
decoded_body_size: 140745
}
},
marks: {
navigationTiming: {
fetchStart: 0,
domainLookupStart: 0,
domainLookupEnd: 0,
connectStart: 0,
connectEnd: 0,
requestStart: 50,
responseStart: 157,
responseEnd: 157,
domLoading: 161,
domInteractive: 540,
domContentLoadedEventStart: 607,
domContentLoadedEventEnd: 610,
domComplete: 1350,
loadEventStart: 1350,
loadEventEnd: 1350
},
agent: {
timeToFirstByte: 157,
domInteractive: 540,
domComplete: 1350
}
},
span_count: {
started: 82
},
sampled: true,
experience: {
tbt: 100,
cls: 0.5,
fid: 50
}
}
}
const defaultSpan = {
span: {
id: '8cabde4612857a8f',
transaction_id: '93a0bcc9a6a4a646',
parent_id: '93a0bcc9a6a4a646',
trace_id: '1acfdeb11a825ddecedf54dcce00994c',
name: 'http://testing.com',
type: 'resource',
subtype: 'script',
start: 25,
duration: 143,
context: {
http: {
url: 'http://testing.com',
response: {}
},
destination: {
service: {
name: 'http://testing.com',
resource: 'testing.com:80',
type: 'resource'
},
address: 'testing.com',
port: 80
}
}
}
}
function getRandomNumber(min, max) {
return Math.random() * (max - min) + min
}
function getRandomInt(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
function generateTransaction(spanCount, sessionId, sequence, baseUrl) {
if (!baseUrl) {
baseUrl = faker.internet.url()
}
const url =
baseUrl +
'/' +
faker.random.words(3).split(' ').join('/').toLocaleLowerCase()
let breakdown = []
let tr = merge({}, defaultTransaction, {
transaction: {
id: generateRandomId(16),
trace_id: generateRandomId(),
duration: Math.floor(getRandomNumber(0, 5000)),
name: faker.random.words(3),
span_count: {
started: spanCount
},
sampled: spanCount != 0,
breakdown,
session: {
id: sessionId,
sequence
},
context: {
tags: {
session_id: sessionId,
session_seq: sequence
},
page: {
referer: faker.internet.url(),
url
}
}
}
})
breakdown.push({
transaction: { name: tr.transaction.name, type: tr.transaction.type },
samples: {
'transaction.duration.count': { value: 1 },
'transaction.duration.sum.us': { value: tr.transaction.duration },
'transaction.breakdown.count': { value: tr.transaction.sampled ? 1 : 0 }
}
})
let payload = [tr]
for (let i = 0; i < spanCount; i++) {
const span = merge({}, defaultSpan, {
span: {
id: generateRandomId(16),
transaction_id: tr.transaction.id,
parent_id: tr.transaction.id,
trace_id: tr.transaction.trace_id,
name: [
'http://testing.com',
generateRandomId(),
generateRandomId() + '.js'
].join('/'),
start: getRandomInt(0, tr.transaction.duration),
duration: getRandomInt(0, tr.transaction.duration)
}
})
payload.push(span)
}
return payload
}
function ndJsonStringify(object) {
return JSON.stringify(object) + '\n'
}
async function postPayload(url, payload) {
let data = payload.map(p => {
return ndJsonStringify(p)
})
try {
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
headers: {
'Content-Type': 'application/x-ndjson',
'X-Forwarded-For': faker.internet.ip(),
'User-Agent': faker.internet.userAgent()
},
body: data.join('')
})
return response.statusText
} catch (error) {
return error
}
}
function asyncTimeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function generatePayloads(transactionCount, trPerSession = 10) {
const spanPerTransaction = 50
let promises = []
let sessionId
let baseUrl
let payloads = []
for (let i = 0; i < transactionCount; i++) {
const sequence = i % trPerSession
if (sequence == 0) {
sessionId = generateRandomId(16)
baseUrl = faker.internet.url()
}
let payload = generateTransaction(
spanPerTransaction,
sessionId,
sequence + 1,
baseUrl
)
payload.unshift(merge({}, defaultMeta))
payloads.push(payload)
let p = postPayload(`${serverUrl}/intake/v2/rum/events`, payload)
promises.push(p)
}
let transactionResponses = await Promise.all(promises)
const responses = {}
transactionResponses.forEach(tr => {
if (!responses[tr]) {
responses[tr] = 1
} else {
responses[tr]++
}
})
const result = {
'@timestamp': Date.now(),
transactionCount,
spanPerTransaction,
apmServer: null,
transformStats: null,
responses
}
try {
let response = await fetch(`${serverUrl}/debug/vars`)
const apmServerResults = await response.json()
result.apmServer = apmServerResults
} catch (error) {
result.apmServer = error
}
if (debug) {
console.log(responses)
result.payloads = payloads
try {
let transformStats = await fetch(
`${esUrl}/_transform/transform_rum_sessions/_stats`,
{
method: 'GET',
headers: {
Authorization: esAuth
}
}
)
result.transformStats = await transformStats.json()
} catch (error) {
result.transformStats = error
}
}
return result
}
const iterations = 10
;(async function () {
const outputFile = process.argv[2]
const results = []
for (let iteration = 0; iteration < iterations; iteration++) {
if (debug) {
console.log(`Iteration: ${iteration}`)
}
const result = await generatePayloads(100)
results.push(result)
/**
* This timeout is added to avoid hitting
* the rate limit on APM server
*/
await asyncTimeout(1000)
}
if (outputFile) {
let ndJSONOutput = ''
for (const result of results) {
ndJSONOutput += ndJsonStringify({
index: {
_index: 'benchmarks-rum-load-test'
}
})
ndJSONOutput += ndJsonStringify(result)
}
await writeFile(join(__dirname, '../', outputFile), ndJSONOutput)
} else {
console.log(results)
}
})()