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: generate User-Agent for transport per APM spec #2402

Merged
merged 2 commits into from
Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 @@ -97,7 +102,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