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
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'
[NEWRELIC] instrumenting method 'scheduleJob'
[NEWRELIC] instrumenting method 'runJobs'
Callback job done
Promise job done
```

## Exploring Telemetry

1. Now you should be able to see `job-queue` instrumented in New Relic. Navigate to 'APM & Services' and then select the 'Example Instrument API App' entity.
2. Then select 'Distributed tracing'. You should see the trace groups `firstTransacation` and `secondTransaction`. Inside these groups will be our custom instrumentation. Select any group and then select a single trace.
3. Toggle 'Show in-process spans' and you will see 'wrappedScheduleJob' which is the custom intrumentation of `job-queue` that we implemented.

![1722009056265](./image/README/1722009056265.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 the queue class
* `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.
69 changes: 69 additions & 0 deletions custom-instrumentation/instrument/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 transacations with startBackgroundTransaction
// because we are not operating inside a typical web framework. If you already
// are operating within a web framework with transactions, you may omit
// the startBackgroundTransaction wrapper.
newrelic.startBackgroundTransaction('firstTransaction', function first() {
amychisholm03 marked this conversation as resolved.
Show resolved Hide resolved
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 second() {
amychisholm03 marked this conversation as resolved.
Show resolved Hide resolved
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 state will be lost here because 'firstTransaction' will have
// already ended the transaction
transaction.end()
})
})
})

// 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.
}

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: function onRequire(shim, jobQueue) {
console.log(`[NEWRELIC] instrumenting job-queue module`)

console.log(`[NEWRELIC] instrumenting method 'scheduleJob'`)
shim.record(
jobQueue.prototype,
'scheduleJob',
function wrapJob(shim, original) {
amychisholm03 marked this conversation as resolved.
Show resolved Hide resolved
return function wrappedScheduleJob(job) {
return original.call(this, shim.bindSegment(job))
}
}
)

console.log(`[NEWRELIC] instrumenting method 'runJobs'`)
shim.record(
amychisholm03 marked this conversation as resolved.
Show resolved Hide resolved
jobQueue.prototype,
'runJobs',
function wrapJob(shim, original) {
return function wrappedRunJobs(job) {
return original.apply(this, shim.bindSegment(job))
}
}
)
},
// The function that will be called if the instrumentation fails
onError: function onError(err) {
// Uh oh! Our instrumentation failed, lets see why:
console.error(err.message, err.stack)

// Let's kill the application when debugging so we don't miss it.
process.exit(-1)
}
})
28 changes: 28 additions & 0 deletions custom-instrumentation/instrument/job-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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(jobs) {
while (jobs.length) {
jobs.pop()()
}
}

Queue.prototype.scheduleJob = function scheduleJob(job) {
const queue = this
process.nextTick(function() {
if (queue.jobs.length === 0) {
setTimeout(queue.runJobs, 1000, queue.jobs)
}
queue.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 Instrument API 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"
}
}