How the APM agent for Node.js works?

Hello, I'm trying to add APM to a Ruby application running with Eventmachine. However, since the Ruby agent doesn't support Eventmachine, I'm trying to see what I can do to make it work.

The problem is that since it works based on an event-loop, it tries to create multiple transactions in the same thread, which is not possible for the Ruby implementation at the moment.

However, I think I can get some inspiration on how the APM agent works for Node.js and try to implement something similar for Ruby.

I wondered if there is something anywhere that could help me understand, or if there is someone here that could explain how they overcame this issue in the Node.js agent.

Great question @m-s-santos -- tracking async state is one of the harder problems we have to solve in the Elastic Node.js APM Agent.

Our basic strategy is to keep track of the "current" transaction. That is, the Instrumentation object has a currentTransaction property. Whenever we need to know what transaction to associate some work with, we reference this property.

How the agent manages to keep currentTransaction up to date is tricky -- in modern times Node.js provides a module named async_hooks which allows an end-user-programmer (in this case the agent is the end-user-programmer) to register a listener object that will fire whenever node is about to create a new async context, or is about to enter a new async context, or is finished with a particular async context.. We use this listener object to keep track of which transaction is associated with each async context, and the currentTransaction property is replaced with a dynamic getter that will return the correct transaction.

Without async-hooks (older versions of the Node.js agent, or if users disable it due to perf. concerns) it gets even trickier. We have a module named patch-async which attempts to wrap every function that may create a new async context. This wrapping includes calling the Instrumentation.bindFunction method (here's an example of this). The bindFunction method wraps the callback of a function that schedules async work. When Node calls that callback function, we swap the transaction. There's also some similar wrapping for native promises, and various places in individual module instrumentation where we need to wrap a callback that's not caught by patch-async. This works OK, but will occasionally result in lost or conflated transaction state if we fail to wrap and bind a callback that's performing async work.

So, at a very high level, it sounds like you'll need to figure out if EventMachine exposes an API that lets you keep track of when new contexts are created/entered/destroyed and/or manually patch/wrap the methods EventMachine's classes/objects to do the same. I hope that helps and good luck!

1 Like

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.