Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for APM Agent Configuration via Kibana #1197

Merged
merged 11 commits into from
Jul 29, 2019
12 changes: 12 additions & 0 deletions docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ A boolean specifying if the agent should collect performance metrics for the app

Note that both `active` and `instrument` needs to be `true` for instrumentation to be running.

[[central-config]]
==== `centralConfig`

* *Type:* Boolean
* *Default:* `true`
* *Env:* `ELASTIC_APM_CENTRAL_CONFIG`

Activate APM Agent Configuration via Kibana.
If set to `true`, the client will poll the APM Server regularly for new agent configuration.

NOTE: This feature requires APM Server v7.3 or later and that the APM Server is configured with `kibana.enabled: true`.

[[async-hooks]]
==== `asyncHooks`

Expand Down
8 changes: 5 additions & 3 deletions docs/setup.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ NOTE: If you are using Babel, you need to use the `elastic-apm-node/start` appro

There are three ways to configure the Node.js agent. In order of precedence (higher overwrites lower):

1. Environment variables.
1. APM Agent Configuration via Kibana. Enabled with <<central-config>>.

2. If calling the `apm.start()` function,
2. Environment variables.

3. If calling the `apm.start()` function,
you can supply a <<agent-configuration-object,configurations object>> as the first argument.

3. Via the <<agent-configuration-file,agent configuration file>>.
4. Via the <<agent-configuration-file,agent configuration file>>.

For information on the available configuration properties, and the expected names of environment variables, see the <<configuration,Configuration options>> documentation.

Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ interface AgentConfigOptions {
logger?: Logger;
metricsInterval?: string; // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it
payloadLogFile?: string;
centralConfig?: boolean;
secretToken?: string;
serverTimeout?: string; // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it
serverUrl?: string;
Expand Down
270 changes: 164 additions & 106 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ var DEFAULTS = {
kubernetesPodUID: undefined,
logLevel: 'info',
metricsInterval: '30s',
centralConfig: true,
serverTimeout: '30s',
sourceLinesErrorAppFrames: 5,
sourceLinesErrorLibraryFrames: 5,
Expand Down Expand Up @@ -106,6 +107,7 @@ var ENV_TABLE = {
logLevel: 'ELASTIC_APM_LOG_LEVEL',
metricsInterval: 'ELASTIC_APM_METRICS_INTERVAL',
payloadLogFile: 'ELASTIC_APM_PAYLOAD_LOG_FILE',
centralConfig: 'ELASTIC_APM_CENTRAL_CONFIG',
secretToken: 'ELASTIC_APM_SECRET_TOKEN',
serverTimeout: 'ELASTIC_APM_SERVER_TIMEOUT',
serverUrl: 'ELASTIC_APM_SERVER_URL',
Expand All @@ -122,6 +124,14 @@ var ENV_TABLE = {
verifyServerCert: 'ELASTIC_APM_VERIFY_SERVER_CERT'
}

var CENTRAL_CONFIG = {
transaction_sample_rate: 'transactionSampleRate'
}

var VALIDATORS = {
transactionSampleRate: numberBetweenZeroAndOne
}

var BOOL_OPTS = [
'active',
'asyncHooks',
Expand All @@ -132,6 +142,7 @@ var BOOL_OPTS = [
'errorOnAbortedRequests',
'filterHttpHeaders',
'instrument',
'centralConfig',
'usePathAsTransactionName',
'verifyServerCert'
]
Expand Down Expand Up @@ -172,116 +183,153 @@ var KEY_VALUE_OPTS = [
]

function config (opts) {
opts = Object.assign(
{},
DEFAULTS, // default options
confFile, // options read from elastic-apm-node.js config file
opts, // options passed in to agent.start()
readEnv() // options read from environment variables
)

// Custom logic for setting serviceName so that an empty string in the config
// doesn't overwrite the serviceName read from package.json
if (!opts.serviceName) opts.serviceName = serviceName
if (!opts.serviceVersion) opts.serviceVersion = serviceVersion

// NOTE: A logger will already exists if a custom logger was given to start()
if (typeof opts.logger === 'undefined') {
opts.logger = consoleLogLevel({
level: opts.logLevel
})
}

normalizeIgnoreOptions(opts)
normalizeKeyValuePairs(opts)
normalizeNumbers(opts)
normalizeBytes(opts)
normalizeArrays(opts)
normalizeTime(opts)
normalizeBools(opts)
truncateOptions(opts)
return new Config(opts)
}

if (typeof opts.transport !== 'function') {
opts.transport = function httpTransport (conf, agent) {
var transport = new ElasticAPMHttpClient({
// metadata
agentName: 'nodejs',
agentVersion: version,
serviceName: conf.serviceName,
serviceVersion: conf.serviceVersion,
frameworkName: conf.frameworkName,
frameworkVersion: conf.frameworkVersion,
globalLabels: maybePairsToObject(conf.globalLabels),
hostname: conf.hostname,
environment: conf.environment,

// Sanitize conf
truncateKeywordsAt: config.INTAKE_STRING_MAX_SIZE,
truncateErrorMessagesAt: conf.errorMessageMaxLength,

// HTTP conf
secretToken: conf.secretToken,
userAgent: userAgent,
serverUrl: conf.serverUrl,
rejectUnauthorized: conf.verifyServerCert,
serverTimeout: conf.serverTimeout * 1000,

// Streaming conf
size: conf.apiRequestSize,
time: conf.apiRequestTime * 1000,

// Debugging
payloadLogFile: conf.payloadLogFile,

// Container conf
containerId: conf.containerId,
kubernetesNodeName: conf.kubernetesNodeName,
kubernetesNamespace: conf.kubernetesNamespace,
kubernetesPodName: conf.kubernetesPodName,
kubernetesPodUID: conf.kubernetesPodUID
class Config {
constructor (opts) {
this.ignoreUrlStr = []
this.ignoreUrlRegExp = []
this.ignoreUserAgentStr = []
this.ignoreUserAgentRegExp = []

Object.assign(
this,
DEFAULTS, // default options
confFile, // options read from elastic-apm-node.js config file
opts, // options passed in to agent.start()
readEnv() // options read from environment variables
)

// Custom logic for setting serviceName so that an empty string in the config
// doesn't overwrite the serviceName read from package.json
if (!this.serviceName) this.serviceName = serviceName
if (!this.serviceVersion) this.serviceVersion = serviceVersion

// NOTE: A logger will already exists if a custom logger was given to start()
if (typeof this.logger === 'undefined') {
this.logger = consoleLogLevel({
level: this.logLevel
})
}

transport.on('error', err => {
agent.logger.error('APM Server transport error:', err.stack)
})
normalize(this)

if (typeof this.transport !== 'function') {
this.transport = function httpTransport (conf, agent) {
var transport = new ElasticAPMHttpClient({
// metadata
agentName: 'nodejs',
agentVersion: version,
serviceName: conf.serviceName,
serviceVersion: conf.serviceVersion,
frameworkName: conf.frameworkName,
frameworkVersion: conf.frameworkVersion,
globalLabels: maybePairsToObject(conf.globalLabels),
hostname: conf.hostname,
environment: conf.environment,

// Sanitize conf
truncateKeywordsAt: config.INTAKE_STRING_MAX_SIZE,
truncateErrorMessagesAt: conf.errorMessageMaxLength,

// HTTP conf
secretToken: conf.secretToken,
userAgent: userAgent,
serverUrl: conf.serverUrl,
rejectUnauthorized: conf.verifyServerCert,
serverTimeout: conf.serverTimeout * 1000,

// APM Agent Configuration via Kibana:
centralConfig: conf.centralConfig,

// Streaming conf
size: conf.apiRequestSize,
time: conf.apiRequestTime * 1000,

// Debugging
payloadLogFile: conf.payloadLogFile,

// Container conf
containerId: conf.containerId,
kubernetesNodeName: conf.kubernetesNodeName,
kubernetesNamespace: conf.kubernetesNamespace,
kubernetesPodName: conf.kubernetesPodName,
kubernetesPodUID: conf.kubernetesPodUID
})

transport.on('config', remoteConf => {
const conf = {}
const unknown = []

for (const [key, value] of entries(remoteConf)) {
const newKey = CENTRAL_CONFIG[key]
if (newKey) {
conf[newKey] = value
} else {
unknown.push(key)
}
}

transport.on('request-error', err => {
const haveAccepted = Number.isFinite(err.accepted)
const haveErrors = Array.isArray(err.errors)
let msg

if (err.code === 404) {
msg = 'APM Server responded with "404 Not Found". ' +
'This might be because you\'re running an incompatible version of the APM Server. ' +
'This agent only supports APM Server v6.5 and above. ' +
'If you\'re using an older version of the APM Server, ' +
'please downgrade this agent to version 1.x or upgrade the APM Server'
} else if (err.code) {
msg = `APM Server transport error (${err.code}): ${err.message}`
} else {
msg = `APM Server transport error: ${err.message}`
}

if (haveAccepted || haveErrors) {
if (haveAccepted) msg += `\nAPM Server accepted ${err.accepted} events in the last request`
if (haveErrors) {
err.errors.forEach(error => {
msg += `\nError: ${error.message}`
if (error.document) msg += `\n Document: ${error.document}`
})
if (unknown.length > 0) {
agent.logger.warn(`Remote config failure. Unsupported config names: ${unknown.join(', ')}`)
}
} else if (err.response) {
msg += `\n${err.response}`
}

agent.logger.error(msg)
})
if (Object.keys(conf).length > 0) {
normalize(conf, agent._conf)

for (const [key, value] of entries(conf)) {
const validator = VALIDATORS[key]
if (validator ? validator(value) : true) {
agent.logger.info(`Remote config success. Updating ${key}: ${value}`)
agent._conf[key] = value
} else {
agent.logger.warn(`Remote config failure. Invalid value for ${key}: ${value}`)
}
}
}
})

transport.on('error', err => {
agent.logger.error('APM Server transport error:', err.stack)
})

transport.on('request-error', err => {
const haveAccepted = Number.isFinite(err.accepted)
const haveErrors = Array.isArray(err.errors)
let msg

if (err.code === 404) {
msg = 'APM Server responded with "404 Not Found". ' +
'This might be because you\'re running an incompatible version of the APM Server. ' +
'This agent only supports APM Server v6.5 and above. ' +
'If you\'re using an older version of the APM Server, ' +
'please downgrade this agent to version 1.x or upgrade the APM Server'
} else if (err.code) {
msg = `APM Server transport error (${err.code}): ${err.message}`
} else {
msg = `APM Server transport error: ${err.message}`
}

if (haveAccepted || haveErrors) {
if (haveAccepted) msg += `\nAPM Server accepted ${err.accepted} events in the last request`
if (haveErrors) {
err.errors.forEach(error => {
msg += `\nError: ${error.message}`
if (error.document) msg += `\n Document: ${error.document}`
})
}
} else if (err.response) {
msg += `\n${err.response}`
}

return transport
agent.logger.error(msg)
})

return transport
}
}
}

return opts
}

function readEnv () {
Expand All @@ -300,12 +348,18 @@ function readEnv () {
return opts
}

function normalizeIgnoreOptions (opts) {
opts.ignoreUrlStr = []
opts.ignoreUrlRegExp = []
opts.ignoreUserAgentStr = []
opts.ignoreUserAgentRegExp = []
function normalize (opts) {
normalizeIgnoreOptions(opts)
normalizeKeyValuePairs(opts)
normalizeNumbers(opts)
normalizeBytes(opts)
normalizeArrays(opts)
normalizeTime(opts)
normalizeBools(opts)
truncateOptions(opts)
}

function normalizeIgnoreOptions (opts) {
if (opts.ignoreUrls) {
opts.ignoreUrls.forEach(function (ptn) {
if (typeof ptn === 'string') opts.ignoreUrlStr.push(ptn)
Expand Down Expand Up @@ -459,3 +513,7 @@ function pairsToObject (pairs) {
return object
}, {})
}

function numberBetweenZeroAndOne (n) {
return n >= 0 && n <= 1
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"console-log-level": "^1.4.0",
"cookie": "^0.4.0",
"core-util-is": "^1.0.2",
"elastic-apm-http-client": "^8.0.0",
"elastic-apm-http-client": "^8.1.0",
"end-of-stream": "^1.4.1",
"fast-safe-stringify": "^2.0.6",
"http-headers": "^3.0.2",
Expand Down
4 changes: 4 additions & 0 deletions test/_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ function clean () {
agent._instrumentation._hook.unhook()
}
agent._metrics.stop()
if (agent._transport && agent._transport.destroy) {
agent._transport.destroy()
}
agent._transport = null
}
}
1 change: 1 addition & 0 deletions test/_apm_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var Agent = require('./_agent')
var defaultAgentOpts = {
serviceName: 'some-service-name',
captureExceptions: false,
centralConfig: false,
logLevel: 'error'
}

Expand Down
Loading