Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apm-lambda-extension/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
.nyc_output
install.yaml
profile.yaml
62 changes: 61 additions & 1 deletion apm-lambda-extension/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,64 @@ The `install` sub-command will automatically

1. Update your Lambda environmental variables
2. Build the Lambda Extension Layer and Publish it to AWS
3. Add the just published layer to your Lambda function's configuration
3. Add the just published layer to your Lambda function's configuration

## Running the Profiler

You can use the `./elastic-lambda.js profile` command to run performance _scenarios_ using the `lpt-0.1.jar` perf. runner. The `profile` sub-command expects a `profile.yaml` file to be present -- copy `profile.yaml.dist` as a starter file. This configuration file contains the location of your downloaded `ltp-0.1.jar` file, and configuration for individual scenarios.

A scenario configuration looks like the following

scenarios:
# each section under scenarios represents a single lambda function
# to deploy and test via lpt-0.1.jar
otel:
function_name_prefix: 'otel-autotest-'
role: '[... enter role ...]'
code: './profile/code'
handler: 'index.handler'
runtime: 'nodejs14.x'
# up to five
layer_arns:
- '... enter first layer'
- '... enter second layer'
# use this value to trigger a build and deploy of the latest extension
# - 'ELASTIC_LATEST'
environment:
variables:
AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-handler'
OPENTELEMETRY_COLLECTOR_CONFIG_FILE: '/var/task/collector.yaml'
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:55681/v1/traces'
OTEL_TRACES_SAMPLER: 'AlwaysOn'
APM_ELASTIC_SECRET_TOKEN: '[... enter secret token ...]'
ELASTIC_APM_SERVER_URL: '[... enter APM Server URL ...]'

Each individual object under the `scenarios` key represents an individual perf. scenario.

**`function_name_prefix`**

The `profile` sub-command will use the `function_name_prefix` configuration value when naming the Lambda function it creates and deploys. This helps ensure your function name will be complete.

**`role`**

AWS needs a _role_ in order to create a Lambda function. Use the `role` field to provide this value.

**`code`**

The `code` configuration value points to a folder that contains file. This folder will be zipped up, and used to upload the source code of the lambda function that the `profile` command creates.

**`handler`**

The `handler` configuration value sets the created lambda function's handler value. The above example is for a Node.js function.

**`runtime`**

The `runtime` configuration value sets the runtime of the created lambda function.

**`layer_arns`**

The `profile` command will use the `layer_arn` values to automatically configure up to five layers in the lambda function it creates for profiling. Use a value of `ELASTIC_LATEST` to build and deploy a layer with the latest lambda extension from this repo.

**`environment`**

Use the `environment` configuration value to set any needed environment variables in the created lambda function.
44 changes: 27 additions & 17 deletions apm-lambda-extension/cli/build-and-publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,38 @@ function getLastJsonFromShellOutput (output) {
return object
}

function cmd () {
if (!process.env.ELASTIC_LAYER_NAME) {
process.env.ELASTIC_LAYER_NAME = 'apm-lambda-extension'
}
console.log('running cd .. && make build-and-publish')
exec('cd .. && make build-and-publish', (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`)
return
function buildAndPublish () {
return new Promise(function (resolve, reject) {
if (!process.env.ELASTIC_LAYER_NAME) {
process.env.ELASTIC_LAYER_NAME = 'apm-lambda-extension'
}
if (stderr) {
console.log(`stderr: ${stderr}`)
return
}
console.log(`stdout: ${stdout}`)
const object = getLastJsonFromShellOutput(stdout)
console.log(`Published Layer as: ${object.LayerVersionArn}`)
console.log('running cd .. && make build-and-publish')
exec('cd .. && make build-and-publish', (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`)
return
}
if (stderr) {
console.log(`stderr: ${stderr}`)
return
}
console.log(`stdout: ${stdout}`)
const object = getLastJsonFromShellOutput(stdout)
console.log(`Published Layer as: ${object.LayerVersionArn}`)
resolve(object.LayerVersionArn)
})
})
}

function cmd () {
buildAndPublish().then(function (arn) {
console.log('FINAL: ' + arn)
})
}

module.exports = {
cmd,

getLastJsonFromShellOutput
getLastJsonFromShellOutput,
buildAndPublish
}
9 changes: 9 additions & 0 deletions apm-lambda-extension/cli/elastic-lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,13 @@ function checkAwsRegion () {
const { cmd } = require('./install')
cmd(argv)
}
).command(
'profile',
'runs the profiler based on configuration in profile.yaml',
function (yargs) {
},
function (argv) {
const { cmd } = require('./profile')
cmd(argv)
}
).demandCommand().recommendCommands().strict().parse()
217 changes: 217 additions & 0 deletions apm-lambda-extension/cli/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
'use strict'
const yaml = require('js-yaml')
const AWS = require('aws-sdk')
const fs = require('fs')
const { exec /* execFile */ } = require('child_process')
const { buildAndPublish } = require('./build-and-publish')

AWS.config.update({ region: 'us-west-2' })
const lambda = new AWS.Lambda({ apiVersion: '2015-03-31' })

function generateZipFile (pathSource, pathDest) {
return new Promise(function (resolve, reject) {
const env = Object.assign({}, process.env)
exec(`rm -f ${pathDest} && cd ${pathSource} && zip -r ${pathDest} .`,
env,
function (error, stdout, stderr) {
if (error) {
reject(error)
} else {
resolve(stdout)
}
}
)
})
}

function convertStdoutTableToObject (string) {
const split = string.split('┌')
split.shift()
const table = split.join('')
const lines = table.replace(/[^\x00-\x7F]/g, '').split('\n').filter(item => item)
let headers
const results = []
for (const [, line] of lines.entries()) {
const cells = line.split(/\s{2,}/).filter((item) => item)
if (!headers) {
headers = cells
continue
}
if (headers.length !== cells.length) {
continue
}
const result = {}
for (let i = 0; i < headers.length; i++) {
result[headers[i]] = cells[i]
}
results.push(result)
}
return results
}

function createFunction (args) {
return lambda.createFunction(args).promise()
}

async function cleanup (functionName) {
await lambda.deleteFunction({
FunctionName: functionName
}).promise()
}

function generateTmpFunctionName (prefix) {
const maxLengthLambda = 64
const name = [prefix, 'apm-profile'].join('')
if (name.length > maxLengthLambda) {
console.log(`final function name ${name} is too long, bailing.`)
process.exit(1)
}
return name
}

async function runScenario (scenario, config) {
return new Promise(async function (resolve, reject) {
const functionName = generateTmpFunctionName(scenario.function_name_prefix)
const tmpZipName = `/tmp/${functionName}.zip`
await generateZipFile(
[__dirname, '/', scenario.code].join(''),
tmpZipName
)

const createFunctionPromise = createFunction({
FunctionName: functionName,
Role: scenario.role,
Code: {
ZipFile: fs.readFileSync(tmpZipName)
},
Handler: scenario.handler,
Runtime: scenario.runtime,
Layers: scenario.layer_arns,
Environment: {
Variables: scenario.environment.variables
}
})

if (!createFunctionPromise) {
console.log('Could not call createFunction, bailing early')
reject(new Error('Could not call createFunction, bailing early'))
}
createFunctionPromise.then(function (resultCreateFunction) {
// need to wait for function to be created and its status
// to no longer be PENDING before we throw traffic at it
async function waitUntilNotPending (toRun, times = 0) {
const maxTimes = 10
const configuration = await lambda.getFunctionConfiguration({
FunctionName: functionName
}).promise()

if (configuration.State === 'Pending' && times <= maxTimes) {
console.log('waiting for function state != Pending')
times++
setTimeout(function () {
waitUntilNotPending(toRun, times)
}, 1000)
} else if (times > maxTimes) {
console.log('waited 10ish seconds and lambda did not activiate, bailing')
process.exit(1)
} else {
toRun()
}
}
waitUntilNotPending(function () {
// invoke test runner here
const times = config.config.n

console.log(`Running profile command with -n ${times} (this may take a while)`)

const env = Object.assign({}, process.env)
exec(`${config.config.path_java} -jar ` +
`${config.config.path_lpt_jar} -n ${times} ` +
`-a ${resultCreateFunction.FunctionArn} `,
env,
function (error, stdout, stderr) {
if (error) {
reject(error)
return
}
console.log('command done')
console.log(stdout)
cleanup(functionName)
resolve((convertStdoutTableToObject(stdout)))
}
)
})
}).catch(function (e) {
console.log('Error creating function')
if (e.statusCode === 409 && e.code === 'ResourceConflictException') {
console.log('Function already exists, deleting. Rerun profiler.')
cleanup(functionName)
} else {
console.log(e)
}
reject(e)
})
})
}

async function runScenarios (config) {
const all = []
for (const [name, scenario] of Object.entries(config.scenarios)) {
console.log(`starting ${name}`)
try {
all.push(await runScenario(scenario, config))
} catch (e) {
console.log('error calling runScenario')
}
}
console.log(all)
}

const FLAG_LATEST = 'ELASTIC_LATEST'
function buildAndDeployArn (config) {
return new Promise(function (resolve, reject) {
const arns = Object.values(config.scenarios).map(function (item) {
return item.layer_arns
}).flat()

if (arns.indexOf(FLAG_LATEST) === -1) {
resolve()
}

// build latest arn, modify config to include it
buildAndPublish().then(function result (arn) {
for (const [key, item] of Object.entries(config.scenarios)) {
for (let i = 0; i < item.layer_arns.length; i++) {
if (config.scenarios[key].layer_arns[i] === FLAG_LATEST) {
config.scenarios[key].layer_arns[i] = arn
} else {
console.log(config.scenarios[key].layer_arns[i])
}
}
}
resolve()
})
})
}

function cmd () {
if (!fs.existsSync([__dirname, '/profile.yaml'].join(''))) {
console.log('no profile.yaml found, please copy profile.yaml.dist and edit with your own values')
return
}
const config = yaml.load(fs.readFileSync([__dirname, '/profile.yaml'].join(''))).profile
// build and deploy the latest extension ARN (if neccesary)
buildAndDeployArn(config).then(function () {
runScenarios(config)
})

// return // tmp
// run all our scenarios
}

module.exports = {
cmd,

// exported for testing
convertStdoutTableToObject
}
28 changes: 28 additions & 0 deletions apm-lambda-extension/cli/profile.yaml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
profile:
config:
path_java: '/path/to/bin/java'
path_lpt_jar: '/path/to/lpt-0.1.jar'
n: 500
scenarios:
# each section under scenarios represents a single lambda function
# to deploy and test via lpt-0.1.jar
otel:
function_name_prefix: 'otel-autotest-'
role: '[... enter role ...]'
code: './profile/code'
handler: 'index.handler'
runtime: 'nodejs14.x'
# up to five
layer_arns:
- '... enter first layer'
- '... enter second layer'
# use this value to trigger a build and deploy of the latest extension
# - 'ELASTIC_LATEST'
environment:
variables:
AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-handler'
OPENTELEMETRY_COLLECTOR_CONFIG_FILE: '/var/task/collector.yaml'
OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:55681/v1/traces'
OTEL_TRACES_SAMPLER: 'AlwaysOn'
APM_ELASTIC_SECRET_TOKEN: '[... enter secret token ...]'
ELASTIC_APM_SERVER_URL: '[... enter APM Server URL ...]'
Loading