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

feat: instrument example #280

Merged
merged 12 commits into from
Aug 13, 2024
1 change: 1 addition & 0 deletions custom-instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

This folder contains example applications using the [Agent API](https://newrelic.github.io/node-newrelic/API.html) to do custom instrumentation.

* [instrument](./instrument) - example application that uses the [newrelic.instrument API](https://newrelic.github.io/node-newrelic/API.html#instrument) and associated [shim API](https://newrelic.github.io/node-newrelic/Shim.html) to instrument a toy queue library called Job Queue
* [instrumentMessages](./instrument-messages) - example application that uses the [newrelic.instrumentMessages API](https://newrelic.github.io/node-newrelic/API.html#instrumentMessages) and associated [messaging shim API](https://newrelic.github.io/node-newrelic/MessageShim.html) to instrument a toy messaging library called Nifty Messages
* [instrumentWebframework](./instrument-webframework) - example application that uses the [newrelic.instrumentWebframework API](https://newrelic.github.io/node-newrelic/API.html#instrumentWebframework) and associated [WebFramework shim API](https://newrelic.github.io/node-newrelic/WebFrameworkShim.html) to instrument a hypothetical web framework
* [attributesAndEvents](./attributes-and-events) - example application that demonstrates how to share custom [attributes](https://newrelic.github.io/node-newrelic/API.html#addCustomAttribute) and [events](https://newrelic.github.io/node-newrelic/API.html#recordCustomEvent)
Expand Down
44 changes: 44 additions & 0 deletions custom-instrumentation/instrument/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Example instrumentation of a basic app
amychisholm03 marked this conversation as resolved.
Show resolved Hide resolved

This example application shows you how to use the [newrelic.instrument](https://newrelic.github.io/node-newrelic/API.html#instrument) and associated [shim](https://newrelic.github.io/node-newrelic/Shim.html) API. It instruments a simple module, a rudimentary job queue (`jobQueue`), and runs a series of basic jobs.

## Getting Started

1. Clone or fork this repository.
2. Navigate to this example's sub directory
```
cd newrelic-node-examples/custom-instrumentation/instrument
```
3. Install dependencies and run application.
```
npm install
cp env.sample .env
# Fill out `NEW_RELIC_LICENSE_KEY` in .env and save
# Start the application
npm start
```
4. The app will automatically start adding example jobs to a queue and run them. You should see the following in the console when the instrumentation takes place.
```
[NEWRELIC] instrumenting 'job-queue'
Callback job done
Promise job done
```

## Exploring Telemetry

1. After a few minutes, you should be able to see `job-queue` instrumented in New Relic. From the dashboard, navigate to 'APM & Services' and then select the 'Example Job Queue App' entity.
2. Then select 'Distributed tracing'. You should see the trace groups `firstTransacation`, `secondTransaction`, and `thirdTransaction`. Inside these groups will be our custom instrumentation. Select any trace group and then select a single trace.
3. Under `firstTransaction` or `secondTransaction`, toggle 'Show in-process spans' and you will see 'scheduleJob - job'; this shows the name of which job was recorded. Under `thirdTransaction`, you will see `queue.runJobs` instrumented as 'runJobs'.

![1722869774240](./image/README/1722869774240.png)

![1722869857647](./image/README/1722869857647.png)

## Description

This application consists of the following files:

* `index.js`: a simple app that schedules jobs and runs them
* `job-queue.js`: an example module that provides a queue class that you can use to run and schedule jobs
* `instrumentation.js`: all of the New Relic instrumentation is in here; the `npm start` command makes sure this module is loaded first
* `newrelic.js`: a basic, sample New Relic configurartion
1 change: 1 addition & 0 deletions custom-instrumentation/instrument/env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEW_RELIC_LICENSE_KEY=
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions custom-instrumentation/instrument/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const newrelic = require('newrelic')
const Queue = require('./job-queue')

function exampleJob() {
// Do whatever work you want here - this is just a placeholder
return 'job done';
}

function cbJob(cb) {
const result = exampleJob();
return cb('Callback ' + result);
}

async function promiseJob() {
return new Promise((resolve, reject) => {
try {
const result = exampleJob()
resolve('Promise ' + result)
}
catch (error) {
reject(error)
}
})
}

function main() {
const queue = new Queue()

// We will be creating our transactions with startBackgroundTransaction
// because this application does not utilize frameworks New Relic already
// instruments. Thus, transactions are not automatically created for use.
// If you are already operating within an instrumented framework, you may
// omit the startBackgroundTransaction wrapper.

// Without instrumentation, executing this code will cause 'firstTransaction'
// to be the active transaction in both 'firstJob' and 'secondJob'. This is
// not the intended behavior. If we instrument queue.scheduleJob, the
// functions will be correctly placed within their respective transactions.
newrelic.startBackgroundTransaction('firstTransaction', function () {
const transaction = newrelic.getTransaction()
queue.scheduleJob(async function firstJob() {
// Do some work - for example, this waits for a promise job to complete
const result = await promiseJob()
console.log(result)
transaction.end()
})
})

newrelic.startBackgroundTransaction('secondTransaction', function () {
const transaction = newrelic.getTransaction()
queue.scheduleJob(function secondJob() {
// Do some work - for example, this waits for a callback job to complete
cbJob(function cb(result) {
console.log(result)
transaction.end()
})
})
})

// Wait for the jobs to be added to the queue and then run them
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

sleep(1000).then(() =>
newrelic.startBackgroundTransaction('thirdTransaction', function () {
const transaction = newrelic.getTransaction()
queue.runJobs()
transaction.end()
// Finally shutdown the agent so it properly flushes all data
newrelic.shutdown({ collectPendingData: true }, () => process.exit(0))
})
)
}

main()
50 changes: 50 additions & 0 deletions custom-instrumentation/instrument/instrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const newrelic = require('newrelic')
const queuePath = require.resolve('./job-queue')

newrelic.instrument({
// The absolute path to the required module
absolutePath: queuePath,
// The module's name
moduleName: 'job-queue',
// The function that will be called once the module is required
onRequire: instrumentMyJobQueue,
// The function that will be called if the instrumentation fails
onError: function onError(err) {
// Uh oh! Our instrumentation failed, let's see why:
console.error(err.message, err.stack)

// Let's kill the application when debugging so we don't miss it.
process.exit(-1)
}
})

function instrumentMyJobQueue(shim, myModule, moduleName) {
console.log(`[NEWRELIC] instrumenting ${moduleName}`)

const proto = myModule.prototype;

shim.record(proto, 'scheduleJob',
function (shim, func, name, args) {
const job = args[0];
return new shim.specs.RecorderSpec({
name: `scheduleJob - ${job.name}`,
callback: shim.LAST
})
}
)
shim.record(proto, 'runJobs',
function (shim, func, name, args) {
return new shim.specs.RecorderSpec({
name: 'runJobs',
callback: shim.LAST
})
}
)
}
27 changes: 27 additions & 0 deletions custom-instrumentation/instrument/job-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

function Queue() {
this.jobs = []
}

Queue.prototype.runJobs = function run() {
const jobs = this.jobs
while (jobs.length) {
const job = jobs.pop()
job()
}
}

Queue.prototype.scheduleJob = function scheduleJob(job) {
const jobs = this.jobs
process.nextTick(function () {
jobs.push(job)
})
}

module.exports = Queue
52 changes: 52 additions & 0 deletions custom-instrumentation/instrument/newrelic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

/**
* New Relic agent configuration.
*
* See lib/config/default.js in the agent distribution for a more complete
* description of configuration variables and their potential values. */

exports.config = {
app_name: ['Example Job Queue App'],
logging: {
/**
* Level at which to log. 'trace' is most useful to New Relic when diagnosing
* issues with the agent, 'info' and higher will impose the least overhead on
* production applications.
*/
level: 'trace'
},
/**
* When true, all request headers except for those listed in attributes.exclude
* will be captured for all traces, unless otherwise specified in a destination's
* attributes include/exclude lists.
*/
allow_all_headers: true,
attributes: {
/**
* Prefix of attributes to exclude from all destinations. Allows * as wildcard
* at end.
*
* NOTE: If excluding headers, they must be in camelCase form to be filtered.
*
* @env NEW_RELIC_ATTRIBUTES_EXCLUDE
*/
exclude: [
'request.headers.cookie',
'request.headers.authorization',
'request.headers.proxyAuthorization',
'request.headers.setCookie*',
'request.headers.x*',
'response.headers.cookie',
'response.headers.authorization',
'response.headers.proxyAuthorization',
'response.headers.setCookie*',
'response.headers.x*'
]
}
}
17 changes: 17 additions & 0 deletions custom-instrumentation/instrument/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "instrument-app",
"version": "1.0.0",
"description": "Example app instrumentating a job queue",
"main": "index.js",
"scripts": {
"start": "node -r ./instrumentation --env-file .env index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"newrelic": "^11.19.0"
},
"devDependencies": {
"@newrelic/eslint-config": "^0.4.0"
}
}