Skip to content

Commit

Permalink
feat: generate User-Agent for transport per APM spec (#2402)
Browse files Browse the repository at this point in the history
https://github.com/elastic/apm/blob/master/specs/agents/transport.md#user-agent
This adds serviceName and serviceVersion to the User-Agent.

Closes: #2364
  • Loading branch information
trentm authored Oct 29, 2021
1 parent 9084f08 commit 052e396
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 10 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Notes:
[float]
===== Features
* The User-Agent header used for communication with APM Server now includes
the `serviceName` and `serviceVersion`. For some users this can be
https://github.com/elastic/apm/issues/509[helpful for APM Server log analysis].
({issues}2364[#2364])
[float]
===== Bug fixes
Expand Down Expand Up @@ -101,7 +106,7 @@ added to multiple events.
before flushing data to APM server. Before this change, a recently ended span
or recently <<apm-capture-error,captured error>> might not yet have completed
processing (for example, stacktrace collection is asynchronous) and might
not be included in the flush call.
not be included in the flush call. ({issues}2294[#2294])
* AWS Lambda changes: Disable metrics collection during the experimental phase
of (re)implementing Lambda support ({pull}2363[#2363]). Some fixes for better
Expand Down
49 changes: 41 additions & 8 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,12 @@ var truncate = require('unicode-byte-truncate')
const REDACTED = require('./constants').REDACTED
var logging = require('./logging')
var version = require('../package').version
var packageName = require('../package').name

const { WildcardMatcher } = require('./wildcard-matcher')
const { CloudMetadata } = require('./cloud-metadata')
const { NoopTransport } = require('./noop-transport')
const { isLambdaExecutionEnviornment } = require('./lambda')

// Standardize user-agent header. Only use "elasticapm-node" if it matches "elastic-apm-node".
if (packageName === 'elastic-apm-node') {
packageName = 'elasticapm-node'
}
var userAgent = `${packageName}/${version}`

let confFile = loadConfigFile()

let serviceName, serviceVersion
Expand Down Expand Up @@ -701,6 +694,45 @@ function loadServerCaCertFile (opts) {
}
}

// Return the User-Agent string the agent will use for its comms to APM Server.
//
// Per https://github.com/elastic/apm/blob/master/specs/agents/transport.md#user-agent
// the pattern is roughly this:
// $repoName/$version ($serviceName $serviceVersion)
//
// The format of User-Agent is governed by https://datatracker.ietf.org/doc/html/rfc7231.
// User-Agent = product *( RWS ( product / comment ) )
// We do not expect `$repoName` and `$version` to have surprise/invalid values.
// However, `$serviceName` and `$serviceVersion` are provided by the user
// and could have invalid characters. `comment` is defined by
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 as:
// comment = "(" *( ctext / quoted-pair / comment ) ")"
// obs-text = %x80-FF
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
// `commentBadChar` below *approximates* these rules, and is used to replace
// invalid characters with '_' in the generated User-Agent string. This
// replacement isn't part of the APM spec.
function userAgentFromConf (conf) {
let userAgent = `apm-agent-nodejs/${version}`

// This regex *approximately* matches the allowed syntax for a "comment".
// It does not handle "quoted-pair" or a "comment" in a comment.
const commentBadChar = /[^\t \x21-\x27\x2a-\x5b\x5d-\x7e\x80-\xff]/g
const commentParts = []
if (conf.serviceName) {
commentParts.push(conf.serviceName.replace(commentBadChar, '_'))
}
if (conf.serviceVersion) {
commentParts.push(conf.serviceVersion.replace(commentBadChar, '_'))
}
if (commentParts.length > 0) {
userAgent += ` (${commentParts.join(' ')})`
}

return userAgent
}

function getBaseClientConfig (conf, agent) {
let clientLogger = null
if (!logging.isLoggerCustom(agent.logger)) {
Expand Down Expand Up @@ -733,7 +765,7 @@ function getBaseClientConfig (conf, agent) {
// HTTP conf
secretToken: conf.secretToken,
apiKey: conf.apiKey,
userAgent: userAgent,
userAgent: userAgentFromConf(conf),
serverUrl: conf.serverUrl,
serverCaCert: loadServerCaCertFile(conf),
rejectUnauthorized: conf.verifyServerCert,
Expand Down Expand Up @@ -781,3 +813,4 @@ module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = CAPTURE_ERROR_LOG_STACK_T
// The following are exported for tests.
module.exports.DEFAULTS = DEFAULTS
module.exports.secondsFromTimeStr = secondsFromTimeStr
module.exports.userAgentFromConf = userAgentFromConf
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"basic-auth": "^2.0.1",
"cookie": "^0.4.0",
"core-util-is": "^1.0.2",
"elastic-apm-http-client": "^10.1.0",
"elastic-apm-http-client": "^10.2.0",
"end-of-stream": "^1.4.4",
"error-callsites": "^2.0.4",
"error-stack-parser": "^2.0.6",
Expand Down
20 changes: 20 additions & 0 deletions test/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,26 @@ test('should accept and normalize ignoreMessageQueues', function (suite) {
suite.end()
})

// Test User-Agent generation. It would be nice to also test against gherkin
// specs from apm.git.
// https://github.com/elastic/apm/blob/master/tests/agents/gherkin-specs/user_agent.feature
test('userAgentFromConf', t => {
t.equal(config.userAgentFromConf({}),
`apm-agent-nodejs/${apmVersion}`)
t.equal(config.userAgentFromConf({ serviceName: 'foo' }),
`apm-agent-nodejs/${apmVersion} (foo)`)
t.equal(config.userAgentFromConf({ serviceName: 'foo', serviceVersion: '1.0.0' }),
`apm-agent-nodejs/${apmVersion} (foo 1.0.0)`)
// ISO-8859-1 characters are generally allowed.
t.equal(config.userAgentFromConf({ serviceName: 'fête', serviceVersion: '2021-été' }),
`apm-agent-nodejs/${apmVersion} (fête 2021-été)`)
// Higher code points are replaced with `_`.
t.equal(config.userAgentFromConf({ serviceName: 'myhomeismy🏰', serviceVersion: 'do you want to build a ☃' }),
`apm-agent-nodejs/${apmVersion} (myhomeismy__ do you want to build a _)`)

t.end()
})

function assertEncodedTransaction (t, trans, result) {
t.comment('transaction')
t.strictEqual(result.id, trans.id, 'id matches')
Expand Down

0 comments on commit 052e396

Please sign in to comment.