From be7bb12945384815fb5bc2b74eb1a63eed2c7648 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 2 Aug 2018 19:36:49 +0200 Subject: [PATCH] feat: generate metadata object automatically (#6) --- README.md | 33 ++-- index.js | 66 +++++++- package.json | 3 +- test/test.js | 374 ++++++++++++++++++++++++++++--------------- test/unref-client.js | 8 +- test/utils.js | 53 ++++-- 6 files changed, 378 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 6457ac8..c101a37 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,10 @@ npm install elastic-apm-http-client const Client = require('elastic-apm-http-client') const client = new Client({ - userAgent: 'My Custom Elastic APM Agent', - meta: function () { - return { - // meta data object sent as the first ndjson object in all HTTP - // requests to the APM Server - } - } + serviceName: 'My App', + agentName: 'my-nodejs-agent', + agentVersion: require('./package.json').version, + userAgent: 'My Custom Elastic APM Agent' }) const span = { @@ -57,14 +54,22 @@ Arguments: - `options` - An object containing config options (see below) +Data sent to the APM Server as part of the metadata package: + +- `agentName` - (required) The APM agent name +- `agentVersion` - (required) The APM agent version +- `serviceName` - (required) The name of the service being instrumented +- `serviceVersion` - The version of the service being instrumented +- `frameworkName` - If the service being instrumented is running a + specific framework, use this config option to log its name +- `frameworkVersion` - If the service being instrumented is running a + specific framework, use this config option to log its version +- `hostname` - Custom hostname (default: OS hostname) + HTTP client configuration: - `userAgent` - (required) The HTTP user agent that your module should identify it self as -- `meta` - (required) A function which will be called every time the a - new HTTP request is being made to the APM Server. It's expected that - you return a metadata object. This object will be sent as the first - ndjson object to the API - `secretToken` - The Elastic APM intake API secret token - `serverUrl` - The APM Server URL (default: `http://localhost:8200`) - `headers` - An object containing extra HTTP headers that should be @@ -97,6 +102,12 @@ Streaming configuration: to the APM Server can be ongoing before it's ended (default: `10000` ms) +Data sanitizing configuration: + +- `truncateStringsAt` - Maximum size in bytes for strings stored as + Elasticsearch keywords. Strings larger than this will be trucated + (default: `1024` bytes) + ### Event: `close` The `close` event is emitted when the client and any of its underlying diff --git a/index.js b/index.js index 95be2e5..3f3ff02 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ 'use strict' const util = require('util') +const os = require('os') const parseUrl = require('url').parse const zlib = require('zlib') const Writable = require('readable-stream').Writable @@ -10,11 +11,20 @@ const eos = require('end-of-stream') const safeStringify = require('fast-safe-stringify') const streamToBuffer = require('fast-stream-to-buffer') const StreamChopper = require('stream-chopper') +const truncate = require('unicode-byte-truncate') const pkg = require('./package') +module.exports = Client + const flush = Symbol('flush') -module.exports = Client +const hostname = os.hostname() +const requiredOpts = [ + 'agentName', + 'agentVersion', + 'serviceName', + 'userAgent' +] // All sockets on the agent are unreffed when they are created. This means that // when those are the only handles left, the `beforeExit` event will be @@ -155,9 +165,8 @@ Client.prototype.destroy = function (err) { } function onStream (opts, client, onerror) { - const meta = opts.meta const serverTimeout = opts.serverTimeout - opts = getRequestOptions(opts, client._agent) + const requestOpts = getRequestOptions(opts, client._agent) return function (stream, next) { const onerrorproxy = (err) => { @@ -170,7 +179,7 @@ function onStream (opts, client, onerror) { client._active = true - const req = client._transport.request(opts, onResult(onerror)) + const req = client._transport.request(requestOpts, onResult(onerror)) const compressor = zlib.createGzip() // Mointor streams for errors so that we can make sure to destory the @@ -219,7 +228,7 @@ function onStream (opts, client, onerror) { }) // All requests to the APM Server must start with a metadata object - stream.write(safeStringify({metadata: meta()}) + '\n') + stream.write(safeStringify({metadata: metadata(opts)}) + '\n') } } @@ -242,8 +251,8 @@ function onResult (onerror) { } function normalizeOptions (opts) { - if (!opts.userAgent) throw new Error('Missing required option: userAgent') - if (!opts.meta) throw new Error('Missing required option: meta') + const missing = requiredOpts.filter(name => !opts[name]) + if (missing.length > 0) throw new Error('Missing required option(s): ' + missing.join(', ')) const normalized = Object.assign({}, opts, {objectMode: true}) @@ -252,6 +261,8 @@ function normalizeOptions (opts) { if (!normalized.time && normalized.time !== 0) normalized.time = 10000 if (!normalized.serverTimeout && normalized.serverTimeout !== 0) normalized.serverTimeout = 15000 if (!normalized.serverUrl) normalized.serverUrl = 'http://localhost:8200' + if (!normalized.hostname) normalized.hostname = hostname + if (!normalized.truncateStringsAt) normalized.truncateStringsAt = 1024 normalized.keepAlive = normalized.keepAlive !== false // process @@ -282,3 +293,44 @@ function getHeaders (opts) { headers['User-Agent'] = opts.userAgent + ' ' + pkg.name + '/' + pkg.version return Object.assign(headers, opts.headers) } + +function metadata (opts) { + var payload = { + service: { + name: opts.serviceName, + runtime: { + name: process.release.name, + version: process.version + }, + language: { + name: 'javascript' + }, + agent: { + name: opts.agentName, + version: opts.agentVersion + } + }, + process: { + pid: process.pid, + ppid: process.ppid, + title: truncate(String(process.title), opts.truncateStringsAt), + argv: process.argv + }, + system: { + hostname: opts.hostname, + architecture: process.arch, + platform: process.platform + } + } + + if (opts.serviceVersion) payload.service.version = opts.serviceVersion + + if (opts.frameworkName || opts.frameworkVersion) { + payload.service.framework = { + name: opts.frameworkName, + version: opts.frameworkVersion + } + } + + return payload +} diff --git a/package.json b/package.json index 84f59a1..9987705 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "ndjson": "^1.5.0", "pump": "^3.0.0", "readable-stream": "^2.3.6", - "stream-chopper": "^1.1.1" + "stream-chopper": "^1.1.1", + "unicode-byte-truncate": "^1.0.0" }, "devDependencies": { "codecov": "^3.0.4", diff --git a/test/test.js b/test/test.js index f15ca1d..7c2212b 100644 --- a/test/test.js +++ b/test/test.js @@ -1,6 +1,7 @@ 'use strict' const path = require('path') +const os = require('os') const exec = require('child_process').exec const http = require('http') const test = require('tape') @@ -12,7 +13,8 @@ const Client = require('../') const APMServer = utils.APMServer const processReq = utils.processReq const assertReq = utils.assertReq -const onmeta = utils.onmeta +const assertMetadata = utils.assertMetadata +const validOpts = utils.validOpts /** * Setup and config @@ -27,57 +29,35 @@ test('package', function (t) { }) test('throw if missing required options', function (t) { - t.throws(function () { - new Client() // eslint-disable-line no-new - }) - t.end() -}) - -test('throw if only userAgent is provided', function (t) { - t.throws(function () { - new Client({userAgent: 'foo'}) // eslint-disable-line no-new - }) - t.end() -}) - -test('throw if only meta is provided', function (t) { - t.throws(function () { - new Client({meta: onmeta}) // eslint-disable-line no-new - }) - t.end() -}) - -test('only userAgent and meta should be required', function (t) { - t.doesNotThrow(function () { - new Client({ // eslint-disable-line no-new - userAgent: 'foo', - meta: onmeta - }) - }) + t.throws(() => new Client(), 'throws if no options are provided') + t.throws(() => new Client({agentName: 'foo'}), 'throws if only agentName is provided') + t.throws(() => new Client({agentVersion: 'foo'}), 'throws if only agentVersion is provided') + t.throws(() => new Client({serviceName: 'foo'}), 'throws if only serviceName is provided') + t.throws(() => new Client({userAgent: 'foo'}), 'throws if only userAgent is provided') + t.throws(() => new Client({agentName: 'foo', agentVersion: 'foo', serviceName: 'foo'}), 'throws if userAgent is missing') + t.throws(() => new Client({agentName: 'foo', agentVersion: 'foo', userAgent: 'foo'}), 'throws if serviceName is missing') + t.throws(() => new Client({agentName: 'foo', serviceName: 'foo', userAgent: 'foo'}), 'throws if agentVersion is missing') + t.throws(() => new Client({agentVersion: 'foo', serviceName: 'foo', userAgent: 'foo'}), 'throws if agentName is missing') + t.doesNotThrow(() => new Client({agentName: 'foo', agentVersion: 'foo', serviceName: 'foo', userAgent: 'foo'}), 'doesn\'t throw if required options are provided') t.end() }) test('should work without new', function (t) { - const client = Client({ - userAgent: 'foo', - meta: onmeta - }) + const client = Client(validOpts()) t.ok(client instanceof Client) t.end() }) test('null value config options shouldn\'t throw', function (t) { t.doesNotThrow(function () { - new Client({ // eslint-disable-line no-new - userAgent: 'foo', // valid, so we don't throw - meta: onmeta, // valid, so we don't throw + new Client(validOpts({ // eslint-disable-line no-new size: null, time: null, serverTimeout: null, type: null, serverUrl: null, keepAlive: null - }) + })) }) t.end() }) @@ -91,11 +71,9 @@ test('no secretToken', function (t) { t.end() }) server.listen(function () { - const client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + const client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.sendSpan({foo: 42}) client.end() }) @@ -109,14 +87,12 @@ test('custom headers', function (t) { server.close() t.end() }).listen(function () { - const client = new Client({ + const client = new Client(validOpts({ serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta, headers: { 'X-Foo': 'bar' } - }) + })) client.sendSpan({foo: 42}) client.end() }) @@ -130,11 +106,9 @@ test('serverUrl contains path', function (t) { server.close() t.end() }).listen(function () { - const client = new Client({ - serverUrl: 'http://localhost:' + server.address().port + '/subpath', - userAgent: 'foo', - meta: onmeta - }) + const client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + '/subpath' + })) client.sendSpan({foo: 42}) client.end() }) @@ -170,6 +144,146 @@ test('allow unauthorized TLS if asked', function (t) { }) }) +test('metadata', function (t) { + t.plan(12) + const opts = { + agentName: 'custom-agentName', + agentVersion: 'custom-agentVersion', + serviceName: 'custom-serviceName', + serviceVersion: 'custom-serviceVersion', + frameworkName: 'custom-frameworkName', + frameworkVersion: 'custom-frameworkVersion', + hostname: 'custom-hostname' + } + const server = APMServer(function (req, res) { + req = processReq(req) + req.once('data', function (obj) { + t.deepEqual(obj, { + metadata: { + service: { + name: 'custom-serviceName', + version: 'custom-serviceVersion', + runtime: { + name: 'node', + version: process.version + }, + language: { + name: 'javascript' + }, + agent: { + name: 'custom-agentName', + version: 'custom-agentVersion' + }, + framework: { + name: 'custom-frameworkName', + version: 'custom-frameworkVersion' + } + }, + process: { + pid: process.pid, + ppid: process.ppid, + title: process.title, + argv: process.argv + }, + system: { + hostname: 'custom-hostname', + architecture: process.arch, + platform: process.platform + } + } + }) + t.ok(semver.valid(obj.metadata.service.runtime.version)) + t.ok(obj.metadata.process.pid > 0) + t.ok(obj.metadata.process.ppid > 0) + t.ok(/node$/.test(obj.metadata.process.title)) + t.ok(Array.isArray(obj.metadata.process.argv)) + t.ok(obj.metadata.process.argv.every(arg => typeof arg === 'string')) + t.ok(obj.metadata.process.argv.every(arg => arg.length > 0)) + t.equal(typeof obj.metadata.system.architecture, 'string') + t.ok(obj.metadata.system.architecture.length > 0) + t.equal(typeof obj.metadata.system.platform, 'string') + t.ok(obj.metadata.system.platform.length > 0) + }) + req.on('end', function () { + res.end() + server.close() + t.end() + }) + }).client(opts, function (client) { + client.sendSpan({foo: 42}) + client.end() + }) +}) + +test('metadata - default values', function (t) { + t.plan(1) + const opts = { + agentName: 'custom-agentName', + agentVersion: 'custom-agentVersion', + serviceName: 'custom-serviceName' + } + const server = APMServer(function (req, res) { + req = processReq(req) + req.once('data', function (obj) { + t.deepEqual(obj, { + metadata: { + service: { + name: 'custom-serviceName', + runtime: { + name: 'node', + version: process.version + }, + language: { + name: 'javascript' + }, + agent: { + name: 'custom-agentName', + version: 'custom-agentVersion' + } + }, + process: { + pid: process.pid, + ppid: process.ppid, + title: process.title, + argv: process.argv + }, + system: { + hostname: os.hostname(), + architecture: process.arch, + platform: process.platform + } + } + }) + }) + req.on('end', function () { + res.end() + server.close() + t.end() + }) + }).client(opts, function (client) { + client.sendSpan({foo: 42}) + client.end() + }) +}) + +test('agentName', function (t) { + t.plan(1) + const server = APMServer(function (req, res) { + req = processReq(req) + req.once('data', function (obj) { + t.equal(obj.metadata.service.name, 'custom') + }) + req.on('end', function () { + res.end() + server.close() + t.end() + }) + }).client({serviceName: 'custom'}, function (client) { + client.sendSpan({foo: 42}) + client.end() + }) +}) + /** * Normal operation */ @@ -180,16 +294,18 @@ dataTypes.forEach(function (dataType) { const sendFn = 'send' + dataType.charAt(0).toUpperCase() + dataType.substr(1) test(`client.${sendFn}() + client.flush()`, function (t) { - t.plan(2 + assertReq.asserts) + t.plan(1 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {[dataType]: {foo: 42}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -203,16 +319,18 @@ dataTypes.forEach(function (dataType) { }) test(`client.${sendFn}(callback) + client.flush()`, function (t) { - t.plan(3 + assertReq.asserts) + t.plan(2 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {[dataType]: {foo: 42}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -230,16 +348,18 @@ dataTypes.forEach(function (dataType) { }) test(`client.${sendFn}() + client.end()`, function (t) { - t.plan(2 + assertReq.asserts) + t.plan(1 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {[dataType]: {foo: 42}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -253,16 +373,18 @@ dataTypes.forEach(function (dataType) { }) test(`single client.${sendFn}`, function (t) { - t.plan(2 + assertReq.asserts) + t.plan(1 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {[dataType]: {foo: 42}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -275,9 +397,9 @@ dataTypes.forEach(function (dataType) { }) test(`multiple client.${sendFn} (same request)`, function (t) { - t.plan(4 + assertReq.asserts) + t.plan(3 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {[dataType]: {req: 1}}, {[dataType]: {req: 2}}, {[dataType]: {req: 3}} @@ -286,7 +408,9 @@ dataTypes.forEach(function (dataType) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -301,7 +425,7 @@ dataTypes.forEach(function (dataType) { }) test(`multiple client.${sendFn} (multiple requests)`, function (t) { - t.plan(8 + assertReq.asserts * 2) + t.plan(6 + assertReq.asserts * 2 + assertMetadata.asserts * 2) let clientReqNum = 0 let clientSendNum = 0 @@ -309,11 +433,11 @@ dataTypes.forEach(function (dataType) { let client const datas = [ - {metadata: {}}, + assertMetadata, {[dataType]: {req: 1, send: 1}}, {[dataType]: {req: 1, send: 2}}, {[dataType]: {req: 1, send: 3}}, - {metadata: {}}, + assertMetadata, {[dataType]: {req: 2, send: 4}}, {[dataType]: {req: 2, send: 5}}, {[dataType]: {req: 2, send: 6}} @@ -324,7 +448,9 @@ dataTypes.forEach(function (dataType) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -350,16 +476,18 @@ dataTypes.forEach(function (dataType) { }) test('client.flush(callback) - with active request', function (t) { - t.plan(5 + assertReq.asserts) + t.plan(4 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {span: {foo: 42}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -377,19 +505,21 @@ test('client.flush(callback) - with active request', function (t) { }) test('client.flush(callback) - with queued request', function (t) { - t.plan(6 + assertReq.asserts * 2) + t.plan(4 + assertReq.asserts * 2 + assertMetadata.asserts * 2) let requests = 0 const datas = [ - {metadata: {}}, + assertMetadata, {span: {req: 1}}, - {metadata: {}}, + assertMetadata, {span: {req: 2}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -410,13 +540,13 @@ test('client.flush(callback) - with queued request', function (t) { }) test('2nd flush before 1st flush have finished', function (t) { - t.plan(6 + assertReq.asserts * 2) + t.plan(4 + assertReq.asserts * 2 + assertMetadata.asserts * 2) let requestStarts = 0 let requestEnds = 0 const datas = [ - {metadata: {}}, + assertMetadata, {span: {req: 1}}, - {metadata: {}}, + assertMetadata, {span: {req: 2}} ] const server = APMServer(function (req, res) { @@ -424,7 +554,9 @@ test('2nd flush before 1st flush have finished', function (t) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { requestEnds++ @@ -445,16 +577,18 @@ test('2nd flush before 1st flush have finished', function (t) { }) test('client.end(callback)', function (t) { - t.plan(3 + assertReq.asserts) + t.plan(2 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {span: {foo: 42}} ] const server = APMServer(function (req, res) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.end() @@ -509,10 +643,10 @@ test('client.sent', function (t) { */ test('client should not hold the process open', function (t) { - t.plan(3 + assertReq.asserts) + t.plan(2 + assertReq.asserts + assertMetadata.asserts) const datas = [ - {metadata: {}}, + assertMetadata, {span: {hello: 'world'}} ] @@ -520,7 +654,9 @@ test('client should not hold the process open', function (t) { assertReq(t, req) req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { res.statusCode = 202 @@ -558,11 +694,9 @@ test('Event: close - if ndjson stream ends', function (t) { server.close() }, 10) }).listen(function () { - client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.on('finish', function () { t.fail('should not emit finish event') @@ -586,11 +720,9 @@ test('Event: close - if ndjson stream is destroyed', function (t) { server.close() }, 10) }).listen(function () { - client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.on('finish', function () { t.fail('should not emit finish event') @@ -614,11 +746,9 @@ test('Event: close - if chopper ends', function (t) { server.close() }, 10) }).listen(function () { - client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.on('finish', function () { t.fail('should not emit finish event') @@ -642,11 +772,9 @@ test('Event: close - if chopper is destroyed', function (t) { server.close() }, 10) }).listen(function () { - client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.on('finish', function () { t.fail('should not emit finish event') @@ -791,11 +919,11 @@ test('socket hang up', function (t) { }) test('socket hang up - continue with new request', function (t) { - t.plan(6 + assertReq.asserts * 2) + t.plan(5 + assertReq.asserts * 2 + assertMetadata.asserts) let reqs = 0 let client const datas = [ - {metadata: {}}, + assertMetadata, {span: {req: 2}} ] const server = APMServer(function (req, res) { @@ -812,7 +940,9 @@ test('socket hang up - continue with new request', function (t) { req = processReq(req) req.on('data', function (obj) { - t.deepEqual(obj, datas.shift()) + const expect = datas.shift() + if (typeof expect === 'function') expect(t, obj) + else t.deepEqual(obj, expect) }) req.on('end', function () { t.pass('should end request') @@ -876,10 +1006,7 @@ test('socket timeout - client request too slow', function (t) { test('client.destroy() - on fresh client', function (t) { t.plan(1) - const client = new Client({ - userAgent: 'foo', - meta: onmeta - }) + const client = new Client(validOpts()) client.on('finish', function () { t.fail('should not emit finish') }) @@ -897,10 +1024,7 @@ test('client.destroy() - should not allow more writes', function (t) { t.plan(12) let count = 0 - const client = new Client({ - userAgent: 'foo', - meta: onmeta - }) + const client = new Client(validOpts()) client.on('error', function (err) { t.ok(err instanceof Error, 'should emit error ' + err.message) }) @@ -943,11 +1067,9 @@ test('client.destroy() - on ended client', function (t) { }) server.listen(function () { - client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.on('finish', function () { t.pass('should emit finish only once') }) @@ -975,11 +1097,9 @@ test('client.destroy() - on client with request in progress', function (t) { }) server.listen(function () { - client = new Client({ - serverUrl: 'http://localhost:' + server.address().port, - userAgent: 'foo', - meta: onmeta - }) + client = new Client(validOpts({ + serverUrl: 'http://localhost:' + server.address().port + })) client.on('finish', function () { t.fail('should not emit finish') }) diff --git a/test/unref-client.js b/test/unref-client.js index f4b48dc..6a6cd39 100644 --- a/test/unref-client.js +++ b/test/unref-client.js @@ -5,10 +5,10 @@ const Client = require('../') const client = new Client({ serverUrl: process.argv[2], secretToken: 'secret', - userAgent: 'foo', - meta: function () { - return {} - } + agentName: 'my-agent-name', + agentVersion: 'my-agent-version', + serviceName: 'my-service-name', + userAgent: 'my-user-agent' }) process.stdout.write(String(Date.now())) diff --git a/test/utils.js b/test/utils.js index 5910e78..9ebf057 100644 --- a/test/utils.js +++ b/test/utils.js @@ -3,6 +3,7 @@ const http = require('http') const https = require('https') const zlib = require('zlib') +const semver = require('semver') const pem = require('https-pem') const ndjson = require('ndjson') const pkg = require('../package') @@ -11,7 +12,8 @@ const Client = require('../') exports.APMServer = APMServer exports.processReq = processReq exports.assertReq = assertReq -exports.onmeta = onmeta +exports.assertMetadata = assertMetadata +exports.validOpts = validOpts function APMServer (opts, onreq) { if (typeof opts === 'function') return APMServer(null, opts) @@ -36,12 +38,10 @@ function APMServer (opts, onreq) { opts = {} } server.listen(function () { - onclient(new Client(Object.assign({ + onclient(new Client(validOpts(Object.assign({ serverUrl: `http${secure ? 's' : ''}://localhost:${server.address().port}`, - secretToken: 'secret', - userAgent: 'foo', - meta: onmeta - }, opts))) + secretToken: 'secret' + }, opts)))) }) return server } @@ -60,10 +60,45 @@ function assertReq (t, req) { t.equal(req.headers['content-type'], 'application/x-ndjson', 'should send reqeust as ndjson') t.equal(req.headers['content-encoding'], 'gzip', 'should compress request') t.equal(req.headers['accept'], 'application/json', 'should expect json in response') - t.equal(req.headers['user-agent'], `foo ${pkg.name}/${pkg.version}`, 'should add proper User-Agent') + t.equal(req.headers['user-agent'], `my-user-agent ${pkg.name}/${pkg.version}`, 'should add proper User-Agent') } assertReq.asserts = 7 -function onmeta () { - return {} +function assertMetadata (t, obj) { + t.deepEqual(Object.keys(obj), ['metadata']) + const metadata = obj.metadata + t.deepEqual(Object.keys(metadata), ['service', 'process', 'system']) + const service = metadata.service + t.equal(service.name, 'my-service-name') + t.equal(service.runtime.name, 'node') + t.equal(service.runtime.version, process.version) + t.ok(semver.valid(service.runtime.version)) + t.equal(service.language.name, 'javascript') + t.equal(service.agent.name, 'my-agent-name') + t.equal(service.agent.version, 'my-agent-version') + const _process = metadata.process + t.ok(_process.pid > 0) + t.ok(_process.ppid > 0) + t.ok(/(\/node|^node)$/.test(_process.title), `process.title should match /(\\/node|^node)$/ (was: ${_process.title})`) + t.ok(Array.isArray(_process.argv), 'process.title should be an array') + t.ok(_process.argv.length >= 2, 'process.title should contain at least two elements') + t.ok(/\/node$/.test(_process.argv[0]), `process.argv[0] should match /\\/node$/ (was: ${_process.argv[0]})`) + t.ok(/\/test\/(test|unref-client)\.js$/.test(_process.argv[1]), `process.argv[1] should match /\\/test\\/(test|unref-client)\\.js$/ (was: ${_process.argv[1]})"`) + const system = metadata.system + t.ok(typeof system.hostname, 'string') + t.ok(system.hostname.length > 0) + t.ok(typeof system.architecture, 'string') + t.ok(system.architecture.length > 0) + t.ok(typeof system.platform, 'string') + t.ok(system.platform.length > 0) +} +assertMetadata.asserts = 22 + +function validOpts (opts) { + return Object.assign({ + agentName: 'my-agent-name', + agentVersion: 'my-agent-version', + serviceName: 'my-service-name', + userAgent: 'my-user-agent' + }, opts) }