Skip to content

Commit

Permalink
feat: instrument example (#280)
Browse files Browse the repository at this point in the history
Co-authored-by: James Sumners <[email protected]>
  • Loading branch information
amychisholm03 and jsumners-nr authored Aug 13, 2024
1 parent e7a78c4 commit 44dbdbb
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 0 deletions.
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

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"
}
}

0 comments on commit 44dbdbb

Please sign in to comment.