diff --git a/index.js b/index.js index 426442093c..384d31af0b 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,8 @@ 'use strict' +const HealthReporter = require('./lib/health-reporter') + // Record opening times before loading any other files. const preAgentTime = process.uptime() const agentStart = Date.now() @@ -156,6 +158,7 @@ function createAgent(config) { 'New Relic requires that you name this application!\n' + 'Set app_name in your newrelic.js or newrelic.cjs file or set environment variable\n' + 'NEW_RELIC_APP_NAME. Not starting!' + agent.healthReporter.setStatus(HealthReporter.STATUS_MISSING_APP_NAME) throw new Error(message) } @@ -169,6 +172,7 @@ function createAgent(config) { agent.start(function afterStart(error) { if (error) { + agent.healthReporter.setStatus(HealthReporter.STATUS_INTERNAL_UNEXPECTED_ERROR) const errorMessage = 'New Relic for Node.js halted startup due to an error:' logger.error(error, errorMessage) diff --git a/lib/health-reporter.js b/lib/health-reporter.js index 1b5b8a5af9..918903ff1e 100644 --- a/lib/health-reporter.js +++ b/lib/health-reporter.js @@ -23,7 +23,9 @@ const VALID_CODES = new Map([ ['NR-APM-008', 'Agent is disabled via configuration.'], ['NR-APM-009', 'Failed to connect to the New Relic data collector.'], ['NR-APM-010', 'Agent config could not be parsed.'], - ['NR-APM-099', 'Agent has shutdown.'] + ['NR-APM-099', 'Agent has shutdown.'], + // Codes 300 through 399 are reserved for the Node.js Agent. + ['NR-APM-300', 'An unexpected error occurred.'] ]) function writeStatus({ file, healthy = true, code, msg, startTime, callback } = {}) { @@ -65,6 +67,9 @@ class HealthReporter { static STATUS_CONFIG_PARSE_FAILURE = 'NR-APM-010' static STATUS_AGENT_SHUTDOWN = 'NR-APM-099' + // STATUS_INTERNAL errors are the Node.js Agent specific error codes. + static STATUS_INTERNAL_UNEXPECTED_ERROR = 'NR-APM-300' + constructor({ logger = defaultLogger, setInterval = global.setInterval } = {}) { const fleetId = process.env.NEW_RELIC_SUPERAGENT_FLEET_ID const outDir = process.env.NEW_RELIC_SUPERAGENT_HEALTH_DELIVERY_LOCATION @@ -135,7 +140,6 @@ class HealthReporter { } if (VALID_CODES.has(status) === false) { - // TODO: if we ever add codes in our reserved block (300-399), account for them here this.#logger.warn(`invalid health reporter status provided: ${status}`) return } diff --git a/test/unit/index.test.js b/test/unit/index.test.js index ca3a4c3199..99a4ecd3d6 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -7,9 +7,11 @@ const test = require('node:test') const assert = require('node:assert') +const tspl = require('@matteo.collina/tspl') const sinon = require('sinon') +const HealthReporter = require('../../lib/health-reporter') const proxyquire = require('proxyquire').noCallThru() const createLoggerMock = require('./mocks/logger') const createMockAgent = require('./mocks/agent') @@ -322,30 +324,52 @@ test('index tests', async (t) => { ) }) - await t.test('should throw error is app name is not set in config', (t) => { + await t.test('should throw error is app name is not set in config', async (t) => { + const plan = tspl(t, { plan: 3 }) + const setStatus = HealthReporter.prototype.setStatus + HealthReporter.prototype.setStatus = (status) => { + plan.equal(status, HealthReporter.STATUS_MISSING_APP_NAME) + } + t.after(() => { + HealthReporter.prototype.setStatus = setStatus + }) + t.nr.processVersionStub.satisfies.onCall(0).returns(true) t.nr.processVersionStub.satisfies.onCall(1).returns(false) t.nr.mockConfig.applications.returns([]) loadIndex(t) - assert.equal(t.nr.loggerMock.error.callCount, 1, 'should log an error') - assert.match( + plan.equal(t.nr.loggerMock.error.callCount, 1, 'should log an error') + plan.match( t.nr.loggerMock.error.args[0][0].message, /New Relic requires that you name this application!/ ) + + await plan.completed }) - await t.test('should log error if agent startup failed', (t) => { + await t.test('should log error if agent startup failed', async (t) => { + const plan = tspl(t, { plan: 3 }) + const setStatus = HealthReporter.prototype.setStatus + HealthReporter.prototype.setStatus = (status) => { + plan.equal(status, HealthReporter.STATUS_INTERNAL_UNEXPECTED_ERROR) + } + t.after(() => { + HealthReporter.prototype.setStatus = setStatus + }) + t.nr.processVersionStub.satisfies.onCall(0).returns(true) t.nr.processVersionStub.satisfies.onCall(1).returns(false) t.nr.mockConfig.applications.returns(['my-app-name']) const err = new Error('agent start failed') t.nr.MockAgent.prototype.start.yields(err) loadIndex(t) - assert.equal(t.nr.loggerMock.error.callCount, 1, 'should log a startup error') - assert.equal( + plan.equal(t.nr.loggerMock.error.callCount, 1, 'should log a startup error') + plan.equal( t.nr.loggerMock.error.args[0][1], 'New Relic for Node.js halted startup due to an error:' ) + + await plan.completed }) await t.test('should log warning if not in main thread and make a stub api', (t) => { diff --git a/test/unit/mocks/agent.js b/test/unit/mocks/agent.js index 31100d18a2..4bb39d447c 100644 --- a/test/unit/mocks/agent.js +++ b/test/unit/mocks/agent.js @@ -4,9 +4,11 @@ */ 'use strict' + const { EventEmitter } = require('events') const util = require('util') const sinon = require('sinon') +const HealthReporter = require('../../../lib/health-reporter') module.exports = (sandbox = sinon, metricsMock) => { function MockAgent(config = {}) { @@ -14,6 +16,7 @@ module.exports = (sandbox = sinon, metricsMock) => { this.config = config this.config.app_name = 'Unit Test App' this.metrics = metricsMock + this.healthReporter = new HealthReporter() } MockAgent.prototype.start = sandbox.stub() MockAgent.prototype.recordSupportability = sandbox.stub()