Skip to content

Commit 3fefa56

Browse files
authored
chore: move transport to its own module apm-client (#3372)
- create a new module named `apm-client` which exports `createApmClient(config, agent)` API - move `NoopTransport` as a new client type in `./lib/apm-client/noop-apm-client.js` and rename it to `NoopApmClient` - create `http-apm-client` module which contains all logic to instantiate an `ElasticAPMHttpClient` - use `createApmClient` API in the agent to set `_transport` property
1 parent 98306d0 commit 3fefa56

15 files changed

+435
-326
lines changed

CHANGELOG.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Notes:
4848
[float]
4949
===== Chores
5050
51+
* Extract configuration's transport property to new `apm-client` module
52+
({pull}3372[#3372])
53+
5154
5255
[[release-notes-3.46.0]]
5356
==== 3.46.0 - 2023/05/15

lib/agent.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,34 @@
66

77
'use strict'
88

9-
var http = require('http')
10-
var path = require('path')
9+
const http = require('http')
10+
const path = require('path')
1111

12-
var isError = require('core-util-is').isError
13-
var Filters = require('object-filter-sequence')
12+
const isError = require('core-util-is').isError
13+
const Filters = require('object-filter-sequence')
1414

1515
const { agentActivationMethodFromStartStack } = require('./activation-method')
1616
const { CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS, CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } = require('./config/schema')
17-
var config = require('./config/config')
18-
var connect = require('./middleware/connect')
17+
const config = require('./config/config')
18+
const connect = require('./middleware/connect')
1919
const constants = require('./constants')
20-
var errors = require('./errors')
20+
const errors = require('./errors')
2121
const { InflightEventSet } = require('./InflightEventSet')
22-
var Instrumentation = require('./instrumentation')
23-
var { elasticApmAwsLambda } = require('./lambda')
24-
var Metrics = require('./metrics')
25-
var parsers = require('./parsers')
26-
var symbols = require('./symbols')
22+
const Instrumentation = require('./instrumentation')
23+
const { elasticApmAwsLambda } = require('./lambda')
24+
const Metrics = require('./metrics')
25+
const parsers = require('./parsers')
26+
const symbols = require('./symbols')
2727
const { frameCacheStats, initStackTraceCollection } = require('./stacktraces')
2828
const Span = require('./instrumentation/span')
2929
const Transaction = require('./instrumentation/transaction')
3030
const { isOTelMetricsFeatSupported, createOTelMeterProvider } = require('./opentelemetry-metrics')
31+
const { createApmClient } = require('./apm-client/apm-client')
3132

32-
var IncomingMessage = http.IncomingMessage
33-
var ServerResponse = http.ServerResponse
33+
const IncomingMessage = http.IncomingMessage
34+
const ServerResponse = http.ServerResponse
3435

35-
var version = require('../package').version
36+
const version = require('../package').version
3637

3738
// ---- Agent
3839

@@ -279,7 +280,7 @@ Agent.prototype.start = function (opts) {
279280
}
280281

281282
initStackTraceCollection()
282-
this._transport = this._conf.transport(this._conf, this)
283+
this._transport = createApmClient(this._conf, this)
283284

284285
let runContextClass
285286
if (this._conf.opentelemetryBridgeEnabled) {

lib/apm-client/apm-client.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and other contributors where applicable.
3+
* Licensed under the BSD 2-Clause License; you may not use this file except in
4+
* compliance with the BSD 2-Clause License.
5+
*/
6+
7+
'use strict'
8+
9+
const ElasticAPMHttpClient = require('elastic-apm-http-client')
10+
11+
const { CENTRAL_CONFIG_OPTS } = require('../config/schema')
12+
const { normalize } = require('../config/config')
13+
const logging = require('../logging')
14+
15+
const { NoopApmClient } = require('./noop-apm-client')
16+
const { getHttpClientConfig } = require('./http-apm-client')
17+
18+
/**
19+
* Returns an APM client suited for the configuration provided
20+
*
21+
* @param {Object} config The agent's configuration
22+
* @param {Object} agent The agents instance
23+
*/
24+
function createApmClient (config, agent) {
25+
if (config.disableSend || config.contextPropagationOnly) {
26+
return new NoopApmClient()
27+
} else if (typeof config.transport === 'function') {
28+
return config.transport(config, agent)
29+
}
30+
31+
const client = new ElasticAPMHttpClient(getHttpClientConfig(config, agent))
32+
33+
client.on('config', remoteConf => {
34+
agent.logger.debug({ remoteConf }, 'central config received')
35+
try {
36+
const conf = {}
37+
const unknown = []
38+
39+
for (const [key, value] of Object.entries(remoteConf)) {
40+
const newKey = CENTRAL_CONFIG_OPTS[key]
41+
if (newKey) {
42+
conf[newKey] = value
43+
} else {
44+
unknown.push(key)
45+
}
46+
}
47+
if (unknown.length > 0) {
48+
agent.logger.warn(`Central config warning: unsupported config names: ${unknown.join(', ')}`)
49+
}
50+
51+
if (Object.keys(conf).length > 0) {
52+
normalize(conf, agent.logger)
53+
for (const [key, value] of Object.entries(conf)) {
54+
const oldValue = agent._conf[key]
55+
agent._conf[key] = value
56+
if (key === 'logLevel' && value !== oldValue && !logging.isLoggerCustom(agent.logger)) {
57+
logging.setLogLevel(agent.logger, value)
58+
agent.logger.info(`Central config success: updated logger with new logLevel: ${value}`)
59+
}
60+
agent.logger.info(`Central config success: updated ${key}: ${value}`)
61+
}
62+
}
63+
} catch (err) {
64+
agent.logger.error({ remoteConf, err }, 'Central config error: exception while applying changes')
65+
}
66+
})
67+
68+
client.on('error', err => {
69+
agent.logger.error('APM Server transport error: %s', err.stack)
70+
})
71+
72+
client.on('request-error', err => {
73+
const haveAccepted = Number.isFinite(err.accepted)
74+
const haveErrors = Array.isArray(err.errors)
75+
let msg
76+
77+
if (err.code === 404) {
78+
msg = 'APM Server responded with "404 Not Found". ' +
79+
'This might be because you\'re running an incompatible version of the APM Server. ' +
80+
'This agent only supports APM Server v6.5 and above. ' +
81+
'If you\'re using an older version of the APM Server, ' +
82+
'please downgrade this agent to version 1.x or upgrade the APM Server'
83+
} else if (err.code) {
84+
msg = `APM Server transport error (${err.code}): ${err.message}`
85+
} else {
86+
msg = `APM Server transport error: ${err.message}`
87+
}
88+
89+
if (haveAccepted || haveErrors) {
90+
if (haveAccepted) msg += `\nAPM Server accepted ${err.accepted} events in the last request`
91+
if (haveErrors) {
92+
for (const error of err.errors) {
93+
msg += `\nError: ${error.message}`
94+
if (error.document) msg += `\n Document: ${error.document}`
95+
}
96+
}
97+
} else if (err.response) {
98+
msg += `\n${err.response}`
99+
}
100+
101+
agent.logger.error(msg)
102+
})
103+
104+
return client
105+
}
106+
107+
module.exports = {
108+
createApmClient
109+
}

lib/apm-client/http-apm-client.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and other contributors where applicable.
3+
* Licensed under the BSD 2-Clause License; you may not use this file except in
4+
* compliance with the BSD 2-Clause License.
5+
*/
6+
7+
'use strict'
8+
9+
const fs = require('fs')
10+
const version = require('../../package').version
11+
const logging = require('../logging')
12+
const { INTAKE_STRING_MAX_SIZE } = require('../config/schema')
13+
const { CloudMetadata } = require('../cloud-metadata')
14+
const { isLambdaExecutionEnvironment } = require('../lambda')
15+
const { isAzureFunctionsEnvironment, getAzureFunctionsExtraMetadata } = require('../instrumentation/azure-functions')
16+
17+
/**
18+
* Returns a HTTP client configuration based on agent configuration options
19+
*
20+
* @param {Object} conf The agent configuration object
21+
* @param {Object} agent
22+
* @returns {Object}
23+
*/
24+
function getHttpClientConfig (conf, agent) {
25+
let clientLogger = null
26+
if (!logging.isLoggerCustom(agent.logger)) {
27+
// https://www.elastic.co/guide/en/ecs/current/ecs-event.html#field-event-module
28+
clientLogger = agent.logger.child({ 'event.module': 'apmclient' })
29+
}
30+
const isLambda = isLambdaExecutionEnvironment()
31+
32+
const clientConfig = {
33+
agentName: 'nodejs',
34+
agentVersion: version,
35+
agentActivationMethod: agent._agentActivationMethod,
36+
serviceName: conf.serviceName,
37+
serviceVersion: conf.serviceVersion,
38+
frameworkName: conf.frameworkName,
39+
frameworkVersion: conf.frameworkVersion,
40+
globalLabels: maybePairsToObject(conf.globalLabels),
41+
hostname: conf.hostname,
42+
environment: conf.environment,
43+
44+
// Sanitize conf
45+
truncateKeywordsAt: INTAKE_STRING_MAX_SIZE,
46+
truncateLongFieldsAt: conf.longFieldMaxLength,
47+
// truncateErrorMessagesAt: see below
48+
49+
// HTTP conf
50+
secretToken: conf.secretToken,
51+
apiKey: conf.apiKey,
52+
userAgent: userAgentFromConf(conf),
53+
serverUrl: conf.serverUrl,
54+
serverCaCert: loadServerCaCertFile(conf.serverCaCertFile),
55+
rejectUnauthorized: conf.verifyServerCert,
56+
serverTimeout: conf.serverTimeout * 1000,
57+
58+
// APM Agent Configuration via Kibana:
59+
centralConfig: conf.centralConfig,
60+
61+
// Streaming conf
62+
size: conf.apiRequestSize,
63+
time: conf.apiRequestTime * 1000,
64+
maxQueueSize: conf.maxQueueSize,
65+
66+
// Debugging/testing options
67+
logger: clientLogger,
68+
payloadLogFile: conf.payloadLogFile,
69+
apmServerVersion: conf.apmServerVersion,
70+
71+
// Container conf
72+
containerId: conf.containerId,
73+
kubernetesNodeName: conf.kubernetesNodeName,
74+
kubernetesNamespace: conf.kubernetesNamespace,
75+
kubernetesPodName: conf.kubernetesPodName,
76+
kubernetesPodUID: conf.kubernetesPodUID
77+
}
78+
79+
// `service_node_name` is ignored in Lambda and Azure Functions envs.
80+
if (conf.serviceNodeName) {
81+
if (isLambda) {
82+
agent.logger.warn({ serviceNodeName: conf.serviceNodeName }, 'ignoring "serviceNodeName" config setting in Lambda environment')
83+
} else if (isAzureFunctionsEnvironment) {
84+
agent.logger.warn({ serviceNodeName: conf.serviceNodeName }, 'ignoring "serviceNodeName" config setting in Azure Functions environment')
85+
} else {
86+
clientConfig.serviceNodeName = conf.serviceNodeName
87+
}
88+
}
89+
90+
// Extra metadata handling.
91+
if (isLambda) {
92+
// Tell the Client to wait for a subsequent `.setExtraMetadata()` call
93+
// before allowing intake requests. This will be called by `apm.lambda()`
94+
// on first Lambda function invocation.
95+
clientConfig.expectExtraMetadata = true
96+
} else if (isAzureFunctionsEnvironment) {
97+
clientConfig.extraMetadata = getAzureFunctionsExtraMetadata()
98+
} else if (conf.cloudProvider !== 'none') {
99+
clientConfig.cloudMetadataFetcher = new CloudMetadata(conf.cloudProvider, conf.logger, conf.serviceName)
100+
}
101+
102+
if (conf.errorMessageMaxLength !== undefined) {
103+
// As of v10 of the http client, truncation of error messages will default
104+
// to `truncateLongFieldsAt` if `truncateErrorMessagesAt` is not specified.
105+
clientConfig.truncateErrorMessagesAt = conf.errorMessageMaxLength
106+
}
107+
108+
return clientConfig
109+
}
110+
111+
// Return the User-Agent string the agent will use for its comms to APM Server.
112+
//
113+
// Per https://github.com/elastic/apm/blob/main/specs/agents/transport.md#user-agent
114+
// the pattern is roughly this:
115+
// $repoName/$version ($serviceName $serviceVersion)
116+
//
117+
// The format of User-Agent is governed by https://datatracker.ietf.org/doc/html/rfc7231.
118+
// User-Agent = product *( RWS ( product / comment ) )
119+
// We do not expect `$repoName` and `$version` to have surprise/invalid values.
120+
// From `validateServiceName` above, we know that `$serviceName` is null or a
121+
// string limited to `/^[a-zA-Z0-9 _-]+$/`. However, `$serviceVersion` is
122+
// provided by the user and could have invalid characters.
123+
//
124+
// `comment` is defined by
125+
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 as:
126+
// comment = "(" *( ctext / quoted-pair / comment ) ")"
127+
// obs-text = %x80-FF
128+
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
129+
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
130+
//
131+
// `commentBadChar` below *approximates* these rules, and is used to replace
132+
// invalid characters with '_' in the generated User-Agent string. This
133+
// replacement isn't part of the APM spec.
134+
function userAgentFromConf (conf) {
135+
let userAgent = `apm-agent-nodejs/${version}`
136+
137+
// This regex *approximately* matches the allowed syntax for a "comment".
138+
// It does not handle "quoted-pair" or a "comment" in a comment.
139+
const commentBadChar = /[^\t \x21-\x27\x2a-\x5b\x5d-\x7e\x80-\xff]/g
140+
const commentParts = []
141+
if (conf.serviceName) {
142+
commentParts.push(conf.serviceName)
143+
}
144+
if (conf.serviceVersion) {
145+
commentParts.push(conf.serviceVersion.replace(commentBadChar, '_'))
146+
}
147+
if (commentParts.length > 0) {
148+
userAgent += ` (${commentParts.join(' ')})`
149+
}
150+
151+
return userAgent
152+
}
153+
154+
/**
155+
* Reads te server CA cert file and returns a buffer with its contents
156+
* @param {string | undefined} serverCaCertFile
157+
* @param {any} logger
158+
* @returns {Buffer}
159+
*/
160+
function loadServerCaCertFile (serverCaCertFile, logger) {
161+
if (serverCaCertFile) {
162+
try {
163+
return fs.readFileSync(serverCaCertFile)
164+
} catch (err) {
165+
logger.error('Elastic APM initialization error: Can\'t read server CA cert file %s (%s)', serverCaCertFile, err.message)
166+
}
167+
}
168+
}
169+
170+
function maybePairsToObject (pairs) {
171+
return pairs ? pairsToObject(pairs) : undefined
172+
}
173+
174+
function pairsToObject (pairs) {
175+
return pairs.reduce((object, [key, value]) => {
176+
object[key] = value
177+
return object
178+
}, {})
179+
}
180+
181+
module.exports = {
182+
getHttpClientConfig,
183+
userAgentFromConf
184+
}

lib/noop-transport.js renamed to lib/apm-client/noop-apm-client.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
'use strict'
88

9-
// A no-op (does nothing) Agent transport -- i.e. the APM server client API
9+
// A no-op (does nothing) APM Client -- i.e. the APM server client API
1010
// provided by elastic-apm-http-client.
1111
//
1212
// This is used for some configurations (when `disableSend=true` or when
1313
// `contextPropagationOnly=true`) and in some tests.
1414

15-
class NoopTransport {
15+
class NoopApmClient {
1616
config (opts) {}
1717

1818
addMetadataFilter (fn) {}
@@ -67,5 +67,5 @@ class NoopTransport {
6767
}
6868

6969
module.exports = {
70-
NoopTransport
70+
NoopApmClient
7171
}

0 commit comments

Comments
 (0)