Skip to content

Commit 334d7ef

Browse files
committed
changes to S3 instrumentation so that its parent/child relationship works (see examples/parent-child.js case)
We will have to do the same for other aws-sdk, for ES, and evaluate for other instrumentations. Note this is an improvement over current behaviour which doesn't handle cases like the parent/child reln above anyway.
1 parent e777572 commit 334d7ef

File tree

5 files changed

+130
-73
lines changed

5 files changed

+130
-73
lines changed

examples/parent-child.js

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// vim: set ts=2 sw=2:
22

3-
var apm = require('./').start({ // elastic-apm-node
3+
var apm = require('../').start({ // elastic-apm-node
44
serviceName: 'parent-child',
55
captureExceptions: false,
66
logUncaughtExceptions: true,
@@ -14,6 +14,7 @@ var apm = require('./').start({ // elastic-apm-node
1414
// disableSend: true
1515
})
1616

17+
const assert = require('assert').strict
1718
const express = require('express')
1819

1920
const app = express()
@@ -39,10 +40,6 @@ app.get('/a', (req, res) => {
3940
}, 10)
4041
})
4142

42-
setTimeout(function () {
43-
console.warn('XXX in unrelated 3s timeout: currTx is: ', apm._instrumentation.currTx())
44-
}, 3000)
45-
4643
app.get('/b', (req, res) => {
4744
var s1 = apm.startSpan('s1')
4845
s1.end()
@@ -130,28 +127,79 @@ app.get('/unended-span', (req, res) => {
130127
// HTTP span
131128
// a-sibling-span
132129
//
133-
// Perhaps this is fine?
130+
// Perhaps this is fine? Nope, it isn't.
131+
//
134132
app.get('/dario-1963', (req, res) => {
133+
const { Client } = require('@elastic/elasticsearch')
134+
const client = new Client({
135+
node: 'http://localhost:9200',
136+
auth: { username: 'admin', password: 'changeme' }
137+
})
138+
// Note: works fine if client.search is under a setImmediate for sep context.
139+
// setImmediate(function () { ... })
135140
client.search({
136-
index: 'kibana_sample_data_logs',
141+
// index: 'kibana_sample_data_logs',
137142
body: { size: 1, query: { match_all: {} } }
138143
}, (err, _result) => {
144+
console.warn('XXX in client.search cb: %s', apm._instrumentation._runCtxMgr.active())
139145
if (err) {
140146
res.send(err)
141147
} else {
142148
res.send('ok')
143149
}
144150
})
145151

152+
console.warn('XXX after client.search sync: %s', apm._instrumentation._runCtxMgr.active())
153+
146154
// What if I add this?
147-
setImmediate(function () {
155+
setImmediate(function aSiblingSpanInHere () {
148156
var span = apm.startSpan('a-sibling-span')
149157
setImmediate(function () {
150158
span.end()
151159
})
152160
})
153161
})
154162

163+
// Want:
164+
// transaction "GET /s3"
165+
// `- span "span1"
166+
// `- span "S3 ListBuckets"
167+
// `- span "GET s3.amazonaws.com/"
168+
// `- span "span3"
169+
// `- span "span2"
170+
//
171+
// Eventually the HTTP span should be removed as exit spans are supported.
172+
app.get('/s3', (req, res) => {
173+
const AWS = require('aws-sdk')
174+
const s3Client = new AWS.S3({ apiVersion: '2006-03-01' })
175+
176+
setImmediate(function () {
177+
var s1 = apm.startSpan('span1')
178+
179+
s3Client.listBuckets({}, function (err, _data) {
180+
if (err) {
181+
res.send(err)
182+
} else {
183+
res.send('ok')
184+
}
185+
s1.end()
186+
})
187+
assert(apm._instrumentation.currSpan() === s1)
188+
189+
setImmediate(function () {
190+
assert(apm._instrumentation.currSpan() === s1)
191+
var s2 = apm.startSpan('span2')
192+
setImmediate(function () {
193+
s2.end()
194+
})
195+
})
196+
197+
assert(apm._instrumentation.currSpan() === s1)
198+
var s3 = apm.startSpan('span3')
199+
s3.end()
200+
})
201+
})
202+
155203
app.listen(port, function () {
156204
console.log(`listening at http://localhost:${port}`)
157205
})

lib/instrumentation/generic-span.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function GenericSpan (agent, ...args) {
2525
// console.warn('XXX new GenericSpan traceContext: ', this._traceContext.toTraceParentString(), this._traceContext.toTraceStateString())
2626

2727
// XXX change this var name to _traceContext.
28+
console.warn('XXX new GenericSpan: opts.childOf=', opts.childOf && (opts.childOf.constructor.name + ' ' + opts.childOf.name))
2829
this._context = TraceContext.startOrResume(opts.childOf, agent._conf, opts.tracestate)
2930

3031
this._agent = agent

lib/instrumentation/http-shared.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ exports.traceOutgoingRequest = function (agent, moduleName, method) {
134134
// shimmer to expose access to the original via a method)? Invoke
135135
// in a separate no-op RunContext???
136136
console.warn('XXX traceOutgoingRequest: start')
137-
// console.warn('XXX curr span:', agent._instrumentation.currentSpan?.name) // considers 'bindingSpan'
138137

139138
// TODO: See if we can delay the creation of span until the `response`
140139
// event is fired, while still having it have the correct stack trace
@@ -178,6 +177,7 @@ exports.traceOutgoingRequest = function (agent, moduleName, method) {
178177
var req = orig.apply(this, newArgs)
179178
if (!span) return req
180179

180+
// XXX
181181
if (getSafeHost(req) === agent._conf.serverHost) {
182182
agent.logger.debug('ignore %s request to intake API %o', moduleName, { id: id })
183183
return req

lib/instrumentation/modules/aws-sdk/s3.js

Lines changed: 71 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -52,76 +52,83 @@ function instrumentationS3 (orig, origArguments, request, AWS, agent, { version,
5252
name += ' ' + resource
5353
}
5454

55-
const span = agent.startSpan(name, TYPE, SUBTYPE, opName)
56-
if (span) {
57-
request.on('complete', function onComplete (response) {
58-
// `response` is an AWS.Response
59-
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Response.html
60-
61-
// Determining the bucket's region.
62-
// `request.httpRequest.region` isn't documented, but the aws-sdk@2
63-
// lib/services/s3.js will set it to the bucket's determined region.
64-
// This can be asynchronously determined -- e.g. if it differs from the
65-
// configured service endpoint region -- so this won't be set until
66-
// 'complete'.
67-
const region = request.httpRequest && request.httpRequest.region
68-
69-
// Destination context.
70-
// '.httpRequest.endpoint' might differ from '.service.endpoint' if
71-
// the bucket is in a different region.
72-
const endpoint = request.httpRequest && request.httpRequest.endpoint
73-
const destContext = {
74-
service: {
75-
name: SUBTYPE,
76-
type: TYPE
77-
}
78-
}
79-
if (endpoint) {
80-
destContext.address = endpoint.hostname
81-
destContext.port = endpoint.port
82-
}
83-
if (resource) {
84-
destContext.service.resource = resource
55+
const ins = agent._instrumentation
56+
const span = ins.currTx().startSpan(name, TYPE, SUBTYPE, opName)
57+
if (!span) {
58+
return orig.apply(request, origArguments)
59+
}
60+
61+
function onComplete (response) {
62+
// `response` is an AWS.Response
63+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Response.html
64+
65+
// Determining the bucket's region.
66+
// `request.httpRequest.region` isn't documented, but the aws-sdk@2
67+
// lib/services/s3.js will set it to the bucket's determined region.
68+
// This can be asynchronously determined -- e.g. if it differs from the
69+
// configured service endpoint region -- so this won't be set until
70+
// 'complete'.
71+
const region = request.httpRequest && request.httpRequest.region
72+
73+
// Destination context.
74+
// '.httpRequest.endpoint' might differ from '.service.endpoint' if
75+
// the bucket is in a different region.
76+
const endpoint = request.httpRequest && request.httpRequest.endpoint
77+
const destContext = {
78+
service: {
79+
name: SUBTYPE,
80+
type: TYPE
8581
}
86-
if (region) {
87-
destContext.cloud = { region }
82+
}
83+
if (endpoint) {
84+
destContext.address = endpoint.hostname
85+
destContext.port = endpoint.port
86+
}
87+
if (resource) {
88+
destContext.service.resource = resource
89+
}
90+
if (region) {
91+
destContext.cloud = { region }
92+
}
93+
span.setDestinationContext(destContext)
94+
95+
if (response) {
96+
// Follow the spec for HTTP client span outcome.
97+
// https://github.com/elastic/apm/blob/master/specs/agents/tracing-instrumentation-http.md#outcome
98+
//
99+
// For example, a S3 GetObject conditional request (e.g. using the
100+
// IfNoneMatch param) will respond with response.error=NotModifed and
101+
// statusCode=304. This is a *successful* outcome.
102+
const statusCode = response.httpResponse && response.httpResponse.statusCode
103+
if (statusCode) {
104+
span._setOutcomeFromHttpStatusCode(statusCode)
105+
} else {
106+
// `statusCode` will be undefined for errors before sending a request, e.g.:
107+
// InvalidConfiguration: Custom endpoint is not compatible with access point ARN
108+
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
88109
}
89-
span.setDestinationContext(destContext)
90-
91-
if (response) {
92-
// Follow the spec for HTTP client span outcome.
93-
// https://github.com/elastic/apm/blob/master/specs/agents/tracing-instrumentation-http.md#outcome
94-
//
95-
// For example, a S3 GetObject conditional request (e.g. using the
96-
// IfNoneMatch param) will respond with response.error=NotModifed and
97-
// statusCode=304. This is a *successful* outcome.
98-
const statusCode = response.httpResponse && response.httpResponse.statusCode
99-
if (statusCode) {
100-
span._setOutcomeFromHttpStatusCode(statusCode)
101-
} else {
102-
// `statusCode` will be undefined for errors before sending a request, e.g.:
103-
// InvalidConfiguration: Custom endpoint is not compatible with access point ARN
104-
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
105-
}
106-
107-
if (response.error && (!statusCode || statusCode >= 400)) {
108-
agent.captureError(response.error, { skipOutcome: true })
109-
}
110+
111+
if (response.error && (!statusCode || statusCode >= 400)) {
112+
agent.captureError(response.error, { skipOutcome: true })
110113
}
114+
}
111115

112-
// Workaround a bug in the agent's handling of `span.sync`.
113-
//
114-
// The bug: Currently this span.sync is not set `false` because there is
115-
// an HTTP span created (for this S3 request) in the same async op. That
116-
// HTTP span becomes the "active span" for this async op, and *it* gets
117-
// marked as sync=false in `before()` in async-hooks.js.
118-
span.sync = false
119-
120-
span.end()
121-
})
116+
// Workaround a bug in the agent's handling of `span.sync`.
117+
//
118+
// The bug: Currently this span.sync is not set `false` because there is
119+
// an HTTP span created (for this S3 request) in the same async op. That
120+
// HTTP span becomes the "active span" for this async op, and *it* gets
121+
// marked as sync=false in `before()` in async-hooks.js.
122+
span.sync = false
123+
124+
span.end()
122125
}
123126

124-
return orig.apply(request, origArguments)
127+
// Derive a new run context from the current one for this span. Then run
128+
// the AWS.Request.send and a 'complete' event handler in that run context.
129+
const runContext = ins._runCtxMgr.active().enterSpan(span) // XXX I don't like `enterSpan` name here, perhaps `newWithSpan()`?
130+
request.on('complete', ins._runCtxMgr.bind(runContext, onComplete))
131+
return ins._runCtxMgr.with(runContext, orig, request, ...origArguments)
125132
}
126133

127134
module.exports = {

lib/instrumentation/span.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ util.inherits(Span, GenericSpan)
1717

1818
function Span (transaction, name, ...args) {
1919
const parent = transaction._agent._instrumentation.currSpan() || transaction
20+
console.warn('XXX new Span(name=%s, args=%s): parent=', name, args, parent.constructor.name, parent.name, parent.ended ? '.ended' : '')
2021
const opts = typeof args[args.length - 1] === 'object'
2122
? (args.pop() || {})
2223
: {}

0 commit comments

Comments
 (0)