diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json index a5ea155309c..8b1dd5f8fe2 100644 --- a/packages/.eslintrc.json +++ b/packages/.eslintrc.json @@ -9,6 +9,7 @@ "expect": true, "sinon": true, "proxyquire": true, + "withNamingSchema": true, "withVersions": true, "withExports": true }, diff --git a/packages/datadog-plugin-amqp10/src/consumer.js b/packages/datadog-plugin-amqp10/src/consumer.js index 06c9bb65aaa..46309c924fa 100644 --- a/packages/datadog-plugin-amqp10/src/consumer.js +++ b/packages/datadog-plugin-amqp10/src/consumer.js @@ -11,11 +11,9 @@ class Amqp10ConsumerPlugin extends ConsumerPlugin { const source = getShortName(link) const address = getAddress(link) - this.startSpan('amqp.receive', { - service: this.config.service || `${this.tracer._service}-amqp`, + this.startSpan({ resource: ['receive', source].filter(v => v).join(' '), type: 'worker', - kind: 'consumer', meta: { 'amqp.link.source.address': source, 'amqp.link.role': 'receiver', diff --git a/packages/datadog-plugin-amqp10/src/producer.js b/packages/datadog-plugin-amqp10/src/producer.js index 80426202b32..4163ac34e7b 100644 --- a/packages/datadog-plugin-amqp10/src/producer.js +++ b/packages/datadog-plugin-amqp10/src/producer.js @@ -13,10 +13,8 @@ class Amqp10ProducerPlugin extends ProducerPlugin { const address = getAddress(link) const target = getShortName(link) - this.startSpan('amqp.send', { - service: this.config.service || `${this.tracer._service}-amqp`, + this.startSpan({ resource: ['send', target].filter(v => v).join(' '), - kind: 'producer', meta: { 'amqp.link.target.address': target, 'amqp.link.role': 'sender', diff --git a/packages/datadog-plugin-amqp10/test/index.spec.js b/packages/datadog-plugin-amqp10/test/index.spec.js index 9fc181f960e..9652274434e 100644 --- a/packages/datadog-plugin-amqp10/test/index.spec.js +++ b/packages/datadog-plugin-amqp10/test/index.spec.js @@ -3,6 +3,8 @@ const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') + describe('Plugin', () => { let tracer let client @@ -63,8 +65,8 @@ describe('Plugin', () => { .use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.send') - expect(span).to.have.property('service', 'test-amqp') + expect(span).to.have.property('name', namingSchema.send.opName) + expect(span).to.have.property('service', namingSchema.send.serviceName) expect(span).to.have.property('resource', 'send amq.topic') expect(span).to.not.have.property('type') expect(span.meta).to.have.property('span.kind', 'producer') @@ -84,7 +86,6 @@ describe('Plugin', () => { sender.send({ key: 'value' }) }) - it('should handle errors', done => { let error @@ -123,6 +124,12 @@ describe('Plugin', () => { expect(promise).to.have.property('value') }) }) + + withNamingSchema( + () => sender.send({ key: 'value' }), + () => namingSchema.send.opName, + () => namingSchema.send.serviceName + ) }) describe('when consuming messages', () => { @@ -130,8 +137,8 @@ describe('Plugin', () => { agent .use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.receive') - expect(span).to.have.property('service', 'test-amqp') + expect(span).to.have.property('name', namingSchema.receive.opName) + expect(span).to.have.property('service', namingSchema.receive.serviceName) expect(span).to.have.property('resource', 'receive amq.topic') expect(span).to.have.property('type', 'worker') expect(span.meta).to.have.property('span.kind', 'consumer') @@ -163,12 +170,18 @@ describe('Plugin', () => { sender.send({ key: 'value' }) }) + + withNamingSchema( + () => sender.send({ key: 'value' }), + () => namingSchema.receive.opName, + () => namingSchema.receive.serviceName + ) }) }) describe('with configuration', () => { beforeEach(() => { - agent.reload('amqp10', { service: 'test' }) + agent.reload('amqp10', { service: 'test-custom-name' }) const amqp = require(`../../../versions/amqp10@${version}`).get() @@ -192,13 +205,19 @@ describe('Plugin', () => { .use(traces => { const span = traces[0][0] - expect(span).to.have.property('service', 'test') + expect(span).to.have.property('service', 'test-custom-name') }, 2) .then(done) .catch(done) sender.send({ key: 'value' }) }) + + withNamingSchema( + () => sender.send({ key: 'value' }), + () => namingSchema.receive.opName, + () => 'test-custom-name' + ) }) }) }) diff --git a/packages/datadog-plugin-amqp10/test/naming.js b/packages/datadog-plugin-amqp10/test/naming.js new file mode 100644 index 00000000000..02e147dfeee --- /dev/null +++ b/packages/datadog-plugin-amqp10/test/naming.js @@ -0,0 +1,24 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + send: { + v0: { + opName: 'amqp.send', + serviceName: 'test-amqp' + }, + v1: { + opName: 'amqp.send', + serviceName: 'test' + } + }, + receive: { + v0: { + opName: 'amqp.receive', + serviceName: 'test-amqp' + }, + v1: { + opName: 'amqp.process', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-amqplib/src/client.js b/packages/datadog-plugin-amqplib/src/client.js index 3e567610402..415808e904d 100644 --- a/packages/datadog-plugin-amqplib/src/client.js +++ b/packages/datadog-plugin-amqplib/src/client.js @@ -7,6 +7,7 @@ const { getResourceName } = require('./util') class AmqplibClientPlugin extends ClientPlugin { static get id () { return 'amqplib' } + static get type () { return 'messaging' } static get operation () { return 'command' } start ({ channel = {}, method, fields }) { @@ -14,10 +15,10 @@ class AmqplibClientPlugin extends ClientPlugin { if (method === 'basic.publish') return const stream = (channel.connection && channel.connection.stream) || {} - const span = this.startSpan('amqp.command', { - service: this.config.service || `${this.tracer._service}-amqp`, + const span = this.startSpan(this.operationName(), { + service: this.config.service || this.serviceName(), resource: getResourceName(method, fields), - kind: 'client', + kind: this.constructor.kind, meta: { 'out.host': stream._host, [CLIENT_PORT_KEY]: stream.remotePort, diff --git a/packages/datadog-plugin-amqplib/src/consumer.js b/packages/datadog-plugin-amqplib/src/consumer.js index fb359556bff..0aed1696507 100644 --- a/packages/datadog-plugin-amqplib/src/consumer.js +++ b/packages/datadog-plugin-amqplib/src/consumer.js @@ -13,11 +13,9 @@ class AmqplibConsumerPlugin extends ConsumerPlugin { const childOf = extract(this.tracer, message) - this.startSpan('amqp.command', { + this.startSpan({ childOf, - service: this.config.service || `${this.tracer._service}-amqp`, resource: getResourceName(method, fields), - kind: 'consumer', type: 'worker', meta: { 'amqp.queue': fields.queue, diff --git a/packages/datadog-plugin-amqplib/src/producer.js b/packages/datadog-plugin-amqplib/src/producer.js index 927992470d2..9c3d1da8d53 100644 --- a/packages/datadog-plugin-amqplib/src/producer.js +++ b/packages/datadog-plugin-amqplib/src/producer.js @@ -13,10 +13,8 @@ class AmqplibProducerPlugin extends ProducerPlugin { if (method !== 'basic.publish') return const stream = (channel.connection && channel.connection.stream) || {} - const span = this.startSpan('amqp.command', { - service: this.config.service || `${this.tracer._service}-amqp`, + const span = this.startSpan({ resource: getResourceName(method, fields), - kind: 'producer', meta: { 'out.host': stream._host, [CLIENT_PORT_KEY]: stream.remotePort, diff --git a/packages/datadog-plugin-amqplib/test/index.spec.js b/packages/datadog-plugin-amqplib/test/index.spec.js index c4a5da95373..db6799939ed 100644 --- a/packages/datadog-plugin-amqplib/test/index.spec.js +++ b/packages/datadog-plugin-amqplib/test/index.spec.js @@ -3,6 +3,8 @@ const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') + describe('Plugin', () => { let tracer let connection @@ -55,8 +57,8 @@ describe('Plugin', () => { agent .use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.command') - expect(span).to.have.property('service', 'test-amqp') + expect(span).to.have.property('name', namingSchema.controlPlane.opName) + expect(span).to.have.property('service', namingSchema.controlPlane.serviceName) expect(span).to.have.property('resource', 'queue.declare test') expect(span).to.not.have.property('type') expect(span.meta).to.have.property('span.kind', 'client') @@ -75,8 +77,8 @@ describe('Plugin', () => { .use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.command') - expect(span).to.have.property('service', 'test-amqp') + expect(span).to.have.property('name', namingSchema.controlPlane.opName) + expect(span).to.have.property('service', namingSchema.controlPlane.serviceName) expect(span).to.have.property('resource', 'queue.delete test') expect(span).to.not.have.property('type') expect(span.meta).to.have.property('span.kind', 'client') @@ -113,6 +115,12 @@ describe('Plugin', () => { error = e } }) + + withNamingSchema( + () => channel.assertQueue('test', {}, () => {}), + () => namingSchema.controlPlane.opName, + () => namingSchema.controlPlane.serviceName + ) }) describe('when publishing messages', () => { @@ -121,8 +129,8 @@ describe('Plugin', () => { .use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.command') - expect(span).to.have.property('service', 'test-amqp') + expect(span).to.have.property('name', namingSchema.send.opName) + expect(span).to.have.property('service', namingSchema.send.serviceName) expect(span).to.have.property('resource', 'basic.publish exchange routingKey') expect(span).to.not.have.property('type') expect(span.meta).to.have.property('out.host', 'localhost') @@ -160,6 +168,15 @@ describe('Plugin', () => { error = e } }) + + withNamingSchema( + () => { + channel.assertExchange('exchange', 'direct', {}, () => {}) + channel.publish('exchange', 'routingKey', Buffer.from('content')) + }, + () => namingSchema.send.opName, + () => namingSchema.send.serviceName + ) }) describe('when consuming messages', () => { @@ -170,8 +187,8 @@ describe('Plugin', () => { agent .use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.command') - expect(span).to.have.property('service', 'test-amqp') + expect(span).to.have.property('name', namingSchema.receive.opName) + expect(span).to.have.property('service', namingSchema.receive.serviceName) expect(span).to.have.property('resource', `basic.deliver ${queue}`) expect(span).to.have.property('type', 'worker') expect(span.meta).to.have.property('span.kind', 'consumer') @@ -236,6 +253,18 @@ describe('Plugin', () => { }) }) }) + + withNamingSchema( + () => { + channel.assertQueue('', {}, (err, ok) => { + if (err) return + channel.sendToQueue(ok.queue, Buffer.from('content')) + channel.consume(ok.queue, () => {}, {}, (err, ok) => {}) + }) + }, + () => namingSchema.receive.opName, + () => namingSchema.receive.serviceName + ) }) }) @@ -260,7 +289,7 @@ describe('Plugin', () => { describe('with configuration', () => { before(() => { - return agent.load('amqplib', { service: 'test' }) + return agent.load('amqplib', { service: 'test-custom-service' }) }) after(() => { @@ -286,7 +315,7 @@ describe('Plugin', () => { it('should be configured with the correct values', done => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', 'test-custom-service') expect(traces[0][0]).to.have.property('resource', 'queue.declare test') }, 2) .then(done) @@ -294,6 +323,12 @@ describe('Plugin', () => { channel.assertQueue('test', {}, () => {}) }) + + withNamingSchema( + () => channel.assertQueue('test', {}, () => {}), + () => namingSchema.controlPlane.opName, + () => 'test-custom-service' + ) }) }) }) diff --git a/packages/datadog-plugin-amqplib/test/naming.js b/packages/datadog-plugin-amqplib/test/naming.js new file mode 100644 index 00000000000..d0ed9b94866 --- /dev/null +++ b/packages/datadog-plugin-amqplib/test/naming.js @@ -0,0 +1,34 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + send: { + v0: { + opName: 'amqp.command', + serviceName: 'test-amqp' + }, + v1: { + opName: 'amqp.send', + serviceName: 'test' + } + }, + receive: { + v0: { + opName: 'amqp.command', + serviceName: 'test-amqp' + }, + v1: { + opName: 'amqp.process', + serviceName: 'test' + } + }, + controlPlane: { + v0: { + opName: 'amqp.command', + serviceName: 'test-amqp' + }, + v1: { + opName: 'amqp.command', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/client.js b/packages/datadog-plugin-google-cloud-pubsub/src/client.js index e71f53ea8bc..8911717f1e5 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/client.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/client.js @@ -4,15 +4,16 @@ const ClientPlugin = require('../../dd-trace/src/plugins/client') class GoogleCloudPubsubClientPlugin extends ClientPlugin { static get id () { return 'google-cloud-pubsub' } + static get type () { return 'messaging' } static get operation () { return 'request' } start ({ request, api, projectId }) { if (api === 'publish') return - this.startSpan('pubsub.request', { - service: this.config.service || `${this.tracer._service}-pubsub`, + this.startSpan(this.operationName(), { + service: this.config.service || this.serviceName(), resource: [api, request.name].filter(x => x).join(' '), - kind: 'client', + kind: this.constructor.kind, meta: { 'pubsub.method': api, 'gcloud.project_id': projectId diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js b/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js index c4e380bb4c8..0b157f5447b 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js @@ -11,11 +11,9 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin { const topic = subscription.metadata && subscription.metadata.topic const childOf = this.tracer.extract('text_map', message.attributes) || null - this.startSpan('pubsub.receive', { + this.startSpan({ childOf, - service: this.config.service, resource: topic, - kind: 'consumer', type: 'worker', meta: { 'gcloud.project_id': subscription.pubsub.projectId, diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/producer.js b/packages/datadog-plugin-google-cloud-pubsub/src/producer.js index fb3295f382c..a34d6bfacd8 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/producer.js @@ -11,10 +11,8 @@ class GoogleCloudPubsubProducerPlugin extends ProducerPlugin { const messages = request.messages || [] const topic = request.topic - const span = this.startSpan('pubsub.request', { // TODO: rename - service: this.config.service || `${this.tracer._service}-pubsub`, + const span = this.startSpan({ // TODO: rename resource: `${api} ${topic}`, - kind: 'producer', meta: { 'gcloud.project_id': projectId, 'pubsub.method': api, // TODO: remove diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js b/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js index d566dde7f77..95aadfe63a0 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js +++ b/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js @@ -5,6 +5,8 @@ const { expectSomeSpan, withDefaults } = require('../../dd-trace/test/plugins/he const id = require('../../dd-trace/src/id') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') + // The roundtrip to the pubsub emulator takes time. Sometimes a *long* time. const TIMEOUT = 30000 @@ -46,8 +48,16 @@ describe('Plugin', () => { pubsub = new lib.PubSub({ projectId: project }) }) describe('createTopic', () => { + withNamingSchema( + async () => pubsub.createTopic(topicName), + () => namingSchema.controlPlane.opName, + () => namingSchema.controlPlane.serviceName + ) + it('should be instrumented', async () => { const expectedSpanPromise = expectSpanWithDefaults({ + name: namingSchema.controlPlane.opName, + service: namingSchema.controlPlane.serviceName, meta: { 'pubsub.method': 'createTopic', 'span.kind': 'client', @@ -68,6 +78,8 @@ describe('Plugin', () => { }, gax) const expectedSpanPromise = expectSpanWithDefaults({ + name: namingSchema.controlPlane.opName, + service: namingSchema.controlPlane.serviceName, meta: { 'pubsub.method': 'createTopic', 'span.kind': 'client', @@ -83,6 +95,8 @@ describe('Plugin', () => { it('should be instrumented w/ error', async () => { const expectedSpanPromise = expectSpanWithDefaults({ + name: namingSchema.controlPlane.opName, + service: namingSchema.controlPlane.serviceName, error: 1, meta: { 'pubsub.method': 'createTopic', @@ -94,7 +108,7 @@ describe('Plugin', () => { try { await publisher.createTopic({ name }) } catch (e) { - // this is just to prevent mocha from crashing + // this is just to prevent mocha from crashing } return expectedSpanPromise }) @@ -111,6 +125,8 @@ describe('Plugin', () => { describe('publish', () => { it('should be instrumented', async () => { const expectedSpanPromise = expectSpanWithDefaults({ + name: namingSchema.send.opName, + service: namingSchema.send.serviceName, meta: { 'pubsub.topic': resource, 'pubsub.method': 'publish', @@ -133,12 +149,22 @@ describe('Plugin', () => { expect(tracer.scope().active()).to.equal(firstSpan) }) }) + + withNamingSchema( + async () => { + const [topic] = await pubsub.createTopic(topicName) + await publish(topic, { data: Buffer.from('hello') }) + }, + () => namingSchema.send.opName, + () => namingSchema.send.serviceName + ) }) describe('onmessage', () => { it('should be instrumented', async () => { const expectedSpanPromise = expectSpanWithDefaults({ - name: 'pubsub.receive', + name: namingSchema.receive.opName, + service: namingSchema.receive.serviceName, type: 'worker', meta: { 'component': 'google-cloud-pubsub', @@ -158,7 +184,8 @@ describe('Plugin', () => { it('should give the current span a parentId from the sender', async () => { const expectedSpanPromise = expectSpanWithDefaults({ - name: 'pubsub.receive', + name: namingSchema.receive.opName, + service: namingSchema.receive.serviceName, meta: { 'span.kind': 'consumer' } }) const [topic] = await pubsub.createTopic(topicName) @@ -183,7 +210,8 @@ describe('Plugin', () => { it('should be instrumented w/ error', async () => { const error = new Error('bad') const expectedSpanPromise = expectSpanWithDefaults({ - name: 'pubsub.receive', + name: namingSchema.receive.opName, + service: namingSchema.receive.serviceName, error: 1, meta: { [ERROR_MESSAGE]: error.message, @@ -218,6 +246,17 @@ describe('Plugin', () => { await publish(topic, { data: Buffer.from('hello') }) return expectedSpanPromise }) + + withNamingSchema( + async () => { + const [topic] = await pubsub.createTopic(topicName) + const [sub] = await topic.createSubscription('foo') + sub.on('message', msg => msg.ack()) + await publish(topic, { data: Buffer.from('hello') }) + }, + () => namingSchema.receive.opName, + () => namingSchema.receive.serviceName + ) }) describe('when disabled', () => { @@ -254,6 +293,7 @@ describe('Plugin', () => { describe('createTopic', () => { it('should be instrumented', async () => { const expectedSpanPromise = expectSpanWithDefaults({ + name: namingSchema.controlPlane.opName, service: 'a_test_service', meta: { 'pubsub.method': 'createTopic' } }) diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/naming.js b/packages/datadog-plugin-google-cloud-pubsub/test/naming.js new file mode 100644 index 00000000000..24908870d2a --- /dev/null +++ b/packages/datadog-plugin-google-cloud-pubsub/test/naming.js @@ -0,0 +1,34 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + send: { + v0: { + opName: 'pubsub.request', + serviceName: 'test-pubsub' + }, + v1: { + opName: 'gcp.pubsub.send', + serviceName: 'test' + } + }, + receive: { + v0: { + opName: 'pubsub.receive', + serviceName: 'test' + }, + v1: { + opName: 'gcp.pubsub.process', + serviceName: 'test' + } + }, + controlPlane: { + v0: { + opName: 'pubsub.request', + serviceName: 'test-pubsub' + }, + v1: { + opName: 'gcp.pubsub.request', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-kafkajs/src/consumer.js b/packages/datadog-plugin-kafkajs/src/consumer.js index c3777a36b00..4bd1367b957 100644 --- a/packages/datadog-plugin-kafkajs/src/consumer.js +++ b/packages/datadog-plugin-kafkajs/src/consumer.js @@ -8,12 +8,9 @@ class KafkajsConsumerPlugin extends ConsumerPlugin { start ({ topic, partition, message }) { const childOf = extract(this.tracer, message.headers) - - this.startSpan('kafka.consume', { + this.startSpan({ childOf, - service: this.config.service || `${this.tracer._service}-kafka`, resource: topic, - kind: 'consumer', type: 'worker', meta: { 'component': 'kafkajs', diff --git a/packages/datadog-plugin-kafkajs/src/producer.js b/packages/datadog-plugin-kafkajs/src/producer.js index 2108f7cd9b5..0a08fc5ba02 100644 --- a/packages/datadog-plugin-kafkajs/src/producer.js +++ b/packages/datadog-plugin-kafkajs/src/producer.js @@ -7,10 +7,8 @@ class KafkajsProducerPlugin extends ProducerPlugin { static get operation () { return 'produce' } start ({ topic, messages }) { - const span = this.startSpan('kafka.produce', { - service: this.config.service || `${this.tracer._service}-kafka`, + const span = this.startSpan({ resource: topic, - kind: 'producer', meta: { 'component': 'kafkajs', 'kafka.topic': topic diff --git a/packages/datadog-plugin-kafkajs/test/index.spec.js b/packages/datadog-plugin-kafkajs/test/index.spec.js index 503171d9258..2c7edf48220 100644 --- a/packages/datadog-plugin-kafkajs/test/index.spec.js +++ b/packages/datadog-plugin-kafkajs/test/index.spec.js @@ -5,6 +5,8 @@ const agent = require('../../dd-trace/test/plugins/agent') const { expectSomeSpan, withDefaults } = require('../../dd-trace/test/plugins/helpers') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') + describe('Plugin', () => { describe('kafkajs', function () { this.timeout(10000) // TODO: remove when new internal trace has landed @@ -31,8 +33,8 @@ describe('Plugin', () => { describe('producer', () => { it('should be instrumented', async () => { const expectedSpanPromise = expectSpanWithDefaults({ - name: 'kafka.produce', - service: 'test-kafka', + name: namingSchema.send.opName, + service: namingSchema.send.serviceName, meta: { 'span.kind': 'producer', 'component': 'kafkajs' @@ -52,7 +54,7 @@ describe('Plugin', () => { it('should be instrumented w/ error', async () => { const producer = kafka.producer() - const resourceName = 'kafka.produce' + const resourceName = namingSchema.send.opName let error @@ -61,7 +63,7 @@ describe('Plugin', () => { expect(span).to.include({ name: resourceName, - service: 'test-kafka', + service: namingSchema.send.serviceName, resource: resourceName, error: 1 }) @@ -86,6 +88,12 @@ describe('Plugin', () => { return expectedSpanPromise } }) + + withNamingSchema( + async () => sendMessages(kafka, testTopic, messages), + () => namingSchema.send.opName, + () => namingSchema.send.serviceName + ) }) describe('consumer', () => { let consumer @@ -98,10 +106,11 @@ describe('Plugin', () => { afterEach(async () => { await consumer.disconnect() }) + it('should be instrumented', async () => { const expectedSpanPromise = expectSpanWithDefaults({ - name: 'kafka.consume', - service: 'test-kafka', + name: namingSchema.receive.opName, + service: namingSchema.receive.serviceName, meta: { 'span.kind': 'consumer', 'component': 'kafkajs' @@ -118,6 +127,7 @@ describe('Plugin', () => { return expectedSpanPromise }) + it('should run the consumer in the context of the consumer span', done => { const firstSpan = tracer.scope().active() @@ -126,7 +136,7 @@ describe('Plugin', () => { try { expect(currentSpan).to.not.equal(firstSpan) - expect(currentSpan.context()._name).to.equal('kafka.consume') + expect(currentSpan.context()._name).to.equal(namingSchema.receive.opName) done() } catch (e) { done(e) @@ -161,8 +171,8 @@ describe('Plugin', () => { it('should be instrumented w/ error', async () => { const fakeError = new Error('Oh No!') const expectedSpanPromise = expectSpanWithDefaults({ - name: 'kafka.consume', - service: 'test-kafka', + name: namingSchema.receive.opName, + service: namingSchema.receive.serviceName, meta: { [ERROR_TYPE]: fakeError.name, [ERROR_MESSAGE]: fakeError.message, @@ -208,6 +218,15 @@ describe('Plugin', () => { .then(() => sendMessages(kafka, testTopic, messages)) .catch(done) }) + + withNamingSchema( + async () => { + await consumer.run({ eachMessage: () => {} }) + await sendMessages(kafka, testTopic, messages) + }, + () => namingSchema.send.opName, + () => namingSchema.send.serviceName + ) }) }) }) diff --git a/packages/datadog-plugin-kafkajs/test/naming.js b/packages/datadog-plugin-kafkajs/test/naming.js new file mode 100644 index 00000000000..fdcb0e03b5a --- /dev/null +++ b/packages/datadog-plugin-kafkajs/test/naming.js @@ -0,0 +1,24 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + send: { + v0: { + opName: 'kafka.produce', + serviceName: 'test-kafka' + }, + v1: { + opName: 'kafka.send', + serviceName: 'test' + } + }, + receive: { + v0: { + opName: 'kafka.consume', + serviceName: 'test-kafka' + }, + v1: { + opName: 'kafka.process', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-rhea/src/consumer.js b/packages/datadog-plugin-rhea/src/consumer.js index b4f8a93bdf8..1adece16fbd 100644 --- a/packages/datadog-plugin-rhea/src/consumer.js +++ b/packages/datadog-plugin-rhea/src/consumer.js @@ -19,12 +19,10 @@ class RheaConsumerPlugin extends ConsumerPlugin { const name = getResourceNameFromMessage(msgObj) const childOf = extractTextMap(msgObj, this.tracer) - this.startSpan('amqp.receive', { + this.startSpan({ childOf, - service: this.config.service, resource: name, type: 'worker', - kind: 'consumer', meta: { 'component': 'rhea', 'amqp.link.source.address': name, diff --git a/packages/datadog-plugin-rhea/src/producer.js b/packages/datadog-plugin-rhea/src/producer.js index da8e6c8eff3..332aff1276d 100644 --- a/packages/datadog-plugin-rhea/src/producer.js +++ b/packages/datadog-plugin-rhea/src/producer.js @@ -9,17 +9,13 @@ class RheaProducerPlugin extends ProducerPlugin { constructor (...args) { super(...args) - this.addTraceSub('encode', this.encode.bind(this)) } start ({ targetAddress, host, port }) { const name = targetAddress || 'amq.topic' - - this.startSpan('amqp.send', { - service: this.config.service || `${this.tracer._service}-amqp-producer`, + this.startSpan({ resource: name, - kind: 'producer', meta: { 'component': 'rhea', 'amqp.link.target.address': name, diff --git a/packages/datadog-plugin-rhea/test/index.spec.js b/packages/datadog-plugin-rhea/test/index.spec.js index c74c1ceb183..a026b1fabaf 100644 --- a/packages/datadog-plugin-rhea/test/index.spec.js +++ b/packages/datadog-plugin-rhea/test/index.spec.js @@ -3,6 +3,7 @@ const { expect } = require('chai') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') describe('Plugin', () => { let tracer @@ -50,10 +51,10 @@ describe('Plugin', () => { agent.use(traces => { const span = traces[0][0] expect(span).to.include({ - name: 'amqp.send', + name: namingSchema.send.opName, resource: 'amq.topic', error: 0, - service: 'test-amqp-producer' + service: namingSchema.send.serviceName }) expect(span).to.not.have.property('type') expect(span.meta).to.include({ @@ -94,6 +95,11 @@ describe('Plugin', () => { context.sender.send(encodedMessage, undefined, 0) }) }) + + withNamingSchema( + () => { context.sender.send({ body: 'Hello World!' }) }, + () => namingSchema.send.opName, () => namingSchema.send.serviceName + ) }) describe('receiving a message', () => { @@ -101,10 +107,10 @@ describe('Plugin', () => { agent.use(traces => { const span = traces[0][0] expect(span).to.include({ - name: 'amqp.receive', + name: namingSchema.receive.opName, resource: 'amq.topic', error: 0, - service: 'test', + service: namingSchema.receive.serviceName, type: 'worker' }) expect(span.meta).to.include({ @@ -126,6 +132,12 @@ describe('Plugin', () => { }) context.sender.send({ body: 'Hello World!' }) }) + + withNamingSchema( + () => { context.sender.send({ body: 'Hello World!' }) }, + () => namingSchema.receive.opName, + () => namingSchema.receive.serviceName + ) }) }) @@ -151,19 +163,25 @@ describe('Plugin', () => { connection.open_receiver('amq.topic') }) + withNamingSchema( + () => { context.sender.send({ body: 'Hello World!' }) }, + () => namingSchema.receive.opName, () => 'a_test_service' + ) + it('should use the configuration for the receiver', (done) => { agent.use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.receive') + expect(span).to.have.property('name', namingSchema.receive.opName) expect(span).to.have.property('service', 'a_test_service') }) .then(done, done) context.sender.send({ body: 'Hello World!' }) }) + it('should use the configuration for the sender', (done) => { agent.use(traces => { const span = traces[0][0] - expect(span).to.have.property('name', 'amqp.send') + expect(span).to.have.property('name', namingSchema.send.opName) expect(span).to.have.property('service', 'a_test_service') }) .then(done, done) @@ -222,7 +240,7 @@ describe('Plugin', () => { describe('client sent message', () => { it('should be instrumented on receiving', done => { - const p = expectReceiving(agent) + const p = expectReceiving(agent, namingSchema) server.on('message', msg => { p.then(done, done) @@ -231,7 +249,7 @@ describe('Plugin', () => { }) it('should be instrumented on sending', done => { - const p = expectSending(agent, null, 'amq.topic.2') + const p = expectSending(agent, namingSchema, null, 'amq.topic.2') server.on('message', msg => { p.then(done, done) @@ -242,7 +260,7 @@ describe('Plugin', () => { describe('server sent message', () => { it('should be instrumented on receiving', done => { - const p = expectReceiving(agent, null, 'amq.topic.2') + const p = expectReceiving(agent, namingSchema, null, 'amq.topic.2') client.on('message', msg => { p.then(done, done) @@ -251,7 +269,7 @@ describe('Plugin', () => { }) it('should be instrumented on sending', done => { - const p = expectSending(agent) + const p = expectSending(agent, namingSchema) client.on('message', msg => { p.then(done, done) @@ -324,7 +342,7 @@ describe('Plugin', () => { describe('client sent message', () => { it('should be instrumented on sending', done => { - const p = expectSending(agent, false) + const p = expectSending(agent, namingSchema, false) server.on('message', msg => { p.then(done, done) @@ -333,7 +351,7 @@ describe('Plugin', () => { }) it('should be instrumented on receiving', done => { - const p = expectReceiving(agent) + const p = expectReceiving(agent, namingSchema) server.on('message', msg => { p.then(done, done) @@ -344,7 +362,7 @@ describe('Plugin', () => { describe('server sent message', () => { it('should be instrumented on sending', done => { - const p = expectSending(agent) + const p = expectSending(agent, namingSchema) client.on('message', msg => { p.then(done, done) @@ -353,7 +371,7 @@ describe('Plugin', () => { }) it('should be instrumented on receiving', done => { - const p = expectReceiving(agent) + const p = expectReceiving(agent, namingSchema) client.on('message', msg => { p.then(done, done) @@ -385,7 +403,7 @@ describe('Plugin', () => { describe('server sent message', () => { it('should be instrumented on sending', done => { - const p = expectSending(agent) + const p = expectSending(agent, namingSchema) client.on('message', msg => { msg.delivery.accept() @@ -395,7 +413,7 @@ describe('Plugin', () => { }) it('should be instrumented on receiving and accepting', done => { - const p = expectReceiving(agent) + const p = expectReceiving(agent, namingSchema) client.on('message', msg => { process.nextTick(() => { @@ -407,7 +425,7 @@ describe('Plugin', () => { }) it('should be instrumented on receiving and rejecting', done => { - const p = expectReceiving(agent, 'rejected') + const p = expectReceiving(agent, namingSchema, 'rejected') client.on('message', msg => { process.nextTick(() => { @@ -419,7 +437,7 @@ describe('Plugin', () => { }) it('should be instrumented on receiving and releasing', done => { - const p = expectReceiving(agent, 'released') + const p = expectReceiving(agent, namingSchema, 'released') client.on('message', msg => { process.nextTick(() => { @@ -431,7 +449,7 @@ describe('Plugin', () => { }) it('should be instrumented on receiving and modifying', done => { - const p = expectReceiving(agent, 'modified') + const p = expectReceiving(agent, namingSchema, 'modified') client.on('message', msg => { process.nextTick(() => { @@ -479,10 +497,10 @@ describe('Plugin', () => { agent.use(traces => { const span = traces[0][0] expect(span).to.include({ - name: 'amqp.send', + name: namingSchema.send.opName, resource: 'amq.topic', error: 1, - service: 'test-amqp-producer' + service: namingSchema.send.serviceName }) expect(span.meta).to.include({ 'span.kind': 'producer', @@ -511,10 +529,10 @@ describe('Plugin', () => { agent.use(traces => { const span = traces[0][0] expect(span).to.include({ - name: 'amqp.receive', + name: namingSchema.receive.opName, resource: 'amq.topic', error: 1, - service: 'test' + service: namingSchema.receive.serviceName }) expect(span.meta).to.include({ 'span.kind': 'consumer', @@ -540,16 +558,16 @@ describe('Plugin', () => { }) }) -function expectReceiving (agent, deliveryState, topic) { +function expectReceiving (agent, namingSchema, deliveryState, topic) { deliveryState = deliveryState || deliveryState === false ? undefined : 'accepted' topic = topic || 'amq.topic' return Promise.resolve().then(() => agent.use(traces => { const span = traces[0][0] expect(span).to.include({ - name: 'amqp.receive', + name: namingSchema.receive.opName, resource: topic, error: 0, - service: 'test', + service: namingSchema.receive.serviceName, type: 'worker' }) const expectedMeta = { @@ -565,16 +583,16 @@ function expectReceiving (agent, deliveryState, topic) { })) } -function expectSending (agent, deliveryState, topic) { +function expectSending (agent, namingSchema, deliveryState, topic) { deliveryState = deliveryState || deliveryState === false ? undefined : 'accepted' topic = topic || 'amq.topic' return Promise.resolve().then(() => agent.use(traces => { const span = traces[0][0] expect(span).to.include({ - name: 'amqp.send', + name: namingSchema.send.opName, resource: topic, error: 0, - service: 'test-amqp-producer' + service: namingSchema.send.serviceName }) expect(span).to.not.have.property('type') const expectedMeta = { diff --git a/packages/datadog-plugin-rhea/test/naming.js b/packages/datadog-plugin-rhea/test/naming.js new file mode 100644 index 00000000000..c68796c22c8 --- /dev/null +++ b/packages/datadog-plugin-rhea/test/naming.js @@ -0,0 +1,24 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + send: { + v0: { + opName: 'amqp.send', + serviceName: 'test-amqp-producer' + }, + v1: { + opName: 'amqp.send', + serviceName: 'test' + } + }, + receive: { + v0: { + opName: 'amqp.receive', + serviceName: 'test' + }, + v1: { + opName: 'amqp.process', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-tedious/test/naming.js b/packages/datadog-plugin-tedious/test/naming.js new file mode 100644 index 00000000000..ab3d95a4e53 --- /dev/null +++ b/packages/datadog-plugin-tedious/test/naming.js @@ -0,0 +1,14 @@ +const { namingResolver } = require('../../dd-trace/test/plugins/helpers') + +module.exports = namingResolver({ + outbound: { + v0: { + opName: 'tedious.request', + serviceName: 'test-mssql' + }, + v1: { + opName: 'sqlserver.query', + serviceName: 'test' + } + } +}) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index f67a38c609d..9ddad638278 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -34,6 +34,19 @@ function safeJsonParse (input) { } } +const namingVersions = ['v0', 'v1'] +const defaultVersion = 'v0' + +function validateNamingVersion (versionString) { + if (!namingVersions.includes(versionString)) { + log.warn( + `Unexpected input for config.spanAttributeSchema, picked default ${defaultVersion}` + ) + return defaultVersion + } + return versionString +} + // Shallow clone with property name remapping function remapify (input, mappings) { if (!input) return @@ -260,7 +273,9 @@ class Config { process.env.DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED, false ) - + const DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = validateNamingVersion( + process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA + ) const DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH = coalesce( process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, '512' @@ -467,6 +482,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) sourceMap: !isFalse(DD_PROFILING_SOURCE_MAP), exporters: DD_PROFILING_EXPORTERS } + this.spanAttributeSchema = DD_TRACE_SPAN_ATTRIBUTE_SCHEMA this.lookup = options.lookup this.startupLogs = isTrue(DD_TRACE_STARTUP_LOGS) // Disabled for CI Visibility's agentless diff --git a/packages/dd-trace/src/plugin_manager.js b/packages/dd-trace/src/plugin_manager.js index 7bbfbaeef6b..dd5f7ed9099 100644 --- a/packages/dd-trace/src/plugin_manager.js +++ b/packages/dd-trace/src/plugin_manager.js @@ -4,6 +4,7 @@ const { channel } = require('../../diagnostics_channel') const { isFalse } = require('./util') const plugins = require('./plugins') const log = require('./log') +const Nomenclature = require('./service-naming') const loadChannel = channel('dd-trace:instrumentation:load') @@ -96,6 +97,7 @@ module.exports = class PluginManager { // like instrumenter.enable() configure (config = {}) { this._tracerConfig = config + Nomenclature.configure(config) for (const name in pluginClasses) { this.loadPlugin(name) diff --git a/packages/dd-trace/src/plugins/client.js b/packages/dd-trace/src/plugins/client.js index a487db54ade..e0fe539ca4a 100644 --- a/packages/dd-trace/src/plugins/client.js +++ b/packages/dd-trace/src/plugins/client.js @@ -1,9 +1,10 @@ 'use strict' -const OutgoingPlugin = require('./outgoing') +const OutboundPlugin = require('./outbound') -class ClientPlugin extends OutgoingPlugin { +class ClientPlugin extends OutboundPlugin { static get operation () { return 'request' } + static get kind () { return 'client' } } module.exports = ClientPlugin diff --git a/packages/dd-trace/src/plugins/consumer.js b/packages/dd-trace/src/plugins/consumer.js index 91689ce1196..88128bff1f6 100644 --- a/packages/dd-trace/src/plugins/consumer.js +++ b/packages/dd-trace/src/plugins/consumer.js @@ -1,9 +1,24 @@ 'use strict' -const IncomingPlugin = require('./incoming') +const InboundPlugin = require('./inbound') -class ConsumerPlugin extends IncomingPlugin { +class ConsumerPlugin extends InboundPlugin { static get operation () { return 'receive' } + static get kind () { return 'consumer' } + static get type () { return 'messaging' } + + startSpan (options) { + const spanDefaults = { + service: this.config.service || this.serviceName(), + kind: this.constructor.kind + } + Object.keys(spanDefaults).forEach( + key => { + if (!options[key]) options[key] = spanDefaults[key] + } + ) + return super.startSpan(this.operationName(), options) + } } module.exports = ConsumerPlugin diff --git a/packages/dd-trace/src/plugins/inbound.js b/packages/dd-trace/src/plugins/inbound.js new file mode 100644 index 00000000000..49a533d92d0 --- /dev/null +++ b/packages/dd-trace/src/plugins/inbound.js @@ -0,0 +1,7 @@ +'use strict' + +const TracingPlugin = require('./tracing') + +class InboundPlugin extends TracingPlugin {} + +module.exports = InboundPlugin diff --git a/packages/dd-trace/src/plugins/incoming.js b/packages/dd-trace/src/plugins/incoming.js deleted file mode 100644 index 2bb5fa0f2a2..00000000000 --- a/packages/dd-trace/src/plugins/incoming.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -const TracingPlugin = require('./tracing') - -class IncomingPlugin extends TracingPlugin {} - -module.exports = IncomingPlugin diff --git a/packages/dd-trace/src/plugins/outgoing.js b/packages/dd-trace/src/plugins/outbound.js similarity index 87% rename from packages/dd-trace/src/plugins/outgoing.js rename to packages/dd-trace/src/plugins/outbound.js index a0bbdacbada..ee9202b4d51 100644 --- a/packages/dd-trace/src/plugins/outgoing.js +++ b/packages/dd-trace/src/plugins/outbound.js @@ -4,7 +4,7 @@ const { CLIENT_PORT_KEY } = require('../constants') const TracingPlugin = require('./tracing') // TODO: Exit span on finish when AsyncResource instances are removed. -class OutgoingPlugin extends TracingPlugin { +class OutboundPlugin extends TracingPlugin { constructor (...args) { super(...args) @@ -29,4 +29,4 @@ class OutgoingPlugin extends TracingPlugin { } } -module.exports = OutgoingPlugin +module.exports = OutboundPlugin diff --git a/packages/dd-trace/src/plugins/producer.js b/packages/dd-trace/src/plugins/producer.js index fd45cbd6f4f..99f914edabe 100644 --- a/packages/dd-trace/src/plugins/producer.js +++ b/packages/dd-trace/src/plugins/producer.js @@ -1,9 +1,24 @@ 'use strict' -const OutgoingPlugin = require('./outgoing') +const OutboundPlugin = require('./outbound') -class ProducerPlugin extends OutgoingPlugin { +class ProducerPlugin extends OutboundPlugin { static get operation () { return 'publish' } + static get kind () { return 'producer' } + static get type () { return 'messaging' } + + startSpan (options) { + const spanDefaults = { + service: this.config.service || this.serviceName(), + kind: this.constructor.kind + } + Object.keys(spanDefaults).forEach( + key => { + if (!options[key]) options[key] = spanDefaults[key] + } + ) + return super.startSpan(this.operationName(), options) + } } module.exports = ProducerPlugin diff --git a/packages/dd-trace/src/plugins/server.js b/packages/dd-trace/src/plugins/server.js index 13684311675..eaf8b5b002b 100644 --- a/packages/dd-trace/src/plugins/server.js +++ b/packages/dd-trace/src/plugins/server.js @@ -1,8 +1,8 @@ 'use strict' -const IncomingPlugin = require('./incoming') +const InboundPlugin = require('./inbound') -class ServerPlugin extends IncomingPlugin { +class ServerPlugin extends InboundPlugin { static get operation () { return 'request' } } diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index 645002f5b6b..0f2ecc28547 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -4,6 +4,7 @@ const Plugin = require('./plugin') const { storage } = require('../../../datadog-core') const analyticsSampler = require('../analytics_sampler') const { COMPONENT } = require('../constants') +const Nomenclature = require('../service-naming') class TracingPlugin extends Plugin { constructor (...args) { @@ -31,6 +32,16 @@ class TracingPlugin extends Plugin { return store && store.span } + serviceName (serviceArgs) { + const { type, id, kind } = this.constructor + return Nomenclature.serviceName(type, kind, id, serviceArgs) + } + + operationName (opNameArgs) { + const { type, id, kind } = this.constructor + return Nomenclature.opName(type, kind, id, opNameArgs) + } + configure (config) { return super.configure({ ...config, diff --git a/packages/dd-trace/src/service-naming/index.js b/packages/dd-trace/src/service-naming/index.js new file mode 100644 index 00000000000..eab4d12c9c4 --- /dev/null +++ b/packages/dd-trace/src/service-naming/index.js @@ -0,0 +1,41 @@ +const { schemaDefinitions } = require('./schemas') + +const kindMap = { + messaging: { + client: 'controlPlane', + consumer: 'inbound', + producer: 'outbound' + } +} + +class SchemaManager { + constructor () { + this.schemas = schemaDefinitions + this.config = { spanAttributeSchema: 'v0' } + } + + get schema () { + return this.schemas[this.version] + } + + get version () { + return this.config.spanAttributeSchema + } + + opName (type, kind, plugin, opNameArgs) { + return this.schema.getOpName(type, kindMap[type][kind], plugin, opNameArgs) + } + + serviceName (type, kind, plugin, serviceNameArgs) { + return this.schema.getServiceName(type, kindMap[type][kind], plugin, serviceNameArgs) + } + + configure (config = {}) { + this.config = config + Object.values(this.schemas).forEach(schemaDef => { + schemaDef.configure(config) + }) + } +} + +module.exports = new SchemaManager() diff --git a/packages/dd-trace/src/service-naming/schemas/definition.js b/packages/dd-trace/src/service-naming/schemas/definition.js new file mode 100644 index 00000000000..0022cc0ab53 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/definition.js @@ -0,0 +1,28 @@ +class SchemaDefinition { + constructor (schema) { + this.schema = schema + } + + getSchemaItem (type, subType, plugin) { + const schema = this.schema + if (schema && schema[type] && schema[type][subType] && schema[type][subType][plugin]) { + return schema[type][subType][plugin] + } + } + + getOpName (type, subType, plugin, opNameArgs) { + const item = this.getSchemaItem(type, subType, plugin) + return item.opName(opNameArgs) + } + + getServiceName (type, subType, plugin, serviceNameArgs) { + const item = this.getSchemaItem(type, subType, plugin) + return item.serviceName(this.service, serviceNameArgs) + } + + configure ({ service }) { + this.service = service + } +} + +module.exports = SchemaDefinition diff --git a/packages/dd-trace/src/service-naming/schemas/index.js b/packages/dd-trace/src/service-naming/schemas/index.js new file mode 100644 index 00000000000..79dda7f9772 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/index.js @@ -0,0 +1,6 @@ +const v0 = require('./v0') +const v1 = require('./v1') + +module.exports = { + schemaDefinitions: { v0, v1 } +} diff --git a/packages/dd-trace/src/service-naming/schemas/v0.js b/packages/dd-trace/src/service-naming/schemas/v0.js new file mode 100644 index 00000000000..aa5eb6fc147 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/v0.js @@ -0,0 +1,66 @@ +const SchemaDefinition = require('./definition') + +function amqpServiceName (service) { + return `${service}-amqp` +} + +const schema = { + messaging: { + outbound: { + amqplib: { + opName: () => 'amqp.command', + serviceName: amqpServiceName + }, + amqp10: { + opName: () => 'amqp.send', + serviceName: amqpServiceName + }, + 'google-cloud-pubsub': { + opName: () => 'pubsub.request', + serviceName: service => `${service}-pubsub` + }, + kafkajs: { + opName: () => 'kafka.produce', + serviceName: service => `${service}-kafka` + }, + rhea: { + opName: () => 'amqp.send', + serviceName: service => `${service}-amqp-producer` + } + }, + inbound: { + amqplib: { + opName: () => 'amqp.command', + serviceName: amqpServiceName + }, + amqp10: { + opName: () => 'amqp.receive', + serviceName: amqpServiceName + }, + 'google-cloud-pubsub': { + opName: () => 'pubsub.receive', + serviceName: service => service + }, + kafkajs: { + opName: () => 'kafka.consume', + serviceName: service => `${service}-kafka` + }, + rhea: { + opName: () => 'amqp.receive', + serviceName: service => service + } + }, + controlPlane: { + amqplib: { + opName: () => 'amqp.command', + serviceName: amqpServiceName + }, + 'google-cloud-pubsub': { + opName: () => 'pubsub.request', + serviceName: service => `${service}-pubsub` + } + } + } +} + +module.exports = new SchemaDefinition(schema) diff --git a/packages/dd-trace/src/service-naming/schemas/v1.js b/packages/dd-trace/src/service-naming/schemas/v1.js new file mode 100644 index 00000000000..709e16aeb0f --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/v1.js @@ -0,0 +1,58 @@ +const SchemaDefinition = require('./definition') + +function identityService (service) { + return service +} + +const amqpInbound = { + opName: () => 'amqp.process', + serviceName: identityService +} + +const amqpOutbound = { + opName: () => 'amqp.send', + serviceName: identityService +} + +const schema = { + messaging: { + outbound: { + amqplib: amqpOutbound, + amqp10: amqpOutbound, + 'google-cloud-pubsub': { + opName: () => 'gcp.pubsub.send', + serviceName: identityService + }, + kafkajs: { + opName: () => 'kafka.send', + serviceName: identityService + }, + rhea: amqpOutbound + }, + inbound: { + amqplib: amqpInbound, + amqp10: amqpInbound, + 'google-cloud-pubsub': { + opName: () => 'gcp.pubsub.process', + serviceName: identityService + }, + kafkajs: { + opName: () => 'kafka.process', + serviceName: identityService + }, + rhea: amqpInbound + }, + controlPlane: { + amqplib: { + opName: () => 'amqp.command', + serviceName: identityService + }, + 'google-cloud-pubsub': { + opName: () => 'gcp.pubsub.request', + serviceName: identityService + } + } + } +} + +module.exports = new SchemaDefinition(schema) diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 0622991797c..ca9a03ffc59 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -90,6 +90,7 @@ describe('Config', () => { expect(config).to.have.property('logLevel', 'debug') expect(config).to.have.property('traceId128BitGenerationEnabled', false) expect(config).to.have.property('traceId128BitLoggingEnabled', false) + expect(config).to.have.property('spanAttributeSchema', 'v0') expect(config).to.have.deep.property('serviceMapping', {}) expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['tracecontext', 'datadog']) expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['tracecontext', 'datadog']) @@ -176,6 +177,7 @@ describe('Config', () => { process.env.DD_TRACE_EXPERIMENTAL_EXPORTER = 'log' process.env.DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED = 'true' process.env.DD_TRACE_EXPERIMENTAL_INTERNAL_ERRORS_ENABLED = 'true' + process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = 'v1' process.env.DD_APPSEC_ENABLED = 'true' process.env.DD_APPSEC_RULES = RULES_JSON_PATH process.env.DD_APPSEC_TRACE_RATE_LIMIT = '42' @@ -213,6 +215,7 @@ describe('Config', () => { expect(config).to.have.property('sampleRate', 0.5) expect(config).to.have.property('traceId128BitGenerationEnabled', true) expect(config).to.have.property('traceId128BitLoggingEnabled', true) + expect(config).to.have.property('spanAttributeSchema', 'v1') expect(config.tags).to.include({ foo: 'bar', baz: 'qux' }) expect(config.tags).to.include({ service: 'service', 'version': '1.0.0', 'env': 'test' }) expect(config).to.have.deep.nested.property('sampler', { @@ -497,6 +500,16 @@ describe('Config', () => { 'DD_TRACE_PROPAGATION_STYLE_EXTRACT environment variables') }) + it('should warn if defaulting to v0 span attribute schema', () => { + process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = 'foo' + + // eslint-disable-next-line no-new + const config = new Config() + + expect(log.warn).to.have.been.calledWith('Unexpected input for config.spanAttributeSchema, picked default v0') + expect(config).to.have.property('spanAttributeSchema', 'v0') + }) + it('should give priority to the common agent environment variable', () => { process.env.DD_TRACE_AGENT_HOSTNAME = 'trace-agent' process.env.DD_AGENT_HOST = 'agent' diff --git a/packages/dd-trace/test/plugin_manager.spec.js b/packages/dd-trace/test/plugin_manager.spec.js index 22613326fae..727b14526e7 100644 --- a/packages/dd-trace/test/plugin_manager.spec.js +++ b/packages/dd-trace/test/plugin_manager.spec.js @@ -6,6 +6,7 @@ const { channel } = require('../../diagnostics_channel') const proxyquire = require('proxyquire') const loadChannel = channel('dd-trace:instrumentation:load') +const Nomenclature = require('../../dd-trace/src/service-naming') describe('Plugin Manager', () => { let tracer @@ -240,6 +241,26 @@ describe('Plugin Manager', () => { loadChannel.publish({ name: 'four' }) expect(instantiated).to.deep.equal(['two', 'four']) }) + describe('service naming schema manager', () => { + const config = { + foo: { 'bar': 1 }, + baz: 2 + } + let configureSpy + + beforeEach(() => { + configureSpy = sinon.spy(Nomenclature, 'configure') + }) + + afterEach(() => { + configureSpy.restore() + }) + + it('is configured when plugin manager is configured', () => { + pm.configure(config) + expect(configureSpy).to.have.been.calledWith(config) + }) + }) it('skips configuring plugins entirely when plugins is false', () => { pm.configurePlugin = sinon.spy() pm.configure({ plugins: false }) diff --git a/packages/dd-trace/test/plugins/helpers.js b/packages/dd-trace/test/plugins/helpers.js index e6f78fd0817..8d1f329de3f 100644 --- a/packages/dd-trace/test/plugins/helpers.js +++ b/packages/dd-trace/test/plugins/helpers.js @@ -5,6 +5,16 @@ const { Int64BE } = require('int64-buffer') // TODO remove dependency const { AssertionError } = require('assert') const { AsyncResource } = require('../../../datadog-instrumentations/src/helpers/instrument') +const Nomenclature = require('../../src/service-naming') + +function resolveNaming (namingSchema) { + return new Proxy(namingSchema, { + get (target, prop, receiver) { + return target[prop][Nomenclature.version] + } + }) +} + function expectSomeSpan (agent, expected, timeout) { return agent.use(traces => { const scoredErrors = [] @@ -112,6 +122,7 @@ module.exports = { compare, deepInclude, expectSomeSpan, + resolveNaming, unbreakThen, withDefaults } diff --git a/packages/dd-trace/test/service-naming/schema.spec.js b/packages/dd-trace/test/service-naming/schema.spec.js new file mode 100644 index 00000000000..e8871b9e674 --- /dev/null +++ b/packages/dd-trace/test/service-naming/schema.spec.js @@ -0,0 +1,88 @@ +const SchemaDefinition = require('../../src/service-naming/schemas/definition') + +describe('Service naming', () => { + let singleton + + describe('Version selection', () => { + beforeEach(() => { + singleton = require('../../src/service-naming') + }) + + afterEach(() => { + delete require.cache[require.resolve('../../src/service-naming')] + }) + + it('Should default to v0 when required', () => { + expect(singleton.version).to.be.equal('v0') + }) + + it('Should grab the version given by `spanAttributeSchema`', () => { + singleton.configure({ spanAttributeSchema: 'MyShinyNewVersion' }) + expect(singleton.version).to.be.equal('MyShinyNewVersion') + }) + + describe('Name resolution proxy', () => { + let singleton + let versions + const extra = { my: { extra: 'args' } } + + before(() => { + versions = { v0: { getOpName: sinon.spy(), getServiceName: sinon.spy() } } + singleton = require('../../src/service-naming') + singleton.schemas = versions + }) + + const forwardList = [['opName', 'getOpName'], ['serviceName', 'getServiceName']] + forwardList.forEach((forwardPair) => { + const [from, to] = forwardPair + it(`should forward service arguments from ${from} to ${to}`, () => { + singleton[from]('storage', 'outbound', 'redis', extra) + expect(versions.v0[to]).to.be.calledWith('storage', 'outbound', 'redis', extra) + }) + }) + }) + }) + + describe('Naming schema definition', () => { + const dummySchema = { + messaging: { + inbound: { + kafka: { + opName: sinon.spy(), + serviceName: sinon.spy() + } + } + } + } + + const resolver = new SchemaDefinition(dummySchema) + resolver.configure({ service: 'test-service' }) + + const extra = { my: { extra: 'args' } } + + describe('Item resolver', () => { + it('should answer undefined on inexistent plugin', () => { + expect(resolver.getSchemaItem('messaging', 'inbound', 'foo')).to.be.equal(undefined) + }) + it('should answer undefined on inexistent i/o dir', () => { + expect(resolver.getSchemaItem('messaging', 'foo', 'kafka')).to.be.equal(undefined) + }) + it('should answer undefined on inexistent type', () => { + expect(resolver.getSchemaItem('foo', 'inbound', 'kafka')).to.be.equal(undefined) + }) + }) + + describe('Operation name getter', () => { + it('should passthrough operation name arguments', () => { + resolver.getOpName('messaging', 'inbound', 'kafka', extra) + expect(dummySchema.messaging.inbound.kafka.opName).to.be.calledWith(extra) + }) + }) + describe('Service name getter', () => { + it('should add service name and passthrough service name arguments', () => { + resolver.getServiceName('messaging', 'inbound', 'kafka', extra) + expect(dummySchema.messaging.inbound.kafka.serviceName).to.be.calledWith('test-service', extra) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 0f64313568d..1ee96737665 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -9,10 +9,13 @@ const externals = require('../plugins/externals.json') const slackReport = require('./slack-report') const metrics = require('../../src/metrics') const agent = require('../plugins/agent') +const Nomenclature = require('../../../dd-trace/src/service-naming') const { storage } = require('../../../datadog-core') +const { schemaDefinitions } = require('../../src/service-naming/schemas') global.withVersions = withVersions global.withExports = withExports +global.withNamingSchema = withNamingSchema const packageVersionFailures = Object.create({}) @@ -44,6 +47,40 @@ function loadInstFile (file, instrumentations) { }) } +function withNamingSchema (spanProducerFn, expectedOpName, expectedServiceName) { + let fullConfig + + describe('service and operation naming', () => { + Object.keys(schemaDefinitions).forEach(versionName => { + describe(`in version ${versionName}`, () => { + before(() => { + fullConfig = Nomenclature.config + Nomenclature.configure({ + spanAttributeSchema: versionName, + service: fullConfig.service // Hack: only way to retrieve the test agent configuration + }) + }) + + after(() => { + Nomenclature.configure(fullConfig) + }) + + it(`should conform to the naming schema`, done => { + agent + .use(traces => { + const span = traces[0][0] + expect(span).to.have.property('name', expectedOpName()) + expect(span).to.have.property('service', expectedServiceName()) + }) + .then(done) + .catch(done) + spanProducerFn() + }) + }) + }) + }) +} + function withVersions (plugin, modules, range, cb) { const instrumentations = typeof plugin === 'string' ? loadInst(plugin) : [].concat(plugin) const names = instrumentations.map(instrumentation => instrumentation.name)