-
Notifications
You must be signed in to change notification settings - Fork 227
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
fix: rewrite ioredis instrumentation: run-context, add destination context, captureError #2460
Conversation
…ntext, captureError - This fixes run-context handling for proper parent/child relationships. The run-context in code in the same tick and the optional callback are no longer changed. - Add destination context, so ioredis usage shows redis on the Service Map. - Capture an error and set the span outcome to "failure" if the command errors. Refs: #2430
This passed a full |
💚 Build Succeeded
Expand to view the summary
Build stats
Test stats 🧪
🤖 GitHub commentsTo re-run your PR in the CI, just comment with:
|
agent.logger.debug('shimming ioredis.Command.prototype.initPromise') | ||
shimmer.wrap(ioredis.Command && ioredis.Command.prototype, 'initPromise', wrapInitPromise) | ||
|
||
if (!enabled) return ioredis |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
REVIEW NOTE: This update of the ioredis instrumentation no longer wraps Command.prototype.initPromise
; it still wraps Redis.prototype.sendCommand
. The only utility of having wrapped initPromise
before was something about "continuation patches" that may have been necessary in the past. This is why the previous instrumentation still wrapped initPromise
even if enabled=false
(from disableInstrumentations
use). I believe this is no longer necessary after the run-context work. See #2444 (a draft PR where I intend to remove this "half-disabling" of instrumentations) for some more discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is no longer necessary after the run-context work.
Does remove the initPromise
wrapping have any effect on the spans/span-tree if someone's using the patch-async
/asyncHooks:false
? If so are you OK with changing that functionality and/or should we mention it somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does remove the initPromise wrapping have any effect on the spans/span-tree if someone's using the patch-async /asyncHooks:false?
No, I don't believe it has any effect. The removed wrappedInitPromise didn't do any promise-y context propagation or binding. My quoting of "continuation patches" was from this single comment on the change that added the "enabled" flag to instrumentations: #353 (comment) It provides no more details.
The test suite passes with asyncHooks: false
. Also the parent/child output from ELASTIC_APM_ASYNC_HOOKS=false node examples/trace-ioredis.js
is as expected:
transaction 035eaa "t1"
`- span 46aff5 "SET"
`- span 4824ad "GET"
`- span 77b874 "GET"
`- span 0d4192 "MULTI"
`- span 537be3 "SET"
`- span 35b9c7 "GET"
`- span 3a4fa9 "EXEC"
`- span 6610bb "HSET"
`- span 21c383 "GET"
`- error 579426 "WRONGTYPE Operation against a key holding the wrong kind of value"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
// wrappedSendCommand, but the finally call will not get fired until the tick | ||
// after the command.callback have fired, so if the transaction is ended in | ||
// the same tick as the call to command.callback, we'll lose the last span | ||
// as it hasn't yet ended. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
REVIEW NOTE: In ioredis, sendCommand(cmd)
is called with a Command
object that always has a .promise
and can optionally have a user-supplied .callback
function. When the command is complete: (a) the callback is called, if any, and (b) the promise is resolved or rejected. Because this is a Promise that resolve/reject is in the next tick.
A change this update makes is to only instrument the promise. The code before this would instrument both, and span.end()
on the first one. This means that a slight behaviour change with this update is that when a user-supplied callback is called, then span is not yet ended. It will be ended in the next tick with the promise. Not that the run-context fixes in this change mean that the span is intentionally not the currentSpan
in the user-callback, so the slight change on end time doesn't have any impact that I can see.
The comment above about "we'll lose the last span" is wrong: the Elastic APM system can handle a trace with a span ending after the transaction ends just fine. It looks like this:
I like that the code is less complex: only needing to command.promise.then()
to handle span.end()
.
setTimeout(function () { | ||
t.notOk(unhandledRejection) | ||
t.end() | ||
}, 0) | ||
}) | ||
agent.captureError = function (err) { throw err } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
REVIEW NOTE: This and the agent.captureError =
override below were removed because they conflict with testing now that the ioredis instrumentation is actually using agent.captureError()
.
…to the span (i.e. it is a child of the span). Before this it was setting parent_id to the *transaction*.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks ok -- does what it says on the tin. One question below before signing off.
agent.logger.debug('shimming ioredis.Command.prototype.initPromise') | ||
shimmer.wrap(ioredis.Command && ioredis.Command.prototype, 'initPromise', wrapInitPromise) | ||
|
||
if (!enabled) return ioredis |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is no longer necessary after the run-context work.
Does remove the initPromise
wrapping have any effect on the spans/span-tree if someone's using the patch-async
/asyncHooks:false
? If so are you OK with changing that functionality and/or should we mention it somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 approving.
The run-context in code in the same tick and the optional callback are
no longer changed.
errors.
Fixes: #2459
Refs: #2430
Checklist