Elastic APM not instrumenting nested routes in separate files on Express server

Problem:
I am using Elastic APM to monitor Express application. However, APM is not able to instrument any HTTP requests when nested routing is implemented across multiple files. It only works when all routes are defined in the same file (app.js).

Actual Behaviour:
No transaction data is being collected for any HTTP requests handled by the nested routes, which limits the visibility and performance insights.

Expected Behaviour:
I want Elastic APM to instrument all HTTP requests, regardless of whether the routes are defined in the same file or across multiple files.

How do I configure Elastic APM to properly instrument nested routes in separate files on my Express server? Is there a specific configuration or workaround I need to implement?

Please note: Due to separation of concerns, we cannot define all routes in the same file (app.js). We need a solution that works with our current code structure.

Code Structure (to reproduce the problem):
app.js: Initializes APM and sets up the Express app.
outerRouter.js: Defines a router and adds it to the app.
innerRouter.js: Defines nested routes and exports the router.

App.js

const apm = require('elastic-apm-node').start({
   secretToken: 'nsdiand',
   serverUrl: 'http://apm-server.domain.com',
   environment: 'production'
 })


import { init } from './outerRouter';


const mysql = require('mysql');
const express = require('express');
const app = express();
const port = 3000;


init(app,express)

// Start the server
app.listen(port, () => {
 console.log(`Server is running on http://localhost:${port}`);
});

outerRouter.js

const { default: newRouter } = require("./newRouter");

export function init(app, express){
   const router=express.Router();
   app.use(router);
   router.use(newRouter);
}

innerRouter.js

const express = require('express');
const newRouter = express.Router();


newRouter.get('/nestedroute', function (req, res, next) {
   res.send("test string")
})
export default newRouter

Version information:

"elastic-apm-node": "^4.0.
"express": "^4.16.4",
"node" : 16.19.1

**Kibana version**: 8.8.1
**Elasticsearch version**: 8.8.0
**APM Server version**: 8.8.1

We've already tried the following:

  1. Manually setting the transaction using apm.startTransaction - didn't work.
  2. Loading the APM module before application starts using -r elastic-apm-node/start.js command line option - didn't work.
  3. apm.middleware.connect() for every nested router. -did'nt worked
  4. Initializing APM in a separate file and import the APM instance from apm.js in nested router files.

I would appreciate any help and suggestions on how to resolve this issue.

@HemdeepSaini Hello, thanks for the question. I was trying to reproduce with the example code that you gave, but there are a few issues in the example code:

There is a mix of require (indicating CommonJS code) and import (indicating ESM code) usage. And there is require usage in files that use the export keyword. Is your project written in TypeScript? Or possibly is your code written in JavaScript, but using ES Modules (Modules: ECMAScript modules | Node.js v21.4.0 Documentation)?

If your project is TypeScript, are you able to show the compiled JavaScript that is being executed? Depending on tsconfig.json settings, there could be something that is surprising or breaking the APM agent.

When I made the following edits to your example files:

% cat app.js
const apm = require('elastic-apm-node').start({
  // ...
})
const { init } = require('./outerRouter');
const express = require('express');
const app = express();
const port = 3000;
init(app,express)
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});

% cat outerRouter.js
const newRouter = require("./innerRouter");
function init(app, express){
   const router=express.Router();
   app.use(router);
   router.use(newRouter);
}
module.exports = {
  init
};

% cat innerRouter.js
const express = require('express');
const newRouter = express.Router();
newRouter.get('/nestedroute', function (req, res, next) {
   res.send("test string")
})
module.exports = newRouter;

and then ran node app.js and called curl http://localhost:3000/nestedroute, the APM agent generated a transaction with the name "GET /nestedroute", as expected.

Hi @trentm,

Thanks for your response. My code is transpiled to commonjs code during build time using Webpack and Babel. I think that may not be the issue.

Secondly, I have tried the minor changes you provided above, still the problem is not resolved.

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