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

fix: rewrite ioredis instrumentation: run-context, add destination context, captureError #2460

Merged
merged 8 commits into from
Nov 24, 2021

Conversation

trentm
Copy link
Member

@trentm trentm commented Nov 19, 2021

  • 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.
  • No longer double-instrument queued ioredis commands.

Fixes: #2459
Refs: #2430

Checklist

…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
@trentm trentm self-assigned this Nov 19, 2021
@github-actions github-actions bot added the agent-nodejs Make available for APM Agents project planning. label Nov 19, 2021
@trentm
Copy link
Member Author

trentm commented Nov 19, 2021

This passed a full TAV=ioredis ./node_modules/.bin/tav on my machine using node v12.22.6.

@apmmachine
Copy link
Contributor

apmmachine commented Nov 19, 2021

💚 Build Succeeded

the below badges are clickable and redirect to their specific view in the CI or DOCS
Pipeline View Test View Changes Artifacts preview preview

Expand to view the summary

Build stats

  • Start Time: 2021-11-24T18:01:03.525+0000

  • Duration: 20 min 49 sec

  • Commit: f63efc0

Test stats 🧪

Test Results
Failed 0
Passed 22
Skipped 0
Total 22

🤖 GitHub comments

To re-run your PR in the CI, just comment with:

  • /test : Re-trigger the build.

  • run module tests for <modules> : Run TAV tests for one or more modules, where <modules> can be either a comma separated list of modules (e.g. memcached,redis) or the string literal ALL to test all modules

  • run benchmark tests : Run the benchmark test only.

  • run elasticsearch-ci/docs : Re-trigger the docs validation. (use unformatted text in the comment!)

@trentm trentm marked this pull request as ready for review November 20, 2021 01:09
@trentm trentm requested a review from astorm November 20, 2021 01:09
Comment on lines -15 to -18
agent.logger.debug('shimming ioredis.Command.prototype.initPromise')
shimmer.wrap(ioredis.Command && ioredis.Command.prototype, 'initPromise', wrapInitPromise)

if (!enabled) return ioredis
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

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"

Copy link
Contributor

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.
Copy link
Member Author

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:

Screen Shot 2021-11-19 at 12 38 51 PM

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 }
Copy link
Member Author

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*.
Copy link
Contributor

@astorm astorm left a 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.

Comment on lines -15 to -18
agent.logger.debug('shimming ioredis.Command.prototype.initPromise')
shimmer.wrap(ioredis.Command && ioredis.Command.prototype, 'initPromise', wrapInitPromise)

if (!enabled) return ioredis
Copy link
Contributor

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?

@trentm trentm requested a review from astorm November 23, 2021 21:49
Copy link
Contributor

@astorm astorm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 approving.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
agent-nodejs Make available for APM Agents project planning.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ioredis: offline-queued commands are double-instrumented
3 participants