If you’re not already familiar with Elastic APM, see my previous article “Solutions: Application Performance Monitoring/Management (APM) Practices.” Today I’ll use NodeJS as an example to show you how to customize Transactions and SPANS.

The Elastic APM agent for Node.js detects your application by grouping incoming HTTP requests into logical buckets. Each HTTP request is recorded in what we call a transaction. If you want to learn more about how NodeJS performs APM operations, see my previous article “Solutions: Providing APM functionality for NodeJS Microservices.” However, if your application is not regular HTTP server or database access, the Node.js agent will have no way of knowing when a transaction should start and end.

For example, if your application is a background job handler or only accepts Websockets, you need to manually start and end transactions. Likewise, if you want to track and time custom spans that happen in your Transactions application, you can add a new SPANS to an existing transaction.

 

The preparatory work

nodejs

The example I use today is at the address: github.com/liu-xiao-gu… . This is a NodeJS application. It provides the following two APIS:

  • /upload-avatar
  • /upload-photos

These two interfaces are used to upload one or more files to the server, respectively. Today, I’m going to show you how to customize transactions in the/upload-Avatar interface and use Elastic APM to track how long each code takes to execute.

 

Elastic Stack installation

In today’s test, we’ll use Docker to install the Elastic Stack. We’re going to start docker. Then download the required docker-comemage. yml file from the following address:

https://github.com/elastic/apm-contrib/tree/master/stack
Copy the code

After downloading docker-comemess. yml, enter the following command in the directory where docker-comemess. yml resides:

docker-compose up
Copy the code

Docker will now start Elasticsearch, Kibana and APM Server when it is fully enabled. We can launch Kibana in a browser:

 

Add custom Transactions and SPANS

We’ll start by opening the index.js file in nodejs:

index.js

// Add this to the VERY top of the first file loaded in your app var apm = require('elastic-apm-node').start({ // Override service name from package.json // Allowed characters: a-z, A-Z, 0-9, -, _, and space serviceName: 'fileupload2', // Use if APM Server requires a token secretToken: '', // Set custom APM Server URL (default: http://localhost:8200) serverUrl: '' }) const express = require('express'); const fileUpload = require('express-fileupload'); const cors = require('cors'); const bodyParser = require('body-parser'); const morgan = require('morgan'); const _ = require('lodash'); const app = express(); // enable files upload app.use(fileUpload({ createParentPath: true })); //add other middleware app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); app.use(morgan('dev')); //start app const port = process.env.PORT || 3000; app.get('/', function (req, res) { res.send('This is so cool! ') }) function runSubTask (name, type, cb) { console.log("Staring span: " + name); var span = apm.startSpan(name) setTimeout(function () { if (span) { console.log("ending span"); span.end() } cb() }, Math.random() * 1000).unref() } app.post('/upload-avatar', async (req, res) => { var name = 'upload-avatar'; var type = 'avatar'; console.log("Starting transaction"); var transaction = apm.startTransaction(name, type); try { if(! req.files) { res.send({ status: false, message: 'No file uploaded' }); } else { console.log("Running fake span"); // Simulate a task doing something runSubTask("fake span", type, function() { //Use the name of the input field (i.e. "avatar") to retrieve the uploaded file let avatar = req.files.avatar; console.log("Staring span: Saving pictures"); var span = apm.startSpan('Saving pictures') //Use the mv() method to place the file in upload directory (i.e. "uploads")  avatar.mv('./uploads/' + avatar.name); if (span) { console.log("ending span"); span.end() } //send response res.send({ status: true, message: 'File is uploaded', data: { name: avatar.name, mimetype: avatar.mimetype, size: avatar.size } }); if (transaction) { console.log("end transaction"); transaction.end(); } }) } } catch (err) { res.status(500).send(err); console.log("Capturing error"); apm.captureError(err); }}); app.post('/upload-photos', async (req, res) => { try { if(! req.files) { res.send({ status: false, message: 'No file uploaded' }); } else { let data = []; //loop all files _.forEach(_.keysIn(req.files.photos), (key) => { let photo = req.files.photos[key]; //move photo to uploads directory photo.mv('./uploads/' + photo.name); //push file details data.push({ name: photo.name, mimetype: photo.mimetype, size: photo.size }); }); //return response res.send({ status: true, message: 'Files are uploaded', data: data }); } } catch (err) { res.status(500).send(err); apm.captureError(err); }}); app.listen(port, () => console.log(`App is listening on port ${port}.`) );Copy the code

The code is actually quite simple. In order for Elastic APM to APM our NodeJS application, we first need to add the following section, which must appear at the beginning of the index.js:

// Add this to the VERY top of the first file loaded in your app
var apm = require('elastic-apm-node').start({
    // Override service name from package.json
    // Allowed characters: a-z, A-Z, 0-9, -, _, and space
    serviceName: 'fileupload2',
  
    // Use if APM Server requires a token
    secretToken: '',
  
    // Set custom APM Server URL (default: http://localhost:8200)
    serverUrl: ''
  })
Copy the code

Here, we define a service name called Fileupload2. This will be seen in the APM interface.

To be able to simulate code that runs a task, I create a method like this:


function runSubTask (name, type, cb) {
    console.log("Staring span: " + name);
    var span = apm.startSpan(name)
    setTimeout(function () {
        if (span) {
            console.log("ending span");
            span.end()
        }
        cb()
    }, Math.random() * 1000).unref()
}
Copy the code

When this code is executed, it is delayed for a while and cb is called back. In the code above, it also uses apm.startSpan() to create a custom span. This will be used to measure how long the code takes to execute.

Let’s look at the following code:

app.post('/upload-avatar', async (req, res) => { var name = 'upload-avatar'; var type = 'avatar'; console.log("Starting transaction"); var transaction = apm.startTransaction(name, type); try { if(! req.files) { res.send({ status: false, message: 'No file uploaded' }); } else { console.log("Running fake span"); // Simulate a task doing something runSubTask("fake span", type, function() { //Use the name of the input field (i.e. "avatar") to retrieve the uploaded file let avatar = req.files.avatar; console.log("Staring span: Saving pictures"); var span = apm.startSpan('Saving pictures') //Use the mv() method to place the file in upload directory (i.e. "uploads")  avatar.mv('./uploads/' + avatar.name); if (span) { console.log("ending span"); span.end() } //send response res.send({ status: true, message: 'File is uploaded', data: { name: avatar.name, mimetype: avatar.mimetype, size: avatar.size } }); if (transaction) { console.log("end transaction"); transaction.end(); } }) } } catch (err) { res.status(500).send(err); console.log("Capturing error"); apm.captureError(err); }});Copy the code

When the excuse/upload-Avatar is called, we use the following method:

    var name = 'upload-avatar';
    var type = 'avatar';
    console.log("Starting transaction");
    var transaction = apm.startTransaction(name, type);
Copy the code

To create a transaction. We create another span in the following code:

runSubTask("fake span", type, function() { //Use the name of the input field (i.e. "avatar") to retrieve the uploaded file let avatar = req.files.avatar; console.log("Staring span: Saving pictures"); var span = apm.startSpan('Saving pictures') //Use the mv() method to place the file in upload directory (i.e. "uploads")  avatar.mv('./uploads/' + avatar.name); if (span) { console.log("ending span"); span.end() } //send response res.send({ status: true, message: 'File is uploaded', data: { name: avatar.name, mimetype: avatar.mimetype, size: avatar.size } }); if (transaction) { console.log("end transaction"); transaction.end(); }Copy the code

In the section where I uploaded files, I also created a span called Saving Pictures. This also allows me to measure how long it takes to save the file. After the file is uploaded, we call:

                if (transaction) {
                    console.log("end transaction");
                    transaction.end();
                }  
Copy the code

To end this transaction. In this exercise, I didn’t modify the interface //upload-photos. I’ll leave that exercise to you.

Next, we can start running our code:

npm install
node index.js
Copy the code

Nodejs runs on port 3000.

This way, we can see it in the APM interface in Kibana:

We can see that a service called Fileupload2 appears.

To call /upload-avatar, use Postman to access the /upload-avatar interface:

We can select our image and click the Send button. In the nodejs interface, we can see:

Our code has been executed correctly and the image has been uploaded successfully.

Let’s go back to the Elastic APM interface and hit Fileupload2:

We can see a transaction called upload-Avatar:

This is obviously the name of the transaction we defined in NodeJS earlier. Click on the upload-Avatar hyperlink above:

Fake Span Saving Pictures Their respective execution times are also shown above.

 

conclusion

In today’s exercise, we showed how to customize our transactions and spans for our NodeJS. This situation can be used for performance monitoring for non-HTTP requests or database access. There are many benefits to optimizing our application. Of course, Elastic APM also has customization mechanisms for other languages. Please refer to Elastic’s technical documentation.

Reference:

【 1 】 www.elastic.co/guide/en/ap…

(2) www.elastic.co/guide/en/ap…