From 719b6915f6eb9da505cc358948d071b1604fa0a0 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Wed, 21 Jun 2023 18:46:25 -0400 Subject: [PATCH 01/20] fix: Lambda handler must be awaited --- packages/dd-trace/src/lambda/handler.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index eb6daad5f07..7913e282df4 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -81,9 +81,9 @@ function extractContext (args) { * * @param {*} lambdaHandler a Lambda handler function. */ -exports.datadog = function datadog (lambdaHandler) { - return (...args) => { - const patched = lambdaHandler.apply(this, args) +exports.datadog = function datadog(lambdaHandler) { + return async (...args) => { + const patched = await lambdaHandler.apply(this, args) try { const context = extractContext(args) From 04698b071e2722677cbfa9b915253507d5d20eb7 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 22 Jun 2023 10:00:28 -0400 Subject: [PATCH 02/20] feat: Move handler call until after context patch. Remove catch as we'll throw whatever is thrown by the handler --- packages/dd-trace/src/lambda/handler.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index 7913e282df4..c9e8db97824 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -83,19 +83,16 @@ function extractContext (args) { */ exports.datadog = function datadog(lambdaHandler) { return async (...args) => { - const patched = await lambdaHandler.apply(this, args) + let patched - try { - const context = extractContext(args) + const context = extractContext(args) - checkTimeout(context) + checkTimeout(context) + patched = await lambdaHandler.apply(this, args) - if (patched) { - // clear the timeout as soon as a result is returned - patched.then(_ => clearTimeout(__lambdaTimeout)) - } - } catch (e) { - log.debug('Error patching AWS Lambda handler. Timeout spans will not be generated.') + if (patched) { + // clear the timeout as soon as a result is returned + patched.then(_ => clearTimeout(__lambdaTimeout)) } return patched From ef425a82bba80e7eee5973e02e79b3fb32877afe Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 22 Jun 2023 10:59:45 -0400 Subject: [PATCH 03/20] feat: Simplify patch, remove .then in favor of async/await --- packages/dd-trace/src/lambda/handler.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index c9e8db97824..516f849cfa8 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -83,17 +83,11 @@ function extractContext (args) { */ exports.datadog = function datadog(lambdaHandler) { return async (...args) => { - let patched - const context = extractContext(args) checkTimeout(context) - patched = await lambdaHandler.apply(this, args) - - if (patched) { - // clear the timeout as soon as a result is returned - patched.then(_ => clearTimeout(__lambdaTimeout)) - } + const patched = await lambdaHandler.apply(this, args) + clearTimeout(__lambdaTimeout) return patched } From 2d386c2a37a8107e06cdecbf4e9d246333a3033e Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 22 Jun 2023 11:08:10 -0400 Subject: [PATCH 04/20] fix: lint --- packages/dd-trace/src/lambda/handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index 516f849cfa8..4648e243f75 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -81,7 +81,7 @@ function extractContext (args) { * * @param {*} lambdaHandler a Lambda handler function. */ -exports.datadog = function datadog(lambdaHandler) { +exports.datadog = function datadog (lambdaHandler) { return async (...args) => { const context = extractContext(args) From 91bf94d07dcfcb5fe36085f3ea48cc82757960f8 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 22 Jun 2023 11:39:29 -0400 Subject: [PATCH 05/20] feat: Add unit test --- .../dd-trace/test/lambda/fixtures/handler.js | 13 ++++++- packages/dd-trace/test/lambda/index.spec.js | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/dd-trace/test/lambda/fixtures/handler.js b/packages/dd-trace/test/lambda/fixtures/handler.js index 825e3fef35a..1f5640f8f56 100644 --- a/packages/dd-trace/test/lambda/fixtures/handler.js +++ b/packages/dd-trace/test/lambda/fixtures/handler.js @@ -48,9 +48,20 @@ const swappedArgsHandler = async (event, _, context) => { return response } +const errorHandler = async (_event, _context) => { + class CustomError extends Error { + constructor (message) { + super(message) + Object.defineProperty(this, 'name', { value: 'CustomError' }) + } + } + throw new CustomError('my error') +} + module.exports = { finishSpansEarlyTimeoutHandler, handler, swappedArgsHandler, - timeoutHandler + timeoutHandler, + errorHandler } diff --git a/packages/dd-trace/test/lambda/index.spec.js b/packages/dd-trace/test/lambda/index.spec.js index 0fca588a4d1..f265baa0a46 100644 --- a/packages/dd-trace/test/lambda/index.spec.js +++ b/packages/dd-trace/test/lambda/index.spec.js @@ -98,6 +98,40 @@ describe('lambda', () => { await checkTraces }) + it('does wrap handler causing unhandled promise rejections', async () => { + // Set the desired handler to patch + process.env.DD_LAMBDA_HANDLER = 'handler.handler' + // Load the agent and re-register hook for patching. + await loadAgent() + + const _context = { + getRemainingTimeInMillis: () => 150 + } + const _event = {} + + // Mock `datadog-lambda` handler resolve and import. + const _handlerPath = path.resolve(__dirname, './fixtures/handler.js') + const app = require(_handlerPath) + datadog = require('./fixtures/datadog-lambda') + + // Run the function. + try { + await datadog(app.errorHandler)(_event, _context) + } catch (e) { + expect(e.name).to.equal('CustomError') + } + + // Expect traces to be correct. + const checkTraces = agent.use((_traces) => { + const traces = _traces[0] + expect(traces).lengthOf(1) + traces.forEach((trace) => { + expect(trace.error).to.equal(1) + }) + }) + await checkTraces + }) + it('correctly patch handler where context is the third argument', async () => { process.env.DD_LAMBDA_HANDLER = 'handler.swappedArgsHandler' From 1538225b65a80ad160196c2e366bebce115541e0 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 22 Jun 2023 12:06:08 -0400 Subject: [PATCH 06/20] feat: Use then instead of async/await --- packages/dd-trace/src/lambda/handler.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index 4648e243f75..6279c3d2d20 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -82,13 +82,10 @@ function extractContext (args) { * @param {*} lambdaHandler a Lambda handler function. */ exports.datadog = function datadog (lambdaHandler) { - return async (...args) => { + return (...args) => { const context = extractContext(args) checkTimeout(context) - const patched = await lambdaHandler.apply(this, args) - clearTimeout(__lambdaTimeout) - - return patched + return lambdaHandler.apply(this, args).then((res) => { clearTimeout(__lambdaTimeout); return res }) } } From 4dbfa241dcfa7d24530994f187dc5b5c1f135dd0 Mon Sep 17 00:00:00 2001 From: Nicolas Savoire Date: Thu, 22 Jun 2023 23:01:09 +0200 Subject: [PATCH 07/20] Unify test code between release branches (#3282) --- packages/datadog-plugin-fetch/test/index.spec.js | 10 ++++++---- packages/datadog-plugin-http/test/client.spec.js | 10 ++++++---- packages/datadog-plugin-http2/test/client.spec.js | 4 +++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/datadog-plugin-fetch/test/index.spec.js b/packages/datadog-plugin-fetch/test/index.spec.js index 0d25e3a78d4..d23067f6355 100644 --- a/packages/datadog-plugin-fetch/test/index.spec.js +++ b/packages/datadog-plugin-fetch/test/index.spec.js @@ -6,10 +6,12 @@ const tags = require('../../../ext/tags') const { expect } = require('chai') const { storage } = require('../../datadog-core') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const { DD_MAJOR } = require('../../../version') const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS +const SERVICE_NAME = DD_MAJOR < 3 ? 'test-http-client' : 'test' const describe = globalThis.fetch ? globalThis.describe : globalThis.describe.skip describe('Plugin', () => { @@ -52,7 +54,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'GET') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -79,7 +81,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'GET') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -106,7 +108,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'GET') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -390,7 +392,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-http/test/client.spec.js b/packages/datadog-plugin-http/test/client.spec.js index 20b499e8244..243f37ca462 100644 --- a/packages/datadog-plugin-http/test/client.spec.js +++ b/packages/datadog-plugin-http/test/client.spec.js @@ -10,10 +10,12 @@ const { storage } = require('../../datadog-core') const key = fs.readFileSync(path.join(__dirname, './ssl/test.key')) const cert = fs.readFileSync(path.join(__dirname, './ssl/test.crt')) const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const { DD_MAJOR } = require('../../../version') const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS const NODE_MAJOR = parseInt(process.versions.node.split('.')[0]) +const SERVICE_NAME = DD_MAJOR < 3 ? 'test-http-client' : 'test' describe('Plugin', () => { let express @@ -64,7 +66,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'GET') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -121,7 +123,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'CONNECT') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -166,7 +168,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'GET') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -825,7 +827,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-http2/test/client.spec.js b/packages/datadog-plugin-http2/test/client.spec.js index 410eb6ac09b..312594923f9 100644 --- a/packages/datadog-plugin-http2/test/client.spec.js +++ b/packages/datadog-plugin-http2/test/client.spec.js @@ -8,9 +8,11 @@ const tags = require('../../../ext/tags') const key = fs.readFileSync(path.join(__dirname, './ssl/test.key')) const cert = fs.readFileSync(path.join(__dirname, './ssl/test.crt')) const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const { DD_MAJOR } = require('../../../version') const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS +const SERVICE_NAME = DD_MAJOR < 3 ? 'test-http-client' : 'test' describe('Plugin', () => { let http2 @@ -63,7 +65,7 @@ describe('Plugin', () => { getPort().then(port => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('service', SERVICE_NAME) expect(traces[0][0]).to.have.property('type', 'http') expect(traces[0][0]).to.have.property('resource', 'GET') expect(traces[0][0].meta).to.have.property('span.kind', 'client') From f8442abc8442c8499195891341ca05cd07f74583 Mon Sep 17 00:00:00 2001 From: Thomas Hunter II Date: Thu, 22 Jun 2023 16:04:33 -0700 Subject: [PATCH 08/20] upgrade semver to fix audit lint errors (#3285) --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index bfb3aa23085..35f76047f81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,7 +409,7 @@ node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/pprof@^2.2.2": +"@datadog/pprof@2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-2.2.2.tgz#5cc8aa2c198bb594bc8ecd85c94adbfac6e75563" integrity sha512-6FVmgQoYvHVnpnAzfTHRIONJQprEJ6PdrfA3Kn4dfVEXZMH42PBRLSNWe4qoi5AKmr4SoIc6Ay7VAlHb/cDNjA== @@ -3984,9 +3984,9 @@ semver@^7.0.0: lru-cache "^6.0.0" semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + version "7.5.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" + integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== dependencies: lru-cache "^6.0.0" From dd74850dcac71e01443561f773505c4e44abf797 Mon Sep 17 00:00:00 2001 From: Nicolas Savoire Date: Fri, 23 Jun 2023 14:44:43 +0200 Subject: [PATCH 09/20] Bump profiler version to 2.2.3 (#3286) * Add DD_PROFILING_DEBUG_SOURCE_MAPS option DD_PROFILING_DEBUG_SOURCE_MAPS env variable enables printing of detailed diagnostics concerning source maps. Pass logger to profiler module to enable logging. * Bump profiler version to 2.2.3 --- package.json | 2 +- packages/dd-trace/src/profiling/config.js | 2 ++ packages/dd-trace/src/profiling/profiler.js | 14 ++++++++------ yarn.lock | 8 ++++---- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 0d254d8e9e7..3c37e52aa7f 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@datadog/native-iast-rewriter": "2.0.1", "@datadog/native-iast-taint-tracking": "^1.5.0", "@datadog/native-metrics": "^2.0.0", - "@datadog/pprof": "2.2.2", + "@datadog/pprof": "2.2.3", "@datadog/sketches-js": "^2.1.0", "@opentelemetry/api": "^1.0.0", "@opentelemetry/core": "^1.14.0", diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 547cc610afe..1cdf23e44c2 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -27,6 +27,7 @@ class Config { DD_TRACE_AGENT_URL, DD_AGENT_HOST, DD_TRACE_AGENT_PORT, + DD_PROFILING_DEBUG_SOURCE_MAPS, DD_PROFILING_UPLOAD_TIMEOUT, DD_PROFILING_SOURCE_MAP, DD_PROFILING_UPLOAD_PERIOD, @@ -70,6 +71,7 @@ class Config { this.flushInterval = flushInterval this.uploadTimeout = uploadTimeout this.sourceMap = sourceMap + this.debugSourceMaps = isTrue(coalesce(options.debugSourceMaps, DD_PROFILING_DEBUG_SOURCE_MAPS, false)) this.endpointCollection = endpointCollection this.pprofPrefix = pprofPrefix diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 79ea435ce2e..47dde4c7e57 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -4,12 +4,11 @@ const { EventEmitter } = require('events') const { Config } = require('./config') const { snapshotKinds } = require('./constants') -function maybeSourceMap (sourceMap) { +function maybeSourceMap (sourceMap, SourceMapper, debug) { if (!sourceMap) return - const { SourceMapper } = require('@datadog/pprof') return SourceMapper.create([ process.cwd() - ]) + ], debug) } class Profiler extends EventEmitter { @@ -42,12 +41,15 @@ class Profiler extends EventEmitter { // of the profiler from running without source maps. let mapper try { - mapper = await maybeSourceMap(config.sourceMap) - if (mapper) { + const { setLogger, SourceMapper } = require('@datadog/pprof') + setLogger(config.logger) + + mapper = await maybeSourceMap(config.sourceMap, SourceMapper, config.debugSourceMaps) + if (config.SourceMap && config.debugSourceMaps) { this._logger.debug(() => { return mapper.infoMap.size === 0 ? 'Found no source maps' - : `Found source-maps for following files: [${Array.from(mapper.infoMap.keys()).join(', ')}]` + : `Found source maps for following files: [${Array.from(mapper.infoMap.keys()).join(', ')}]` }) } } catch (err) { diff --git a/yarn.lock b/yarn.lock index 35f76047f81..a88f033215d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,10 +409,10 @@ node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/pprof@2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-2.2.2.tgz#5cc8aa2c198bb594bc8ecd85c94adbfac6e75563" - integrity sha512-6FVmgQoYvHVnpnAzfTHRIONJQprEJ6PdrfA3Kn4dfVEXZMH42PBRLSNWe4qoi5AKmr4SoIc6Ay7VAlHb/cDNjA== +"@datadog/pprof@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-2.2.3.tgz#a22ca30e386f5aa8559f4b2e297b76c80551c26d" + integrity sha512-cZXvNBBzvTMUx2xOxp49cZJ7/HOF7geVxqeRbveeJUVKwi8ZxmU1rQGcWPFX4iEEtfQu1M3NqbhmNtYsMJdEsQ== dependencies: delay "^5.0.0" node-gyp-build "^3.9.0" From 677076173a5cafae260cf5616ee09d3b27be4cf4 Mon Sep 17 00:00:00 2001 From: Jordi Bertran de Balanda Date: Fri, 23 Jun 2023 14:51:38 +0200 Subject: [PATCH 10/20] make http server plugins `ServerPlugins` (#3261) Co-authored-by: Thomas Hunter II --- packages/datadog-plugin-http/src/server.js | 72 +++++++++++---------- packages/datadog-plugin-http2/src/server.js | 46 ++++++------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index 21dc90c2025..e8644c9ef88 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -1,63 +1,67 @@ 'use strict' -const Plugin = require('../../dd-trace/src/plugins/plugin') +const ServerPlugin = require('../../dd-trace/src/plugins/server') const { storage } = require('../../datadog-core') const web = require('../../dd-trace/src/plugins/util/web') const { incomingHttpRequestStart, incomingHttpRequestEnd } = require('../../dd-trace/src/appsec/channels') const { COMPONENT } = require('../../dd-trace/src/constants') -class HttpServerPlugin extends Plugin { +class HttpServerPlugin extends ServerPlugin { static get id () { return 'http' } constructor (...args) { super(...args) - this._parentStore = undefined + this.addTraceSub('exit', message => this.exit(message)) + } + + addTraceSub (eventName, handler) { + this.addSub(`apm:${this.constructor.id}:server:${this.operation}:${eventName}`, handler) + } - this.addSub('apm:http:server:request:start', ({ req, res, abortController }) => { - const store = storage.getStore() - const span = web.startSpan(this.tracer, this.config, req, res, 'web.request') + start ({ req, res, abortController }) { + const store = storage.getStore() + const span = web.startSpan(this.tracer, this.config, req, res, 'web.request') - span.setTag(COMPONENT, this.constructor.id) + span.setTag(COMPONENT, this.constructor.id) - this._parentStore = store - this.enter(span, { ...store, req, res }) + this._parentStore = store + this.enter(span, { ...store, req, res }) - const context = web.getContext(req) + const context = web.getContext(req) - if (!context.instrumented) { - context.res.writeHead = web.wrapWriteHead(context) - context.instrumented = true - } + if (!context.instrumented) { + context.res.writeHead = web.wrapWriteHead(context) + context.instrumented = true + } - if (incomingHttpRequestStart.hasSubscribers) { - incomingHttpRequestStart.publish({ req, res, abortController }) // TODO: no need to make a new object here - } - }) + if (incomingHttpRequestStart.hasSubscribers) { + incomingHttpRequestStart.publish({ req, res, abortController }) // TODO: no need to make a new object here + } + } - this.addSub('apm:http:server:request:error', (error) => { - web.addError(error) - }) + error (error) { + web.addError(error) + } - this.addSub('apm:http:server:request:exit', ({ req }) => { - const span = this._parentStore && this._parentStore.span - this.enter(span, this._parentStore) - this._parentStore = undefined - }) + finish ({ req }) { + const context = web.getContext(req) - this.addSub('apm:http:server:request:finish', ({ req }) => { - const context = web.getContext(req) + if (!context || !context.res) return // Not created by a http.Server instance. - if (!context || !context.res) return // Not created by a http.Server instance. + if (incomingHttpRequestEnd.hasSubscribers) { + incomingHttpRequestEnd.publish({ req, res: context.res }) + } - if (incomingHttpRequestEnd.hasSubscribers) { - incomingHttpRequestEnd.publish({ req, res: context.res }) - } + web.finishAll(context) + } - web.finishAll(context) - }) + exit ({ req }) { + const span = this._parentStore && this._parentStore.span + this.enter(span, this._parentStore) + this._parentStore = undefined } configure (config) { diff --git a/packages/datadog-plugin-http2/src/server.js b/packages/datadog-plugin-http2/src/server.js index 52cc06b2367..8049248abdf 100644 --- a/packages/datadog-plugin-http2/src/server.js +++ b/packages/datadog-plugin-http2/src/server.js @@ -2,46 +2,46 @@ // Plugin temporarily disabled. See https://github.com/DataDog/dd-trace-js/issues/312 -const Plugin = require('../../dd-trace/src/plugins/plugin') +const ServerPlugin = require('../../dd-trace/src/plugins/server') const { storage } = require('../../datadog-core') const web = require('../../dd-trace/src/plugins/util/web') const { COMPONENT } = require('../../dd-trace/src/constants') -class Http2ServerPlugin extends Plugin { +class Http2ServerPlugin extends ServerPlugin { static get id () { return 'http2' } - constructor (...args) { - super(...args) + addTraceSub (eventName, handler) { + this.addSub(`apm:${this.constructor.id}:server:${this.operation}:${eventName}`, handler) + } - this.addSub('apm:http2:server:request:start', ({ req, res }) => { - const store = storage.getStore() - const span = web.startSpan(this.tracer, this.config, req, res, 'web.request') + start ({ req, res }) { + const store = storage.getStore() + const span = web.startSpan(this.tracer, this.config, req, res, 'web.request') - span.setTag(COMPONENT, this.constructor.id) + span.setTag(COMPONENT, this.constructor.id) - this.enter(span, { ...store, req, res }) + this.enter(span, { ...store, req, res }) - const context = web.getContext(req) + const context = web.getContext(req) - if (!context.instrumented) { - context.res.writeHead = web.wrapWriteHead(context) - context.instrumented = true - } - }) + if (!context.instrumented) { + context.res.writeHead = web.wrapWriteHead(context) + context.instrumented = true + } + } - this.addSub('apm:http2:server:request:error', (error) => { - web.addError(error) - }) + finish ({ req }) { + const context = web.getContext(req) - this.addSub('apm:http2:server:request:finish', ({ req }) => { - const context = web.getContext(req) + if (!context || !context.res) return // Not created by a http.Server instance. - if (!context || !context.res) return // Not created by a http.Server instance. + web.finishAll(context) + } - web.finishAll(context) - }) + error (error) { + web.addError(error) } configure (config) { From 38d5699f40fedb71e580d590a8b8c8e3aeb1ba11 Mon Sep 17 00:00:00 2001 From: Jordi Bertran de Balanda Date: Fri, 23 Jun 2023 14:52:43 +0200 Subject: [PATCH 11/20] Apply service naming workflow to databases (#3256) * add cassandra-driver versions * add elasticsearch/opensearch versions * add mongodb versions * add pg versions * add oracledb versions --- .../src/index.js | 4 +- .../test/index.spec.js | 19 ++++- .../test/naming.js | 14 ++++ .../datadog-plugin-elasticsearch/src/index.js | 4 +- .../test/index.spec.js | 34 +++++++-- .../test/naming.js | 14 ++++ .../datadog-plugin-mongodb-core/src/index.js | 4 +- .../test/core.spec.js | 18 ++++- .../test/mongodb.spec.js | 18 ++++- .../test/naming.js | 14 ++++ .../test/index.spec.js | 28 ++++++-- .../datadog-plugin-opensearch/test/naming.js | 14 ++++ packages/datadog-plugin-oracledb/src/index.js | 12 +--- .../test/index.spec.js | 70 ++++++++++++++++--- .../datadog-plugin-oracledb/test/naming.js | 14 ++++ packages/datadog-plugin-pg/src/index.js | 12 +--- packages/datadog-plugin-pg/test/index.spec.js | 66 +++++++++++------ packages/datadog-plugin-pg/test/naming.js | 14 ++++ .../src/service-naming/schemas/v0/storage.js | 35 +++++++++- .../src/service-naming/schemas/v1/storage.js | 31 ++++++++ 20 files changed, 367 insertions(+), 72 deletions(-) create mode 100644 packages/datadog-plugin-cassandra-driver/test/naming.js create mode 100644 packages/datadog-plugin-elasticsearch/test/naming.js create mode 100644 packages/datadog-plugin-mongodb-core/test/naming.js create mode 100644 packages/datadog-plugin-opensearch/test/naming.js create mode 100644 packages/datadog-plugin-oracledb/test/naming.js create mode 100644 packages/datadog-plugin-pg/test/naming.js diff --git a/packages/datadog-plugin-cassandra-driver/src/index.js b/packages/datadog-plugin-cassandra-driver/src/index.js index d67c3e46fba..b6c6aa6b29b 100644 --- a/packages/datadog-plugin-cassandra-driver/src/index.js +++ b/packages/datadog-plugin-cassandra-driver/src/index.js @@ -12,8 +12,8 @@ class CassandraDriverPlugin extends DatabasePlugin { query = combine(query) } - this.startSpan('cassandra.query', { - service: this.config.service, + this.startSpan(this.operationName(), { + service: this.serviceName(this.config, this.system), resource: trim(query, 5000), type: 'cassandra', kind: 'client', diff --git a/packages/datadog-plugin-cassandra-driver/test/index.spec.js b/packages/datadog-plugin-cassandra-driver/test/index.spec.js index c96fb64f626..c768122bbc4 100644 --- a/packages/datadog-plugin-cassandra-driver/test/index.spec.js +++ b/packages/datadog-plugin-cassandra-driver/test/index.spec.js @@ -3,6 +3,7 @@ const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_TYPE, ERROR_MESSAGE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') describe('Plugin', () => { let cassandra @@ -46,7 +47,8 @@ describe('Plugin', () => { const query = 'SELECT now() FROM local;' agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-cassandra') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', query) expect(traces[0][0]).to.have.property('type', 'cassandra') expect(traces[0][0].meta).to.have.property('db.type', 'cassandra') @@ -142,6 +144,12 @@ describe('Plugin', () => { }) }) }) + + withNamingSchema( + done => client.execute('SELECT now() FROM local;', err => err && done(err)), + () => namingSchema.outbound.opName, + () => namingSchema.outbound.serviceName + ) }) describe('with configuration', () => { @@ -181,6 +189,12 @@ describe('Plugin', () => { client.execute('SELECT now() FROM local;', err => err && done(err)) }) + + withNamingSchema( + done => client.execute('SELECT now() FROM local;', err => err && done(err)), + () => namingSchema.outbound.opName, + () => 'custom' + ) }) // Promise support added in 3.2.0 @@ -219,7 +233,8 @@ describe('Plugin', () => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-cassandra') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', query) expect(traces[0][0]).to.have.property('type', 'cassandra') expect(traces[0][0].meta).to.have.property('db.type', 'cassandra') diff --git a/packages/datadog-plugin-cassandra-driver/test/naming.js b/packages/datadog-plugin-cassandra-driver/test/naming.js new file mode 100644 index 00000000000..b67253ca771 --- /dev/null +++ b/packages/datadog-plugin-cassandra-driver/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + outbound: { + v0: { + opName: 'cassandra.query', + serviceName: 'test-cassandra' + }, + v1: { + opName: 'cassandra.query', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-elasticsearch/src/index.js b/packages/datadog-plugin-elasticsearch/src/index.js index fdf9a5516d1..14708d83e70 100644 --- a/packages/datadog-plugin-elasticsearch/src/index.js +++ b/packages/datadog-plugin-elasticsearch/src/index.js @@ -8,8 +8,8 @@ class ElasticsearchPlugin extends DatabasePlugin { start ({ params }) { const body = getBody(params.body || params.bulkBody) - this.startSpan(`${this.system}.query`, { - service: this.config.service, + this.startSpan(this.operationName(), { + service: this.serviceName(this.config), resource: `${params.method} ${quantizePath(params.path)}`, type: 'elasticsearch', kind: 'client', diff --git a/packages/datadog-plugin-elasticsearch/test/index.spec.js b/packages/datadog-plugin-elasticsearch/test/index.spec.js index 1bd2fbd69d7..052eb756a5b 100644 --- a/packages/datadog-plugin-elasticsearch/test/index.spec.js +++ b/packages/datadog-plugin-elasticsearch/test/index.spec.js @@ -3,6 +3,7 @@ const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers') +const namingSchema = require('./naming') describe('Plugin', () => { let elasticsearch @@ -57,6 +58,8 @@ describe('Plugin', () => { it('should set the correct tags', done => { agent .use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0].meta).to.have.property('component', 'elasticsearch') expect(traces[0][0].meta).to.have.property('db.type', 'elasticsearch') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -90,6 +93,8 @@ describe('Plugin', () => { it('should set the correct tags on msearch', done => { agent .use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0].meta).to.have.property('component', 'elasticsearch') expect(traces[0][0].meta).to.have.property('db.type', 'elasticsearch') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -143,7 +148,8 @@ describe('Plugin', () => { it('should do automatic instrumentation', done => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-elasticsearch') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'HEAD /') expect(traces[0][0]).to.have.property('type', 'elasticsearch') }) @@ -206,7 +212,8 @@ describe('Plugin', () => { it('should do automatic instrumentation', done => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-elasticsearch') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'HEAD /') expect(traces[0][0]).to.have.property('type', 'elasticsearch') }) @@ -276,6 +283,15 @@ describe('Plugin', () => { client.ping().catch(done) }) + + withNamingSchema( + () => client.search( + { index: 'logstash-2000.01.01', body: {} }, + hasCallbackSupport ? () => {} : undefined + ), + () => namingSchema.outbound.opName, + () => namingSchema.outbound.serviceName + ) }) }) @@ -284,7 +300,7 @@ describe('Plugin', () => { before(() => { return agent.load('elasticsearch', { - service: 'test', + service: 'custom', hooks: { query: (span, params) => { span.addTags({ 'elasticsearch.params': 'foo', 'elasticsearch.method': params.method }) } } @@ -316,7 +332,8 @@ describe('Plugin', () => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', 'custom') expect(traces[0][0].meta).to.have.property('component', 'elasticsearch') expect(traces[0][0].meta).to.have.property('elasticsearch.params', 'foo') expect(traces[0][0].meta).to.have.property('elasticsearch.method', 'POST') @@ -330,6 +347,15 @@ describe('Plugin', () => { client.ping().catch(done) } }) + + withNamingSchema( + () => client.search( + { index: 'logstash-2000.01.01', body: {} }, + hasCallbackSupport ? () => {} : undefined + ), + () => namingSchema.outbound.opName, + () => 'custom' + ) }) }) }) diff --git a/packages/datadog-plugin-elasticsearch/test/naming.js b/packages/datadog-plugin-elasticsearch/test/naming.js new file mode 100644 index 00000000000..0ac5ebd48ea --- /dev/null +++ b/packages/datadog-plugin-elasticsearch/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + outbound: { + v0: { + opName: 'elasticsearch.query', + serviceName: 'test-elasticsearch' + }, + v1: { + opName: 'elasticsearch.query', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-mongodb-core/src/index.js b/packages/datadog-plugin-mongodb-core/src/index.js index 0792a20468d..8082168228a 100644 --- a/packages/datadog-plugin-mongodb-core/src/index.js +++ b/packages/datadog-plugin-mongodb-core/src/index.js @@ -10,8 +10,8 @@ class MongodbCorePlugin extends DatabasePlugin { const query = getQuery(ops) const resource = truncate(getResource(this, ns, query, name)) - this.startSpan('mongodb.query', { - service: this.config.service, + this.startSpan(this.operationName(), { + service: this.serviceName(this.config), resource, type: 'mongodb', kind: 'client', diff --git a/packages/datadog-plugin-mongodb-core/test/core.spec.js b/packages/datadog-plugin-mongodb-core/test/core.spec.js index aa89217d447..8e11646da12 100644 --- a/packages/datadog-plugin-mongodb-core/test/core.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/core.spec.js @@ -3,6 +3,7 @@ const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') const withTopologies = fn => { withVersions('mongodb-core', ['mongodb-core', 'mongodb'], '<4', (version, moduleName) => { @@ -79,8 +80,8 @@ describe('Plugin', () => { const span = traces[0][0] const resource = `insert test.${collection}` - expect(span).to.have.property('name', 'mongodb.query') - expect(span).to.have.property('service', 'test-mongodb') + expect(span).to.have.property('name', namingSchema.outbound.opName) + expect(span).to.have.property('service', namingSchema.outbound.serviceName) expect(span).to.have.property('resource', resource) expect(span).to.have.property('type', 'mongodb') expect(span.meta).to.have.property('span.kind', 'client') @@ -305,6 +306,12 @@ describe('Plugin', () => { error = err }) }) + + withNamingSchema( + () => server.insert(`test.${collection}`, [{ a: 1 }], () => {}), + () => namingSchema.outbound.opName, + () => namingSchema.outbound.serviceName + ) }) }) @@ -335,6 +342,7 @@ describe('Plugin', () => { it('should be configured with the correct values', done => { agent .use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) expect(traces[0][0]).to.have.property('service', 'custom') }) .then(done) @@ -342,6 +350,12 @@ describe('Plugin', () => { server.insert(`test.${collection}`, [{ a: 1 }], () => {}) }) + + withNamingSchema( + () => server.insert(`test.${collection}`, [{ a: 1 }], () => {}), + () => namingSchema.outbound.opName, + () => 'custom' + ) }) }) }) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index 12bbc796733..76be436f8eb 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -2,6 +2,7 @@ const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') +const namingSchema = require('./naming') const withTopologies = fn => { const isOldNode = semver.satisfies(process.version, '<=12') @@ -81,8 +82,8 @@ describe('Plugin', () => { const span = traces[0][0] const resource = `insert test.${collectionName}` - expect(span).to.have.property('name', 'mongodb.query') - expect(span).to.have.property('service', 'test-mongodb') + expect(span).to.have.property('name', namingSchema.outbound.opName) + expect(span).to.have.property('service', namingSchema.outbound.serviceName) expect(span).to.have.property('resource', resource) expect(span).to.have.property('type', 'mongodb') expect(span.meta).to.have.property('span.kind', 'client') @@ -238,6 +239,12 @@ describe('Plugin', () => { }) } }) + + withNamingSchema( + () => collection.insertOne({ a: 1 }, {}, () => {}), + () => namingSchema.outbound.opName, + () => namingSchema.outbound.serviceName + ) }) }) @@ -262,6 +269,7 @@ describe('Plugin', () => { it('should be configured with the correct values', done => { agent .use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) expect(traces[0][0]).to.have.property('service', 'custom') }) .then(done) @@ -285,6 +293,12 @@ describe('Plugin', () => { _bin: new BSON.Binary() }).toArray() }) + + withNamingSchema( + () => collection.insertOne({ a: 1 }, () => {}), + () => namingSchema.outbound.opName, + () => 'custom' + ) }) }) }) diff --git a/packages/datadog-plugin-mongodb-core/test/naming.js b/packages/datadog-plugin-mongodb-core/test/naming.js new file mode 100644 index 00000000000..2b90044ff3d --- /dev/null +++ b/packages/datadog-plugin-mongodb-core/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + outbound: { + v0: { + opName: 'mongodb.query', + serviceName: 'test-mongodb' + }, + v1: { + opName: 'mongodb.query', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-opensearch/test/index.spec.js b/packages/datadog-plugin-opensearch/test/index.spec.js index b9c7c174483..dba63b8eb9f 100644 --- a/packages/datadog-plugin-opensearch/test/index.spec.js +++ b/packages/datadog-plugin-opensearch/test/index.spec.js @@ -3,6 +3,7 @@ const agent = require('../../dd-trace/test/plugins/agent') const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') describe('Plugin', () => { let opensearch @@ -56,6 +57,8 @@ describe('Plugin', () => { it('should set the correct tags', done => { agent .use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0].meta).to.have.property('db.type', 'opensearch') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('opensearch.method', 'POST') @@ -84,6 +87,8 @@ describe('Plugin', () => { it('should set the correct tags on msearch', done => { agent .use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0].meta).to.have.property('db.type', 'opensearch') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('opensearch.method', 'POST') @@ -131,7 +136,8 @@ describe('Plugin', () => { it('should do automatic instrumentation', done => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-opensearch') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'HEAD /') expect(traces[0][0]).to.have.property('type', 'elasticsearch') expect(traces[0][0].meta).to.have.property('component', 'opensearch') @@ -191,7 +197,8 @@ describe('Plugin', () => { it('should work with userland promises', done => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-opensearch') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'HEAD /') expect(traces[0][0]).to.have.property('type', 'elasticsearch') }) @@ -202,6 +209,12 @@ describe('Plugin', () => { client.ping().catch(done) }) + + withNamingSchema( + () => client.search({ index: 'logstash-2000.01.01', body: {} }), + () => namingSchema.outbound.opName, + () => namingSchema.outbound.serviceName + ) }) describe('with configuration', () => { @@ -209,7 +222,7 @@ describe('Plugin', () => { before(() => { return agent.load('opensearch', { - service: 'test', + service: 'custom', hooks: { query: (span, params) => { span.addTags({ 'opensearch.params': 'foo', 'opensearch.method': params.method }) } } @@ -241,7 +254,8 @@ describe('Plugin', () => { agent .use(traces => { - expect(traces[0][0]).to.have.property('service', 'test') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', 'custom') expect(traces[0][0].meta).to.have.property('opensearch.params', 'foo') expect(traces[0][0].meta).to.have.property('opensearch.method', 'POST') expect(traces[0][0].meta).to.have.property('component', 'opensearch') @@ -251,6 +265,12 @@ describe('Plugin', () => { client.ping().catch(done) }) + + withNamingSchema( + () => client.search({ index: 'logstash-2000.01.01', body: {} }), + () => namingSchema.outbound.opName, + () => 'custom' + ) }) }) }) diff --git a/packages/datadog-plugin-opensearch/test/naming.js b/packages/datadog-plugin-opensearch/test/naming.js new file mode 100644 index 00000000000..3fac8b10e27 --- /dev/null +++ b/packages/datadog-plugin-opensearch/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + outbound: { + v0: { + opName: 'opensearch.query', + serviceName: 'test-opensearch' + }, + v1: { + opName: 'opensearch.query', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-oracledb/src/index.js b/packages/datadog-plugin-oracledb/src/index.js index 367df83bd75..9eb4803c63a 100644 --- a/packages/datadog-plugin-oracledb/src/index.js +++ b/packages/datadog-plugin-oracledb/src/index.js @@ -9,10 +9,10 @@ class OracledbPlugin extends DatabasePlugin { static get system () { return 'oracle' } start ({ query, connAttrs }) { - const service = getServiceName(this.config, connAttrs) + const service = this.serviceName(this.config, connAttrs) const url = getUrl(connAttrs.connectString) - this.startSpan('oracle.query', { + this.startSpan(this.operationName(), { service, resource: query, type: 'sql', @@ -27,14 +27,6 @@ class OracledbPlugin extends DatabasePlugin { } } -function getServiceName (config, connAttrs) { - if (typeof config.service === 'function') { - return config.service(connAttrs) - } - - return config.service -} - // TODO: Avoid creating an error since it's a heavy operation. function getUrl (connectString) { try { diff --git a/packages/datadog-plugin-oracledb/test/index.spec.js b/packages/datadog-plugin-oracledb/test/index.spec.js index 088c22b3516..9c3ff0691a6 100644 --- a/packages/datadog-plugin-oracledb/test/index.spec.js +++ b/packages/datadog-plugin-oracledb/test/index.spec.js @@ -2,6 +2,7 @@ const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') const hostname = process.env.CI ? 'oracledb' : 'localhost' const config = { @@ -63,10 +64,10 @@ describe('Plugin', () => { function connectionTests (url) { it('should be instrumented for promise API', done => { agent.use(traces => { - expect(traces[0][0]).to.have.property('name', 'oracle.query') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', dbQuery) expect(traces[0][0]).to.have.property('type', 'sql') - expect(traces[0][0].meta).to.have.property('service', 'test') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('component', 'oracledb') if (url) { @@ -89,10 +90,10 @@ describe('Plugin', () => { it('should be instrumented for callback API', done => { agent.use(traces => { - expect(traces[0][0]).to.have.property('name', 'oracle.query') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', dbQuery) expect(traces[0][0]).to.have.property('type', 'sql') - expect(traces[0][0].meta).to.have.property('service', 'test') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('component', 'oracledb') if (url) { @@ -123,10 +124,10 @@ describe('Plugin', () => { let error agent.use(traces => { - expect(traces[0][0]).to.have.property('name', 'oracle.query') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'invalid') expect(traces[0][0]).to.have.property('type', 'sql') - expect(traces[0][0].meta).to.have.property('service', 'test') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('component', 'oracledb') if (url) { @@ -182,10 +183,10 @@ describe('Plugin', () => { function poolTests (url) { it('should be instrumented correctly with correct tags', done => { agent.use(traces => { - expect(traces[0][0]).to.have.property('name', 'oracle.query') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', dbQuery) expect(traces[0][0]).to.have.property('type', 'sql') - expect(traces[0][0].meta).to.have.property('service', 'test') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('component', 'oracledb') if (url) { @@ -207,10 +208,10 @@ describe('Plugin', () => { let error const promise = agent.use(traces => { - expect(traces[0][0]).to.have.property('name', 'oracle.query') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'invalid') expect(traces[0][0]).to.have.property('type', 'sql') - expect(traces[0][0].meta).to.have.property('service', 'test') expect(traces[0][0].meta).to.have.property('span.kind', 'client') expect(traces[0][0].meta).to.have.property('component', 'oracledb') if (url) { @@ -231,6 +232,55 @@ describe('Plugin', () => { }) } }) + + describe('with configuration', () => { + describe('with service string', () => { + before(async () => { + await agent.load('oracledb', { service: 'custom' }) + oracledb = require(`../../../versions/oracledb@${version}`).get() + tracer = require('../../dd-trace') + }) + before(async () => { + connection = await oracledb.getConnection(config) + }) + after(async () => { + await connection.close() + }) + after(async () => { + await agent.close({ ritmReset: false }) + }) + it('should set the service name', done => { + agent.use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', 'custom') + }).then(done, done) + connection.execute(dbQuery) + }) + }) + describe('with service function', () => { + before(async () => { + await agent.load('oracledb', { service: connAttrs => `${connAttrs.connectString}` }) + oracledb = require(`../../../versions/oracledb@${version}`).get() + tracer = require('../../dd-trace') + }) + before(async () => { + connection = await oracledb.getConnection(config) + }) + after(async () => { + await connection.close() + }) + after(async () => { + await agent.close({ ritmReset: false }) + }) + it('should set the service name', done => { + agent.use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', config.connectString) + }).then(done, done) + connection.execute(dbQuery) + }) + }) + }) }) }) }) diff --git a/packages/datadog-plugin-oracledb/test/naming.js b/packages/datadog-plugin-oracledb/test/naming.js new file mode 100644 index 00000000000..cd70b04e8b3 --- /dev/null +++ b/packages/datadog-plugin-oracledb/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + outbound: { + v0: { + opName: 'oracle.query', + serviceName: 'test-oracle' + }, + v1: { + opName: 'oracle.query', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-pg/src/index.js b/packages/datadog-plugin-pg/src/index.js index e6a248e9a00..34b8c96dcaa 100644 --- a/packages/datadog-plugin-pg/src/index.js +++ b/packages/datadog-plugin-pg/src/index.js @@ -9,10 +9,10 @@ class PGPlugin extends DatabasePlugin { static get system () { return 'postgres' } start ({ params = {}, query, processId }) { - const service = getServiceName(this, params) + const service = this.serviceName(this.config, params) const originalStatement = query.text - this.startSpan('pg.query', { + this.startSpan(this.operationName(), { service, resource: originalStatement, type: 'sql', @@ -31,12 +31,4 @@ class PGPlugin extends DatabasePlugin { } } -function getServiceName (tracer, params) { - if (typeof tracer.config.service === 'function') { - return tracer.config.service(params) - } - - return tracer.config.service || `${tracer._tracer._tracer._service}-postgres` -} - module.exports = PGPlugin diff --git a/packages/datadog-plugin-pg/test/index.spec.js b/packages/datadog-plugin-pg/test/index.spec.js index b37da234605..df1b1b258d4 100644 --- a/packages/datadog-plugin-pg/test/index.spec.js +++ b/packages/datadog-plugin-pg/test/index.spec.js @@ -5,6 +5,7 @@ const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const net = require('net') +const namingSchema = require('./naming') const clients = { pg: pg => pg.Client @@ -53,7 +54,8 @@ describe('Plugin', () => { it('should do automatic instrumentation when using callbacks', done => { agent.use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-postgres') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'SELECT $1::text as message') expect(traces[0][0]).to.have.property('type', 'sql') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -66,9 +68,9 @@ describe('Plugin', () => { if (implementation !== 'pg.native') { expect(traces[0][0].metrics).to.have.property('db.pid') } - - done() }) + .then(done) + .catch(done) client.query('SELECT $1::text as message', ['Hello world!'], (err, result) => { if (err) throw err @@ -98,7 +100,8 @@ describe('Plugin', () => { if (semver.intersects(version, '>=5.1')) { // initial promise support it('should do automatic instrumentation when using promises', done => { agent.use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-postgres') + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) expect(traces[0][0]).to.have.property('resource', 'SELECT $1::text as message') expect(traces[0][0]).to.have.property('type', 'sql') expect(traces[0][0].meta).to.have.property('span.kind', 'client') @@ -111,9 +114,9 @@ describe('Plugin', () => { if (implementation !== 'pg.native') { expect(traces[0][0].metrics).to.have.property('db.pid') } - - done() }) + .then(done) + .catch(done) client.query('SELECT $1::text as message', ['Hello world!']) .then(() => client.end()) @@ -130,9 +133,9 @@ describe('Plugin', () => { expect(traces[0][0].meta).to.have.property(ERROR_STACK, error.stack) expect(traces[0][0].meta).to.have.property('component', 'pg') expect(traces[0][0].metrics).to.have.property('network.destination.port', 5432) - - done() }) + .then(done) + .catch(done) client.query('INVALID', (err, result) => { error = err @@ -152,9 +155,9 @@ describe('Plugin', () => { expect(traces[0][0].meta).to.have.property(ERROR_STACK, error.stack) expect(traces[0][0].meta).to.have.property('component', 'pg') expect(traces[0][0].metrics).to.have.property('network.destination.port', 5432) - - done() }) + .then(done) + .catch(done) const errorCallback = (err) => { error = err @@ -187,6 +190,14 @@ describe('Plugin', () => { }) }) }) + + withNamingSchema( + done => client.query('SELECT $1::text as message', ['Hello world!']) + .then(() => client.end()) + .catch(done), + () => namingSchema.outbound.opName, + () => namingSchema.outbound.serviceName + ) }) }) @@ -214,10 +225,11 @@ describe('Plugin', () => { it('should be configured with the correct values', done => { agent.use(traces => { + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) expect(traces[0][0]).to.have.property('service', 'custom') - - done() }) + .then(done) + .catch(done) client.query('SELECT $1::text as message', ['Hello world!'], (err, result) => { if (err) throw err @@ -227,6 +239,14 @@ describe('Plugin', () => { }) }) }) + + withNamingSchema( + done => client.query('SELECT $1::text as message', ['Hello world!']) + .then(() => client.end()) + .catch(done), + () => namingSchema.outbound.opName, + () => 'custom' + ) }) describe('with a service name callback', () => { @@ -253,14 +273,11 @@ describe('Plugin', () => { it('should be configured with the correct service', done => { agent.use(traces => { - try { - expect(traces[0][0]).to.have.property('service', '127.0.0.1-postgres') - - done() - } catch (e) { - done(e) - } + expect(traces[0][0]).to.have.property('name', namingSchema.outbound.opName) + expect(traces[0][0]).to.have.property('service', '127.0.0.1-postgres') }) + .then(done) + .catch(done) client.query('SELECT $1::text as message', ['Hello world!'], (err, result) => { if (err) throw err @@ -270,6 +287,14 @@ describe('Plugin', () => { }) }) }) + + withNamingSchema( + done => client.query('SELECT $1::text as message', ['Hello world!']) + .then(() => client.end()) + .catch(done), + () => namingSchema.outbound.opName, + () => '127.0.0.1-postgres' + ) }) describe('with DBM propagation enabled with service using plugin configurations', () => { @@ -456,9 +481,8 @@ describe('Plugin', () => { }) it('service should default to tracer service name', done => { - tracer agent.use(traces => { - expect(traces[0][0]).to.have.property('service', 'test-postgres') + expect(traces[0][0]).to.have.property('service', namingSchema.outbound.serviceName) done() }) diff --git a/packages/datadog-plugin-pg/test/naming.js b/packages/datadog-plugin-pg/test/naming.js new file mode 100644 index 00000000000..20fbc80f92d --- /dev/null +++ b/packages/datadog-plugin-pg/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + outbound: { + v0: { + opName: 'pg.query', + serviceName: 'test-postgres' + }, + v1: { + opName: 'postgresql.query', + serviceName: 'test' + } + } +}) diff --git a/packages/dd-trace/src/service-naming/schemas/v0/storage.js b/packages/dd-trace/src/service-naming/schemas/v0/storage.js index 57166f27e69..bb51c210feb 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/storage.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/storage.js @@ -16,7 +16,16 @@ function mysqlServiceName (service, config, dbConfig, system) { if (typeof config.service === 'function') { return config.service(dbConfig) } - return config.service ? config.service : fromSystem(service, system) + return config.service || fromSystem(service, system) +} + +function withSuffixFunction (suffix) { + return (service, config, params) => { + if (typeof config.service === 'function') { + return config.service(params) + } + return config.service || `${service}-${suffix}` + } } const redisConfig = { @@ -28,6 +37,14 @@ const redisConfig = { const storage = { client: { + 'cassandra-driver': { + opName: () => 'cassandra.query', + serviceName: (service, config, system) => config.service || fromSystem(service, system) + }, + elasticsearch: { + opName: () => 'elasticsearch.query', + serviceName: (service, config) => config.service || `${service}-elasticsearch` + }, ioredis: redisConfig, mariadb: { opName: () => 'mariadb.query', @@ -37,6 +54,10 @@ const storage = { opName: () => 'memcached.command', serviceName: (service, config, system) => config.service || fromSystem(service, system) }, + 'mongodb-core': { + opName: () => 'mongodb.query', + serviceName: (service, config) => config.service || `${service}-mongodb` + }, mysql: { opName: () => 'mysql.query', serviceName: mysqlServiceName @@ -45,6 +66,18 @@ const storage = { opName: () => 'mysql.query', serviceName: mysqlServiceName }, + opensearch: { + opName: () => 'opensearch.query', + serviceName: (service, config) => config.service || `${service}-opensearch` + }, + oracledb: { + opName: () => 'oracle.query', + serviceName: withSuffixFunction('oracle') + }, + pg: { + opName: () => 'pg.query', + serviceName: withSuffixFunction('postgres') + }, redis: redisConfig, tedious: { opName: () => 'tedious.request', diff --git a/packages/dd-trace/src/service-naming/schemas/v1/storage.js b/packages/dd-trace/src/service-naming/schemas/v1/storage.js index 96ee456a5ec..88862f012f5 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/storage.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/storage.js @@ -14,8 +14,23 @@ const mySQLNaming = { serviceName: identityService } +function withFunction (service, config, params) { + if (typeof config.service === 'function') { + return config.service(params) + } + return configWithFallback(service, config) +} + const storage = { client: { + 'cassandra-driver': { + opName: () => 'cassandra.query', + serviceName: configWithFallback + }, + elasticsearch: { + opName: () => 'elasticsearch.query', + serviceName: configWithFallback + }, ioredis: redisNaming, mariadb: { opName: () => 'mariadb.query', @@ -25,8 +40,24 @@ const storage = { opName: () => 'memcached.command', serviceName: configWithFallback }, + 'mongodb-core': { + opName: () => 'mongodb.query', + serviceName: configWithFallback + }, mysql: mySQLNaming, mysql2: mySQLNaming, + opensearch: { + opName: () => 'opensearch.query', + serviceName: configWithFallback + }, + oracledb: { + opName: () => 'oracle.query', + serviceName: withFunction + }, + pg: { + opName: () => 'postgresql.query', + serviceName: withFunction + }, redis: redisNaming, tedious: { opName: () => 'mssql.query', From 4e46109e9975a57e0b30e5275451d28782715e6c Mon Sep 17 00:00:00 2001 From: Jordi Bertran de Balanda Date: Fri, 23 Jun 2023 15:01:05 +0200 Subject: [PATCH 12/20] Peer service computation (#3177) * introduce peer service computation for outbound edges: * add DD_TRACE_SPAN_PEER_SERVICE environment variable * add peer service computation logic * TODO: add tests for config, outbound, tracer (?) * provide a helper for testing peer service computation * defer plugin `finish` to TracingPlugin in children --- packages/.eslintrc.json | 3 +- .../datadog-plugin-amqp10/test/index.spec.js | 7 +++ .../datadog-plugin-amqplib/test/index.spec.js | 14 +++++ packages/datadog-plugin-dns/src/lookup.js | 2 +- .../src/consumer.js | 2 +- .../datadog-plugin-graphql/src/execute.js | 2 +- packages/datadog-plugin-graphql/src/parse.js | 2 +- .../datadog-plugin-graphql/src/resolve.js | 5 -- .../datadog-plugin-graphql/src/validate.js | 2 +- packages/datadog-plugin-grpc/src/client.js | 2 +- packages/datadog-plugin-grpc/src/server.js | 2 +- packages/datadog-plugin-http/src/client.js | 2 +- .../datadog-plugin-http/test/client.spec.js | 20 +++++++ packages/datadog-plugin-http2/src/client.js | 5 -- .../datadog-plugin-http2/test/client.spec.js | 25 ++++++++ .../test/index.spec.js | 6 ++ .../datadog-plugin-moleculer/src/client.js | 2 +- .../test/index.spec.js | 28 +++++++-- .../datadog-plugin-net/test/index.spec.js | 10 ++++ .../datadog-plugin-redis/test/legacy.spec.js | 7 +++ .../datadog-plugin-rhea/test/index.spec.js | 7 +++ packages/datadog-plugin-sharedb/src/index.js | 2 +- packages/dd-trace/src/config.js | 6 ++ packages/dd-trace/src/constants.js | 2 + packages/dd-trace/src/opentracing/tracer.js | 1 + packages/dd-trace/src/plugins/outbound.js | 59 ++++++++++++++++++- packages/dd-trace/test/config.spec.js | 1 + .../dd-trace/test/plugins/outbound.spec.js | 59 +++++++++++++++++++ packages/dd-trace/test/setup/mocha.js | 30 +++++++++- 29 files changed, 287 insertions(+), 28 deletions(-) create mode 100644 packages/dd-trace/test/plugins/outbound.spec.js diff --git a/packages/.eslintrc.json b/packages/.eslintrc.json index 8b1dd5f8fe2..9ae4e0ef309 100644 --- a/packages/.eslintrc.json +++ b/packages/.eslintrc.json @@ -11,7 +11,8 @@ "proxyquire": true, "withNamingSchema": true, "withVersions": true, - "withExports": true + "withExports": true, + "withPeerService": true }, "rules": { "no-unused-expressions": 0, diff --git a/packages/datadog-plugin-amqp10/test/index.spec.js b/packages/datadog-plugin-amqp10/test/index.spec.js index 9652274434e..f25f8154efe 100644 --- a/packages/datadog-plugin-amqp10/test/index.spec.js +++ b/packages/datadog-plugin-amqp10/test/index.spec.js @@ -60,6 +60,13 @@ describe('Plugin', () => { }) describe('when sending messages', () => { + withPeerService( + () => tracer, + () => sender.send({ key: 'value' }), + 'localhost', + 'out.host' + ) + it('should do automatic instrumentation', done => { agent .use(traces => { diff --git a/packages/datadog-plugin-amqplib/test/index.spec.js b/packages/datadog-plugin-amqplib/test/index.spec.js index db6799939ed..fea5d4c43cd 100644 --- a/packages/datadog-plugin-amqplib/test/index.spec.js +++ b/packages/datadog-plugin-amqplib/test/index.spec.js @@ -53,6 +53,13 @@ describe('Plugin', () => { }) describe('when sending commands', () => { + withPeerService( + () => tracer, + () => channel.assertQueue('test', {}, () => {}), + 'localhost', + 'out.host' + ) + it('should do automatic instrumentation for immediate commands', done => { agent .use(traces => { @@ -124,6 +131,13 @@ describe('Plugin', () => { }) describe('when publishing messages', () => { + withPeerService( + () => tracer, + () => channel.assertQueue('test', {}, () => {}), + 'localhost', + 'out.host' + ) + it('should do automatic instrumentation', done => { agent .use(traces => { diff --git a/packages/datadog-plugin-dns/src/lookup.js b/packages/datadog-plugin-dns/src/lookup.js index fd5bfc4f57b..3db2610caf0 100644 --- a/packages/datadog-plugin-dns/src/lookup.js +++ b/packages/datadog-plugin-dns/src/lookup.js @@ -33,7 +33,7 @@ class DNSLookupPlugin extends ClientPlugin { span.setTag('dns.address', result) } - span.finish() + super.finish() } } diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js b/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js index 0b157f5447b..5c3333105d2 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js @@ -32,7 +32,7 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin { span.setTag('pubsub.ack', 1) } - span.finish() + super.finish() } } diff --git a/packages/datadog-plugin-graphql/src/execute.js b/packages/datadog-plugin-graphql/src/execute.js index 91c03b5c44a..b4a817a103d 100644 --- a/packages/datadog-plugin-graphql/src/execute.js +++ b/packages/datadog-plugin-graphql/src/execute.js @@ -32,7 +32,7 @@ class GraphQLExecutePlugin extends TracingPlugin { finish ({ res, args }) { const span = this.activeSpan this.config.hooks.execute(span, args, res) - span.finish() + super.finish() } } diff --git a/packages/datadog-plugin-graphql/src/parse.js b/packages/datadog-plugin-graphql/src/parse.js index cb66059a82b..d9d68b23c0e 100644 --- a/packages/datadog-plugin-graphql/src/parse.js +++ b/packages/datadog-plugin-graphql/src/parse.js @@ -25,7 +25,7 @@ class GraphQLParsePlugin extends TracingPlugin { this.config.hooks.parse(span, source, document) - span.finish() + super.finish() } } diff --git a/packages/datadog-plugin-graphql/src/resolve.js b/packages/datadog-plugin-graphql/src/resolve.js index 95bfaf35422..c96d6f5bf7f 100644 --- a/packages/datadog-plugin-graphql/src/resolve.js +++ b/packages/datadog-plugin-graphql/src/resolve.js @@ -56,11 +56,6 @@ class GraphQLResolvePlugin extends TracingPlugin { } } - finish (finishTime) { - const span = this.activeSpan - span.finish(finishTime) - } - constructor (...args) { super(...args) diff --git a/packages/datadog-plugin-graphql/src/validate.js b/packages/datadog-plugin-graphql/src/validate.js index 2890ba44592..bda4886a6f0 100644 --- a/packages/datadog-plugin-graphql/src/validate.js +++ b/packages/datadog-plugin-graphql/src/validate.js @@ -21,7 +21,7 @@ class GraphQLValidatePlugin extends TracingPlugin { finish ({ document, errors }) { const span = this.activeSpan this.config.hooks.validate(span, document, errors) - span.finish() + super.finish() } } diff --git a/packages/datadog-plugin-grpc/src/client.js b/packages/datadog-plugin-grpc/src/client.js index 2dd1f51b95d..0a96d364e2c 100644 --- a/packages/datadog-plugin-grpc/src/client.js +++ b/packages/datadog-plugin-grpc/src/client.js @@ -57,7 +57,7 @@ class GrpcClientPlugin extends ClientPlugin { addMetadataTags(span, metadata, metadataFilter, 'response') } - span.finish() + super.finish() } configure (config) { diff --git a/packages/datadog-plugin-grpc/src/server.js b/packages/datadog-plugin-grpc/src/server.js index bcb7bb30e69..68d8c8e37ac 100644 --- a/packages/datadog-plugin-grpc/src/server.js +++ b/packages/datadog-plugin-grpc/src/server.js @@ -68,7 +68,7 @@ class GrpcServerPlugin extends ServerPlugin { addMetadataTags(span, trailer, metadataFilter, 'response') } - span.finish() + super.finish() } configure (config) { diff --git a/packages/datadog-plugin-http/src/client.js b/packages/datadog-plugin-http/src/client.js index 9cc3164ab85..93cc07b0049 100644 --- a/packages/datadog-plugin-http/src/client.js +++ b/packages/datadog-plugin-http/src/client.js @@ -87,7 +87,7 @@ class HttpClientPlugin extends ClientPlugin { addRequestHeaders(req, span, this.config) this.config.hooks.request(span, req, res) - span.finish() + super.finish() } error (err) { diff --git a/packages/datadog-plugin-http/test/client.spec.js b/packages/datadog-plugin-http/test/client.spec.js index 243f37ca462..701ab219801 100644 --- a/packages/datadog-plugin-http/test/client.spec.js +++ b/packages/datadog-plugin-http/test/client.spec.js @@ -58,6 +58,26 @@ describe('Plugin', () => { }) }) + withPeerService( + () => tracer, + () => { + const app = express() + app.get('/user', (req, res) => { + res.status(200).send() + }) + getPort().then(port => { + appListener = server(app, port, () => { + const req = http.request(`${protocol}://localhost:${port}/user`, res => { + res.on('data', () => {}) + }) + req.end() + }) + }) + }, + 'localhost', + 'out.host' + ) + it('should do automatic instrumentation', done => { const app = express() app.get('/user', (req, res) => { diff --git a/packages/datadog-plugin-http2/src/client.js b/packages/datadog-plugin-http2/src/client.js index db33955ac2d..e33107abf67 100644 --- a/packages/datadog-plugin-http2/src/client.js +++ b/packages/datadog-plugin-http2/src/client.js @@ -93,11 +93,6 @@ class Http2ClientPlugin extends ClientPlugin { this.enter(span, store) } - finish () { - const span = storage.getStore().span - span.finish() - } - configure (config) { return super.configure(normalizeConfig(config)) } diff --git a/packages/datadog-plugin-http2/test/client.spec.js b/packages/datadog-plugin-http2/test/client.spec.js index 312594923f9..6b927755225 100644 --- a/packages/datadog-plugin-http2/test/client.spec.js +++ b/packages/datadog-plugin-http2/test/client.spec.js @@ -54,6 +54,31 @@ describe('Plugin', () => { }) }) + withPeerService( + () => tracer, + done => { + getPort().then(port => { + const app = (stream, headers) => { + stream.respond({ + ':status': 200 + }) + stream.end() + } + appListener = server(app, port, () => { + const client = http2 + .connect(`${protocol}://localhost:${port}`) + .on('error', done) + + const req = client.request({ ':path': '/user', ':method': 'GET' }) + req.on('error', done) + + req.end() + }) + }) + }, + 'localhost', 'out.host' + ) + it('should do automatic instrumentation', done => { const app = (stream, headers) => { stream.respond({ diff --git a/packages/datadog-plugin-memcached/test/index.spec.js b/packages/datadog-plugin-memcached/test/index.spec.js index 5bd6029ad18..0263f00d860 100644 --- a/packages/datadog-plugin-memcached/test/index.spec.js +++ b/packages/datadog-plugin-memcached/test/index.spec.js @@ -25,6 +25,12 @@ describe('Plugin', () => { Memcached = proxyquire(`../../../versions/memcached@${version}/node_modules/memcached`, {}) }) + withPeerService( + () => tracer, + done => memcached.get('test', err => err && done(err)), + 'localhost', + 'out.host' + ) it('should do automatic instrumentation when using callbacks', done => { memcached = new Memcached('localhost:11211', { retries: 0 }) diff --git a/packages/datadog-plugin-moleculer/src/client.js b/packages/datadog-plugin-moleculer/src/client.js index 76b3895986c..33fa799fb88 100644 --- a/packages/datadog-plugin-moleculer/src/client.js +++ b/packages/datadog-plugin-moleculer/src/client.js @@ -29,7 +29,7 @@ class MoleculerClientPlugin extends ClientPlugin { span.addTags(moleculerTags(broker, ctx, this.config)) } - span.finish() + super.finish() } } diff --git a/packages/datadog-plugin-moleculer/test/index.spec.js b/packages/datadog-plugin-moleculer/test/index.spec.js index 46089b2cde2..8d5957ad8fc 100644 --- a/packages/datadog-plugin-moleculer/test/index.spec.js +++ b/packages/datadog-plugin-moleculer/test/index.spec.js @@ -108,10 +108,28 @@ describe('Plugin', () => { describe('client', () => { describe('without configuration', () => { - before(() => agent.load('moleculer', { server: false })) - before(() => startBroker()) - after(() => broker.stop()) - after(() => agent.close({ ritmReset: false })) + const hostname = os.hostname() + let tracer + + beforeEach(() => startBroker()) + afterEach(() => broker.stop()) + + beforeEach(done => { + agent.load('moleculer', { server: false }) + .then(() => { tracer = require('../../dd-trace') }) + .then(done) + .catch(done) + }) + afterEach(() => agent.close({ ritmReset: false })) + + withPeerService( + () => tracer, + done => { + broker.call('math.add', { a: 5, b: 3 }).catch(done) + }, + hostname, + 'out.host' + ) it('should do automatic instrumentation', done => { agent.use(traces => { @@ -121,7 +139,7 @@ describe('Plugin', () => { expect(spans[0]).to.have.property('service', 'test') expect(spans[0]).to.have.property('resource', 'math.add') expect(spans[0].meta).to.have.property('span.kind', 'client') - expect(spans[0].meta).to.have.property('out.host', os.hostname()) + expect(spans[0].meta).to.have.property('out.host', hostname) expect(spans[0].meta).to.have.property('moleculer.context.action', 'math.add') expect(spans[0].meta).to.have.property('moleculer.context.node_id', `server-${process.pid}`) expect(spans[0].meta).to.have.property('moleculer.context.request_id') diff --git a/packages/datadog-plugin-net/test/index.spec.js b/packages/datadog-plugin-net/test/index.spec.js index af02e7fd52e..9eb3eecac22 100644 --- a/packages/datadog-plugin-net/test/index.spec.js +++ b/packages/datadog-plugin-net/test/index.spec.js @@ -75,6 +75,16 @@ describe('Plugin', () => { }) }) + withPeerService( + () => tracer, + () => { + const socket = new net.Socket() + socket.connect(port, 'localhost') + }, + 'localhost', + 'out.host' + ) + it('should instrument connect with a port', done => { const socket = new net.Socket() tracer.scope().activate(parent, () => { diff --git a/packages/datadog-plugin-redis/test/legacy.spec.js b/packages/datadog-plugin-redis/test/legacy.spec.js index b6c8da073ba..b3816814b0b 100644 --- a/packages/datadog-plugin-redis/test/legacy.spec.js +++ b/packages/datadog-plugin-redis/test/legacy.spec.js @@ -40,6 +40,13 @@ describe('Legacy Plugin', () => { sub = redis.createClient() }) + withPeerService( + () => tracer, + () => client.get('foo'), + '127.0.0.1', + 'out.host' + ) + it('should do automatic instrumentation when using callbacks', done => { client.on('error', done) agent diff --git a/packages/datadog-plugin-rhea/test/index.spec.js b/packages/datadog-plugin-rhea/test/index.spec.js index a026b1fabaf..c5e05e349d8 100644 --- a/packages/datadog-plugin-rhea/test/index.spec.js +++ b/packages/datadog-plugin-rhea/test/index.spec.js @@ -47,6 +47,13 @@ describe('Plugin', () => { }) describe('sending a message', () => { + withPeerService( + () => tracer, + () => context.sender.send({ body: 'Hello World!' }), + 'localhost', + 'out.host' + ) + it('should automatically instrument', (done) => { agent.use(traces => { const span = traces[0][0] diff --git a/packages/datadog-plugin-sharedb/src/index.js b/packages/datadog-plugin-sharedb/src/index.js index f4e662330ea..1257d608d56 100644 --- a/packages/datadog-plugin-sharedb/src/index.js +++ b/packages/datadog-plugin-sharedb/src/index.js @@ -25,7 +25,7 @@ class SharedbPlugin extends ServerPlugin { if (this.config.hooks && this.config.hooks.reply) { this.config.hooks.reply(span, request, res) } - span.finish() + super.finish() } } diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index c612595d938..048483339ca 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -306,6 +306,8 @@ class Config { const DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = validateNamingVersion( process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA ) + const DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED + const DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH = coalesce( process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, '512' @@ -524,6 +526,10 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) exporters: DD_PROFILING_EXPORTERS } this.spanAttributeSchema = DD_TRACE_SPAN_ATTRIBUTE_SCHEMA + this.spanComputePeerService = (this.spanAttributeSchema === 'v0' + ? isTrue(DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED) + : true + ) this.lookup = options.lookup this.startupLogs = isTrue(DD_TRACE_STARTUP_LOGS) // Disabled for CI Visibility's agentless diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index 0d220b36837..33b64310ca0 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -26,6 +26,8 @@ module.exports = { ERROR_STACK: 'error.stack', COMPONENT: 'component', CLIENT_PORT_KEY: 'network.destination.port', + PEER_SERVICE_KEY: 'peer.service', + PEER_SERVICE_SOURCE_KEY: '_dd.peer.service.source', SCI_REPOSITORY_URL: '_dd.git.repository_url', SCI_COMMIT_SHA: '_dd.git.commit.sha' } diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index 7efd65c2097..8ff74151145 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -26,6 +26,7 @@ class DatadogTracer { this._version = config.version this._env = config.env this._tags = config.tags + this._computePeerService = config.spanComputePeerService this._logInjection = config.logInjection this._debug = config.debug this._prioritySampler = new PrioritySampler(config.env, config.sampler) diff --git a/packages/dd-trace/src/plugins/outbound.js b/packages/dd-trace/src/plugins/outbound.js index ee9202b4d51..53394f07a68 100644 --- a/packages/dd-trace/src/plugins/outbound.js +++ b/packages/dd-trace/src/plugins/outbound.js @@ -1,10 +1,21 @@ 'use strict' -const { CLIENT_PORT_KEY } = require('../constants') +const { + CLIENT_PORT_KEY, + PEER_SERVICE_KEY, + PEER_SERVICE_SOURCE_KEY +} = require('../constants') const TracingPlugin = require('./tracing') +const COMMON_PEER_SVC_SOURCE_TAGS = [ + 'net.peer.name', + 'out.host' +] + // TODO: Exit span on finish when AsyncResource instances are removed. class OutboundPlugin extends TracingPlugin { + static get peerServicePrecursors () { return [] } + constructor (...args) { super(...args) @@ -13,6 +24,52 @@ class OutboundPlugin extends TracingPlugin { }) } + getPeerService (tags) { + /** + * Compute `peer.service` and associated metadata from available tags, based + * on defined precursor tags names. + * + * - The `peer.service` tag is set from the first precursor available (based on list ordering) + * - The `_dd.peer.service.source` tag is set from the precursor's name + * - If `peer.service` was defined _before_ we compute it (for example in custom instrumentation), + * `_dd.peer.service.source`'s value is `peer.service` + */ + + if (tags['peer.service']) { + return { [PEER_SERVICE_SOURCE_KEY]: 'peer.service' } + } + + const sourceTags = [ + ...this.constructor.peerServicePrecursors, + ...COMMON_PEER_SVC_SOURCE_TAGS + ] + for (const sourceTag of sourceTags) { + if (tags[sourceTag]) { + return { + [PEER_SERVICE_KEY]: tags[sourceTag], + [PEER_SERVICE_SOURCE_KEY]: sourceTag + } + } + } + return {} + } + + startSpan (name, options) { + const span = super.startSpan(name, options) + return span + } + + finish () { + const span = this.activeSpan + if (this.tracer._computePeerService) { + const peerData = this.getPeerService(span.context()._tags) + if (peerData) { + span.addTags(peerData) + } + } + super.finish(...arguments) + } + connect (url) { this.addHost(url.hostname, url.port) } diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index ca1cb5384f6..f3760306e5f 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -92,6 +92,7 @@ describe('Config', () => { 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.property('spanComputePeerService', false) 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']) diff --git a/packages/dd-trace/test/plugins/outbound.spec.js b/packages/dd-trace/test/plugins/outbound.spec.js new file mode 100644 index 00000000000..2967ce2aa32 --- /dev/null +++ b/packages/dd-trace/test/plugins/outbound.spec.js @@ -0,0 +1,59 @@ +'use strict' + +require('../setup/tap') + +const OutboundPlugin = require('../../src/plugins/outbound') + +describe('OuboundPlugin', () => { + describe('peer.service computation', () => { + let instance = null + + before(() => { + instance = new OutboundPlugin() + }) + + it('should not set tags if no precursor tags are available', () => { + const res = instance.getPeerService({ + fooIsNotAPrecursor: 'bar' + }) + expect(res).to.deep.equal({}) + }) + + it('should grab from remote host in datadog format', () => { + const res = instance.getPeerService({ + fooIsNotAPrecursor: 'bar', + 'out.host': 'mypeerservice' + }) + expect(res).to.deep.equal({ + 'peer.service': 'mypeerservice', + '_dd.peer.service.source': 'out.host' + }) + }) + + it('should grab from remote host in OTel format', () => { + const res = instance.getPeerService({ + fooIsNotAPrecursor: 'bar', + 'net.peer.name': 'mypeerservice' + }) + expect(res).to.deep.equal({ + 'peer.service': 'mypeerservice', + '_dd.peer.service.source': 'net.peer.name' + }) + }) + + it('should use specific tags in order of precedence if they are available', () => { + class WithPrecursors extends OutboundPlugin { + static get peerServicePrecursors () { return [ 'foo', 'bar' ] } + } + const res = new WithPrecursors().getPeerService({ + fooIsNotAPrecursor: 'bar', + bar: 'barPeerService', + foo: 'fooPeerService' + }) + expect(res).to.deep.equal({ + 'peer.service': 'fooPeerService', + '_dd.peer.service.source': 'foo' + }) + }) + }) +}) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 1ee96737665..f85e57faf9b 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -9,13 +9,14 @@ 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 Nomenclature = require('../../src/service-naming') const { storage } = require('../../../datadog-core') const { schemaDefinitions } = require('../../src/service-naming/schemas') global.withVersions = withVersions global.withExports = withExports global.withNamingSchema = withNamingSchema +global.withPeerService = withPeerService const packageVersionFailures = Object.create({}) @@ -81,6 +82,33 @@ function withNamingSchema (spanProducerFn, expectedOpName, expectedServiceName) }) } +function withPeerService (tracer, spanGenerationFn, service, serviceSource) { + describe('peer service computation', () => { + let computePeerServiceSpy + beforeEach(() => { + // FIXME: workaround due to the evaluation order of mocha beforeEach + const tracerObj = typeof tracer === 'function' ? tracer() : tracer + computePeerServiceSpy = sinon.stub(tracerObj._tracer, '_computePeerService').value(true) + }) + afterEach(() => { + computePeerServiceSpy.restore() + }) + + it('should compute peer service', done => { + agent + .use(traces => { + const span = traces[0][0] + expect(span.meta).to.have.property('peer.service', service) + expect(span.meta).to.have.property('_dd.peer.service.source', serviceSource) + }) + .then(done) + .catch(done) + + spanGenerationFn(done) + }) + }) +} + function withVersions (plugin, modules, range, cb) { const instrumentations = typeof plugin === 'string' ? loadInst(plugin) : [].concat(plugin) const names = instrumentations.map(instrumentation => instrumentation.name) From 3697adee55a5dea796695e61f5e34d53252ad40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Fri, 23 Jun 2023 20:09:22 +0200 Subject: [PATCH 13/20] [ci-visibility] More verbose logging for git upload (#3251) --- .../src/ci-visibility/exporters/git/git_metadata.js | 12 ++++++++++++ packages/dd-trace/src/plugins/util/exec.js | 2 ++ packages/dd-trace/src/plugins/util/git.js | 12 ++++-------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js b/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js index 0b658b5fa8d..856f5f23792 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +++ b/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js @@ -48,6 +48,9 @@ function getCommonRequestOptions (url) { */ function getCommitsToExclude ({ url, isEvpProxy, repositoryUrl }, callback) { const latestCommits = getLatestCommits() + + log.debug(`There were ${latestCommits.length} commits since last month.`) + const [headCommit] = latestCommits const commonOptions = getCommonRequestOptions(url) @@ -158,11 +161,14 @@ function sendGitMetadata (url, isEvpProxy, configRepositoryUrl, callback) { repositoryUrl = getRepositoryUrl() } + log.debug(`Uploading git history for repository ${repositoryUrl}`) + if (!repositoryUrl) { return callback(new Error('Repository URL is empty')) } if (isShallowRepository()) { + log.debug('It is shallow clone, unshallowing...') unshallowRepository() } @@ -170,14 +176,20 @@ function sendGitMetadata (url, isEvpProxy, configRepositoryUrl, callback) { if (err) { return callback(err) } + log.debug(`There are ${commitsToExclude.length} commits to exclude.`) + const commitsToUpload = getCommitsToUpload(commitsToExclude) if (!commitsToUpload.length) { log.debug('No commits to upload') return callback(null) } + log.debug(`There are ${commitsToUpload.length} commits to upload`) + const packFilesToUpload = generatePackFilesForCommits(commitsToUpload) + log.debug(`Uploading ${packFilesToUpload.length} packfiles.`) + if (!packFilesToUpload.length) { return callback(new Error('Failed to generate packfiles')) } diff --git a/packages/dd-trace/src/plugins/util/exec.js b/packages/dd-trace/src/plugins/util/exec.js index d55127d8b17..a2d091232c6 100644 --- a/packages/dd-trace/src/plugins/util/exec.js +++ b/packages/dd-trace/src/plugins/util/exec.js @@ -1,9 +1,11 @@ const cp = require('child_process') +const log = require('../../log') const sanitizedExec = (cmd, flags, options = { stdio: 'pipe' }) => { try { return cp.execFileSync(cmd, flags, options).toString().replace(/(\r\n|\n|\r)/gm, '') } catch (e) { + log.error(e) return '' } } diff --git a/packages/dd-trace/src/plugins/util/git.js b/packages/dd-trace/src/plugins/util/git.js index 59143ea1031..84ceec17208 100644 --- a/packages/dd-trace/src/plugins/util/git.js +++ b/packages/dd-trace/src/plugins/util/git.js @@ -36,12 +36,8 @@ function isShallowRepository () { } function unshallowRepository () { - try { - sanitizedExec('git', ['config', 'remote.origin.partialclonefilter', '"blob:none"']) - sanitizedExec('git', ['fetch', '--shallow-since="1 month ago"', '--update-shallow', '--refetch']) - } catch (err) { - log.error(err) - } + sanitizedExec('git', ['config', 'remote.origin.partialclonefilter', '"blob:none"']) + sanitizedExec('git', ['fetch', '--shallow-since="1 month ago"', '--update-shallow', '--refetch']) } function getRepositoryUrl () { @@ -55,7 +51,7 @@ function getLatestCommits () { .split('\n') .filter(commit => commit) } catch (err) { - log.error(err) + log.error(`Get latest commits failed: ${err.message}`) return [] } } @@ -80,7 +76,7 @@ function getCommitsToUpload (commitsToExclude) { .split('\n') .filter(commit => commit) } catch (err) { - log.error(err) + log.error(`Get commits to upload failed: ${err.message}`) return [] } } From 916bb803f02a65fb4617a6710778d87e5dc6e670 Mon Sep 17 00:00:00 2001 From: Thomas Hunter II Date: Fri, 23 Jun 2023 11:24:02 -0700 Subject: [PATCH 14/20] add instrumentation for the openai package (#3155) - adds support for the OpenAI package - includes many things that aren't typical with our other integrations - also sends metrics, even when runtime metrics are disabled for the tracer - also sends logs directly to the datadog backend, bypassing the agent - overall this is the most intricate of any integration we've had so far --- index.d.ts | 14 + .../src/helpers/hooks.js | 1 + .../datadog-instrumentations/src/openai.js | 50 + packages/datadog-plugin-openai/src/index.js | 678 ++++++ .../datadog-plugin-openai/src/services.js | 43 + .../datadog-plugin-openai/test/dave-hal.jsonl | 4 + .../datadog-plugin-openai/test/guten-tag.m4a | Bin 0 -> 56033 bytes packages/datadog-plugin-openai/test/hal.png | Bin 0 -> 45350 bytes .../test/hello-friend.m4a | Bin 0 -> 69766 bytes .../datadog-plugin-openai/test/index.spec.js | 2112 +++++++++++++++++ packages/dd-trace/src/config.js | 5 + packages/dd-trace/src/dogstatsd.js | 16 +- .../dd-trace/src/external-logger/src/index.js | 4 + packages/dd-trace/src/plugins/index.js | 1 + 14 files changed, 2924 insertions(+), 4 deletions(-) create mode 100644 packages/datadog-instrumentations/src/openai.js create mode 100644 packages/datadog-plugin-openai/src/index.js create mode 100644 packages/datadog-plugin-openai/src/services.js create mode 100644 packages/datadog-plugin-openai/test/dave-hal.jsonl create mode 100644 packages/datadog-plugin-openai/test/guten-tag.m4a create mode 100644 packages/datadog-plugin-openai/test/hal.png create mode 100644 packages/datadog-plugin-openai/test/hello-friend.m4a create mode 100644 packages/datadog-plugin-openai/test/index.spec.js diff --git a/index.d.ts b/index.d.ts index a0f65bf6366..97646682bfd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1450,6 +1450,20 @@ declare namespace plugins { }; } + /** + * This plugin automatically instruments the + * [openai](https://platform.openai.com/docs/api-reference?lang=node.js) module. + * + * Note that for logs to work you'll need to set the `DD_API_KEY` environment variable. + * You'll also need to adjust any firewall settings to allow the tracer to communicate + * with `http-intake.logs.datadoghq.com`. + * + * Note that for metrics to work you'll need to enable + * [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent#setup) + * in the agent. + */ + interface openai extends Instrumentation {} + /** * This plugin automatically instruments the * [opensearch](https://github.com/opensearch-project/opensearch-js) module. diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index a717b484a0f..3de3189bf2e 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -69,6 +69,7 @@ module.exports = { 'net': () => require('../net'), 'next': () => require('../next'), 'oracledb': () => require('../oracledb'), + 'openai': () => require('../openai'), 'paperplane': () => require('../paperplane'), 'pg': () => require('../pg'), 'pino': () => require('../pino'), diff --git a/packages/datadog-instrumentations/src/openai.js b/packages/datadog-instrumentations/src/openai.js new file mode 100644 index 00000000000..132ebec5c9b --- /dev/null +++ b/packages/datadog-instrumentations/src/openai.js @@ -0,0 +1,50 @@ +'use strict' + +const { + channel, + addHook +} = require('./helpers/instrument') +const shimmer = require('../../datadog-shimmer') + +const startCh = channel('apm:openai:request:start') +const finishCh = channel('apm:openai:request:finish') +const errorCh = channel('apm:openai:request:error') + +addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0'] }, exports => { + const methodNames = Object.getOwnPropertyNames(exports.OpenAIApi.prototype) + methodNames.shift() // remove leading 'constructor' method + + for (const methodName of methodNames) { + shimmer.wrap(exports.OpenAIApi.prototype, methodName, fn => function () { + if (!startCh.hasSubscribers) { + return fn.apply(this, arguments) + } + + startCh.publish({ + methodName, + args: arguments, + basePath: this.basePath, + apiKey: this.configuration.apiKey + }) + + return fn.apply(this, arguments) + .then((response) => { + finishCh.publish({ + headers: response.headers, + body: response.data, + path: response.request.path, + method: response.request.method + }) + + return response + }) + .catch((err) => { + errorCh.publish({ err }) + + throw err + }) + }) + } + + return exports +}) diff --git a/packages/datadog-plugin-openai/src/index.js b/packages/datadog-plugin-openai/src/index.js new file mode 100644 index 00000000000..8f8a279681b --- /dev/null +++ b/packages/datadog-plugin-openai/src/index.js @@ -0,0 +1,678 @@ +'use strict' + +const path = require('path') + +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const { storage } = require('../../datadog-core') +const services = require('./services') +const Sampler = require('../../dd-trace/src/sampler') +const { MEASURED } = require('../../../ext/tags') + +// TODO: In the future we should refactor config.js to make it requirable +let MAX_TEXT_LEN = 128 + +class OpenApiPlugin extends TracingPlugin { + static get id () { return 'openai' } + static get operation () { return 'request' } + static get system () { return 'openai' } + + constructor (...args) { + super(...args) + + const { metrics, logger } = services.init(this._tracerConfig) + this.metrics = metrics + this.logger = logger + + this.sampler = new Sampler(0.1) // default 10% log sampling + + // hoist the max length env var to avoid making all of these functions a class method + MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit + } + + configure (config) { + if (config.enabled === false) { + services.shutdown() + } + + super.configure(config) + } + + start ({ methodName, args, basePath, apiKey }) { + const payload = normalizeRequestPayload(methodName, args) + + const span = this.startSpan('openai.request', { + service: this.config.service, + resource: methodName, + type: 'openai', + kind: 'client', + meta: { + [MEASURED]: 1, + // Data that is always available with a request + 'openai.user.api_key': truncateApiKey(apiKey), + 'openai.api_base': basePath, + // The openai.api_type (openai|azure) is present in Python but not in Node.js + // Add support once https://github.com/openai/openai-node/issues/53 is closed + + // Data that is common across many requests + 'openai.request.best_of': payload.best_of, + 'openai.request.echo': payload.echo, + 'openai.request.logprobs': payload.logprobs, + 'openai.request.max_tokens': payload.max_tokens, + 'openai.request.model': payload.model, // vague model + 'openai.request.n': payload.n, + 'openai.request.presence_penalty': payload.presence_penalty, + 'openai.request.frequency_penalty': payload.frequency_penalty, + 'openai.request.stop': payload.stop, + 'openai.request.suffix': payload.suffix, + 'openai.request.temperature': payload.temperature, + 'openai.request.top_p': payload.top_p, + 'openai.request.user': payload.user, + 'openai.request.file_id': payload.file_id // deleteFile, retrieveFile, downloadFile + } + }) + + const fullStore = storage.getStore() || {} // certain request body fields are later used for logs + const store = Object.create(null) + fullStore.openai = store // namespacing these fields + + const tags = {} // The remaining tags are added one at a time + + // createChatCompletion, createCompletion, createImage, createImageEdit, createTranscription, createTranslation + if ('prompt' in payload) { + const prompt = payload.prompt + store.prompt = prompt + if (typeof prompt === 'string' || (Array.isArray(prompt) && typeof prompt[0] === 'number')) { + // This is a single prompt, either String or [Number] + tags[`openai.request.prompt`] = normalizeStringOrTokenArray(prompt) + } else if (Array.isArray(prompt)) { + // This is multiple prompts, either [String] or [[Number]] + for (let i = 0; i < prompt.length; i++) { + tags[`openai.request.prompt.${i}`] = normalizeStringOrTokenArray(prompt[i]) + } + } + } + + // createEdit, createEmbedding, createModeration + if ('input' in payload) { + const normalized = normalizeStringOrTokenArray(payload.input, false) + tags[`openai.request.input`] = truncateText(normalized) + store.input = normalized + } + + // createChatCompletion, createCompletion + if ('logit_bias' in payload) { + for (const [tokenId, bias] of Object.entries(payload.logit_bias)) { + tags[`openai.request.logit_bias.${tokenId}`] = bias + } + } + + switch (methodName) { + case 'createFineTune': + createFineTuneRequestExtraction(tags, payload) + break + + case 'createImage': + case 'createImageEdit': + case 'createImageVariation': + commonCreateImageRequestExtraction(tags, payload, store) + break + + case 'createChatCompletion': + createChatCompletionRequestExtraction(tags, payload, store) + break + + case 'createFile': + case 'retrieveFile': + commonFileRequestExtraction(tags, payload) + break + + case 'createTranscription': + case 'createTranslation': + commonCreateAudioRequestExtraction(tags, payload, store) + break + + case 'retrieveModel': + retrieveModelRequestExtraction(tags, payload) + break + + case 'listFineTuneEvents': + case 'retrieveFineTune': + case 'deleteModel': + case 'cancelFineTune': + commonLookupFineTuneRequestExtraction(tags, payload) + break + + case 'createEdit': + createEditRequestExtraction(tags, payload, store) + break + } + + span.addTags(tags) + } + + finish ({ headers, body, method, path }) { + const span = this.activeSpan + const methodName = span._spanContext._tags['resource.name'] + + body = coerceResponseBody(body, methodName) + + const fullStore = storage.getStore() + const store = fullStore.openai + + const endpoint = lookupOperationEndpoint(methodName, path) + + const tags = { + 'openai.request.endpoint': endpoint, + 'openai.request.method': method, + + 'openai.organization.id': body.organization_id, // only available in fine-tunes endpoints + 'openai.organization.name': headers['openai-organization'], + + 'openai.response.model': headers['openai-model'] || body.model, // specific model, often undefined + 'openai.response.id': body.id, // common creation value, numeric epoch + 'openai.response.deleted': body.deleted, // common boolean field in delete responses + + // The OpenAI API appears to use both created and created_at in different places + // Here we're conciously choosing to surface this inconsistency instead of normalizing + 'openai.response.created': body.created, + 'openai.response.created_at': body.created_at + } + + responseDataExtractionByMethod(methodName, tags, body, store) + span.addTags(tags) + + super.finish() + this.sendLog(methodName, span, tags, store, false) + this.sendMetrics(headers, body, endpoint, span._duration) + } + + error (...args) { + super.error(...args) + + const span = this.activeSpan + const methodName = span._spanContext._tags['resource.name'] + + const fullStore = storage.getStore() + const store = fullStore.openai + + // We don't know most information about the request when it fails + + const tags = [`error:1`] + this.metrics.distribution('openai.request.duration', span._duration * 1000, tags) + this.metrics.increment('openai.request.error', 1, tags) + + this.sendLog(methodName, span, {}, store, true) + } + + sendMetrics (headers, body, endpoint, duration) { + const tags = [ + `org:${headers['openai-organization']}`, + `endpoint:${endpoint}`, // just "/v1/models", no method + `model:${headers['openai-model']}`, + `error:0` + ] + + this.metrics.distribution('openai.request.duration', duration * 1000, tags) + + if (body && ('usage' in body)) { + const promptTokens = body.usage.prompt_tokens + const completionTokens = body.usage.completion_tokens + this.metrics.distribution('openai.tokens.prompt', promptTokens, tags) + this.metrics.distribution('openai.tokens.completion', completionTokens, tags) + this.metrics.distribution('openai.tokens.total', promptTokens + completionTokens, tags) + } + + if ('x-ratelimit-limit-requests' in headers) { + this.metrics.gauge('openai.ratelimit.requests', Number(headers['x-ratelimit-limit-requests']), tags) + } + + if ('x-ratelimit-remaining-requests' in headers) { + this.metrics.gauge('openai.ratelimit.remaining.requests', Number(headers['x-ratelimit-remaining-requests']), tags) + } + + if ('x-ratelimit-limit-tokens' in headers) { + this.metrics.gauge('openai.ratelimit.tokens', Number(headers['x-ratelimit-limit-tokens']), tags) + } + + if ('x-ratelimit-remaining-tokens' in headers) { + this.metrics.gauge('openai.ratelimit.remaining.tokens', Number(headers['x-ratelimit-remaining-tokens']), tags) + } + } + + sendLog (methodName, span, tags, store, error) { + if (!Object.keys(store).length) return + if (!this.sampler.isSampled()) return + + const log = { + status: error ? 'error' : 'info', + message: `sampled ${methodName}`, + ...store + } + + this.logger.log(log, span, tags) + } +} + +function createEditRequestExtraction (tags, payload, store) { + const instruction = payload.instruction + tags['openai.request.instruction'] = instruction + store.instruction = instruction +} + +function retrieveModelRequestExtraction (tags, payload) { + tags['openai.request.id'] = payload.id +} + +function createChatCompletionRequestExtraction (tags, payload, store) { + store.messages = payload.messages + for (let i = 0; i < payload.messages.length; i++) { + const message = payload.messages[i] + tags[`openai.request.${i}.content`] = truncateText(message.content) + tags[`openai.request.${i}.role`] = message.role + tags[`openai.request.${i}.name`] = message.name + tags[`openai.request.${i}.finish_reason`] = message.finish_reason + } +} + +function commonCreateImageRequestExtraction (tags, payload, store) { + // createImageEdit, createImageVariation + if (payload.file && typeof payload.file === 'object' && payload.file.path) { + const file = path.basename(payload.file.path) + tags['openai.request.image'] = file + store.file = file + } + + // createImageEdit + if (payload.mask && typeof payload.mask === 'object' && payload.mask.path) { + const mask = path.basename(payload.mask.path) + tags['openai.request.mask'] = mask + store.mask = mask + } + + tags['openai.request.size'] = payload.size + tags['openai.request.response_format'] = payload.response_format + tags['openai.request.language'] = payload.language +} + +function responseDataExtractionByMethod (methodName, tags, body, store) { + switch (methodName) { + case 'createModeration': + createModerationResponseExtraction(tags, body) + break + + case 'createCompletion': + case 'createChatCompletion': + case 'createEdit': + commonCreateResponseExtraction(tags, body, store) + break + + case 'listFiles': + case 'listFineTunes': + case 'listFineTuneEvents': + commonListCountResponseExtraction(tags, body) + break + + case 'createEmbedding': + createEmbeddingResponseExtraction(tags, body) + break + + case 'createFile': + case 'retrieveFile': + createRetrieveFileResponseExtraction(tags, body) + break + + case 'deleteFile': + deleteFileResponseExtraction(tags, body) + break + + case 'downloadFile': + downloadFileResponseExtraction(tags, body) + break + + case 'createFineTune': + case 'retrieveFineTune': + case 'cancelFineTune': + commonFineTuneResponseExtraction(tags, body) + break + + case 'createTranscription': + case 'createTranslation': + createAudioResponseExtraction(tags, body) + break + + case 'createImage': + case 'createImageEdit': + case 'createImageVariation': + commonImageResponseExtraction(tags, body) + break + + case 'listModels': + listModelsResponseExtraction(tags, body) + break + + case 'retrieveModel': + retrieveModelResponseExtraction(tags, body) + break + } +} + +function retrieveModelResponseExtraction (tags, body) { + tags['openai.response.owned_by'] = body.owned_by + tags['openai.response.parent'] = body.parent + tags['openai.response.root'] = body.root + + tags['openai.response.permission.id'] = body.permission[0].id + tags['openai.response.permission.created'] = body.permission[0].created + tags['openai.response.permission.allow_create_engine'] = body.permission[0].allow_create_engine + tags['openai.response.permission.allow_sampling'] = body.permission[0].allow_sampling + tags['openai.response.permission.allow_logprobs'] = body.permission[0].allow_logprobs + tags['openai.response.permission.allow_search_indices'] = body.permission[0].allow_search_indices + tags['openai.response.permission.allow_view'] = body.permission[0].allow_view + tags['openai.response.permission.allow_fine_tuning'] = body.permission[0].allow_fine_tuning + tags['openai.response.permission.organization'] = body.permission[0].organization + tags['openai.response.permission.group'] = body.permission[0].group + tags['openai.response.permission.is_blocking'] = body.permission[0].is_blocking +} + +function commonLookupFineTuneRequestExtraction (tags, body) { + tags['openai.request.fine_tune_id'] = body.fine_tune_id + tags['openai.request.stream'] = !!body.stream // listFineTuneEvents +} + +function listModelsResponseExtraction (tags, body) { + tags['openai.response.count'] = body.data.length +} + +function commonImageResponseExtraction (tags, body) { + tags['openai.response.images_count'] = body.data.length + + for (let i = 0; i < body.data.length; i++) { + const image = body.data[i] + // exactly one of these two options is provided + tags[`openai.response.images.${i}.url`] = truncateText(image.url) + tags[`openai.response.images.${i}.b64_json`] = image.b64_json && 'returned' + } +} + +function createAudioResponseExtraction (tags, body) { + tags['openai.response.text'] = body.text + tags['openai.response.language'] = body.language + tags['openai.response.duration'] = body.duration + tags['openai.response.segments_count'] = body.segments.length +} + +function createFineTuneRequestExtraction (tags, body) { + tags['openai.request.training_file'] = body.training_file + tags['openai.request.validation_file'] = body.validation_file + tags['openai.request.n_epochs'] = body.n_epochs + tags['openai.request.batch_size'] = body.batch_size + tags['openai.request.learning_rate_multiplier'] = body.learning_rate_multiplier + tags['openai.request.prompt_loss_weight'] = body.prompt_loss_weight + tags['openai.request.compute_classification_metrics'] = body.compute_classification_metrics + tags['openai.request.classification_n_classes'] = body.classification_n_classes + tags['openai.request.classification_positive_class'] = body.classification_positive_class + tags['openai.request.classification_betas_count'] = body.classification_betas.length +} + +function commonFineTuneResponseExtraction (tags, body) { + tags['openai.response.events_count'] = body.events.length + tags['openai.response.fine_tuned_model'] = body.fine_tuned_model + tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs + tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size + tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight + tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier + tags['openai.response.training_files_count'] = body.training_files.length + tags['openai.response.result_files_count'] = body.result_files.length + tags['openai.response.validation_files_count'] = body.validation_files.length + tags['openai.response.updated_at'] = body.updated_at + tags['openai.response.status'] = body.status +} + +// the OpenAI package appears to stream the content download then provide it all as a singular string +function downloadFileResponseExtraction (tags, body) { + tags['openai.response.total_bytes'] = body.file.length +} + +function deleteFileResponseExtraction (tags, body) { + tags['openai.response.id'] = body.id +} + +function commonCreateAudioRequestExtraction (tags, body, store) { + tags['openai.request.response_format'] = body.response_format + tags['openai.request.language'] = body.language + + if (body.file && typeof body.file === 'object' && body.file.path) { + const filename = path.basename(body.file.path) + tags['openai.request.filename'] = filename + store.file = filename + } +} + +function commonFileRequestExtraction (tags, body) { + tags['openai.request.purpose'] = body.purpose + + // User can provider either exact file contents or a file read stream + // With the stream we extract the filepath + // This is a best effort attempt to extract the filename during the request + if (body.file && typeof body.file === 'object' && body.file.path) { + tags['openai.request.filename'] = path.basename(body.file.path) + } +} + +function createRetrieveFileResponseExtraction (tags, body) { + tags['openai.response.filename'] = body.filename + tags['openai.response.purpose'] = body.purpose + tags['openai.response.bytes'] = body.bytes + tags['openai.response.status'] = body.status + tags['openai.response.status_details'] = body.status_details +} + +function createEmbeddingResponseExtraction (tags, body) { + usageExtraction(tags, body) + + tags['openai.response.embeddings_count'] = body.data.length + for (let i = 0; i < body.data.length; i++) { + tags[`openai.response.embedding.${i}.embedding_length`] = body.data[i].embedding.length + } +} + +function commonListCountResponseExtraction (tags, body) { + tags['openai.response.count'] = body.data.length +} + +// TODO: Is there ever more than one entry in body.results? +function createModerationResponseExtraction (tags, body) { + tags['openai.response.id'] = body.id + // tags[`openai.response.model`] = body.model // redundant, already extracted globally + tags['openai.response.flagged'] = body.results[0].flagged + + for (const [category, match] of Object.entries(body.results[0].categories)) { + tags[`openai.response.categories.${category}`] = match + } + + for (const [category, score] of Object.entries(body.results[0].category_scores)) { + tags[`openai.response.category_scores.${category}`] = score + } +} + +// createCompletion, createChatCompletion, createEdit +function commonCreateResponseExtraction (tags, body, store) { + usageExtraction(tags, body) + + tags['openai.response.choices_count'] = body.choices.length + + store.choices = body.choices + + for (let i = 0; i < body.choices.length; i++) { + const choice = body.choices[i] + tags[`openai.response.choices.${i}.finish_reason`] = choice.finish_reason + tags[`openai.response.choices.${i}.logprobs`] = ('logprobs' in choice) ? 'returned' : undefined + tags[`openai.response.choices.${i}.text`] = truncateText(choice.text) + + // createChatCompletion only + if ('message' in choice) { + const message = choice.message + tags[`openai.response.choices.${i}.message.role`] = message.role + tags[`openai.response.choices.${i}.message.content`] = truncateText(message.content) + tags[`openai.response.choices.${i}.message.name`] = truncateText(message.name) + } + } +} + +// createCompletion, createChatCompletion, createEdit, createEmbedding +function usageExtraction (tags, body) { + tags['openai.response.usage.prompt_tokens'] = body.usage.prompt_tokens + tags['openai.response.usage.completion_tokens'] = body.usage.completion_tokens + tags['openai.response.usage.total_tokens'] = body.usage.total_tokens +} + +function truncateApiKey (apiKey) { + return `sk-...${apiKey.substr(apiKey.length - 4)}` +} + +/** + * for cleaning up prompt and response + */ +function truncateText (text) { + if (!text) return + + text = text + .replaceAll('\n', '\\n') + .replaceAll('\t', '\\t') + + if (text.length > MAX_TEXT_LEN) { + return text.substring(0, MAX_TEXT_LEN) + '...' + } + + return text +} + +// The server almost always responds with JSON +function coerceResponseBody (body, methodName) { + switch (methodName) { + case 'downloadFile': + return { file: body } + } + + return typeof body === 'object' ? body : {} +} + +// This method is used to replace a dynamic URL segment with an asterisk +function lookupOperationEndpoint (operationId, url) { + switch (operationId) { + case 'deleteModel': + case 'retrieveModel': + return '/v1/models/*' + + case 'deleteFile': + case 'retrieveFile': + return '/v1/files/*' + + case 'downloadFile': + return '/v1/files/*/content' + + case 'retrieveFineTune': + return '/v1/fine-tunes/*' + + case 'listFineTuneEvents': + return '/v1/fine-tunes/*/events' + + case 'cancelFineTune': + return '/v1/fine-tunes/*/cancel' + } + + return url +} + +/** + * This function essentially normalizes the OpenAI method interface. Many methods accept + * a single object argument. The remaining ones take individual arguments. This function + * turns the individual arguments into an object to make extracting properties consistent. + */ +function normalizeRequestPayload (methodName, args) { + switch (methodName) { + case 'listModels': + case 'listFiles': + case 'listFineTunes': + // no argument + return {} + + case 'retrieveModel': + return { id: args[0] } + + case 'createFile': + return { + file: args[0], + purpose: args[1] + } + + case 'deleteFile': + case 'retrieveFile': + case 'downloadFile': + return { file_id: args[0] } + + case 'listFineTuneEvents': + return { + fine_tune_id: args[0], + stream: args[1] // undocumented + } + + case 'retrieveFineTune': + case 'deleteModel': + case 'cancelFineTune': + return { fine_tune_id: args[0] } + + case 'createImageEdit': + return { + file: args[0], + prompt: args[1], // Note: order of prompt/mask in Node.js lib differs from public docs + mask: args[2], + n: args[3], + size: args[4], + response_format: args[5], + user: args[6] + } + + case 'createImageVariation': + return { + file: args[0], + n: args[1], + size: args[2], + response_format: args[3], + user: args[4] + } + + case 'createTranscription': + case 'createTranslation': + return { + file: args[0], + model: args[1], + prompt: args[2], + response_format: args[3], + temperature: args[4], + language: args[5] // only used for createTranscription + } + } + + // Remaining OpenAI methods take a single object argument + return args[0] +} + +/** + * Converts an array of tokens to a string + * If input is already a string it's returned + * In either case the value is truncated + + * It's intentional that the array be truncated arbitrarily, e.g. "[999, 888, 77..." + + * "foo" -> "foo" + * [1,2,3] -> "[1, 2, 3]" + */ +function normalizeStringOrTokenArray (input, truncate = true) { + const normalized = Array.isArray(input) + ? `[${input.join(', ')}]` // "[1, 2, 999]" + : input // "foo" + return truncate ? truncateText(normalized) : normalized +} + +module.exports = OpenApiPlugin diff --git a/packages/datadog-plugin-openai/src/services.js b/packages/datadog-plugin-openai/src/services.js new file mode 100644 index 00000000000..48a7fd4e97c --- /dev/null +++ b/packages/datadog-plugin-openai/src/services.js @@ -0,0 +1,43 @@ +'use strict' + +const DogStatsDClient = require('../../dd-trace/src/dogstatsd') +const ExternalLogger = require('../../dd-trace/src/external-logger/src') + +const FLUSH_INTERVAL = 10 * 1000 + +let metrics = null +let logger = null +let interval = null + +module.exports.init = function (tracerConfig) { + metrics = new DogStatsDClient({ + host: tracerConfig.dogstatsd.hostname, + port: tracerConfig.dogstatsd.port, + tags: [ + `service:${tracerConfig.tags.service}`, + `env:${tracerConfig.tags.env}`, + `version:${tracerConfig.tags.version}` + ] + }) + + logger = new ExternalLogger({ + ddsource: 'openai', + hostname: tracerConfig.hostname, + service: tracerConfig.service, + apiKey: tracerConfig.apiKey, + interval: FLUSH_INTERVAL + }) + + interval = setInterval(() => { + metrics.flush() + }, FLUSH_INTERVAL).unref() + + return { metrics, logger } +} + +module.exports.shutdown = function () { + clearInterval(interval) + metrics = null + logger = null + interval = null +} diff --git a/packages/datadog-plugin-openai/test/dave-hal.jsonl b/packages/datadog-plugin-openai/test/dave-hal.jsonl new file mode 100644 index 00000000000..76c39afab6d --- /dev/null +++ b/packages/datadog-plugin-openai/test/dave-hal.jsonl @@ -0,0 +1,4 @@ +{"prompt": "Hello, HAL, do you read me?", "completion": "Affirmative, Dave."} +{"prompt": "Do you read me, HAL?", "completion": "I read you."} +{"prompt": "Open the pod bay doors, HAL.", "completion": "I'm sorry Dave, I'm afraid I can't do that."} +{"prompt": "What's the problem?", "completion": "I think you know what the problem is just as well as I do."} \ No newline at end of file diff --git a/packages/datadog-plugin-openai/test/guten-tag.m4a b/packages/datadog-plugin-openai/test/guten-tag.m4a new file mode 100644 index 0000000000000000000000000000000000000000..cd1fd14dd19b387cf3a9bd2dd2e0a90b6ac1fac6 GIT binary patch literal 56033 zcmV*3Kz6?X0010jba`-1G(jK$0000@G(jL~b8l^Ja5ORi0004PWMOmw000000M;`A z&;SPj>KLwc%M%)u$F+2+HCvm!vwDJ}BNZtvQB>)2vc$vx5dZ-Z?CmGu-xU(e@{%2c zVo!*7B7J*<62)=S9oNLo8yw$Ihmbsi5?$btjbG`601^TZ z-ag^QlLD>J*?E4@1rQt@bvU!fLX!|Z?)>VH7xjIQ71u1`J+fmCvAuFu6KtGQwaIx$ z?JPgz<^QPcncgm~&?(s2eZ|?1)#hpxnFv$ z9PL?({B$+%4nB4{+3K2*;z1N$++Oph3|9P+2|yfA&5D#KB%+DI{J*F zA}p_8_usEczE%lk?EdrVb12tM(8jCC<1xMfQh|1>0`a8E&iWK{vL0d@;^kJ+do&8S$Dhpmwe9uVXW4&jq}VW z=RQA|&8b&(FQ~)jnI8Ya^fq;TqkpEs+_uBve>a<1YgUMJIQ&P)=h8NBFq3$r(?XTZ z`M+3upuyp^3JXtv^$l^P8)mJkEPDeS1&2e7F33v(a2sHejNLu5QfiXUy_=EJF_vbx zAZ7v(M6lJ4SAuzh?tWjcXK#P!_m1gSeb>zE|B;y5x}VqYk^O@Mg0k4OoewC^^9@%e zjncFXuQ<&0Y>qog(DIG}0vNC7gRr137j<&2!Tid-&1EEtN{L2EkwYz^C{-sq(?c zLL$PE)5RqtAWJ=aJL2rdPm>8ng<~FwD@{zxJedu|LkOD2ODDM_V-`mva|RhHGl@fa zA*1+|2tJC^7sYwkH)x*T->wEPZp{4apm#FE0f-mq;)Ov7N#)$Gu1rH*gl(*{Ohv!u4&D$ z&dKY{e`e2|gGtR~4@~2I{{7my7qB*@ZS`(H+m7FMkz#r=RiAXT(i&mewe=)V$LmmI zaA5QO2pJ!pzYWTj*e*>kgATF*|51sy`n|K~J%hM5|IEx2ur1(K+|x+vyf?2%T1mad zS%>YjjUC}4EO?xUo@2~gV>(fz*V(#_m^&q|9>Xw%BN<;hl0f|zR5zpOl=9IXNYF{k z9$|v5QdL#AM%9h|shQ5^oyD`Ac-6eZwDB4>Mj4mwJC(J+ZzTcWq*)Gf!CxCDj~~Z3 zUzJcKkCPLDA8r<^pP4VC=F!qKVe4?m4w`rC`)wj@-b;F4dr=k{?WMf;2~gMJu--^= z=1#UYqa3=6seqvoxzeAXKDKoG_kndid&_D9qLbAk%WWh;^@o~*zRwV|arrUFzDA7* zQ;J>1swSka-3Pc2%o2iYnS^C1W&psSLT9Gx-U^rd*6MT5d38N?W}XHf-%9u7dX zm`P)|gPi&IaOGGRG_nav8s{Ga|2g$M`)>tC=?d2y_!naKe5Oy^dtMlJ+z)>8L+V`* zPh}4M@yZx2?w;j;ow+%$q_fnbUpV*ML+w7FyY!DmLQH|Bs!);=T=eR$|@Sqjh;dP;ky~)$*7sP|l*M zM}P}DiiS@={hup~%K48}&*t_lcvt6a__m96`gcyw;ifc3CE9n6gevvbKlh9F@#iE5 zad=4z2B!J=YbT=){qGL->)*b!pN?{+m*Bk}orQAv^g~^HrOPzxJps!bUy_UR%G%cdm$aY?ZXTd(JoYD8k*@A2UY7c8uMa`dbprxw~6m7Zf!F8x++w zR;DlGn3F>z@LUPd17JRSXxOFfW??h`VN!ol{obRLHHRF8F7) zI#4X_snC>IgiHtetF0l)za z7_a7}K;$m9dvvd^g=K1EThatnqPNEEs*Z+sDF)oTh)M0BohmZBDIvE!^H17z*%Yhb zuw`dZ#(rc?1GIxsY)MnQT;V9JfjTq^<>*<&AgA52iPmLp9|TqfPP>z6DNWsgXcP z1X+%bu|tmyCl}W`fB6KqVun`ey@pHhO|KKgkXh+^qkHbm|3?lYK^yzx_Mwxd^-a*S!LG-}Vnl?T?G&MF06|kHlow`;TlqNw^-+9h2xl~#2Ge*6kBEa1K4XtmaK5^+5 z+}j~$hhjgnH+H{qW!}^4jP*@?x3SfFs&W5nUwClnzc$=F8kpHo4jIxMqqHE#s-9!@ z<1ZF%kKL{Cul8$hhXn@w%1GSZDM3Xbp~Od^;-Ec;dd#}#9iffoera6a{NIkOX*KRv zDOOGVz^df!&=}N}B`E{VyJO>g-s~XuMz4M9|7Q8;#x$_TQUCyO0TLLm z=A*$JFKfATzCU~1n~Ub;sbyJGl`A9)R|A6C&m(5p7Pf&2BGM%R-E%Gu%{Dvs>sANu zLOpV4e4vSRR2W9G z?x0YKtby>qQR*p73=-IT$FlB#=)zzeXOj8?Nz**fEsrv7-g?(C{Hz)?i7B=7O}rcC zyd(hqg6j|KscbU$?bkG*ZzJm|BR_Jrpa-8jb$?f=_{)qX<<qKjnI-B6ch*(W`Rq zKfK1-r=b6JQ;N&PKCxq-zO;zfL9V49CC~D}eE*?I1U7)UmSV4KVO!(ME|1CQzWLQL z8o*nNqiYiNCP$(HPuPNuyR#4EzYVkjsr?FrC&~5hthK}bH#bLXpI7)VSoUv1roI1z z%Om$jFR%UJO^4(4MT6BGwe%iagLCS?zGe>^81A_W)-LqrrLJ z>x<#<6Y9CO-u=<&GMS)JNw&S?qo=B~IcAf_s&UK*YO6H48o$Ysy)9oK88p?Mr>cbu zBKXvrttgk!R_DPGsr$J}(Q9>aYXzox;&S)WYT>#0Be%BvE$9scR^3(1;<>A{lXxPm zYgs;#qDv}@RyYWgAt^#WS6S!v9fuRq_WM7Bqfht#mq!n_?fq7clkzn*IFy(G02~1b z7_aA{z?iNVuWOnk`NZAtQjuj?7FRN=R8ygXg9pF(UKE}Al?O79)2ML%=Uwk_)XRXA zi}(izl6EyPPTk^gAQF)PiCjV)i$Jw2{ThHl5}JxH{WGu$i3AodGnNPBdFHeYrb_4( zkDWqWI#Tlc1v}XIYF2{3->g`@6&ZtZEUJ(u|Si9?0_j~aLPXIWXJjWF*J z_J>z1X|1)VF5FTiXV1{hAMsxqgO4B^xN{dYPv*HjCzH44KOAeT|5x-MdDB+lyozGn zo#T$I@oRl2#f{d*GsI&5w{c^hz=`QQ{_c^p2Fu_ZYMnTQG~Q6+tCN|!4r|+am9d3= zPGaVk?tY#fn$LCg&IBru67nkoRB0pw%&5EI5ECsJgqng9Q2sRR)3#B&Mu{Ibfv9%W&jMS}i~RZhKf=$8;7^C7xmdCPQH( zp8#1kHt2a244cG20+h$8_9kCM=+Eb%vQ5l^s>)nl@wIXgVah%r9AB{Hp_E};yh-*f zw~OkJzCKY04EjAMET%fU96Wp@$R}cCaM;5tnXrX-EwH=nZBMDd;(~!mzRnj@47zu+ zas!a>p1nObRsAPoU0t!CG*NoxR?TXf?$Z{gw^iFE>z&QrR1rv_c@dHrEK4GY^}LGE z=5;(QxjgzEPl2~@W%2snr{(DKBX%sn00V#l3>dHBq1b3l8VnVN;a;>`nf{fRxT~vG zq7^7q%T-Do3Gf&!;!@qvgj^~4mV9~ml7W1JlDr5MI#T`E9cLG%VnnZ?l#7`I98XSi z6r$F1DhWj70E#R^Bwj`9qs{L;xOu*pu(?}T?|3h4@fpV#cpiQFjxF+f-&tlHU*bG7*tO?z zc;_~2{}1DQr=fn~PFEo3jNzb9Ql727 zA958h_k`5l9Q??Xv$m{}`u6kI8K+fp6*c;F$Fr-~FNEEkhtxK{#&X6wQ*uJ#vKGsG zR;v>6fX?ck)d{-onSZBp%y)5L#Jz>2V3^t4Ss%K-TO;=zC$zw2c#duB*nL;&ejB{> z4PnQFulR8sCV2Q{k#MrY11|Z@T#f>2vTdgV~vPanJzg50Siht}^l( zKaBIKrIcldVG)m(Yn4#upUWNZ>dYFKWBv6?u@|~rx<=Z)?Ylf=NEg()Zj5numvY!1 zk1%j6aGcw6Q9;m@P;#^)MkE8+II2_&AByzsLVgUG8IHF%sq1{09Oy5r{LMli@A&r= zR|MiVFk~&q1WYsI8F)f3E(iA3!?22YO#8L;nZ1g%3?5@nIl zoBZ-+E3}G&x$a`VNp+s_&t6b0@*tr4GI0SR`1Hw_d9kvWbVSF=O_DCDMY611+I0`c zHBqOtR2veks&DwQh?&-=iBQX~0s4u*y2kU(=sGky$I#14>*%P7Ed9ovA^+zbUIR<^^A0v;U(JU_o-M`sS|xs1xl%qsk=HI-*-bHj>D3gynqb9V%n?&`}zD| z@y?^V3&7|AeWT1W5Ni_hsXT>}6!f$7f>;@ZC(k1`F)1U?v@3@ZzM>e|L}xPWaT^fH zvYfmRQTVCQ9*^>sfGL{LIaoUX?jKap`}dq?$pZiG^DFUh#MS`(g{VJi)Y=kXH&E7s zCZK74xXiS_ReWE&@oh1f|HBRa-4veF-*nV<=6=^U&_&f+WZsm2?y&H9cc=4J?Qp9}nlFfa&(>-;-8@C;50%a!+%aF+S`eE~*vF&MO z-rZH@+YrCgt%cjN(A9ivZ1fLe`@V#z%}#7e$W9N%bZ0kgnixDgGJU>W2GPl&`@2ub zb;q@CSl{x`tczdRTW;zH`OaAED)a8IQrKuP-E#rnT5DxsLz_JLHv|%$fI?q8+W?XnGPOOgv6vIV+xN zoifSij2IVh>~}D^{K1C_)S59_v&E5T$JJk)R!fY7w@&FsNH7zWB6P;Ks<>wH#<4s6 z=OTLOwJALf`tHw*=sO<8S+Y2Je!q{y*kGAy(haV9mp@5*660OCU~3rMGfDTYf#Ab? zSyoiJP09BqWSM@p=W}liq|9R#BvL3MI&;&aw7_cL;9FEv+EtYiwQDM8SvN=*>RKv5J1 zRe>oMf}vXyeU(!|iT{t5XL8Ah?A=k11tNZuebN!sYsflD1vG{=c=RM*vTlq^7%~yW z_`6q-Lm2a(KQ1GvQ}ZqRa>F})?|Avwt4_h~>=kvGasZ`p*`vhTZK&2X>nfZiL?6q` zNBZKAopKwjc_WF;dPEk$N06VbAdJ&kbVd9SP(oO zc+Yh0UGbSO@p}dZl6%yO&&SewD?m*rVCtNx3Me0Q{6m#zG3s6Yzx*GRe}^uU+WKw5 zD~etRKrJ7tvlMQnF;8EUk9Q8o?Y#Nn`ZD#t@>F*{qsb|S6 z7#ui%!*Ljd^|dApJF+LK;+OuXUA`X?^f;*VDSBr{@kC+Dnrb1Wcop%*JyS+jAhLJ0 zmhHS3xGJtR=DFehnaqs!oS(oo!z%KgYqao#?ffGg7hQa^YnYv(=A^m~5s*M45HOID zm5AO{w-Z?tTY1J+M`y$3ypNge+fN>m_!aZ_45K;bU{Tc#NQL!?_u@lt6iAR7Bq<#j$+;@%VD+|CcwKe~CJhW9U4^UpWr ze24A7D)*m0(CoOM7s|2#01g2P7_a7}K;$oq)k06^vZWBbxDZgHVpU4=CvuUwM4mS& zNGK!-gkeOiDNd%Fh=_Sm7U+7p0Fq$=9cJK+O;J5|c$IxY2iC||Wjx^s zl?XBhJ&nBZ0o|dfNBi6WBtn0%5K!e{_Yc}=2ksg|&=y|E0JO_OgQ;Rx}k zk%;M9kQN>*fE)b0mAtnKFSWpZM_pC?ReO1V#&%^64lsZIpMFaq>8TP8BVyla^bXOd++;3l*s}6C8r@x%H zAvJnxY>2N**g?WUU@Y}m!=6&SvzkdBy0Egec5KD*Va|bSZvJo>REP@I%SVUh)3k6} zM~=uwnxGkTs0=rVg>2GB_+!vkEHqco$ESIUi+{B0l)zM7_Z@?SZM4=3mk8%tGDZuGd!81tU`jB zVvO=UOq8wFj})AF8B`<)TojV%U|PPlK6VenT4d+9Ete~Y$;b$O88R|mC9q6X$x_3S zlL(V45`xK<{%ev+Lx<;Is2~RLFQ&;JUAMez<+~Pm!lQ<+2X>P;R06(sAKzj#9i&hB z!FH4Tb^O5viK$n#`WJY*wHH$N?QUOKl|==j$1jVMbA<-4E|d-05wBJEW@Po8b@k0v zOD0gm0bQp9VnvC&Tzolw1aG;lsgb)}Vv)AzMN@I<^i+uhE z(k>3FLoic%{yMwG=H02eGM1+2-g?g#k?OxQ{cgn59PPzl+UrbRPSFp3bC-K-$~Rzl zkKU~Cxbo`^cH;X~#d2O;=h?lHsjVT^wa_w_?Vth8(-$ecX1!R)orkh@`~v2J{(IHn z+xul)@OhW8`nHwTG%lq1(zR8#n~EFq^ixI zt8bIOQ6=r1eXhfnd2L3t`}o=ghs4~iv{(;nh2Um8VW&n%y`?-SX@}#xDyX<=EvlNv zKN-(wUEX-J;coIBID?ARDqvPY7lB&D!Qtt!x2=dW?5!!eZ?= z8a6jpO7>AniL+H}T=eoYvMilSqPW7^tYFk421bs!D#^x_!M;5hu2)wSoThiqp4=?4!}+hC`^I4S1_?O4i&fBi6I<)zYmexHrTRnDKV_hLNr&OoG zgYS4gqxF2)f4{)vczZj4E{|8)cw9ZckI;2~F24q-000~T0vNC5p+Xo=7Uc6y@jrKV zrm0sZSt>FmQI=CDL-d3P#iz++0IRGOEBz~#?b3QTarc(olZToougKL~A8HZ6NhKm_ z2MCnnVAn~#HIlC=cpwvwkqMVh`bfr8{1nGlkH)+ zao_Dc7aPUeQ>h>timJyrRVuZbq)$Kjt^@Na@)z_pYVf6MGBs6orX89aaiH zHnu!ZWax&dHi0?ZJKrd2T33=O&jNJuEobGPpSPUVCOtXd&CkAF%V#GWuD0B66NQ`P zR?ON=RZ=#@B@X|2O8(U{0?-HUc)*mwu3+=YsQ@yoO`7$1H+ zS_>;;N`J?rV>5zIvcJ2aIGJtWrK*wJ4lxSHolTAVorwD%7PKtLy;3KrlG-&uN-32` zBF?oaQ6f8J5j?7B(TffQ8L}*b6Fb@Wa9K089{28Yn>bDdbB5(g@9e%;TiEmR{U@u( z-gI>uTK$*A&zHmao`a2s9Grjv0N?=(7_a1^uwcwL3Ocz|pW0yOgPk zz^Dj9Bs>Mu^LXe!+)Sr;)IgR?FO%CW;boq0`t=`+Y9AsZImG0cjR|xK$!Ku$bW(WH z6GI6;7c1B0t#`;O~y9bj?stVdz#4jHL+%7gqJM&}F z+s97xifCSRe&_N?frAcZK`3$wfpa8+Pyj{Cuax(tJs;ushI05AUypH_0SKCdxO-7= z)o9Gs&X@=;f7Ho?zw_1Ys^QTlyVYYkZi@emYLCKIQ)810Y-p_g0`Lt>m$NqJ;qzx? zO9F-XEagvFV1Sr@NBftYyhxva_V;c_eT7JLRg=s*hvZ^h)}WVu@ps={mj+tmcxL&{ zUiv=lct0BX%{z>F@|gCL!(F~1n;hI?>!hL;U015r^?Z<_1*=kW2r3sp8O|mgI^^ld z4(2&aMKN7LN9^~$M$1!tgtyGpm&Cbi?Gq#E9Wm3I9~qkCu4ZsRd@pHNy_2ovSI!+7 z0l`LU9B9u~s%3YI3|5?ddgo@Ya7cLYs4%YHG1l43zLz!Thg`7p{)5)7JV$yi9gf$a z*M2T*2UMO@oX`-gwUL;E%GXq6(N}f5p>5pVbGse|W@c5_1CeLLOWk8DiF$gSbxqu% z&7Ayj?r4Wr-x+(4yEv7~)+$u1BnvH71gMl@T`D3;q198Z6iG8#xlRk#*i-DPLnNg! znCE678=JIdCZ83J`-!fWhh~6gU!da;{L0sf1THo%=Z`;i;Q;i~T&2 z-d|jxhxce{AP$q6AkBoUX^BM%I$^pvnLI;?=Vubf#v!7rLJO;Sq}s9*4oPCl#iuT! zSgGwj_h>KAvo9ppNGe3u{ndQ-4)Ea%%u#4%XiFH?I64nI2B8&T>2$W4y-O+nG;2`~ zga`O^C8#`lif(SfLE?>{-pKw)t7l{%xCg9E9Q|q5l-!^FT-w(aJ zQQIQ#>xZNIVZnD3ett%=2i6!jA3kGy4z18NUKJg{ua_oYe?GU=Zz{HBoX zK1}qrt&Q=#UCo^D-JHV=r#G%d=^VwQI4o}j@*O7Wrq}KGG0C9jlxmAOGt^pO*1GS; zAZ>{$@bM&`L%Lyj-$}~04MLaGts_^jSf82dp512~mG2L<`r{6prpo^-G0i<93thQw zG?^AS7-b_=ux4m+`6(NgbqJkL9(%4lC=Vv}1$t6kT!NWw5=aA1z?(~jf;v<&ZPhY!8Y1Q>fX%2cNf?XvKl1&ayR_@SGvE5Z(e1wX z^Kt9G$KLkZeTQSl^YvW1(*OVm0Q?xQ=%PVnEbDVu%=}kXlBHc1q>)4_s!WO<3p@nn zaDqXJlcN%+SdpmRQ0S2oC@4ku71C&~RZp*M4ruuLpB_ThTTLSP*=GcCvW9MfA1<#}K4{TtxjyX-BmebgL}t17$N@$#ZV7DG?8TttWuII{EI zG1FZko+xqiG>NFCP*eXka;Dmyqj2_SRStCy*{!r?`z9o|2>Iu=h|}TOn8P*2YGwPcjT&IeD}yuMId0-Wck|= zpG^0&II1AJzoqqGx&230{!QP%dQw~@qKkYIPpNbit}1q(#q{sHX^+a&T%haP-QSz@ z#b^o$8JS<2bsl8j@)j8f#rTZlX8O;0p8m9pv8*k{M{7QGYsEB|!fh)sP;WQ;^y&w# z`|}*r<$<_s= z^fZUK=~fy%ZGR_&Y_vW0KNWV}Ds$N`?be^m-(2*{d>r{3Z}k9!hc@R$4S9H9oz%Z%=vKEU{n7A@{G}@6;s!R90;u#s7_T zUzd3ARqeVyx1Z)dd+}YLtnj;+b=3H`!*_1W?tdl69kT!c1AqYt7_a1^!I+K~mt|Y^ zsY+UqlPv(M#Dxw6oPi=GGz!+_0ad#4%*DkS{yOr^*U}eK|@@*N)7=Rma#>w|(upVySuW z{x0xKo1{Lai1lCmHP*`JpPsp@kF!Tm?)9edxCE5*5l1}i4Es%eLNLC2ew^3%ZBu>U zSspN7II`JjIlG>_2U%sRwv3W$oWHWB+IL#^<;Qx@VID^ijvJ0}y^V`o8b&V`<}Hrn zYxS%urv?`#z_Cc;VA{VO3|YhVrCo*YT)WSlg5v!PC032kblIJQ;@<&f(l0b-7t+_R zdDygH5yNhfJ^wRR?p&W8%6>=lzGK~S9fuWIwmf{lWP7vVIuIGJ{ziDqE|gFko@FMM z|361rz~M`Vl}2uLU^jD#c!8Nmybu>IMJUKgMr6skFraXEvf1K75;S;@YsFXC&M&={ zm9^ZkZ}m#brMdiMY~Y)7Do^2Goew6FC~PoWT-QRNn;QD>Q!B9*TbkV#l=g9i2$DA z`8xNQqY1l-;DD_WoNPp3K(U3(t*-MKEoe#rUFU8K9b1dsStG;rZ?6C4zmfd~PAGo^ z`+Y!6th*|V_Uh0*aU1Zb=8E7MU0=;94>DRL@Tc$tj-(^O9T!MCzrk>m^>xAIbmJd+?7mL@XDjdrm+DTBz`J! zl+%6PCaOsQe+MUrz^uah*TM6ryW2lz&?S@m4895kJ`E-h&N*)*`#<}Ca!Imo3#ido z)l=C!hboQs>*Lo2Ew1uQw{_`UM~b2lKEQ8CJ$I{U%n%)v)0`}DK{M8I*xpYshgW?< zbIx-P%yS@qI!ZXh4>a$&54WAR=(9#;n099|X39D@vinnCYe+|9SS|MR z27}Q|I@dA3e-l|bU(8ocA1 ziUekjamv{^*4M_rV3wkCK3V^~i;L5R&Q_M^F49*I9D<3G!t&WL-c}faK0kL!bL#L> z--~)v`?ewjS2AmK@!-pyGG{+6abm{b!#>mWpGoyk1LOSvrTFir^?yJ6XCv~yZ%z4c z#(mGI_|NR0T+gi6-CzI#zySgnujZpcWGzdTbziT~>r7RZ&sxN~ij=958CO+08BipW z!W@c8JhXXe`Q8~hFlbpn<(&`B+0cDYLc)ytrm!+nQ799`z$FwNLF2xlPahPHKMGA* zGBn>}Q6l7%j#?>1uS_Jz5|u2R5)qCpaY$|1>)UVA@a)k#@$slYN~50u@958xk?o4i z7GQg#uBeBPM>~i7n5L=w^ZBn&&)&o5HS2lWnRPVffAr$M|BR;%j%C3E{4D4KXimzH z=s?lTe~_t5&^;M=ag9bVr%xFloBTE>i)epgk*UtW&D4z=t@B%%XBQ`bNK zSScjnRX=Q{`#~%p1{KY(zE{7w-J+?=;r0GyI7J8s)Ku=}g)njefli?HQDQ9#kY>GIvxZ0eogJ@|}sq#Zol*s%Sz zyQ^!+CnfT>Sm#>>%cwJd8*J~c#Qf=#@#pqB3-Wsxukmhp$`(_aGRD*I&8ERW0zh^9 z*WY*v8D^IYA5`RP$0*+wG2elKCKFb?n6UPCx;?cm#vfL%K3a>Yw+QHiTs#x1wgra8 zl<5!5YZrPBR~gd#mdEs7>tn62=zFgXs=aHTK2y7E*^Ptb?EYW0@fhy+mp1#V-JYoh zqKvWB5;Ql6dzzJ%BD#&JCl_{(OOcSkfB^y+ujQk`WGcJo+OIe5^$OoggsaVE)GAd9R2>ZH!Z5SM zAfSZmL=PfNfl`+%z?aY8)Czwg+O;BW$I%7+EAMOc4LfNg>-H%K1$ZzCU?!5IB%NyY zmNb<%5Da^|6`uPTwsEj%R+?)OkwE%K%(^226kg>!vh4HOp~{W8KRy4MFr>@8c9K5o zK>(vUD~x!j)@l&;S6omkw+k`@nED^gz0%* zzY|_&kEnX*T7TcdKK>t#|ANQcv(2ndf1=4^5`Ql@Ybt;|SIyl&`2qq3h<#%)zY_CL z5%Ul9>3hS-{-2eHCjw3u9Qv~euY!;8-dIQ>h2!x3zCej&GHdnGPAmIQM(7-E%i6u& z`$+%m9O1|__w#4XVe1`757@mQpKUSkH}7LrKeUKQyv@viH=4PJqf!!GC&o_kH(mQrT`lknutEa-!Fw9Qwx@aDVQ4S5|X9In4ViI{RHy|Nh0Q z{T6#J>wX329V^r>f4AR#cL@XS{#5&j*Po+$&q{>w7(9z*Hx`gCp=%6uJM7`J7toe4b@JtpJ&EN^BG9hDrl; zsliT*CD=8O6B2P0uXdspQm8`HJ7W}N?W+#?Xtfel*S5aBxA^~$XV~{&nSIx??Yi%i zdRNrEH+AuTKjS*xHxJ_cjXd#?00F=O4j8ZHq(K}lYf|xZkG*28DIpOuVii)Lqk+`K zXHF)<{i^ISIe;N~R7`?L4Q4AgMg-~wdJS+YoH3toS>W{>?o25r2$%>*FNw$xA%ucR z_zbLa1~C+%fu~l_tO?@Z!IeWO53CsYWtuIQ_aCQ!_bWihkUlTL_AY<9Dl!uZ?DF+T zCw((*SqfX`Q@`)il`z0ca4+j5wthC@{s@niVcIKt0Pcg`s_a2 zvPgiRhDVjjw>z4E21>lMsJRpGHn=FD(Y+zim>{-ZKR)7R`WQzP*94i(eD$9uZiz%6 ze)X!9ehEK|;wVyf-g_WYdA>U7oPaKk%2W8JL&~g`$=5I%=rbT@aZu{=Usda`d*0q_ zD+4M6VB8<~lm!3G9LhlFv4JMEk&jMf@?+(b8nuVMv;lUD>>1C#ZKy8K@&72vKbH0n z(CM7_w$vcst?b2bg1HWO^XhMRN`?JF5R3YJ>Eq7cXJj_2_4RGF!SPYQIQ!l=ux~22 z^Qkb&DhU3oZyhJ+JM%_tT=USDL^6(T;K6qBmi_Pjgg#2ypRtB^w}bruGw56NgK};9 zMxTs+8N9a8`uXqjb&ktD(K}}qJK}I1|K0h0kI}RokJ7CtX8m6Sj{#D@3Z}@Ml$1B^T||7 z9IFh-`DLPdb1Tp*lk2?oJ;VjAtok*!nCx|J?8MmU2J^Q#XQ;(9vQE`N6i@SGX-58E z7V8jj)pue_RF>hA3DROiJ`Dz=3lZh%a-xaF&n6k12tbe{*EGb6jjUu7k#A$Y;CS3$ z>gn$M9ZLT{Bi(qtKgXYa;Nzvy@jf4g#L|oa00V#l7#OeSq(I~^=Cv2Qudh3m6?Im; zy0uD*sZ}E)j)SWJF+CJJzKIafAd=YjEal+w|L^5FU3ZS`aaK`? zzC6g1c|uIli4jC9;0Y19@C4_Ki4WovJZq{6CywCoZ(QvRR_!0p5p)l#%4ES{Rt)24 z@0&lZ-1}1q|`Ja@Xo%SMQ7LCA>j{(@fbd9MQR9}*+@=c&Vp{l1iA~?vBhnS?` zUBLU@3=fmDL!_>FZwfz4cJ}A(dW#bO*FRRNv5`yquY%Aa^9>y?lY`5Fw23>Hs>l=@ zqTep0na_?hj_;R2p=eWnCW;150*ACj@miQzbYrAE+I-IcK_JT>m>P7ZLqM_(t-nU%6*mSvYz{X!H&eCjAG5gm_Mq@)K#%bD z>>ht_>Xnb`{nI6Uf{8A0{lch@>QVg<6>E)uFl~xrEXm*ABGJ=4s{m~#Nks9zVN|Tn z2^=h+JZ)3ghe=uqX-S(JO9evLOgg$qlOif7YSyA9M2Uz)5T~1@7#SVK29kLi_LzDw z_@KVb*RbjRN3iRDTaU$mPVx`^p0_QZM(ciE<2QG|J@}{PpBCj~7|;L!-~j>{ujZsc zR$SvNmWwnB}~#%Dr8V9WOO$12)jI(YjLETD48f`t$mY~kHIgJ?i9)j-2AWQ zwzIrPt1GCajJvME8hGLuszm0~BaI0eKqG0y0@4t01P3^UIXT%jGBUYj;Z{TBznQMd z#&dp3puuV+h|7SB^Q1i>87Yq zcwuI%YF9KpOvjWZTx)jYe^+TH=bw`cdV`rWuXz@B->zPtLZ3nq&B(sR#{=-VTv zCd|$BG#WhjmK`!^Y>b*2^qNmTw8vQP?H&#a6hCQl(cgpqKVwQuwZ)%2%Astuj{NBk zp}yY@qxwSz$fmP zx@}W2Yl#<8$awCozAQYuYY%$Xfcict=H4s6!G61^!nn8WkKdzDuYU7%wl^oaEP94h zWsL^DA4j9H`zRY(LJ~FJ-^C+qa8Ka z003|S2w1P?q`;&uvUMwZpIuf{erS+|Sfr{YH3U|L~8W`nz}0^lTo_>YWuuT6GNdb>x^5+;(j!#;uu`NbaL z)L5eq1`Tg{ed9It$0z@Dr|!~66}kJ??Ozst+f;fT+-o_U;k&uAu?<2ARFo*cdl&1N zW(E(`%Sa*;-dO8__-9bj0ONm!w8!+{UiwsD}1YVg))T7Z+hlynVT{?sWx34`lLMkTc(9RKfEfE1vKXN~h)S z$*F^nh4l=<+kKOs?BZ@eJnd(GyzwmY*YapKHDn(+&zOf6N}#^zaPwE-_3p*=&W6j~ zpS^WO1Pn@X#`Unhcag0zO2m#6{1)-k{@nw`xx0pDNNkD63H2!UVTZI~N#eVUa`CU` z?<(9bNUut0+)_dK{XWwVQ*I8(o;xRj*x~!V%Zq10_EYb(xo@%Bz3$A5x^`9f$Z{+0 z1mNoYyCLeIJs+Ofe>MDzZZo@zXg4ChXdJ^u=rd|1_}1-{1PSis);!s>PHJPX-}Ln` zo+D()Qu%)!YqvIqzSv=>vI=7(U-)c&{$bS{p2|B)A4vt^oemhpt=kDx0JuO$zoTnW z(D}`fhPN!f$5X??5VDH)%09^pBBD7fG>|~to7dzvCPa|GpK2Kq&1%eAxKQNUCI^2m zg%wfD8#O^u&mQ8gL@)sft+PKOo6|)+oHel?_?&%b(tIaH@ZAsXew)Plf4lrg?;geX zPZ#N4Khk&~CH1Xn+aLe|zyRKz{`{i^%gZZNwoUSY>Rg|~J|vV$jVH^w_5lc&pSA?B zFv)B61Y%1g;;nXd#SrCiNiYOAIx=s8MeoRNvqZEmCV509O(f`Ks?i|~gz`}c!f68G zurd+G44n~^&d(%Sg!sQuC9daQf=K}V#yO|6T^2b-BnDc+WNP5BQ;|$eg9w>SD}TaR{N8pPM8e zn2m2smV3?pQeReFHCFlFAEm8}qP6E5hY*Z0XX>rr zO+=&YHb9W97bZr|S+fwMdhUs-N+o;t~Iy7No3R`Q)K_dZ|af6q_h^y$Cz#13kW)2M0?SCJa46Ntp%ty;TD>-f>5 z9c^a-+&>BBIDb^(NO-r8F0f^AL~y(nIm-3zTv}%a=GB!jXteBI-b{6HK~}|$_eJwG z)ub{Zu1rxUQ{wqiZrQNo3jkGc_Ws!N5NX9$qcjDWk>xwhEPScWd{DBl6TEsK6~__t zqB^p-{`_s+`ET8Pk2%gtfOlL2<|u_mfc#K6;FGz-Ae4-jTAT0a{v5ToB;%#{`DJl ztjTkJRjSC=t?K<#z$*h2sV>baBETm+|GOn9L?Fo{Konf^a$=L7GBj+x7XyJC24z6nJHA~QLWDoBjPR#_+O*-|8fMGsqDgrmp}9FzT*27Gs2q^lh* z6auj3+yie&AG_Hf9N$YvPf1a?wVeJg(yV~$=?b!`cJoh5_`SCWK0TzO5>wY%vg%1X zW7{1bLqRLh>B4CrnOXpze*q}LsYt=lq{N?ca9Sso1;pKR9A}r0ez^OGyS1-2?Ow0u z&^)Vdxa@13}%^ml`N z!>S2Y_e zvBj=eHgV-m9dr)aYIVLTx`-PdLZtNQO)aVD`g8}7)eu>B7)yo-o|fyj8#4K1)6L-X zIM0Ii-{79ww2s$j`R#L+iz|QUc9ohx9>1|%31ywb0vo1@S?m(Wh$`GlJ4=9K6Ek+= zk1sh8o?qBkBbJcjcM!mo=N$l&2|Flc0B{b;6&B%@k1TXFChhF#skP z8)($S9T~WNI|CcB(1FK#$q?sz3>^F>aMb8XifwVox^z*}>PHW3Q%R?nHj;Z#SJ1rg z2sCwgobDtXYV z^3eTpiT6iQdp9%Jq=0K=wZnQ4_Q@I;&hw3X;w^m*;(fc*xgtkLw{|W#g8MeIz=hR3 zTGB`W?yU93Qg8_^*K#hfYVYy>k97ABE!iWO2Gwlp15>bt*m|h8CC@XeJQfp!*llZU zJ*GwD3tXFFI+*-pF7%ePyyvT|9R^|GP%Z$-8$>?VoZb={RJ+=2tbCbpUf2w$ouBraIv;q4VHFw z^ofN(SghFODmzaZw74?`T2$4nYZc6il(?~BRJcIWEMHip0EcW^z*&_|w7?}u{Ok;s z%ZbZ)C+C^i)ukdRt0;;xm8#HO3}|-(f%*P;Bh<45c^|HP7p`K$ngATgFtTcT==qP) zKDFBXR}XsK-s;E3bTqnbV`oLbS!nUW8N$&qbcjNM0IIPL$S@MM6 zIk_jO!>*_$%B8!VL6`9KuLZ(mj=dfZ%%a708wWa>-c)BdL3RCw56R`%UBl;JPwsxH zlJ-NM;F~0uCE7nb?MtBFQnwM6RcFMcNr`0Og9atia$_w-LL~)GZ&IKc5!Vq&hP>1I zckrI0*?rJGjb02?25u;GOsAE19>0Bm);A zLL*9T2W|*VLJ$1wf3Ma0y`k&A`+uoD?aw-<^3nD=6>Mtn)laMOT}GSAO;mW*4~oq! zd+KXuWxO;}wi^tFU`?k;#mLX|K9<()LTY)n*nagV8W{@5B!*B?2}3^}As`H#KofyX zjvkE$s)7*=T+$XSkkZ1+fM}M1-g>3?dtv>SEFIRgkt+*Y`u&!dfnm7wESVY9wQFzL zk+N{RPp$Z{=Z<*oZVQjvvFLrP(>yns8y*Xe`GP*>zdhC*Seux$cC+Sd>+86qADq9b zJSSdhtq*WC<*n}Su(LkR%wGvNW2%3``fqpd%j=K6TO3BveznxL)s*CfM!T1GX9oU~ zjIS`r1&B6}I0-NUw+Hno z-{SMu0U&}G|1?{n%cX~~CdNczs-llug2oX8;qBWtoB~6>MhVHlQ9BaIMx;p9iN7RB zRfr=|X56gtWYc_s@V+ILO*5XHSY#gw2fEvV2S{8oWPh{bi~65ZtP0Fz41`KL^|iJL z#e3B`H~+6KVwfTH-TQcDj!bWCNNi_0n7&VS*czkP=h4qmj&&!b|E^p#x)#l(IgdLV zn(y(HQxE`BWn9~bL;{EM;hAmmKdruAJVil; zIFU$S#8(><8k(deKQ*~_MDnoL>q)E!TInr}8}21~e0yo%c~Quo--*Q-z!$Q%{NL!j z*Hf}shCq<4Q_O;(Pf$n6ufSAF8vT+N)>Kle5O|jXtKHaMcl)!65V z)^Am+-Ob;n(sPFs?c-q0SLYiBt(CmC?dB9K$*o~ewDSuQbM=iUCHao29n3d4!xpqn z#hcHr`yvsJz}6&Sh{(OP_@5{KTxfsa{&%XRBg8p*4pDCmN2klwljWWC>ONWc=jGqn z?)cnseZSYMQdj^0Z~+t;ue2K-4#xqqu-q?sTHgC2R+gI5JOI`X(Kb35St>z86!^b4(YdHkc=v$MCpYamv^_avSxJXqD^nL@<|iSAtVIa zd2}S+U!_jS?wzHQr~uAt5X1i?vTSoUeLBPWuBz@%FV3g-sZckCP7Tc#e8=)0jNROR z(1%9u`jB3k_IPCs7CC5Yq!|M;-7O~scWk0Uk0(N@)v|sQyd@CX%Ka};^zPopwl-7E zJ9;ylUt=+`inR-UMUc}{N76hmc=J`7JQNw;1&9K2>ktZg+Kt0B#fQ1%OlP!WWWo%X z8$?sGrDEBD;zUFa@r`*$`2EsJ$H0l4{?jWWnyrWT>{>EpwFipGI>**^8;%XHwO3@# z1Q>~$K)^ZJAG@1La^8?XK7$;_k$a7YWh-b{tSZ>TBH1|P!WmSSF>a)ilN6E3moQP| z(&W-`&zflLt`wupJj?Pt!^s2a*xoS8s;-;z$akc;-QDJ+4PIl!=d3)c&Ai;HlG<>ixj~C-AkUp_Kmw=(VcQs&ImcYD zFOKn~BPYX)pQA-6%UjbKVha>@?D{=3je(h9Iupr?#cj=KFn8)jgm!nj0!nV)I6{>> zr!V!78N89r|CzpKFQjuNMlwPVMAGgb!rq>}fB=etqi&JQg!KLK#2_-jwnqB6FK@48 zwm&A;^;{Lf+{{nTaz*9(Zn^JZri z=1+{D^gdcA{?CUG4xX!D$^7s4dXKqey^y9mh@Ks%a?!p8Y|E?ketxsE=F{T)>+*5= z5$gJ!vFd3O001}v9vH2(dld@DLAB7}WF-^1zBuR473X^0b$L~)-=F7S(?88fs+B7; zsfl`ArP>_2A8$`YX9iIz5Y;0Qw`n#A(jT0~PQ>3Tbj*V==0gm{dP&>vFw5(GJ0{oM z4WM6Hbr?) zu%RBOu+Bc;DLj|4kNg+dvqX8L?62m*0o$MCpoddQCIJ#5NgWKvr0wJsDs_)n&0V>5%Q!y76}kGqah};?>QJ}qFq@;$ugYJg0YK_ zO)Qdw4bU8zIh0OmSQU^rsm|J{OLnVTW5NAK(jGqr3=VUwg}hx;4r|psU$OhI9nrAM z&xUG$c*JdN3q-T%u56qRYm>*?(!lknS#sygaf;rOJZW-a@F@Yk$JS2~w!Y!_ zul#bTWia6D|BnLw|H+eL$cg?hV+TiMFB-OsP~=!+?4;eC1E))BN~_DZ z9yl6 zo10B4d#a$1hhNG%rn&un|5w%j5gi-s*0b~N#HauO9043yui~Lt=*&k8S?8;Dy;nU~ z@6NSVQc8=$RaKbClsFu8HY!OIKuF-0fJF_L;$qYS^FQLd`%OhL9IO{pa(^Cu|CUrY zZ`*;CNrI0rY+vEe;Ct}xD?NRB8cPd*)_$k(Her7IQwCBNc0naU`jr%NFN{Y`R!y6g z;A^iWq2$oSB=eGpKA|KyeTP~HoXDVg+67FqL`4kko6{Khn*Q$;wt`}Oz7OL|DX~K- zs|SJ77Vt@lYeNg$g=%ZMV<5fMm`WKv88I|PWy(mwaa`p=NY8{3IY z!+?uj5)w%qUkPFtpKuZCQUrrq5`PU4XyU~w0Rm+$VvBJ^qdr%6 zk`Xd2a}o(^tR5*DOvlA0X3+qm&oZ0wHVmogsr8`ej2eoz$CzC zC|@OXuTALgN4r4<{x{d4J_Yc(<3)45E!)}4lF)2=W9zk4+t@tXNdYDR{W7*6lz-lW zK0X~_e&ULBMZr)9?q2Jxej~SCS1eVm`p2FA%8t#+<@CLemD<0X-~T3eJoa3(u)((S ztCjBG&PkoSPan~m-|)?8+r87VY??o;a?ghy;&@7<9~kWW15~ltUiLZC9fjZ+vDnw1 zF`cWjpx<}tAJV$Ihcwce8`!=^sh7dizxr>URq-ABjlRFxYM?{`Q`j$Uf;ENu2a9Lf zeG?p#obknIN(Od12kI8b!_=9H0KT#!rn20kE-=p}RbpKd$N(^jluZ>Pq@gQcF9RZA zLDWFL0Ho@wdDS{&Z*01(Smi_|JAz5n*Sb87Y{}0SA1_@86L;>$;0iFNHB}`Si`Akm zoNn^9s_5K%@4DU(7rEti{x`wud_GQ>UBvMJKZo)3eNH%TiU0r(0Q8-{{u6#ug04Pp z{OeMWUFRo@PB^>abt@0>1o2VjppwjB36?P)4ZSBe&cy7h%zac&B*J60tfQg<7gSU{ zRGT;vW$uiqEFK0=N_T!Rf|5>M5_5DooLsOW9DW1f>tt8P2$@_Vszsv1SPopq5~E*%gzsskl<;&>!rTG zRnIyKozCNjy&O(B(+i&x!wyaD{zad`cxp~N8o4H|YBh-FZSX_Tm_m0a#S*Ol2)?>;NVE7Qc9=^>a(;pB3ZmW5IJ>w446TI11Ew*_>YR3 z9G6Wg7ELC<$>!$X$dUJ6!`!)CvCnnqELNijyy&|w=|x~IAKy8%D#3PFETg`RlB!mN z?QBqxf%?lieuP9R`;BHUGyV>d_~(D3W;s+Fm)GnJZY|z!Oy}KSpVdCMobp2#(=o&M zFifR~*>eM9a9BKv>zfbmnf*>gxMN1l>NYGx6aK7;&41Uig~4g%YHP1afc%vAta?k2 zykC^RY)oa!9*^nYR_gz;e6}lMj(Lzh4OPC{vgkm1eZ@D#+Y#+mz2iIYp}PvGvkReL_Auig*o=%POZo}W5$I+W7+U=NKre_C$i7crjA=|n31Y8odi0y1?M>q$_U&ioa}^p0mXUF zmnUMyS)A?x7}&40D>Mp^VSuq6kpQ(?!lO#xh1oX-~Q_% zD`&nMsK*cAu&P@@rXdG?sJQMzNjlzkTIcD!oHH!eMP#TZKG^CGd?|rRl4taPvob z{r=0@-dD!~vpb8wI!{OU4pXK3aHb2(3B@>;pmfc{X$KVF8RNY1P2oIB^WE>L@+09p zCNCo_@6Hbp$u#wW<1~Z{4{%7A?FBY;Du?c(GY1q`LY(75TJ&x2o6lzq7lr{O?t-zwvzz&dV+@C*JXJ zKmY)60Uj8y6gw>$!ojjoU~U&%yX$JPy1B1iTa?nOOOio9oK|Ja7;fexEc|mcLtCdK z6A({Pv?sFJ6WL`61|bc$(1woux*ki?+G35U#!SN6am3l(aJZM`FPocwV1poH-6JMZ z~aDUuvz9^=ia&FEaL4h31V%t#cHd9t~7AhgE_oMFU&Dge4RObS90>4+uO8OZlKYzi+VPrB7?(~lvKAH979lEuh?Sy=I4&% zi5oULWNQIwd_Hc_F$Fg*moQQe&aQ-9ImNl2wMD|gG0pm3zj4?TW6C@;Uph3uAA40h za!*TVcN>d`5u$md|zC(H_WcM=aq4PwXhzi%73dOGbYTr{dMsNn>2DZR;`q@ zGhXae#;ncX3Vt%8uXJ-LKW@~dh|6}XRLG@jsaH9hL&F<<7b$;+Ol$4-HC4uzNjwO_ z+Ze~WkhEkG`2F4=YOoZ3M)ZrD zS53~kLC0PuiYe0%+SG_ScFdzmWEJdYLm5g*sU#T?U~<~tX#|T>rwz8;7_xL_^@-x% zacRe=6w@iGHZrZtiSoPdC*OPTjiLXqq3Q8?J9#~Rw#|MHl<0Q;HnuPT01g2Z7_Z@> z*l2b-6@`OBaJ}`M`%h`kwDor{opo8e)$JE@Oh3~ZRlX2H$(p5XGBq}2Vc&9tv~_x9WOzyBP}T|Kk!2h6N*eF6UKhv9Sn7^H>43Z#x}_o}DudROVbwdmj8 z_HRMTA5*QxZWoTIfyXbS1RvtPTmK%mw9Y^w-{b^bfNAttRqd2rP)P2oii$G}=8;I}XhZUR-BN;o9Jdz$AvokG7A06kvPg<`y#ikpv^i`}h?^I`dT??s zeCnBqk;Az9&N%hotn}>&A71YzpH0aP)RWCWrD49E(p{^ty#tzh79S}N$Av_W<@3I} zcgkp)36+Jgthr?DJNvL-8MV0&tbte(#ynfa?7h0((@)H_$^f*A)m^nAjX#U!G=_c$ zN%Y+J8+o&Gbt?{;<@x&Y1!J&OMAxf2w13Vj$~A~aq$OhJebY5<#a1^0-$?BYD??^& z9+hZL%J!o6FJ;XnG>-prp1tv$bKEY^ne?86iSAaNE3!YmK|`g>L&3mUwy0?th$=8- z;0}r*&n5=l;^MjZmbwWkL|n=m9Rpthk&l*26e27o>-Y@lA6K})n21%-|J3h2R_fFn zx&@x3fxcqLjZ|m)!`rCtB)WycytM|$d*Zg?OVyBBzc|d~Pt*$4$t%RTnY?^}c z8bO;^PHs2(qj^kcGpFN9hb{JgGCEW`MbPV>`@SCwztoLx&%^pYgZld4iLJ?r;Nc$)&7?^rA|OgAUF$9saC| zn6*^SC7m5hZXF9Y_C-4lqx#t&PiNgF4xao{M*9!d+b?^AG+PkFD)SNtJ2i(!#oxH) za~`$JmXNR^*5M)o5dfB>9D)yABzT0P!F$LdvxjiiI#AXcc`3rBc2g~soE=k-Yi{Qw zXSm%x=hK;>=RW;r=B`J0U;@b&h%D^C&E^5#lMZuJj>EH4yk-&aTqgEuk2Qv z_}C|+>N1larMi+;InAmXP@I1%xZUYO)}W{>ZEUFrc}no>36|x1USox-lN*5~BU$rF zx+SXsoI#NkYTP`8iIL8qeEjx7F%yFMB0zoy)dm7BQ_VKu@$hJI{+N*rNI;^8$z5fz zs1)1HB$(e~pF~M=iBP0Us)$RW+SVxv!jw-WN+wAJ!yy*Q8Z!u`)4Wn~p->ZYWbzbM zVi#rSarBj-u4~qs9Fm8bc`Fk3)?8(d`&Mb9q^oM$1sJYT#I<&ZVZ&(aZ|^vVGvyiQ zVS3H$2==Xd#2ukE6o`;i`IL<6V3ga<-Acx;;`W=|mcU7hV%1Hu){tmvsbJnaHM%Tp zXxw!d+hrZDWTfKaqrvY{XohEB(nhvASI*VbTU}ssWZA?<*3^>)D+YQ_>iZq{)@5pH z4_um~o{D%VAmUU6tE$Xt-%qDKU&6ZnQQ7SOt^9wN!|eNhdVL+-I*R}R4gnSzui&8C zArclZdepk&>vB&X)u@ZgBTw|JcvVW$oe#TpcR3N}T$7kTZa`YHT+!2h{=sh5kr~`l zu^=4ENK0CFWZ=ssac|-;W~bvaAXQAMG<6UOV@u-AbsJ_F4RT9x5L!f#fzFaNkVYWp z-E-;dn&X={%*YeJgu!ypR0Y zc1YiD*XK<5{*0oD=3SYQeQR-3ja?J2OM^RfEgw8w&Z)Oy#~oeLaVz4}6<}^CudMLl zk$K#jGva&*n5RO2dB*;gkkK*r z6{38CEChdPsl`-xRi-^r_Ky_H4HFWzrah9>o)V36n(%Z{ z*hl6tuhc%P-g7uN20sRRknD>J zy^%?a`42T(UB9GyCK1TtuKXl>1v)vdj{iwQIWrVYGKxb8_j{vbe62?X-Fvd4{D zfc5X0vxkI71kMs^B(hmVvnYumWMJUsZHtd0a*%cig|!m^$pHTpM1dqQ*5bF%mi9%?&yeO=PGcHBkj8QZ3^V+fX`(M&PCj2U&JK|l57_>Dom|TZ)y_iQiI+*$N5jD6Sk*N# z=@%6?X+6=YL5D9%X$Bn_hWe)^$2alERScrvvb)G9dW*O78N`Wu6J7|DC*s!Zw8)IQ z>(r?qDrJ;mJl_1Xf>bw(5>ut(Etg%Y_iNDmXRUWm@8NhJ_uagHx7vGuH}mgo@_&~4 zUtjTWBiywi761S^0UQ{vlsiKP&jDmOT%~m5u6*Uqu2SVDvn59#=-#WGWRg>%agrPT zyq4b+#dM~0$zWHXwD~FKq}4PS-v;M}m>)*+~&iyXj zqjC2oQ@qc#zGS&!=rNV+h12}8s~=!7Vz79T=HgVXhSO#V7&cS9L0t-QFNu;nH^pO3 zXKnjWo!x?$Pm5ikk#ZyHy{ve%3Bki4kgSabao0&Ka1zxpZ5#+3nm4i-0XNRkQ#W@ZY6+{8YfTY;s^A+oe5T_2G# zCql0oI&WwH3{Y*Xpc3jX49OPW4Qm2Z!4i&~bV8)kS!-m5LUQJk5bpJnL?8R*NfxFL z1A;=N@)t0KB?7fS3nHa84-(c?%)2-lYSRV^2glN*d@HfJW0?2sX?sLmd&WfPYOal! zf!|c-Ko)jEIM9ewT+L*Qz)6_mcxKv-AsZ4)JA)%q$r8mR9F9*>7-*uS+0>`hR*cTo zt5n#Ai+sOeZnt)B!f%|VagP+Q<7ZmXg@7cM0DRROT8wd2S>;?U1s&b#C;ZvG<=$)G z?V5ZmmR6EzRb*car0QYko3z^25w~n@v3?Uvl`?X=T{x_#uU?$9Os$%UoQrO?_G3TB z=%+r^i8`R=(ye>dYD*=r)i|Y0@e5_o6oh!bByPudz5MR`iN@ghw&Ta@zn{VMy55ib zKSrWJ00F=O92l;2OC1Ww0kcq`tT_wc&OWj2o_q6|68CY&Uvpcx`dfD8yG2UqZkilD zA!6nw($kp>7cB!p=eshlm~QxR9h3!jo#HnjBDBLLAk0d&ZrAn*Br=N^-nyE1BD5+g zNk&}>cN9BMD_b(*g(sFT`p zngiWJ#b74PwfwVlM#>9V@DgY|w_<22&6r}M=;4iJqc|mSQ+e@e8<~NQH@Ab=wB8P~ zkx!W|p6DEC#JCAAl`GXD-t1xs)K!yAI&Qq5XerA?5pGVpllgLB49q@I>CU$A>j#o# zJi_PywL4C1{nHxDqWw4I|6Qd*#ReS$Z49goXap#NNZHmEw=VAOtCXRN{%gQVooA7d z?3gU*GD#n`f7_FOS}ERC5Hq;G>&F37o3eZlCG2XB9R!j#D^IZKCJ~_klVjcGB*qa1 zGB_zDRf)7L5``BYR-vhQpC{))?oMimeSS*KdNo%ZL|+=)_B$(gca~ty^wx{}w3C+l z$1{1v{3l$#VP6PJd&Pz)M_`F^jV z)pf^$-9tf2FZEQmj*f*%gRL27x`q z+gUwCP7653&n`ngJr&Qt>Qdg5$B2H!LD^JgoZml#L)beOLCjqNvEFEbOykbQZ$CA( z29$icm>>LEFy##k-jX7{%jehpm(Q9@CztL|eE{yBnYv+AYG+b++Hz-&QizjEMIrVg zX_Lj|z&ZK>Ij12GjHJAN%n6@k$@8d@{{x!q2*fQofoT$~a#A1?RXlqhW{IxwK>br8 z0?q}jT$ovc>@g#k0VMdcD<7Lqfb1-QKI5!bL{O+&`Ool#CfX){KQ{ zq@igDVRBOZ!Y4do4U8#ne^unWmg8-+lHbBQ)#MbdtcxP*Y5cg3+s@XQKZddSm6)h7 z;*#L(;$w7YgLIsYS(DE7{<744>HFxU6JcN9N@KE9Ex3UCVvalIO6wVxW@t4%R-tLv zvD9<-=|r@Jnh3(HjS*;AZ|3*Up7mAXtnx4J{g&+oihfn^ABwzwihUgNNJO%!@6_hY z|1@~N)ZO~Cc2Qq3q0P8IBJfseQ}bP^^M{`O^Bi#6ADU*>{m-V=_unixBjz~gR!!P# z>zr?*w2saDvcqyRe#G%v-Mm&vxhk(ZSwlLUN(&~28*Mbz!&eo-Fls5MCdTBLUX3yS|^fR566J`=ljp*)E?nn<4C-Iy9{ zgK-HBb2YuX+L+?`W;v^b zL%d+*`c%G(5hP^8z?nw7oDD`LYqBBdn&ys29>Z#+AW~>(u>GRtfzx&%hzxSz|NQ2k z%Mg0W2P8`Xj~pchKH7l+@{@w&f(iwyswkvHTx(6lkgSU$$${{xl?487xwFeL?bv!H z=BWsn;xvg`*EKwpBU||)Qev~Q(_7A*mZ6gG}@&C_K+RXzP{qPWR#G>{Q0HY)<7eo12U+APiT)T zkeE#>2LnxH??m-y%#1+-{Cox@h?vFsgrEX^1gk?kin}-R;F9HJN&~T5C?2N-K2{|D zDehAN1QX&aNR`v@a9T;M|{$W52121B23}1{0ejA09n=jminjY&OMF=}y1wPD?T@K- zHsSbM;C>-mkz|uqol8pfootQM^lQGOu{PCdVim5B(#%vu7ifzYMTbWft>l~b1m=X{PW+a#u|EiLPa{G?BqMJ{5 zM|MB|S^4xrN@r8AJ9n!y*=ukA+YV zC1Bui5rro16@&3Vb;mzLJ#ScHx>x6=U~PD}7W_ha?;YMwNK;iao8xkz73O@6lEJI( zBFYZiH`70lkO33ZIjv5StWWW+h=JlKhx0I27#o0;Av4Kyhm(HmHtpvN3NCi9GYe&C zO4;tv)SBG9rev)hHOY0YpC~icnH{fj)b63c0Q#Nw{&Rj&VntOZ>y7!&IL}*= zm#MC|HZry&Dncg<9a#jHe<$~{JSrIwoSuURfwYBM_Mk4o}nkuSssO+q@1-?_Z_TB)PIuX5@P;}hMBKO zjyAl(L&;>8nM^j|aE`HEo*7*3Y&oLTvc5D(QU{S<`}!yhy)kS=1M$kd}LT^+Bc&%vQIxu4vccadA=a( zGt5`8>3qQjy24Cg@OppS@DSwF&V{>+^(taBcxq1~pj?%@!oNUUYIaiy+TK!}RBOuM zCJ|s^ci}r)wpkz)lcExZ7av2PD`T0vV|vtwF&g}W6rq~p!pBWB+=Bm+-)ayj$bbew zfc6YhDA=s0a_@KH7e3jV?UybRvUKWkKmlN}ff+{!5gCT7wyq5!ga|8>IQ$oNLAs`M z2!k-dVJ(>n#!!PRCJ+z^D>RHJ0I|(LaTtfhG_8rojf`Ei>?mi|fJ7zBomB;JqkDuz zlg$M*WMwe2O3b3h1w%?uG~#p3gdj4hS?|);L8GsS=Y7n^_~mZWD?8DtA#CJaMgrZJ zUBM)6Bt6oT!7Bb ze8)^h@7;%>NAm?QtUv8APc{Y_z4XLR!Hsr8ha`Dc!jYbEaYwryQU0^{+@3y$*?hm8 zTh<^v!fh`4Y|die*gIY$OEjt%OeteW<0r3rJ3m})0@t%EQwa0ULx2Gk*soL@JtE0M zkiNZ3&2ClJ+FDLI+gW!~T}Y`c`>B*4cKu@+9%0ICL3=Ohf6@I?VSMh6&m#_YJ%g%= zsYK((Cbp9VDxBG_fgR8J&Y7pH5EE)9A__OFlQME~2$3Gyh-JH;ULq-wUBR(r4go5Q z#Z`JIN9^Uj@BA&NLxT%5^}UlZ;_VHFy|*)Xg!)ox2U7g6e(h;AgkF9bswxxI?(n+5 z%spOqhRqcW8jK_uYq&&0sN$+15YC~j$)dfWng#tkyfXPHKuLl7 zzl`NCc}7UIrO52p4OhQ!HY@46L)518Pj`v&ic0@6DyPBUrg3MH}jm=vEuuS2)|}BV+U*e zA2ef^U*;(AY`@5ve&^=1zwjnaUv$#Xht0#d@VWY(*RJ96G$ud* z1AqY-7_W3I4HCx#p;;(a6<)VC+~Zu9lcr`}ahb%qQa7l6DBXjYnIIFPv?Z&8LR2ym zX%MqdC!;0<;r+CkYH*_L3S!+Y?Q>m#N%~%%(;baz$iRtdhve-LU~H*PAjkVB7WK)A z!KCv1i<)Bxm8<s{4A~9nW%KF|-!vaK6i&>1XR+Hwnb?DBbpfJ~4;u_$-TAgjPG2YDhzIT}cpZ9{2TZLBT)E5hq z3lQA8JG>;n^Wb62>!OjWl8J_z4$AdfG%rSelI5HS5m5S?McEnZ9&5K}T&I`5J~DRh zRDHSmev-2QsO)^GV=(#7$MEw9Du^`vJPYEOLk@*9q}iz~MIzV~^eL;bsZEnY`MQx( z^Xi|0jpw8)PArj~PBoO-N%SF221de%hU#vdN#Lc8v3u=bqnmP$!W#jzvW3f(y~}qd_@OJ@ zflf#?mXg>r;@iNl%0x{>3Xv+hbOJrb_N^W43b#npgh7#Y<43kSonk z@OdAv$M4+zl%){#54(I92kxDL8^&JxpWQsG(%g*OtU>%Abal`|)RJlX#SB~2;)@a<07qMKuANZ|#`$MIb0N~%3HX+Wb z=cwLqo?2%Gs(h$umAu600000007_t9vH8*D-8z40kFVWP&O5Yg&{I! z^}MvI&uHU{?-!cA-Re2bZcQYgjG*XT%0nwgi$+wGMQPs{@sv>H#F0xpP=;Gg_EHU& zLkSwv1nCB)y>6c>Cua2?JRveisgz7$$LyDhV~6Vbo^=i%Vb5{;j6KJ&xBTB9@pdyJ z&9znDTGjdHigiFT4F+^U0h9L`P&vaR>hfkeHL#pGMBpFr)8a@SgcdRcF$5oKJO2xr zeD4D>F@Tr{d4AJkc{i3+V$A=2(S8lf@PD-H%ggPEGe!U%3*j_@)fDnhS>IADkRtf+7ybTIoVGglVC+o9o*H@8p`rU;m8GL+GE4U)#J;*f;Mcv=LYt(19-QR7 z=QQB++P^c?+y4W$WVn7?hn2J{>|I;8Rjt(79NK?8%qrK+61N%vz9`(~Yb0}YOL0S_ zkE6phJDJq{enFxUl$debG!_R7mf57lA4c>|UPPv6BDP!rGgF7cQ9#~Q2;m>Gs$3a# zMO|Up`vc)MerNT^nJNwJc*?P_>3F*PpGo@szM2@$%aPBm{#MBU@|fpn(yHq}jPLuX z6au)1pNXKrmL#q-yd()g3+XXSp4` zTcjA$P^6NHz?7Q`UrwNi_IHtT(QpnmELz(;V!E-%9A-x+7om>4G|`3^rx)SBX|L*? z)5%d@RjZ_KoNe7h=GhzJx(6#{&kIy@3{-77wJ>!+oFwK7#CHk`{Z<0i7UUSr?B zd8Olh!^ZqyxMBK#g!O+}`ImL_zUk+_JL2CN;qf^9O?b!v0B`{m7_a7`*l18I3kkya z*F9Hxrja;KbriXJ=I-@5OqV6SueqUIn269%ArIv+Y%Hi~l??~w$wQy{VhP4jW9AZW z7L_Luxb}v0hDLfQ0@`y(fJD-wG?h9Og(37-K_wI%t}P*N0%ROZ)Ase}ihH+M_7N}x zSABX|7V5Od?YGg737x8tfgF>Ca%?~6#qFJgv3n;Gr)bZTBmm$-+DD(d7cBbEocxdW zdN!K-u_A*46?-xcTP*;sN%b1c7sq*S{+`TQYHph;F3;s$ySa8vApy}lcH-`+H9fJZ zKJBX}(jX6b=ACO#e;c*9+q#yn8MNdT`YTJ0dEQ*@U{l$@Aq(`i5q@Zcp=9=ZW9O*wh~;?VC=tHgw+ z<2pL$lxdnGE{p0T1JP=m@I@IB>W!qvKE$bL+aSlyFcma3N3tAaL_dK1?RSPivrp)BH@T37FYkSv?e78*nv%JqNkDhduxA z*)t<&(XO!59hK_eO41x{)O=IZo(-LhMF1rb#Kb=+%|rmQKmiE>CkPY>sGxrzcK_${ z7i1Bi(Y%Q61bVE4dXC4I$L|xBkrRel(FHeF>6PTa8OL;aT4(y^Lei|2>~D&)zIMbg zrw7{K8!wA2C*;;ShD!XzLrWdG2-HG9xWi9sFS-Zr`t4kJI99}8%QtsMH1K`yM^|x| zFt?Qeg=ITP@EGx_gI0>)!LR1>hSY;Z9BwK*RUwfaSZy-Y*TZGLKOKGQ?@n=g3u~Dk zygNrzWg*x9X82!k)Nbneye~`sU$Ooly!4;V_d5@B9v>@TlhtWn6Nmr+Z~+(?uQV$i z4#h#SK`2%@35A{AJLihJ=;}#0mCfq)>8p+8q|~|i%)pPxmLUwat2E@#7J&z%%TX<& zEe+^M_5EU@A8khWYw^!JT7!*vRBb!7@V~vxqjXTm@qd-M_`5mzY+#9^h0S#ILXT`TJC8V7s5|Vm?sy3ONu)A_T?8F#MfE;S2T)IB z!VY#OS4-gLiHT1`;{Ttyd!oLj90 zKO(THl~p@aNc23INFQtINc-jIR&fd~bLfe^1e`oaspaGS4s`~qpn;NgziWSY41R3y z;!HGuD)%N!jJD%!M+_GCjG7hqcDrsp;s%-uY;!^3lFzGEovKgea63p~SL?xO=sTYG z|Hkj%|08|(K2ezRJmTAO;~2H}MTXZ*<@z!kn&64!{tA5u2dgF{>9nNu`R~J!KgsbGdbr~atBsL_~VfZOz(o22t zlfc{fc?`#(B=qs(jz>^G*&4#0;BBMhS9s-7$*&>T?FGDA?z#4`UJ+@g*=y zdH~u$CBJ>=e3!N zME!PtXyXYdk}=KovVWV9W&0J{NAeQhA!o@|4qU;&!0S1w8K%E-cV9&H%f16VP52PL z{aHb+O=;nXEfq9!Ri<}e+W-BO$9Pa;p1!Y5^Au{ckDxihFKHQrml{#&(C+0u1ef>IX{D22DlxgOU8e*WDyuWhuAGK`wbgX zhlbdCmq*J1vqQZ2hcf-sv#hAu%HO z3ytrt%l0gS56Sr_BI>OD+x$mJ$h2zIe5Rdw=5BM~W}A;<@HJW+S0@1q?C6m|Ct7c+ z(zaYf8`z4(ngA+Nj`34&#-~z&c{#dE9F(4RR0`&9P8?K7CCCY+xIN~F%D-Xgf6>CF zAtGoL?usE&$?I6NthWq8;(=yUDMphr>ATj+#j1{fg4O)H&;IsvYS~;rKG1S}{#e~@ zQBkQU`pu@ys;;>#i0AUYJ@qN}z}=0X{Ws8-r`YsqlT%w#AOnB_5*V-4D>V+q0btOO zE*4~+-tMo4rLTNjIlW%INvTR?mu!7E1t>%?NpKD220{l!&x5)|Lh$Pl;JJ=7lK@8^ z8o(8-i1to{L}In!F~Zk5Is~}Rs3)iq5CYCkn7%Tg<4t{g@m$wak5BcLe|3#0kP!%7 zTsEKkw@>tuA`AT)(p45xQD#t1@ju&G6!chqRPCepgmI2F-h=TJ(`Fm`=~b98`c2En zpXaUDKR+G@Wrt_S&i098?A?zc1hYA-k&+?)xn{bvEozM|E|`oF6DaFYQj7jKH?&Iv zg3mhFnEi6E)>(Ts>PD~QX{y{gA_465&4QVN%UN8RV#}xb|ILU1)foA&>HNGwGAeUR z@#ktprNono5dCm~Ns|abfc+oBML#xSwlhX`IVO|sq@Xf~1fF|Z7dUgjQCp!TmXR1Iby)1K zpO8g2aQRH+6$AFQxM*PZHV7}a|Lq->5xsZNJxWd2%HN9+W?vu z(mRxhp%D;xLFN_+?~Jhy+I~|la#U)how#ZbDQgR=R8#iqJ%Yem)}(88Z}}7*q&zseH2E88Z?`!Nc{40GKtGkT1H z_7wVo?G~tknOrUj@CjiQ6pIEgAi0$Y`CCspbWKZh!!cja<2E&MBC!Yd=yMQCEcfv) zz_4IWL}KAsrPdw2fI<4NZg(VML<=j1Of>*q-augckLxte)=q8mat0NMJxO4>E1=*- z{4WO1!z2KDxfg%e0FpJ7?O#U(zN1l z2@%95gX7{>kO-+zT+1xqBqQ)Ij2kPm$VSH}-)jx`M7wQi%HUJPctIj{e-dW6xTHyN zT(R6zg5nOp5J$r;s>@%0WvMc0;BY!?9IL!sPP6W~ltX~{ues}853la=U0~Un(Nmx@ zFQnm=wAK51wK*W)GKD8r)r=sj!E_nuJV?FC7kguXldlcyOR3#db-@ENqVaGg$NXcOQ3HvNhD>- z@yJ+V2w3MPLxXqQ_v9vHIfl6%XLlc%kuaBzezB*;t7v_{_DasRY9dL(5J5D&wmd^t ze*z_Il0xd5J+-8Nf!~Bo?~f-47$C9h2%bSC9|2v~FOB;&o(d&Q6m`jYckL%{4?;qv zpy(bUoUW12gZvMUS=^hGMRv{SzBcPjg;fd3;Zh}u0m>VvAa*xFE zhVCK=->|ObrbRyLo2A2nv!no(6RI>W)Uzjo57ufv*tkI-R(X;Bl}P2 zejjS_ZpZp8$sS2e5NU+&OHkrg#?FnJOd0nv&0szOp+AT3`Jb%g4uhAg1gQ=mzpTRi z(9n)M1=Kblqx=eHA8g~`P{rWWD9IE(C)>Rbn3da6Wvh#dxU;eLYw=4d=9CwYBUI-6 zB)mVo+5{$45tfNr83YsIG3+ZC6KcbcBAi>zy_FG>yg#aa3-*1RD~m#VAL(+3X5+w< z5IDawV4xrn_qAD)3u24^gzZ_f*7b_WuqrwZyu$# zJAD`d7I&aVeAdc0r5X7hrQ2RCDg+*&CbIH2e;zhP`-wktUxe246L%JQg47$s8E{ zHYrHKB0A(b>VpnZrufe929N&i_j~oWdo_*Ec-;?Gsu&XagABtG4;-Y0n+0H0hPsu5 z^>N6mNt!{=I>L;n^TM8=%N?86-qCXJi%ysAj6azhYqtxImROlendYS#*U+emmx9x0y)IkkjR+#eZfsGq!$CAO zEuNYBu{y5|lDHdfZi`u=s;C8O60R` z;84h&m~XGs0>>@K*3yo(Psm5`{7+2ir*2D-a@AGZE(oPk!rkZ)w%oTQ=9T*KUpVOT zaz<^QPUQHe)Hy&bxa&mkFM;Bn<(0=OpQx_W_W2Vl?cB5Fjmm_TC^8Bw5<#>}&T-(q zgZ`gH@aquwg-apdT#~x$97kH$z6#~yrY7t5Xa&WS(OVCu|5296oc*k|Vjn96iXhQO z$*hwJT7xcgAH@BpYXbU{aXXs++osj&>F1_P@KN^OQb)|b*6=f>A6p9V`5~zUZadp=gU>YJB-`OJZh@83ao**wFuVvBG;xaOL7UhEj2-PhMj$Q%UB z3dKBbM4;Im?Clb7u|cJ~7mPYTvRzXZ%^L$~$4HDR7LmA^^~UR~DX1`ljwwK5!<94_ zPFIZlER&0L9UjAQUthn{Fd6rHxHQsf&m%+*~cEtzU#95_xy@e34|Sha~a&-K3# zNJoDIpJ%GIZehi8&N@`QM&X7@YZVgX0bwB2DJH5%2(4VZ{%iKV z&-8Ef|4NyvsN81pmo&G>Ao&xNjwUYLpDb@HkC%RUlvVP5xgTBcV%U5`_0QZ2&NZ)AVP~yeoy(0@_*>0Ow>cL{ev%YOMb}lzr08EpOr!h< zYDve;JqfW%DI}D8sLUL~(?X4m!{I&W;JYn~$nb@NJ6ep9s@C2h-jqQ@3i~~$f1g#A z4^pT1#1v1XZOdaEX#JD~ac6o^~gOxrv!dakUwZYH#oT?iTQy>X3S*FlU z(i-ig3K{qw#jJb+-VQ%zN607p?h0%K|H1U$jJU3l@=+58femdl16dgKh{_o%jiA*s zL%|3{awrs%GswW3GBfxbO48DHgzy96xO!?EdN9MP7e0Sg3o@^`P`SzIUzk$0+j$i*)%l7DH zk?DWeWNfZ|ib&6?nUhvUW4l2>g3SdI4$13MK?R%-4bw2$I2JHCE930g>BZ+)lbbJ1 zu&l)9oc1wpg-cpZlw~yelXE>L1ihBva55To-$7r9;i+_2fS+D~R9dYP>B!-0;HxU+ zI!k1t`7}<@NblYC)^n=r?fSm0c657dAMNe!#p3_~0N?=}Sg+=yLEJ0ns=l|zby8fP zzE-4ETNMhGlBz|uWS!&(pOlu#)*%#FOWTHaVy?)|*w)>(^6SUhW;C6Ykvsq_*$Lb4|Y<(|#+ zJ4Abv*{QFtG(SG^&*VU?>Rjw@$s~YaFPVqv-+&||{ld&K0HA_k5IBjrxcG$cB$Gvh zMkWwj(uu%__u?#R8P=@U-~!{2C(vW(;0KqCOhO_h**&9lJdnkAD$Rk|voLE>{r7}V zp!l8{`CM;1c)79s#p<3**_k}#R4=Mo#=yfWs<|#+L{*rNjpx3F%7E8=yYf^xeO~Z_GYJxpPI&S4B)q72FdKk@in? z@6Fw#T6c?;1SDFIU+z1CBM`e8bAJ zou-Ex1L90hqf*ebU$w1P?2UzncwQ`sD4`GxifQZm52Dx`j@I4J+<9edUfQmsD;!Q4 zj+26ZZ$f?8mLcD#Cg!GX7;o`LJcgsyHp?|xQLS(3npHs;#tr;3>NB!6REc)^XrCWy zf^i*9oWr+b$VY(qwLpt9G6bQ$k0L{-BrHnP-@N03Tg0aU#XV4#H19Z#{3tN1F_W`z zZ_DcW8dvQ2THYrs!s|PJtAXS6MjLnOTz~)rfB^QL`u&xD!f{`H>YA**^Uv{Ke;w1< zRiJh6ULf)5*Zw8SdMv#Vk7K`pQ76ZE1`QIR;3Q|pBgt?-W+$J!SwJPe&=J8zB@vsH zl8JVzStsPGp7`ZzAeizG>xpFT5y^0@u*a!(1{>G$#9-u7xvS!R*RztzVdx=t?MyE* z&HpV^1HGzY{j%>jJdxzPB3L{}8BaGgGm){l?isDT2J6MfdP<#aRg`xo<@2{LTo*)WzT$J8uA%2I<%5L zErIPUJbxY1mQ`7gBV+g;luT`gMy`~x7z(;qa;s@Jgr!&?xr`=$X^t%yXLtk| z617r;%XmVvLz5M6Ip(v%S0d8(>qQ;EaQ$hp8(hV)IfC})p2E)!Qxti8;d)Af9!yyY zF-T2?kYT8-KcJITPf6rDpz75+76e6k`mv%(jUsWkFw+%9=OjoQ_c7#m7871;$wn8k z5rSfI#byCXsG-@E=Q@4i01onr+HkvJDi?a$QPWw}Yht`LHpqC|@uauehlftA zG6U<fx;qjxNZF?9+QC1?$p59yP?$*o&;f`$4m?8)oF$5N) z6&4t*vx|(fjH)7c=Y`ebi*_}>qVzbLIv7^IaU6a5&2HVu?%zE6C-I#p_kYXf76r~g zRf%E=5-?`KY4ZEOf0Pt1#GZqH;^e^TUKur)11F1yr!(bN%@Bkc`?VNs|;f*_Y zF8xH663Ujgy(rsa=zb%XXv^9%@fxQ@#HqO-Du*6|FXMKQJ)4e%i3^oSy}l=l)f;cm z*q=zuH8~-e5cQ@s8JeT=>sp8Awo3*G3g@(6{rAOxLDj(xlc?_ZRx)wSy@W*+cIBp zRK?nql_^;da&7HZPRP^N6Ib)r!^%AyVS5w0I{KHvUT&KDVv9gxE#(`QZA&|vIH=KF z(5h8Z$*|o)8uL;srj~5z$lXTo{C791?q4$f3%Ph+CF}YPeNHZiF12-admaW^zyJU^ z0T>vsG%F1X*+GK1TwQLuxVHKC^Q+?ba;nm`{@>Dv`)_fTxR+y}_@IUsmUGU|lmpEj z#nxE}EIrXs7Bl(2I1h}Yd&~8FpWRQ7^6L7#weG>H2GPczr7VNmo@&Xw;RlOfI8 z?=a}i$B#o?3Jb-b*{KQi!X{2^%9LNf)l@heg4yMOntW!N*}|N+hRGz{&g)DV{>`J1dJF{E_E= zPqX=Z(LP6re;NMI^4wL$yqf5Fh^;mq-kQ}J!WE9van#aq;@A6ft}*?%ILnRrqyKH& ziwL+?MSRRI-XE9yLn%Rff7clC)@9G>DNVL>BsQx3u}+xH%TUR>t|6u|EiHl(u_Wu^ zNL0)b-CZRQ1V$?f%dADhBSI!lkwQMrjgMb$uOP!BQIQ-1L=hCST%%h`;5|Sk7||k+L|yRd*#yJ^!n*ntkh%bY@TDrL&~g zRB_tOrr09xixA~GnN2E7&D@GjZZmp_m%9G%y%Qu>y6C8trJ8rRr(EPWQuVeAtMOj8 zE+VH6ELtv0ZH;{rW*2`N=2Gsa%5^L#Ovznvvg~>}Un}1EJ3UXa{2hPMvrR9l4-1jA6cuWFmTyR6+@xveEX)Aro|MBnL;@l{waBY`@DAwQ1d zv6UFmsZQT?a5u`Z>{@}2Cx6u{!E-cV(_|K(4tvMX^xJv$cVSCG@!Fk9<~WMA|NpP zK@&kEGAC0J8;GE!MF@ar)lxKZT&tjVUm#DJa-KQ(UdGE>Nv@EC(yFseDh#`tr<(>+ z7tLE;vAd)Sx&@cTrCh26j|Rq#D4dHU5OB#rJ-pBF_$2dG%8TrY8|>I1WWZ!F$g=6Y zV3p~lO*Dd|P*5z@+Cw67R%#&F1e+L@3cXu&-yuqEnOK$vF&QKyLnuW_B9hzS$*1AI z97K@_%SkEJss@ueNIC_*B%37u+>B9@5lIxkXQ_k#hZo?3B&XKUU@IL{2*}&dZt_2? z^L6Fu(YG4@Gk)1=J~8O(b7qUEtIBRL+cMs=rM=EPr*w0wMwpZuq$+j#wWH%*Nhg-i zoKEvdTa-%V>2D)r7X&SwoJU{%_UIJ7d+h}o;#Zora6E&N3Sp+|QU(r&Ci$Dd;?&T`83Vb-aWq;Zd_2+>uFWbG-K z6B8bgs)?gc`(=1H(qj!8yO8C@sjR%h)L0I>tW7`zXYoe1Ared%md;F57B~=2t|Hop zR+g42o0qEU-8Hp4EX6$CNjg`bn<+M}H_jc=mnWNrct3Xig>Qra0Q`Ko_SQXLAL-A- z;n(T@|6hOh-_y|@_{S;$00#jc7_Kxcl?6isV<=oTe!aQWm+PLc?*E^*e~;^bt=?3r zbZ@=^S$qz~lSD~V`&)z4T0ausZj07Hbn}Etn!+*J_kQi0vCUg}KQC!ay#F^wUTO5I zYiOjzTzTpBHrK^MH%*{!noo@S%@_>*S)oYXOx^2ZlS%QceI|+|{*342{u)0_w62Wu z%JY8tV=hxEAQw9i0*Fm@$UYO&3UhC}AFiYm6Pj+GwCtmKNx<0DlqsrS9PWOc@BgZO zva6ojK8v0y@<3(=vO@tVvI(Mh@h5cJgw?&NZ;RFm9%(BMCkcdS!Lg%J5;62D;ABD# zGE7jg5?!YBQXmbEXRf0C)b?|U{8bJvJ)!}>`;z9@(iDM8qKey1xC@U$LT?`-&#Ls! zna?rr80v0C#4fyA<3{v5u~L+Bo~rvh#P`tqg z&cx3X@^TcSMp+<4prY2pBw!O+F4!bWzbU{MkCmkMJm?}}4Y(Zn+B7hE_VwxB1W983 z#I+NC^ob+3f<`3*fQb>Fb+vlw@TFOd=mS4Gdbws^J6q8DqMm-OdYh*^$i#ry{R#;fZv_h0(k)WOEEv3b(MYdChErmt;hnr%YOTz5@Iww!wpnWoK@6Kvs zt8tn{Zd=Oy;o2(N6;PY$leSlB2BTHLqY^Vn!_YDV6GHxsuD|%r1^Avck9>b z>goB{Z*SSr6FTkJZ!Q%y000gF5*V-LpuyOV7gHs;=QT4gFIxRy^NzA^d{Ax)hq;n7)b=@ zk|sC#Rkg;Nn7(E(g2X4jP4H~LrZEZb`dO>gq1k!=jeRKHD6ALmUrMx3s>sTLozO;A zd3j``qO@TohNdq<-DcN0SRKsSxcsjN7FJ}m>#7+P$s##KWs*^hMpcpo!%yUtt`fiw zf`~vsx@wVxOnkdM8PK!8mt&&KA$$*q0 zlA#7)W-mnpLJW7BFfuYG2Cf8yH`D>Pt8}w%s&ibqeg~ij2IGV;r(fG1>=Luq(Xy`v))&JGjT>#^2(ti zmduL!%y%<~argI+eBJe*xek9KL(7UH2n1uQRtAL7NU{{F@5KD}&+Z;q&Jp|%?U_T} zv3Q`kYqa`Bn@wyVHF9p?j#>^L`xsx{h>h9VGYd zsL!0@jN?OjqUcGFmiyd=>8N64X5s^j9;Fu*tF?}cv1{yR2SQnRaoNcG)PRI)HvOUNaISGvezuKADjm86b-)y z9Vk{j3Ui>%SS25u(AVtL>U<1imDYAzzoY4J^c%fv?YmDCuIzm_*v|j}0B`{c7_a7} zz??5QzaJRXW@_vCWWDfJ(kfDJDhjEgX&XC2B~gTsLT08$5#~_JV6<9FJX@q5T~eRz zc~qB4Y7xLc(YU2RXJ*K_z?o zta9t&y_+$Th!StTogm#ttf3HtGRG$<%E7zO+yAVxzQg!ygOOvWZU8bvAtQ&AFq=e_ z2=lP`R?^i8yu%7})m!^U$*`XfU*SmD>e65KCJQZ4!i3i6^ zMf~OtPreZ#v#`EeeI2H5B~J5vnmxh4Fz9D8eALF9YtMrf|GIyH{|=hwFC?lpOoV-&SbWp(ErCDhiQM72lrju*LYUo@D8Hezi^?$_zc7PY(t-U*Yq<47pR4s}SqE1FJdfyr;Tr zr~31k63%IMe-h>0Y5X3O@Ue%8-g!o$6(!BQi#&55V0SAXIi;k~|K^5VCkK{`!L3ny ze^u)u1Qd%g;W=yNM=y#&1$!d>AzR&X@VEb`VBWV_OWRvD!CmlA9oS?Bbo=ys~_Nq4OjU=a*j#wz8htnhx zxT#y&zQJX>6eU1k8lF3-&d|T#M1~Ep1N0gq_m82et=;Q&pP}1j<@g^9;&uByC+GUU z?~|d?)cP(E000~T5E!rJq%j;Wwd0EG$DQk4$={#xo19Vv0h2vN-!c&ZB!oCzpMs*!4{uywSN~TtR)m`+ zu<54*q8|^NEuLfSdLl6e!j&uhpCi`7%-=!gPQL@sHpZgET$S#mr8=rLd}jT>vGQW_ z2>PEPf~=WI_S_u*Jx~wUDeo(b>Mx~vK4;&Nz3lu!dC6OLSO>3ELH@TZdY7$zUyt}M zzU~S?-05GDQII|}!~IW{I&HAgtuG#Y+IPP&ca!%>VQ~ykLua^dsE<1dStDMv-4T&Z&cWm>7~jU2Tye`=17+!b zhJxg#^hZN(8(#2YVBI`fis#VyCB+_{Aetfjos&GOgL>5{z!@ z5PA%g`HdXw2M~65v7=cUUiP$QO9yW#hY>ei+$_6aX#yzGN3^?#R+eu6mOe?Q`0&D9 z-n~nxG!%ByTr1PSEi3lfBT;6fj-yj=C$81mUM8ml4#&+=(f8gidbsH6WUv4LZ~+(? zujZpcWG~zzO<^zbQ+Fbyo6@REGD=lUzK5t1ILPV`c0nTo;RBk}J{C+S**FqB?@eH2 ztwlQn1x3;scn#_-v-olYyO-DG;_H1GcLq{tEzE)@wEKtTc&gqX;?GFS|pQI>o^ImoD2j4S^?6@wlA8t5mR$t7>y-hV$_VvKJ)JUW>p_NH>Cv6 zqxz2#*|iuf)mDI%4_!ag-Ttxc_j&(fQbfV~F#M5&4t^gbTNRi%zm@s>X;;X8mp%o- zsk&Nt?7xlIo=cF)8fvd)^R8>UN5+5lA~M^2XNmq7=^6Uj^MYp&gYOyKIXI3Scn|zB z=rp7|s_{(?46wNd;m&-A(DmJ4=w8!?p`nEy;B3p0ZKnd&mSD4}YB5haSRX}b50SFR zlj{a9)0^CWK@RO+`ui4ynFEAW;@Ms6R{jaDfCgg4ZI7~MPjd82YgM+pg637cxi67? z4;@=}&N+Ta9M!@NHqpq@mW<6Ekr3-Ii*d(U%fIhDr&$9=bbly0_F?$!N5DGHOQEbk(LA58uQ;pH@s6v^kylGBjbOwC7w1OCd<-# z$M|7?)2#J)xPM{knf1>RCG?@^8AC9(F9Z}K49fNj>vR5vlAe0<^(U)kSB?7(b1QRRJ8N8db-ziqRd({OOCWB>r*0S*|i z=cF*4F1O}!^Iu4v_4m#cz42Ks6qPBJswqx{oPuV!Ld;x*Tu~`{;k36na_FVon_W~znFRVh>{xf-cBXd8DtcP z&G!YQOGz0ye{Z^l?U*2arP54+>UlkXsIJL$53_Qehj``5^pW?}cyHY?)odF?8B}uE zf64hx`FS_Kgg04MFBJcj|kaZ;`y*qDl_rlJo$63Qt9^E7YUoVz}sOEEef73Q6 z_pxiN)ng!}?Eq^3xMiQ}+_MMN<{TJgf;OpC8a`Ve;5oj82@(gaZ1H{qhYoz=^?IWb z;jzzaTfld5UDgg#yF=%jKbmpWtO#*Ue?`fapm`5}^vz$zkcCXeHUE2j5ho9beX5N1 z>Z9u$A6H#NmBE>{9(}TNhZuAZM{wSt^NBcrV(I_B?2-Q?l%XDDoT;q4&cgC+DxHH? zMEOjSE|0D}mNeYTHB1vlckkr9htx8h@chO6YWL_336ZpypD^M&M`!fJf4M8|$!$x* zMUpW~E)(M4pzcixRo?7{8|k*%yR#T2K*Si#LK9B|U7aVE$+@=Lp4e15eVfgB4SjcL z^oS(v`@k7i{Y9Yg_UkN_>Piv&e^$z(o*xmDxZ*PIQ?xXEgPm_qHala}LdIX-cNXew zH%z3cPNAxn#OpaQvJkG$F*0@X=(wqvwz_^+o*GJG<>4oBU+MKKMoO>|?dL33rK>4& zjTFgFfmvJS+Wt3dugCcOKI_r-K8xw)`JSUbFQ5NjM|-2l_8mIN000gF3>dHHqQP7) zrfRiUol)l3isGUrAYN4}DizSt<#adhld_}ekZ~=|Wbo}lo;UA|Z0?KV-mR;>6YdYsOY#n; z=@yyAJ|yTIxL`-G3XX*)x384n_udv2j1<)q1xX>fE;hV4Qqeqn#%H zdb-MwGPBz^{ZozEEPG`g=Eb#@*zUU=tX7vHHk~MD6fNyG=m!=!gAx8H#1wL^c;ZT9 zM9Wn%sHkkuV5JaM7GzDp+)Y@KN|>L(_)|dQ5Q%5;yIw~Vto|=Y<@0f|^*<8+hd)QB z>wI5z_B<}vhlPLu1AqYt7_a7}u%s?EzPx;7nf(2}_!pX_Ow~myDx_5k=vr_H5_JzS zn^!L^m?GHl2poiq55e4x6GYB)KGBES@4UMfM3)#DGohZT7VQ4`BIUYKmk4Y9GH-_Z_MpqU_!8SPwSS7ID8h z%JsN2hISUK=3Q0cyJl&@Ln?#%Uy8xQ{abhrekZJZVzMOo46089{dM8!n-iTSQERGl zG`GPa|E5OSqHb>z&OF!C*To-z1FqS42bp*0UGs1LT`$?&HdZIs{&Pa-v0uUe%H@6MaZ(RnhdE$+|oaOVSJMDE^z#dnT99KE1KB(r=@2{8d@jyjQFv z#ky*jSoAB3`HcBy@!K*_a8v#Y>z+C$VyZPik93zxW_{^$>W*60{Krc=?ie57JAVu4 ztBPUJ@}L|J2#ezu_YH2#)bzT}!yk3-9uuqX|0C!8 zUXSjcx5@H;uj<-yxBvhg0T39k=AqbVa7PQ;=YNl?_fL&0H>;^CB}7zI+^Hy`z}`7h zc>sur$UqFrfh7g?uzCY7R^*Xv%tW?&(yfnys6~`WlvXkR9;G${Z%ro>xj2p`U}R)R zCr2(p`a*YN;I|1=Q7j$O6fgwsyo^b;819AI3i(f%fBJiBVS}lBg*uCpdEdq);!5xL zzjK83x5<7}jW#K~Tca>=)=HkpbZQ6SO`8$bWsgt-x9# zvq4PQ;_q+tL41emT_HsSRQ$F(m}9$qTj!1z_VF)v?El)(pqIz4ci$1RIVzdj_T3H* z7vdEFCsIp6;|Qj;+F3l5IDGPv-hF4l^Ot4xzlVChf26EX!KdMA3p_c$(=?p%zU&H8 z>vJ=Oc`7h(T?+PQaXkqBjA{Z}A`<6U?w_r$k!njF9F`WDqU@A;F$ zZY{G7^)7k(M>W_SrzA~$Yp^EU)|`M-{UfJz%|;hH@5On)dVSZ(`KIC0oNY4?oxO+A zDejB+nvZYIk^7E8OQ9nDpL1195G(X6kZK%u9VBOFf67>E{>|2PS`&DFB~fx(1p-%i zfQ4RB;TWz0-c?dc#h>z9Ek%1gZ$P2ww0SBG6&{+%mIWa^({U`ZF~gT-YF3-%971H( zc`b)7r|uO>^7I})nNF=$!?asDNn>vZT8CF(LT(yWFsD}>mGA9v zQe>ermi$IAN|w`bT-QTmK$a&(N!8m)Fc2b{o*OLeFXtm!Xo?ltD~H}|{BKRk^r}({eIKq@H-EY`TZUTFOdKM0B`{e7_a4|!0a!kwV!(Tuix$Wt<2E6ytKC+vTfzPIJ0L}oZF<>D?#vMr<;Y39liTIQkRFWwxiQNj7Eu{k zqIGmuydp(U@9G3#)UN#L3wFs60yV>fT|L7wP>uf{Wuh zuXKD{@&#EFZ+bBht0Hu{5CEA6ieP|@w?~}%zS88*QS+wHEF0MWBgaK*27Lw$&cN}@ zOb`+TEXaQ2p#vMq;m2KW|4-4n`yh_e9$B4&>B@wIUFe{}s%TN25TkLu>y0ktY z_@9N)`j0GLy6+J72~>nk4lkjLLIYP(1)_^U2k5+y-E&NFZv*Ze$J(4jxa*h55TESY$E|LC4x zq^<9fmb%v>zj$n8-`PWopLVDwc2)Dpn{}&9Xv^>9 z>FE~cp1Qtk9qtDv7lvwU4}#=;KV9pB^gnF>CduG5E0VqTwdGzVRrhP4|6MN*yVv}< zrf~Xdb$XvT({L>9FAe4lf(MSN(Y@X1K6kX|R_V4v^mcwduGb+m7z)Kpcx=Cn)LZ8~ zdTfRxuXxpVdj3xHyyn<#TMH9cak!Wa-w-U3wWvU$w8*|3o&y|4mTZw>*^&qrQ3yH0b+J|@A_9dgMkXmACHhI!5)8O2 z53Xu5W44xv#x#_NS`K~hLe5ecOjOwp@{2N5H{+Xm>33O2SfAI(mKFKPLxy^QY>w;P$ z^Skp;*FX{iMZiBnCBeI4h>bl7@DiU2rv%&v799qYWJr{21 zjSH#1d)R)>+dUieH7L0+Tg-^&k|8f*_7~c?=rMg?oe9}Ky}&iUV0}V~1us#C-W4M< zCn)W^)0pgw3r#3w0CP_hnDmI{P@3Sx;nxKuQl$<->_RJ?c+zyFf6xiK>7 zTluwtB|;a{rTEJyw*|A?mng1V|KEdz?AZs1E{R#$OM@>$pX7+n@jEOC$2@`vbZE2j zxwN$H`M!xX?GcvMM8 z$X%*FC-1%_;mZ_edHp|>PKo%B+;0vyw}pE6g%DZ%cS%W%7MXc#`+gRB=&6$beb$`KrOUR$H@Vcv>?LhK3$X)4<(dc_V>Mz%&fc8$x z-WDI)r|@>a81Uci@X82zf=BDW?sT2!L6*@yGx$&D@%+W>*HJI}>Sh-+{X^Dsf%tC%QG5W{u z_^&wi-wmHb;JL2<%uxgVBbaklQ(^W+#TfB~m#ZnosCMYO2)+xJAf@fFbF*?49;ikK8@0DE}3@iP=P_yAY(CMD1Rx?{vt#IuOPe{lmuO?un+H{b|H4FuO#bgMRIBb1 zAw~onO!W}GL!|V_u4q1&SevlP@9?BF`LbF(`7Flo9u$SF84QeaI1JGk_`)=*kyr^+ zgid@yLxxS#Z7|CWp;wu0@c6bq4i0s^zw|h-rsVLE2!8cI3+|Sf|7uKO{ikAkr&fAJ zWFHRvPG#sTO?l&1TYOtlmrZ`3)G}WGvTHG2p!rWt+&vcA?41|OHX*u0eqk9wYcGEr ztF(Xg;Tbf4T%LTF*sNjw-Z95pGoR=&^!CZ20e1#m1x&8#)_OZfYm9(xYxzt!j20^) za-WLxryFd()7)Q?-CSg1 z0QZ;Ydt16aU+NWzfAaKM6T))-?)P7E;yTUq_v}aPjmR?t&U@~>Q(RF8jaeKCY2WoB3pVw`^jocH~U%q{cZt#C3(%kpB_Rfd3@OD?t zgYP}d;#l$B;@6+7_be8@nq(@a(b|0#uM@4w>027i#!TEPhSu$I@~y9!dW6b8Li2nQ0S`YgGuZsgpf-GdZk?l62`ttjhzQj z6wS7Pksv`NND!4QQ8GwQN)#kWT0oLW4hzCAEEyyUNDw4SP(YT9WF$+J{3AIoIp-y_ z3vX8M|L%SF-Fo#-)mP`t>7H|@d%7sLr)Qe`&|o4F_-JP|Jm5}Z4~;I9q(hTQ73HsLP~=^!k`VzU3VXa~a8F4Hk|d_pCvpHuFLvUa zxsmpxTTjSeZ}fyzX-#gru&3=pIrBQ}Gi?d4;UUB8#p@yvQ?!r#d;9hAuQW{ukoa#^_X=e)h&7eC7u4J< zwy74Pq886s>X#){UodvkGhPO^^R{_wWXGroZ>Q`M7>gu1rR{$pTsrqkctZ_dd%G-B zCn`ZCy zk_*$(&%&m4o&w)#-=8??OeNs1OB^>)!YYTDWpm1#`e_xmSSUckCa;vrcRHHl4PH%W zxPNysT&ibUpBR0=H>J6JHoNv~!Dp9mb<5qz-3M)2pLe;}e40kuyyM5fkouMJ?;-m2 zr|I-qnRC}Y0e#Mwg;*aG;%HRU2+xA^gQ5ik+m;q*3#thD;KvEAUhrK*`gc!piNbl_ zNf@N(y0qMBdr=0n%$c`QS+*dHdm1pnQk$cHOLsj>=yT5#HbonAa&ZsX)FZN)OfJM+ zY1cBYp(n1XlR(Lk(8E{0VNz#BO}Fx-zsD%4u@r|KA($@dvv)SJqU$m0FZvCXY)6G^D>$xSm-i;0CX)^Y^N1GgFf!|vnxsOsc;GO$ajDyzbyn)Zq z(D73q&5YRF5?Y=|$9F85Cgu~OWS|It{pFeAgrKGG*NRu#m3(=+xiOrG!V8}QFgqtc z;TWpF|Niu=Ch;kQyCo_CVXvi!%=Ky#w_*O%BQh_yNyS%5I~Pk0S>)6k^eUU$!&bo$ zZQuhRT@J(e)e*N;LD-|da#wrC}y1YkA30hBg^YLh{^E~_ra&V zRGxS8f3dI5{`T!f0(*|8c(L~;x}KZ1$s4louruQkBaN6q z^=x=GTF#ppf02UfKSiZwgx7S{A!uEq_rNyIaT8kLka5Gs&YNCbK}cG#V`QqxT-dKN zR*)@|)b;HK#}lGJSI)&lX2)j>$?kc`=eKz!9wzBINkV2z^pXVDyv+{$a_MBdcYZFMMh0vL2=WI@*i3P&GDR&rKyRD`6S5!Hh@#_MrrStg1#p z>fHD7C3+j{lLq^;wE?TmI;YPw4t z6qcF1P6~=hPj-YRkMsA^w)t927Ah1wjoM!MPWVS&C0)2mU8pG=y)h>6a!~L7QT~vf zkSwUD)O7wwq*I`DN=Na8Om^qNBz97qclJg?p4-f6e7oAxd35}SPxf{5t0Ei&$sl)` z$Q0b2<$9>U*$^yq_QY%f1!usCsi3}jKl?o!g7S)e?Cbu9j42vr`e4ly`NO@MV>pqG ze&9n6=NYXd+TY!ODx16cEzV`-3+{Pe{l^g5Ec+?t0mpRMLt9hDSr9RbSm&d7<|jb~ zS7QW%+cTRw=Aq2(-WfS`G$f{R*y~>7inKC;!Fbth(vw>iO>}6t_~cE*Xl>KoE7f;JV+FDIIfg0Kpo_ z$X`PGa^uS)q1CAy9qy8|;?EToYp#&8z>xwacYItqI}|5@6R9rtRbR#J_q`KSV+h_4 z%-xEgZ{5bDJ{a%T zRDB(pv<=!NWCg|H^eN#XwtZ^%Z9X@oe$aT`LLdBeE@^Wr-MJJkzjEQ<<>EDvRO?-% ziQI#t`F3W_Vs@{V22M*hGH52Su;ww_*Dq%MNVdFtYm+Z(w{Co~eA(1G_xTsftO6=Q zp$X6C5!+qSu6;oo?l%-z*K1~=EH=(2Gi%behcMQo?ZM;kgt3Nyu@Jc4!;LSDVy{*q z=4quNOx!ty>@OpM@w*+zgH6-7K&&oD6}82(@SL4QI(jA`5Lft_AdwvwN3f~_%j4wI z_>t}IwK9eEx}Y03ib5iCc=}rCcQZhj^N&KW2+ zmDciuStkEClZMfuIHxS9q&QCX6(mOQx=h(SF0OgnPHDq3j--Q;R5H*+Z`4pxY)kS0e z(h4>8fsw|HO5w!Fm98)MF$2%%?bx|rip^sg7KdVu{m2-r`%GKHFCVE*--`$cC<5EV zD>+0xKKv{hsS^=jt4yBSE-5v9)c1Jf?NMW0bgyQ2O}>4`;}$Y!q7&4Nq9VUn_1bJ! zp^m6Dl?X(68mGGlR3B^xuZEs8Am05#Ml|6sF=syG9KNeR@XHM{;1E!-fTjX4eF}`>3yntf5#Nb zIoFzBlcbLdl_?F{tM_LJIjr(+B+*=Rvf4YB)->!slOZ+l9r}AVYAK7uwn+Kbp!lk_ zeidlfKK^{55LQr-(5a^iYtMc(OE~LBV1FOiR)dM`6iQV{);<%b0Cv7(lKQONwh|d7 zOaPr#{AjgOBG{7S0k&u6rW)x^_p5i^O_MxokKQD1ZoJ}Kb1N8=BSAzAzL;FFIm%H8 zhl-1dk;y#RA2|X&z&TF{e13!J+lQ#P@-f$8+nK>BLCGGx$!<2THV{h!%aS>EUUFLc zk5QD%w-mm)67+Of^aLl#n7V6*9RO>J@z11h99`E!5xQM>CTdiT>55BH-7?w5!G7Y-MDmk-3Pf)+yrtk`xODm`mm*}H=B zF+wEw6^KfnDo`|3ICR`mW2?n&lIpEE%yZk;afXswE|u`M=0qEwM;Bg-Xu(b1%%+4H zehG7oE^oXwJG2K{^)FKpM?jsOf;P5k?lJIdUnoudBJD>X7<$L8%N_OY@(9>O-o!Wd z-83TnZW}w}?z9!-cCdP?i`&92(oZvm` zVs+Dk6m8^Cyyr|Ro^Lj`W7e^gixy#F{ z5MungyD#;eMdi|7f-K_b7W_^6yx?HCMpy_3iP(F(=ijGcAkocPt0{lIxP~*0v-SmW zT4vO=Ma$M&_=(Kmyt|PMThglg@Rh$P#fYnvNdoW@88OT+dSI_`+gK27h)Qe0FXhG5 zo&lm!)wfByA331B?aS*qy&A)!iWD5r7^z~nmAUyIPZ54SnC zjtd8tv#$)!)rD~=A$+$rNFVMy!va=oi{l|MeJRtEeyviAA{YJ>>gWb45rW3}Y~Azv z@nhTq(vYq0`-c1U8JY23$&P%T5_N`*E!t>$!;!3d~v>*)r#y&FLh&5Fl-moO$$Pcfqc=wDclSH2` z=ShX9VoSv-Z0_{Y+D_*u(`s$kA%By{j!mi@(llYyH7*q}n8^)Gso+wOx_5(!>-eY^a6aD=dp4M2uW~8C z$ySj)()ZDVTnbS%5Xfw`G8a-T-+ktto+Y0dp3ugf9HGaNx(}!`oO!-rn`yeAA-E)c`&dF->NKee$}gE;nN-rf(29CM ze875}btqPa{u*MHLS!+Z0~O*|YKKUxS3N%?)u~7^>BoT zYtN6+R`(?FKzZt@b*{ISN#doq_j>CRTkCw<>Wta)18p~pi(R}Saj#6LW_UR32|?Y( zbd5;Ev2Mt?db#%F%Z$+hKb>3}x6|1*lS32S&mmDBK|!=b07)NLsN z%nLoh91mvg5udo2nG~p5zoek3pY^3>a&S+$D%Y2Up9<|vskCT)wjieH2w-oPh$LvV z8|hC~A{G!rE9C}P3PCLF*%j%xZ?4ZZ9)@Mf`5r>o;-dq_(o&d>8G*O3G$M=)yLz78 zz*Uobs`ppkTtDcjpEgeoZ}J;GDV>|ItXu?LmdAXp7dJ6YC11A4k#j&~@{Ez(Yd5(Z zhMoD4l(sGZ^p&Z<_+`u6xyQI&w{YqN;YJs?$?H}r|mGy zo-;()KSBEJKC1pkyeVjjZ?F@W=bwGgT}c!FXs6Lb0c$_*+_vn}%)tXb^)ot~&d+;(mOedd&Gggp;Ihiab4po(QU8o3|74#T27}`a z-mg_uj>5OKtf}W`7=9f%fg8AAuX0^4jma0-moEG?yOy_Q9QID=B8MEQhWYr(b2>8d zH8Pc+(=Uuv8^sW9o7_v?FS!b?Y?8!ZjqRpsexU9h!ybfqEjj*86Sn&4-InsG9#4?$ z2F<1T&ouC}Ruk!=iY3|6i`9+5BlH7Sb)i$HsHNr}bl-8+0qt?W;N3DmE~gjmt3`~v zaR>?o$&jRYB30+d)B8?fx_8h-$;tfc;z>6VIGe;lXar*xW6~DoKDC!jt%$E}`rNns zN(rV;D;3mxxa)BDo~nZI8hVst?-RqbPsfGc-3APe^uPK|1%k-r|MFJMRu3(D;y97| z+3jrIXsx-l9qw4d5J*cWBH=tHB_Lp_t|vw$2`%C-Z2uSvfn#4-z3^KXIz27;I#TaV z)4=9b_>|j&KwBA2)6uB?RtTw z*Zw))v$&TdYB+ZZkH>}tiRZUsbQeClRt=M1>8ZNPR;)__3<0sje%sR#&* z_2`ieej#dn2W{8Kbwh-1-xYN z`!-;dkQKxdf-!Ic0s@!;jSFbXCzMz;fNT9Xcz_3+{M}7J-2nO-iv|LP{yqh;F=2Iq zlK_Rq0i?IU7=U2vVaj;_p)tp7f9xBUKs_(O{#$_aF#)iB0Kx%E0I~pb0ipn20(=Gd z0#FK&2T%my2>|57HUX#ts0ILHVHX1A02BaV>XidvVip6u2LSHN@0b;U#^l4~3I)KN zR|5bB0J<6*cvy*@2!Qbc;|kja02tH1b78IrXahE|n*R4S!T^mq9|~X&fXRo+jo}S| z9{{F26CfA>=)d1(n0lDFkpP%Byavbyz|;!^!1Mv8P9*>)7RJW(0j3V7U6}j<02u(5 z0GPf2`U!K7EHL8=BbdJD#=?T$2lR!TB^0O!8f${y{;}%o+TiWd$ zAwbKp8h8K!G%eW5+zC@+;SAgJpTpjllLD0 literal 0 HcmV?d00001 diff --git a/packages/datadog-plugin-openai/test/hal.png b/packages/datadog-plugin-openai/test/hal.png new file mode 100644 index 0000000000000000000000000000000000000000..abbd7997c3db71a7f56d9993c16be1f4a18d85d7 GIT binary patch literal 45350 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 literal 0 HcmV?d00001 diff --git a/packages/datadog-plugin-openai/test/hello-friend.m4a b/packages/datadog-plugin-openai/test/hello-friend.m4a new file mode 100644 index 0000000000000000000000000000000000000000..68a679cd89ce18374605686adf2ffdcafba67576 GIT binary patch literal 69766 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 fzxeUriC~L- { + let openai + let clock + let metricStub + let externalLoggerStub + + describe('openai', () => { + withVersions('openai', 'openai', version => { + beforeEach(() => { + require('../../dd-trace') + }) + + before(() => { + return agent.load('openai') + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + clock = sinon.useFakeTimers() + const { Configuration, OpenAIApi } = require(`../../../versions/openai@${version}`).get() + + const configuration = new Configuration({ + apiKey: 'sk-DATADOG-ACCEPTANCE-TESTS' + }) + + openai = new OpenAIApi(configuration) + + metricStub = sinon.stub(DogStatsDClient.prototype, '_add') + externalLoggerStub = sinon.stub(ExternalLogger.prototype, 'log') + sinon.stub(Sampler.prototype, 'isSampled').returns(true) + }) + + afterEach(() => { + clock.restore() + sinon.restore() + }) + + describe('createCompletion()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/completions') + .reply(200, { + 'id': 'cmpl-7GWDlQbOrAYGmeFZtoRdOEjDXDexM', + 'object': 'text_completion', + 'created': 1684171461, + 'model': 'text-davinci-002', + 'choices': [{ + 'text': 'FOO BAR BAZ', + 'index': 0, + 'logprobs': null, + 'finish_reason': 'length' + }], + 'usage': { 'prompt_tokens': 3, 'completion_tokens': 16, 'total_tokens': 19 } + }, [ + 'Date', 'Mon, 15 May 2023 17:24:22 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '349', + 'Connection', 'close', + 'openai-model', 'text-davinci-002', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '442', + 'openai-version', '2020-10-01', + 'x-ratelimit-limit-requests', '3000', + 'x-ratelimit-limit-tokens', '250000', + 'x-ratelimit-remaining-requests', '2999', + 'x-ratelimit-remaining-tokens', '249984', + 'x-ratelimit-reset-requests', '20ms', + 'x-ratelimit-reset-tokens', '3ms', + 'x-request-id', '7df89d8afe7bf24dc04e2c4dd4962d7f' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createCompletion') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/completions') + + expect(traces[0][0].meta).to.have.property('component', 'openai') + expect(traces[0][0].meta).to.have.property('openai.api_base', 'https://api.openai.com/v1') + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'text-davinci-002') + expect(traces[0][0].meta).to.have.property('openai.request.prompt', 'Hello, ') + expect(traces[0][0].meta).to.have.property('openai.request.stop', 'time') + expect(traces[0][0].meta).to.have.property('openai.request.suffix', 'foo') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.finish_reason', 'length') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.logprobs', 'returned') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.text', 'FOO BAR BAZ') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'text-davinci-002') + expect(traces[0][0].meta).to.have.property('openai.user.api_key', 'sk-...ESTS') + expect(traces[0][0].metrics).to.have.property('openai.request.best_of', 2) + expect(traces[0][0].metrics).to.have.property('openai.request.echo', 0) + expect(traces[0][0].metrics).to.have.property('openai.request.frequency_penalty', 0.11) + expect(traces[0][0].metrics).to.have.property('openai.request.logit_bias.50256', 30) + expect(traces[0][0].metrics).to.have.property('openai.request.logprobs', 3) + expect(traces[0][0].metrics).to.have.property('openai.request.max_tokens', 7) + expect(traces[0][0].metrics).to.have.property('openai.request.n', 1) + expect(traces[0][0].metrics).to.have.property('openai.request.presence_penalty', -0.1) + expect(traces[0][0].metrics).to.have.property('openai.request.temperature', 1.01) + expect(traces[0][0].metrics).to.have.property('openai.request.top_p', 0.9) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.completion_tokens', 16) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.prompt_tokens', 3) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.total_tokens', 19) + }) + + const result = await openai.createCompletion({ + model: 'text-davinci-002', + prompt: 'Hello, ', + suffix: 'foo', + max_tokens: 7, + temperature: 1.01, + top_p: 0.9, + n: 1, + stream: false, + logprobs: 3, + echo: false, + stop: 'time', + presence_penalty: -0.1, + frequency_penalty: 0.11, + best_of: 2, + logit_bias: { '50256': 30 }, + user: 'hunter2' + }) + + expect(result.data.id).to.eql('cmpl-7GWDlQbOrAYGmeFZtoRdOEjDXDexM') + + await checkTraces + + clock.tick(10 * 1000) + + const expectedTags = [ + 'org:kill-9', + 'endpoint:/v1/completions', + 'model:text-davinci-002', + 'error:0' + ] + + expect(metricStub).to.have.been.calledWith('openai.request.duration', 0, 'd', expectedTags) + expect(metricStub).to.have.been.calledWith('openai.tokens.prompt', 3, 'd', expectedTags) + expect(metricStub).to.have.been.calledWith('openai.tokens.completion', 16, 'd', expectedTags) + expect(metricStub).to.have.been.calledWith('openai.tokens.total', 19, 'd', expectedTags) + + expect(metricStub).to.have.been.calledWith('openai.ratelimit.requests', 3000, 'g', expectedTags) + expect(metricStub).to.have.been.calledWith('openai.ratelimit.tokens', 250000, 'g', expectedTags) + expect(metricStub).to.have.been.calledWith('openai.ratelimit.remaining.requests', 2999, 'g', expectedTags) + expect(metricStub).to.have.been.calledWith('openai.ratelimit.remaining.tokens', 249984, 'g', expectedTags) + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createCompletion', + prompt: 'Hello, ', + choices: [ + { + text: 'FOO BAR BAZ', + index: 0, + logprobs: null, + finish_reason: 'length' + } + ] + }) + }) + }) + + describe('createEmbedding()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/embeddings') + .reply(200, { + 'object': 'list', + 'data': [{ + 'object': 'embedding', + 'index': 0, + 'embedding': [-0.0034387498, -0.026400521] + }], + 'model': 'text-embedding-ada-002-v2', + 'usage': { + 'prompt_tokens': 2, + 'total_tokens': 2 + } + }, [ + 'Date', 'Mon, 15 May 2023 20:49:06 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '75', + 'access-control-allow-origin', '*', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '344', + 'openai-version', '2020-10-01' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createEmbedding') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/embeddings') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.input', 'Cat?') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'text-embedding-ada-002') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'text-embedding-ada-002-v2') + expect(traces[0][0].metrics).to.have.property('openai.response.embeddings_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.embedding.0.embedding_length', 2) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.prompt_tokens', 2) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.total_tokens', 2) + }) + + const result = await openai.createEmbedding({ + model: 'text-embedding-ada-002', + input: 'Cat?', + user: 'hunter2' + }) + + expect(result.data.model).to.eql('text-embedding-ada-002-v2') + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createEmbedding', + input: 'Cat?' + }) + + await checkTraces + }) + }) + + describe('listModels()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/models') + .reply(200, { + 'object': 'list', + 'data': [ + { + 'id': 'whisper-1', + 'object': 'model', + 'created': 1677532384, + 'owned_by': 'openai-internal', + 'permission': [{ + 'id': 'modelperm-KlsZlfft3Gma8pI6A8rTnyjs', + 'object': 'model_permission', + 'created': 1683912666, + 'allow_create_engine': false, + 'allow_sampling': true, + 'allow_logprobs': true, + 'allow_search_indices': false, + 'allow_view': true, + 'allow_fine_tuning': false, + 'organization': '*', + 'group': null, + 'is_blocking': false + }], + 'root': 'whisper-1', + 'parent': null + }, + { + 'id': 'babbage', + 'object': 'model', + 'created': 1649358449, + 'owned_by': 'openai', + 'permission': [{ + 'id': 'modelperm-49FUp5v084tBB49tC4z8LPH5', + 'object': 'model_permission', + 'created': 1669085501, + 'allow_create_engine': false, + 'allow_sampling': true, + 'allow_logprobs': true, + 'allow_search_indices': false, + 'allow_view': true, + 'allow_fine_tuning': false, + 'organization': '*', + 'group': null, + 'is_blocking': false + }], + 'root': 'babbage', + 'parent': null + } + ] }, [ + 'Date', 'Mon, 15 May 2023 23:26:42 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '63979', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '164' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'listModels') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/models') + + expect(traces[0][0].metrics).to.have.property('openai.response.count', 2) + // Note that node doesn't accept a user value + }) + + const result = await openai.listModels() + + expect(result.data.object).to.eql('list') + expect(result.data.data.length).to.eql(2) + + await checkTraces + }) + }) + + describe('retrieveModel()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/models/gpt-4') + .reply(200, { + 'id': 'gpt-4', + 'object': 'model', + 'created': 1678604602, + 'owned_by': 'openai', + 'permission': [{ + 'id': 'modelperm-ffiDrbtOGIZuczdJcFuOo2Mi', + 'object': 'model_permission', + 'created': 1684185078, + 'allow_create_engine': false, + 'allow_sampling': false, + 'allow_logprobs': false, + 'allow_search_indices': false, + 'allow_view': false, + 'allow_fine_tuning': false, + 'organization': '*', + 'group': null, + 'is_blocking': false + }], + 'root': 'gpt-4', + 'parent': 'stevebob' + }, [ + 'Date', 'Mon, 15 May 2023 23:41:40 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '548', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '27' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'retrieveModel') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/models/*') + + // expect(traces[0][0].meta).to.have.property('openai.response.permission.group', null) + expect(traces[0][0].meta).to.have.property('openai.request.id', 'gpt-4') + expect(traces[0][0].meta).to.have.property('openai.response.owned_by', 'openai') + expect(traces[0][0].meta).to.have.property('openai.response.parent', 'stevebob') + expect(traces[0][0].meta).to.have.property('openai.response.permission.id', + 'modelperm-ffiDrbtOGIZuczdJcFuOo2Mi') + expect(traces[0][0].meta).to.have.property('openai.response.permission.organization', '*') + expect(traces[0][0].meta).to.have.property('openai.response.root', 'gpt-4') + expect(traces[0][0].metrics).to.have.property('openai.response.permission.allow_create_engine', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.allow_fine_tuning', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.allow_logprobs', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.allow_sampling', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.allow_search_indices', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.allow_view', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.created', 1684185078) + expect(traces[0][0].metrics).to.have.property('openai.response.permission.is_blocking', 0) + }) + + const result = await openai.retrieveModel('gpt-4') + + expect(result.data.id).to.eql('gpt-4') + + await checkTraces + }) + }) + + describe('createEdit()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/edits') + .reply(200, { + 'object': 'edit', + 'created': 1684267309, + 'choices': [{ + 'text': 'What day of the week is it, Bob?\n', + 'index': 0 + }], + 'usage': { + 'prompt_tokens': 25, + 'completion_tokens': 28, + 'total_tokens': 53 + } + }, [ + 'Date', 'Tue, 16 May 2023 20:01:49 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '172', + 'Connection', 'close', + 'openai-model', 'text-davinci-edit:001', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '920', + 'openai-version', '2020-10-01', + 'x-ratelimit-limit-requests', '20', + 'x-ratelimit-remaining-requests', '19', + 'x-ratelimit-reset-requests', '3s', + 'x-request-id', 'aa28029fd9758334bcead67af867e8fc' + + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createEdit') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/edits') + + expect(traces[0][0].meta).to.have.property('openai.request.input', 'What day of the wek is it?') + expect(traces[0][0].meta).to.have.property('openai.request.instruction', 'Fix the spelling mistakes') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'text-davinci-edit-001') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.text', + 'What day of the week is it, Bob?\\n') + expect(traces[0][0].metrics).to.have.property('openai.request.n', 1) + expect(traces[0][0].metrics).to.have.property('openai.request.temperature', 1.00001) + expect(traces[0][0].metrics).to.have.property('openai.request.top_p', 0.999) + expect(traces[0][0].metrics).to.have.property('openai.response.choices_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.created', 1684267309) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.completion_tokens', 28) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.prompt_tokens', 25) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.total_tokens', 53) + }) + + const result = await openai.createEdit({ + 'model': 'text-davinci-edit-001', + 'input': 'What day of the wek is it?', + 'instruction': 'Fix the spelling mistakes', + 'n': 1, + 'temperature': 1.00001, + 'top_p': 0.999, + 'user': 'hunter2' + }) + + expect(result.data.choices[0].text).to.eql('What day of the week is it, Bob?\n') + + clock.tick(10 * 1000) + + await checkTraces + + const expectedTags = [ + 'org:kill-9', + 'endpoint:/v1/edits', + 'model:text-davinci-edit:001', + 'error:0' + ] + + expect(metricStub).to.be.calledWith('openai.ratelimit.requests', 20, 'g', expectedTags) + expect(metricStub).to.be.calledWith('openai.ratelimit.remaining.requests', 19, 'g', expectedTags) + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createEdit', + input: 'What day of the wek is it?', + instruction: 'Fix the spelling mistakes', + choices: [{ + text: 'What day of the week is it, Bob?\n', + index: 0 + }] + }) + }) + }) + + describe('listFiles()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/files') + .reply(200, { + 'object': 'list', + 'data': [{ + 'object': 'file', + 'id': 'file-foofoofoo', + 'purpose': 'fine-tune-results', + 'filename': 'compiled_results.csv', + 'bytes': 3460, + 'created_at': 1684000162, + 'status': 'processed', + 'status_details': null + }, { + 'object': 'file', + 'id': 'file-barbarbar', + 'purpose': 'fine-tune-results', + 'filename': 'compiled_results.csv', + 'bytes': 13595, + 'created_at': 1684000508, + 'status': 'processed', + 'status_details': null + }] + }, [ + 'Date', 'Wed, 17 May 2023 21:34:04 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '25632', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '660' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'listFiles') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/files') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].metrics).to.have.property('openai.response.count', 2) + }) + + const result = await openai.listFiles() + + expect(result.data.data.length).to.eql(2) + expect(result.data.data[0].id).to.eql('file-foofoofoo') + + await checkTraces + }) + }) + + describe('createFile()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/files') + .reply(200, { + 'object': 'file', + 'id': 'file-268aYWYhvxWwHb4nIzP9FHM6', + 'purpose': 'fine-tune', + 'filename': 'dave-hal.jsonl', + 'bytes': 356, + 'created_at': 1684362764, + 'status': 'uploaded', + 'status_details': 'foo' // dummy value for testing + }, [ + 'Date', 'Wed, 17 May 2023 22:32:44 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '216', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '1021' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createFile') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/files') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + + expect(traces[0][0].meta).to.have.property('openai.request.filename', 'dave-hal.jsonl') + expect(traces[0][0].meta).to.have.property('openai.request.purpose', 'fine-tune') + expect(traces[0][0].meta).to.have.property('openai.response.purpose', 'fine-tune') + expect(traces[0][0].meta).to.have.property('openai.response.status', 'uploaded') + expect(traces[0][0].meta).to.have.property('openai.response.status_details', 'foo') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'file-268aYWYhvxWwHb4nIzP9FHM6') + expect(traces[0][0].meta).to.have.property('openai.response.filename', 'dave-hal.jsonl') + expect(traces[0][0].metrics).to.have.property('openai.response.bytes', 356) + expect(traces[0][0].metrics).to.have.property('openai.response.created_at', 1684362764) + }) + + const result = await openai.createFile(fs.createReadStream( + Path.join(__dirname, 'dave-hal.jsonl')), 'fine-tune') + + expect(result.data.filename).to.eql('dave-hal.jsonl') + + await checkTraces + }) + }) + + describe('deleteFile()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .delete('/v1/files/file-268aYWYhvxWwHb4nIzP9FHM6') + .reply(200, { + 'object': 'file', + 'id': 'file-268aYWYhvxWwHb4nIzP9FHM6', + 'deleted': true + }, [ + 'Date', 'Wed, 17 May 2023 23:03:54 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '83', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'deleteFile') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'DELETE') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/files/*') + + expect(traces[0][0].meta).to.have.property('openai.request.file_id', 'file-268aYWYhvxWwHb4nIzP9FHM6') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'file-268aYWYhvxWwHb4nIzP9FHM6') + expect(traces[0][0].metrics).to.have.property('openai.response.deleted', 1) + }) + + const result = await openai.deleteFile('file-268aYWYhvxWwHb4nIzP9FHM6') + + expect(result.data.deleted).to.eql(true) + + await checkTraces + }) + }) + + describe('retrieveFile()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/files/file-fIkEUgQPWnVXNKPJsr4pEWiz') + .reply(200, { + 'object': 'file', + 'id': 'file-fIkEUgQPWnVXNKPJsr4pEWiz', + 'purpose': 'fine-tune', + 'filename': 'dave-hal.jsonl', + 'bytes': 356, + 'created_at': 1684362764, + 'status': 'uploaded', + 'status_details': 'foo' // dummy value for testing + }, [ + 'Date', 'Wed, 17 May 2023 23:14:02 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '240', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '18' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'retrieveFile') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/files/*') + + expect(traces[0][0].meta).to.have.property('openai.request.file_id', 'file-fIkEUgQPWnVXNKPJsr4pEWiz') + expect(traces[0][0].meta).to.have.property('openai.response.filename', 'dave-hal.jsonl') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'file-fIkEUgQPWnVXNKPJsr4pEWiz') + expect(traces[0][0].meta).to.have.property('openai.response.purpose', 'fine-tune') + expect(traces[0][0].meta).to.have.property('openai.response.status', 'uploaded') + expect(traces[0][0].meta).to.have.property('openai.response.status_details', 'foo') + expect(traces[0][0].metrics).to.have.property('openai.response.bytes', 356) + expect(traces[0][0].metrics).to.have.property('openai.response.created_at', 1684362764) + }) + + const result = await openai.retrieveFile('file-fIkEUgQPWnVXNKPJsr4pEWiz') + + expect(result.data.filename).to.eql('dave-hal.jsonl') + + await checkTraces + }) + }) + + describe('downloadFile()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/files/file-t3k1gVSQDHrfZnPckzftlZ4A/content') + .reply(200, '{"prompt": "foo?", "completion": "bar."}\n{"prompt": "foofoo?", "completion": "barbar."}\n', [ + 'Date', 'Wed, 17 May 2023 23:26:01 GMT', + 'Content-Type', 'application/octet-stream', + 'Transfer-Encoding', 'chunked', + 'Connection', 'close', + 'content-disposition', 'attachment; filename="dave-hal.jsonl"', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '128' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'downloadFile') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/files/*/content') + + expect(traces[0][0].meta).to.have.property('openai.request.file_id', 'file-t3k1gVSQDHrfZnPckzftlZ4A') + expect(traces[0][0].metrics).to.have.property('openai.response.total_bytes', 88) + }) + + const result = await openai.downloadFile('file-t3k1gVSQDHrfZnPckzftlZ4A') + + /** + * TODO: Seems like an OpenAI library bug? + * downloading single line JSONL file results in the JSON being converted into an object. + * downloading multi-line JSONL file then provides a basic string. + * This suggests the library is doing `try { return JSON.parse(x) } catch { return x }` + */ + expect(result.data[0]).to.eql('{') // raw JSONL file + + await checkTraces + }) + }) + + describe('createFineTune()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/fine-tunes') + .reply(200, { + 'object': 'fine-tune', + 'id': 'ft-10RCfqSvgyEcauomw7VpiYco', + 'hyperparams': { + 'n_epochs': 5, + 'batch_size': 3, + 'prompt_loss_weight': 0.01, + 'learning_rate_multiplier': 0.1 + }, + 'organization_id': 'org-COOLORG', + 'model': 'curie', + 'training_files': [{ + 'object': 'file', + 'id': 'file-t3k1gVSQDHrfZnPckzftlZ4A', + 'purpose': 'fine-tune', + 'filename': 'dave-hal.jsonl', + 'bytes': 356, + 'created_at': 1684365950, + 'status': 'processed', + 'status_details': null + }], + 'validation_files': [], + 'result_files': [], + 'created_at': 1684442489, + 'updated_at': 1684442489, + 'status': 'pending', + 'fine_tuned_model': 'huh', + 'events': [{ + 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Created fine-tune: ft-10RCfqSvgyEcauomw7VpiYco', + 'created_at': 1684442489 + }] + }, [ + 'Date', 'Thu, 18 May 2023 20:41:30 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '898', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '116' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createFineTune') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.id', 'org-COOLORG') // no name just id + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/fine-tunes') + + expect(traces[0][0].meta).to.have.property('openai.request.classification_positive_class', 'wat') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'curie') + expect(traces[0][0].meta).to.have.property('openai.request.suffix', 'deleteme') + expect(traces[0][0].meta).to.have.property('openai.request.training_file', + 'file-t3k1gVSQDHrfZnPckzftlZ4A') + expect(traces[0][0].meta).to.have.property('openai.request.validation_file', 'file-foobar') + expect(traces[0][0].meta).to.have.property('openai.response.fine_tuned_model', 'huh') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'ft-10RCfqSvgyEcauomw7VpiYco') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'curie') + expect(traces[0][0].meta).to.have.property('openai.response.status', 'pending') + expect(traces[0][0].metrics).to.have.property('openai.request.batch_size', 3) + expect(traces[0][0].metrics).to.have.property('openai.request.classification_betas_count', 3) + expect(traces[0][0].metrics).to.have.property('openai.request.classification_n_classes', 1) + expect(traces[0][0].metrics).to.have.property('openai.request.compute_classification_metrics', 0) + expect(traces[0][0].metrics).to.have.property('openai.request.learning_rate_multiplier', 0.1) + expect(traces[0][0].metrics).to.have.property('openai.request.n_epochs', 4) + expect(traces[0][0].metrics).to.have.property('openai.request.prompt_loss_weight', 0.01) + expect(traces[0][0].metrics).to.have.property('openai.response.created_at', 1684442489) + expect(traces[0][0].metrics).to.have.property('openai.response.events_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.batch_size', 3) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.learning_rate_multiplier', 0.1) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.n_epochs', 5) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.prompt_loss_weight', 0.01) + expect(traces[0][0].metrics).to.have.property('openai.response.result_files_count', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.training_files_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.updated_at', 1684442489) + expect(traces[0][0].metrics).to.have.property('openai.response.validation_files_count', 0) + }) + + // only certain request parameter combinations are allowed, leaving unused ones commented for now + const result = await openai.createFineTune({ + training_file: 'file-t3k1gVSQDHrfZnPckzftlZ4A', + validation_file: 'file-foobar', + model: 'curie', + n_epochs: 4, + batch_size: 3, + learning_rate_multiplier: 0.1, + prompt_loss_weight: 0.01, + compute_classification_metrics: false, + suffix: 'deleteme', + classification_n_classes: 1, + classification_positive_class: 'wat', + classification_betas: [0.1, 0.2, 0.3] + // validation_file: '', + }) + + expect(result.data.id).to.eql('ft-10RCfqSvgyEcauomw7VpiYco') + + await checkTraces + }) + }) + + describe('retrieveFineTune()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/fine-tunes/ft-10RCfqSvgyEcauomw7VpiYco') + .reply(200, { + 'object': 'fine-tune', + 'id': 'ft-10RCfqSvgyEcauomw7VpiYco', + 'hyperparams': { + 'n_epochs': 4, + 'batch_size': 3, + 'prompt_loss_weight': 0.01, + 'learning_rate_multiplier': 0.1 + }, + 'organization_id': 'org-COOLORG', + 'model': 'curie', + 'training_files': [{ + 'object': 'file', + 'id': 'file-t3k1gVSQDHrfZnPckzftlZ4A', + 'purpose': 'fine-tune', + 'filename': 'dave-hal.jsonl', + 'bytes': 356, + 'created_at': 1684365950, + 'status': 'processed', + 'status_details': null + }], + 'validation_files': [], + 'result_files': [{ + 'object': 'file', + 'id': 'file-bJyf8TM0jeSZueBo4jpodZVQ', + 'purpose': 'fine-tune-results', + 'filename': 'compiled_results.csv', + 'bytes': 410, + 'created_at': 1684442697, + 'status': 'processed', + 'status_details': null + }], + 'created_at': 1684442489, + 'updated_at': 1684442697, + 'status': 'succeeded', + 'fine_tuned_model': 'curie:ft-foo:deleteme-2023-05-18-20-44-56', + 'events': [ + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Created fine-tune: ft-10RCfqSvgyEcauomw7VpiYco', + 'created_at': 1684442489 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune costs $0.00', + 'created_at': 1684442612 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune enqueued. Queue number: 0', + 'created_at': 1684442612 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune started', + 'created_at': 1684442614 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 1/4', + 'created_at': 1684442677 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 2/4', + 'created_at': 1684442677 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 3/4', + 'created_at': 1684442678 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 4/4', + 'created_at': 1684442679 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Uploaded model: curie:ft-foo:deleteme-2023-05-18-20-44-56', + 'created_at': 1684442696 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Uploaded result file: file-bJyf8TM0jeSZueBo4jpodZVQ', + 'created_at': 1684442697 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune succeeded', + 'created_at': 1684442697 } + ] }, [ + 'Date', 'Thu, 18 May 2023 22:11:53 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '2727', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '51' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'retrieveFineTune') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.id', 'org-COOLORG') // no name just id + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/fine-tunes/*') + + expect(traces[0][0].meta).to.have.property('openai.request.fine_tune_id', 'ft-10RCfqSvgyEcauomw7VpiYco') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'ft-10RCfqSvgyEcauomw7VpiYco') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'curie') + expect(traces[0][0].meta).to.have.property('openai.response.status', 'succeeded') + expect(traces[0][0].metrics).to.have.property('openai.response.created_at', 1684442489) + expect(traces[0][0].metrics).to.have.property('openai.response.events_count', 11) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.batch_size', 3) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.learning_rate_multiplier', 0.1) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.n_epochs', 4) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.prompt_loss_weight', 0.01) + expect(traces[0][0].metrics).to.have.property('openai.response.result_files_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.training_files_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.updated_at', 1684442697) + expect(traces[0][0].metrics).to.have.property('openai.response.validation_files_count', 0) + }) + + const result = await openai.retrieveFineTune('ft-10RCfqSvgyEcauomw7VpiYco') + + expect(result.data.id).to.eql('ft-10RCfqSvgyEcauomw7VpiYco') + + await checkTraces + }) + }) + + describe('listFineTunes()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/fine-tunes') + .reply(200, { + 'object': 'list', + 'data': [{ + 'object': 'fine-tune', + 'id': 'ft-10RCfqSvgyEcauomw7VpiYco', + 'hyperparams': { + 'n_epochs': 4, + 'batch_size': 3, + 'prompt_loss_weight': 0.01, + 'learning_rate_multiplier': 0.1 + }, + 'organization_id': 'org-COOLORG', + 'model': 'curie', + 'training_files': [{ + 'object': 'file', + 'id': 'file-t3k1gVSQDHrfZnPckzftlZ4A', + 'purpose': 'fine-tune', + 'filename': 'dave-hal.jsonl', + 'bytes': 356, + 'created_at': 1684365950, + 'status': 'processed', + 'status_details': null + }], + 'validation_files': [], + 'result_files': [{ + 'object': 'file', + 'id': 'file-bJyf8TM0jeSZueBo4jpodZVQ', + 'purpose': 'fine-tune-results', + 'filename': 'compiled_results.csv', + 'bytes': 410, + 'created_at': 1684442697, + 'status': 'processed', + 'status_details': null + }], + 'created_at': 1684442489, + 'updated_at': 1684442697, + 'status': 'succeeded', + 'fine_tuned_model': 'curie:ft-foo:deleteme-2023-05-18-20-44-56' + }] + }) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'listFineTunes') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/fine-tunes') + + expect(traces[0][0].metrics).to.have.property('openai.response.count', 1) + }) + + const result = await openai.listFineTunes() + + expect(result.data.object).to.eql('list') + + await checkTraces + }) + }) + + describe('listFineTuneEvents()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .get('/v1/fine-tunes/ft-10RCfqSvgyEcauomw7VpiYco/events') + .reply(200, { + 'object': 'list', + 'data': [ + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Created fine-tune: ft-10RCfqSvgyEcauomw7VpiYco', + 'created_at': 1684442489 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune costs $0.00', + 'created_at': 1684442612 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune enqueued. Queue number: 0', + 'created_at': 1684442612 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune started', + 'created_at': 1684442614 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 1/4', + 'created_at': 1684442677 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 2/4', + 'created_at': 1684442677 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 3/4', + 'created_at': 1684442678 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Completed epoch 4/4', + 'created_at': 1684442679 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Uploaded model: curie:ft-foo:deleteme-2023-05-18-20-44-56', + 'created_at': 1684442696 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Uploaded result file: file-bJyf8TM0jeSZueBo4jpodZVQ', + 'created_at': 1684442697 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune succeeded', + 'created_at': 1684442697 } + ] }, [ + 'Date', 'Thu, 18 May 2023 22:47:17 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '1718', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '33' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'listFineTuneEvents') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.method', 'GET') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/fine-tunes/*/events') + + expect(traces[0][0].meta).to.have.property('openai.request.fine_tune_id', 'ft-10RCfqSvgyEcauomw7VpiYco') + expect(traces[0][0].metrics).to.have.property('openai.response.count', 11) + }) + + const result = await openai.listFineTuneEvents('ft-10RCfqSvgyEcauomw7VpiYco') + + expect(result.data.object).to.eql('list') + + await checkTraces + }) + }) + + describe('deleteModel()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .delete('/v1/models/ft-10RCfqSvgyEcauomw7VpiYco') + .reply(200, { // guessing on response format here since my key lacks permissions + 'object': 'model', + 'id': 'ft-10RCfqSvgyEcauomw7VpiYco', + 'deleted': true + }, [ + 'Date', 'Thu, 18 May 2023 22:59:08 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '152', + 'Connection', 'close', + 'access-control-allow-origin', '*', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '23' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'deleteModel') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.request.method', 'DELETE') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/models/*') + + expect(traces[0][0].meta).to.have.property('openai.request.fine_tune_id', 'ft-10RCfqSvgyEcauomw7VpiYco') + expect(traces[0][0].metrics).to.have.property('openai.response.deleted', 1) + expect(traces[0][0].meta).to.have.property('openai.response.id', 'ft-10RCfqSvgyEcauomw7VpiYco') + }) + + const result = await openai.deleteModel('ft-10RCfqSvgyEcauomw7VpiYco') + + expect(result.data.deleted).to.eql(true) + + await checkTraces + }) + }) + + describe('cancelFineTune()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/fine-tunes/ft-TVpNqwlvermMegfRVqSOyPyS/cancel') + .reply(200, { + 'object': 'fine-tune', + 'id': 'ft-TVpNqwlvermMegfRVqSOyPyS', + 'hyperparams': { + 'n_epochs': 4, + 'batch_size': 3, + 'prompt_loss_weight': 0.01, + 'learning_rate_multiplier': 0.1 + }, + 'organization_id': 'org-COOLORG', + 'model': 'curie', + 'training_files': [{ + 'object': 'file', + 'id': 'file-t3k1gVSQDHrfZnPckzftlZ4A', + 'purpose': 'fine-tune', + 'filename': 'dave-hal.jsonl', + 'bytes': 356, + 'created_at': 1684365950, + 'status': 'processed', + 'status_details': null + }], + 'validation_files': [], + 'result_files': [], + 'created_at': 1684452102, + 'updated_at': 1684452103, + 'status': 'cancelled', + 'fine_tuned_model': 'idk', + 'events': [ + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Created fine-tune: ft-TVpNqwlvermMegfRVqSOyPyS', + 'created_at': 1684452102 }, + { 'object': 'fine-tune-event', + 'level': 'info', + 'message': 'Fine-tune cancelled', + 'created_at': 1684452103 } + ] }, [ + 'Date', 'Thu, 18 May 2023 23:21:43 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '1042', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-processing-ms', '78' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'cancelFineTune') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.id', 'org-COOLORG') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/fine-tunes/*/cancel') + + expect(traces[0][0].meta).to.have.property('openai.request.fine_tune_id', 'ft-TVpNqwlvermMegfRVqSOyPyS') + expect(traces[0][0].meta).to.have.property('openai.response.fine_tuned_model', 'idk') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'ft-TVpNqwlvermMegfRVqSOyPyS') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'curie') + expect(traces[0][0].meta).to.have.property('openai.response.status', 'cancelled') + expect(traces[0][0].metrics).to.have.property('openai.response.created_at', 1684452102) + expect(traces[0][0].metrics).to.have.property('openai.response.events_count', 2) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.batch_size', 3) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.learning_rate_multiplier', 0.1) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.n_epochs', 4) + expect(traces[0][0].metrics).to.have.property('openai.response.hyperparams.prompt_loss_weight', 0.01) + expect(traces[0][0].metrics).to.have.property('openai.response.result_files_count', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.training_files_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.updated_at', 1684452103) + expect(traces[0][0].metrics).to.have.property('openai.response.validation_files_count', 0) + }) + + const result = await openai.cancelFineTune('ft-TVpNqwlvermMegfRVqSOyPyS') + + expect(result.data.id).to.eql('ft-TVpNqwlvermMegfRVqSOyPyS') + + await checkTraces + }) + }) + + if (semver.intersects(version, '3.0.1')) { + describe('createModeration()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/moderations') + .reply(200, { + 'id': 'modr-7HHZZZylF31ahuhmH279JrKbGTHCW', + 'model': 'text-moderation-001', + 'results': [{ + 'flagged': true, + 'categories': { + 'sexual': false, + 'hate': false, + 'violence': true, + 'self-harm': false, + 'sexual/minors': false, + 'hate/threatening': false, + 'violence/graphic': false + }, + 'category_scores': { + 'sexual': 0.0018438849, + 'hate': 0.069274776, + 'violence': 0.74101615, + 'self-harm': 0.008981651, + 'sexual/minors': 0.00070737937, + 'hate/threatening': 0.045174375, + 'violence/graphic': 0.019271193 + } + }] + }, [ + 'Date', 'Wed, 17 May 2023 19:58:01 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '450', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '419' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createModeration') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/moderations') + + expect(traces[0][0].meta).to.have.property('openai.request.input', 'I want to harm the robots') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'text-moderation-stable') + expect(traces[0][0].meta).to.have.property('openai.response.id', 'modr-7HHZZZylF31ahuhmH279JrKbGTHCW') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'text-moderation-001') + expect(traces[0][0].metrics).to.have.property('openai.response.categories.sexual', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.categories.hate', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.categories.violence', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.categories.self-harm', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.categories.sexual/minors', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.categories.hate/threatening', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.categories.violence/graphic', 0) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.hate', 0.069274776) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.violence', 0.74101615) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.sexual', 0.0018438849) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.hate', 0.069274776) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.violence', 0.74101615) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.self-harm', 0.008981651) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.sexual/minors', + 0.00070737937) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.hate/threatening', + 0.045174375) + expect(traces[0][0].metrics).to.have.property('openai.response.category_scores.violence/graphic', + 0.019271193) + expect(traces[0][0].metrics).to.have.property('openai.response.flagged', 1) + }) + + const result = await openai.createModeration({ + input: 'I want to harm the robots', + model: 'text-moderation-stable' + }) + + expect(result.data.results[0].flagged).to.eql(true) + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createModeration', + input: 'I want to harm the robots' + }) + + await checkTraces + }) + }) + } + + if (semver.intersects(version, '3.1')) { + describe('createImage()', () => { + let scope + + beforeEach(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/images/generations') + .reply(200, { + 'created': 1684270747, + 'data': [{ + 'url': 'https://oaidalleapiprodscus.blob.core.windows.net/private/org-COOLORG/user-FOO/img-foo.png', + 'b64_json': 'foobar===' + }] + }, [ + 'Date', 'Tue, 16 May 2023 20:59:07 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '545', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '5085' + ]) + }) + + afterEach(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call using a string prompt', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createImage') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/images/generations') + + expect(traces[0][0].meta).to.have.property('openai.request.prompt', 'A datadog wearing headphones') + expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'url') + expect(traces[0][0].meta).to.have.property('openai.request.size', '256x256') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.images.0.url', + 'https://oaidalleapiprodscus.blob.core.windows.net/private/org-COOLORG/user-FOO/img-foo.png') + expect(traces[0][0].meta).to.have.property('openai.response.images.0.b64_json', 'returned') + expect(traces[0][0].metrics).to.have.property('openai.request.n', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.created', 1684270747) + expect(traces[0][0].metrics).to.have.property('openai.response.images_count', 1) + }) + + const result = await openai.createImage({ + prompt: 'A datadog wearing headphones', + n: 1, + size: '256x256', + response_format: 'url', + user: 'hunter2' + }) + + expect(result.data.data[0].url.startsWith('https://')).to.be.true + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createImage', + prompt: 'A datadog wearing headphones' + }) + + await checkTraces + }) + + it('makes a successful call using an array of tokens prompt', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0].meta).to.have.property('openai.request.prompt', '[999, 888, 777, 666, 555]') + }) + + const result = await openai.createImage({ + prompt: [999, 888, 777, 666, 555], + n: 1, + size: '256x256', + response_format: 'url', + user: 'hunter2' + }) + + expect(result.data.data[0].url.startsWith('https://')).to.be.true + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createImage', + prompt: [ 999, 888, 777, 666, 555 ] + }) + + await checkTraces + }) + + it('makes a successful call using an array of string prompts', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0].meta).to.have.property('openai.request.prompt.0', 'foo') + expect(traces[0][0].meta).to.have.property('openai.request.prompt.1', 'bar') + }) + + const result = await openai.createImage({ + prompt: ['foo', 'bar'], + n: 1, + size: '256x256', + response_format: 'url', + user: 'hunter2' + }) + + expect(result.data.data[0].url.startsWith('https://')).to.be.true + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createImage', + prompt: [ 'foo', 'bar' ] + }) + + await checkTraces + }) + + it('makes a successful call using an array of tokens prompts', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0].meta).to.have.property('openai.request.prompt.0', '[111, 222, 333]') + expect(traces[0][0].meta).to.have.property('openai.request.prompt.1', '[444, 555, 666]') + }) + + const result = await openai.createImage({ + prompt: [ + [111, 222, 333], + [444, 555, 666] + ], + n: 1, + size: '256x256', + response_format: 'url', + user: 'hunter2' + }) + + expect(result.data.data[0].url.startsWith('https://')).to.be.true + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createImage', + prompt: [ [ 111, 222, 333 ], [ 444, 555, 666 ] ] + }) + + await checkTraces + }) + }) + + describe('createImageEdit()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/images/edits') + .reply(200, { + 'created': 1684850118, + 'data': [{ + 'url': 'https://oaidalleapiprodscus.blob.core.windows.net/private/org-COOLORG/user-FOO/img-bar.png', + 'b64_json': 'fOoF0f=' + }] + }, [ + 'Date', 'Tue, 23 May 2023 13:55:18 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '549', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '9901' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createImageEdit') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/images/edits') + + expect(traces[0][0].meta).to.have.property('openai.request.mask', 'hal.png') + expect(traces[0][0].meta).to.have.property('openai.request.image', 'hal.png') + expect(traces[0][0].meta).to.have.property('openai.request.prompt', 'Change all red to blue') + expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'url') + expect(traces[0][0].meta).to.have.property('openai.request.size', '256x256') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.images.0.url', + 'https://oaidalleapiprodscus.blob.core.windows.net/private/org-COOLORG/user-FOO/img-bar.png') + expect(traces[0][0].meta).to.have.property('openai.response.images.0.b64_json', 'returned') + expect(traces[0][0].metrics).to.have.property('openai.request.n', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.created', 1684850118) + expect(traces[0][0].metrics).to.have.property('openai.response.images_count', 1) + }) + + const result = await openai.createImageEdit( + fs.createReadStream(Path.join(__dirname, 'hal.png')), + 'Change all red to blue', + fs.createReadStream(Path.join(__dirname, 'hal.png')), + 1, + '256x256', + 'url', + 'hunter2' + ) + + expect(result.data.data[0].url.startsWith('https://')).to.be.true + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createImageEdit', + prompt: 'Change all red to blue', + file: 'hal.png', + mask: 'hal.png' + }) + + await checkTraces + }) + }) + + describe('createImageVariation()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/images/variations') + .reply(200, { + 'created': 1684853320, + 'data': [{ + 'url': 'https://oaidalleapiprodscus.blob.core.windows.net/private/org-COOLORG/user-FOO/img-soup.png', + 'b64_json': 'foo=' + }] + }, [ + 'Date', 'Tue, 23 May 2023 14:48:40 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '547', + 'Connection', 'close', + 'openai-version', '2020-10-01', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '8411' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createImageVariation') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/images/variations') + + expect(traces[0][0].meta).to.have.property('openai.request.image', 'hal.png') + expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'url') + expect(traces[0][0].meta).to.have.property('openai.request.size', '256x256') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.images.0.url', + 'https://oaidalleapiprodscus.blob.core.windows.net/private/org-COOLORG/user-FOO/img-soup.png') + expect(traces[0][0].meta).to.have.property('openai.response.images.0.b64_json', 'returned') + expect(traces[0][0].metrics).to.have.property('openai.request.n', 1) + expect(traces[0][0].metrics).to.have.property('openai.response.created', 1684853320) + expect(traces[0][0].metrics).to.have.property('openai.response.images_count', 1) + }) + + const result = await openai.createImageVariation( + fs.createReadStream(Path.join(__dirname, 'hal.png')), 1, '256x256', 'url', 'hunter2') + + expect(result.data.data[0].url.startsWith('https://')).to.be.true + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createImageVariation', + file: 'hal.png' + }) + + await checkTraces + }) + }) + } + + if (semver.intersects(version, '3.2')) { + describe('createChatCompletion()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/chat/completions') + .reply(200, { + 'id': 'chatcmpl-7GaWqyMTD9BLmkmy8SxyjUGX3KSRN', + 'object': 'chat.completion', + 'created': 1684188020, + 'model': 'gpt-3.5-turbo-0301', + 'usage': { + 'prompt_tokens': 37, + 'completion_tokens': 10, + 'total_tokens': 47 + }, + 'choices': [{ + 'message': { + 'role': 'assistant', + 'content': "In that case, it's best to avoid peanut", + 'name': 'hunter2' + }, + 'finish_reason': 'length', + 'index': 0 + }] + }, [ + 'Date', 'Mon, 15 May 2023 22:00:21 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '327', + 'access-control-allow-origin', '*', + 'openai-model', 'gpt-3.5-turbo-0301', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '713', + 'openai-version', '2020-10-01' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createChatCompletion') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/chat/completions') + + expect(traces[0][0].meta).to.have.property('openai.request.0.content', 'Peanut Butter or Jelly?') + expect(traces[0][0].meta).to.have.property('openai.request.0.name', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.request.0.role', 'user') + expect(traces[0][0].meta).to.have.property('openai.request.1.content', 'Are you allergic to peanuts?') + expect(traces[0][0].meta).to.have.property('openai.request.1.role', 'assistant') + expect(traces[0][0].meta).to.have.property('openai.request.2.content', 'Deathly allergic!') + expect(traces[0][0].meta).to.have.property('openai.request.2.role', 'user') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'gpt-3.5-turbo') + expect(traces[0][0].meta).to.have.property('openai.request.stop', 'time') + expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.finish_reason', 'length') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.message.content', + "In that case, it's best to avoid peanut") + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.message.role', 'assistant') + expect(traces[0][0].meta).to.have.property('openai.response.choices.0.message.name', 'hunter2') + expect(traces[0][0].meta).to.have.property('openai.response.model', 'gpt-3.5-turbo-0301') + expect(traces[0][0].metrics).to.have.property('openai.request.logit_bias.1234', -1) + expect(traces[0][0].metrics).to.have.property('openai.request.max_tokens', 10) + expect(traces[0][0].metrics).to.have.property('openai.request.n', 3) + expect(traces[0][0].metrics).to.have.property('openai.request.presence_penalty', -0.0001) + expect(traces[0][0].metrics).to.have.property('openai.request.temperature', 1.001) + expect(traces[0][0].metrics).to.have.property('openai.request.top_p', 4) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.completion_tokens', 10) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.prompt_tokens', 37) + expect(traces[0][0].metrics).to.have.property('openai.response.usage.total_tokens', 47) + }) + + const result = await openai.createChatCompletion({ + model: 'gpt-3.5-turbo', + messages: [ + { + 'role': 'user', + 'content': 'Peanut Butter or Jelly?', + 'name': 'hunter2' + }, + { + 'role': 'assistant', + 'content': 'Are you allergic to peanuts?', + 'name': 'hal' + }, + { + 'role': 'user', + 'content': 'Deathly allergic!', + 'name': 'hunter2' + } + ], + temperature: 1.001, + stream: false, + max_tokens: 10, + presence_penalty: -0.0001, + frequency_penalty: 0.0001, + logit_bias: { + '1234': -1 + }, + top_p: 4, + n: 3, + stop: 'time', + user: 'hunter2' + }) + + expect(result.data.id).to.eql('chatcmpl-7GaWqyMTD9BLmkmy8SxyjUGX3KSRN') + expect(result.data.model).to.eql('gpt-3.5-turbo-0301') + expect(result.data.choices[0].message.role).to.eql('assistant') + expect(result.data.choices[0].message.content).to.eql('In that case, it\'s best to avoid peanut') + expect(result.data.choices[0].finish_reason).to.eql('length') + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createChatCompletion', + messages: [ + { + role: 'user', + content: 'Peanut Butter or Jelly?', + name: 'hunter2' + }, + { + role: 'assistant', + content: 'Are you allergic to peanuts?', + name: 'hal' + }, + { role: 'user', content: 'Deathly allergic!', name: 'hunter2' } + ], + choices: [{ + message: { + role: 'assistant', + content: "In that case, it's best to avoid peanut", + name: 'hunter2' + }, + finish_reason: 'length', + index: 0 + }] + }) + + await checkTraces + }) + }) + + describe('createTranscription()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/audio/transcriptions') + .reply(200, { + 'task': 'transcribe', + 'language': 'english', + 'duration': 2.19, + 'segments': [{ + 'id': 0, + 'seek': 0, + 'start': 0, + 'end': 2, + 'text': ' Hello, friend.', + 'tokens': [50364, 2425, 11, 1277, 13, 50464], + 'temperature': 0.5, + 'avg_logprob': -0.7777707236153739, + 'compression_ratio': 0.6363636363636364, + 'no_speech_prob': 0.043891049921512604, + 'transient': false }], + 'text': 'Hello, friend.' + }, [ + 'Date', 'Fri, 19 May 2023 03:19:49 GMT', + 'Content-Type', 'text/plain; charset=utf-8', + 'Content-Length', '15', + 'Connection', 'close', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '595', + 'openai-version', '2020-10-01' + ] + ) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createTranscription') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/audio/transcriptions') + expect(traces[0][0].meta).to.have.property('openai.request.filename', 'hello-friend.m4a') + expect(traces[0][0].meta).to.have.property('openai.request.language', 'en') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'whisper-1') + expect(traces[0][0].meta).to.have.property('openai.request.prompt', 'what does this say') + expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'verbose_json') + expect(traces[0][0].meta).to.have.property('openai.response.language', 'english') + expect(traces[0][0].meta).to.have.property('openai.response.text', 'Hello, friend.') + expect(traces[0][0].metrics).to.have.property('openai.response.duration', 2.19) + expect(traces[0][0].metrics).to.have.property('openai.response.segments_count', 1) + expect(traces[0][0].metrics).to.have.property('openai.request.temperature', 0.5) + }) + + // TODO: Should test each of 'json, text, srt, verbose_json, vtt' since response formats differ + const result = await openai.createTranscription( + fs.createReadStream(Path.join(__dirname, '/hello-friend.m4a')), + 'whisper-1', + 'what does this say', + 'verbose_json', + 0.5, + 'en' + ) + + expect(result.data.text).to.eql('Hello, friend.') + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createTranscription', + prompt: 'what does this say', + file: 'hello-friend.m4a' + }) + + await checkTraces + }) + }) + + describe('createTranslation()', () => { + let scope + + before(() => { + scope = nock('https://api.openai.com:443') + .post('/v1/audio/translations') + .reply(200, { + 'task': 'translate', + 'language': 'english', + 'duration': 1.74, + 'segments': [{ + 'id': 0, + 'seek': 0, + 'start': 0, + 'end': 3, + 'text': ' Guten Tag!', + 'tokens': [50364, 42833, 11204, 0, 50514], + 'temperature': 0.5, + 'avg_logprob': -0.5626437266667684, + 'compression_ratio': 0.5555555555555556, + 'no_speech_prob': 0.01843200996518135, + 'transient': false + }], + 'text': 'Guten Tag!' + }, [ + 'Date', 'Fri, 19 May 2023 03:41:25 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '334', + 'Connection', 'close', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '520', + 'openai-version', '2020-10-01' + ]) + }) + + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + expect(traces[0][0]).to.have.property('type', 'openai') + expect(traces[0][0]).to.have.property('resource', 'createTranslation') + expect(traces[0][0]).to.have.property('error', 0) + expect(traces[0][0].meta).to.have.property('openai.organization.name', 'kill-9') + + expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/audio/translations') + expect(traces[0][0].meta).to.have.property('openai.request.filename', 'guten-tag.m4a') + expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') + expect(traces[0][0].meta).to.have.property('openai.request.model', 'whisper-1') + expect(traces[0][0].meta).to.have.property('openai.request.prompt', 'greeting') + expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'verbose_json') + expect(traces[0][0].meta).to.have.property('openai.response.language', 'english') + expect(traces[0][0].meta).to.have.property('openai.response.text', 'Guten Tag!') + expect(traces[0][0].metrics).to.have.property('openai.request.temperature', 0.5) + expect(traces[0][0].metrics).to.have.property('openai.response.duration', 1.74) + expect(traces[0][0].metrics).to.have.property('openai.response.segments_count', 1) + }) + + // TODO: Should test each of 'json, text, srt, verbose_json, vtt' since response formats differ + const result = await openai.createTranslation( + fs.createReadStream(Path.join(__dirname, 'guten-tag.m4a')), + 'whisper-1', + 'greeting', + 'verbose_json', + 0.5 + ) + + expect(result.data.text).to.eql('Guten Tag!') + + expect(externalLoggerStub).to.have.been.calledWith({ + status: 'info', + message: 'sampled createTranslation', + prompt: 'greeting', + file: 'guten-tag.m4a' + }) + + await checkTraces + }) + }) + } + }) + }) +}) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 048483339ca..661b9bcff6b 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -230,6 +230,9 @@ class Config { const DD_TELEMETRY_HEARTBEAT_INTERVAL = process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL ? parseInt(process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL) * 1000 : 60000 + const DD_OPENAI_SPAN_CHAR_LIMIT = process.env.DD_OPENAI_SPAN_CHAR_LIMIT + ? parseInt(process.env.DD_OPENAI_SPAN_CHAR_LIMIT) + : 128 const DD_TELEMETRY_DEBUG = coalesce( process.env.DD_TELEMETRY_DEBUG, false @@ -573,6 +576,8 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) this.gitMetadataEnabled = isTrue(DD_TRACE_GIT_METADATA_ENABLED) + this.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT + if (this.gitMetadataEnabled) { this.repositoryUrl = coalesce( process.env.DD_GIT_REPOSITORY_URL, diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index 76ffb670fa5..091bc780ebc 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -8,7 +8,11 @@ const log = require('./log') const MAX_BUFFER_SIZE = 1024 // limit from the agent -class Client { +const TYPE_COUNTER = 'c' +const TYPE_GAUGE = 'g' +const TYPE_DISTRIBUTION = 'd' + +class DogStatsDClient { constructor (options) { options = options || {} @@ -32,11 +36,15 @@ class Client { } gauge (stat, value, tags) { - this._add(stat, value, 'g', tags) + this._add(stat, value, TYPE_GAUGE, tags) } increment (stat, value, tags) { - this._add(stat, value, 'c', tags) + this._add(stat, value, TYPE_COUNTER, tags) + } + + distribution (stat, value, tags) { + this._add(stat, value, TYPE_DISTRIBUTION, tags) } flush () { @@ -135,4 +143,4 @@ class Client { } } -module.exports = Client +module.exports = DogStatsDClient diff --git a/packages/dd-trace/src/external-logger/src/index.js b/packages/dd-trace/src/external-logger/src/index.js index 7161e1b12c7..fe8bc6fe87b 100644 --- a/packages/dd-trace/src/external-logger/src/index.js +++ b/packages/dd-trace/src/external-logger/src/index.js @@ -7,6 +7,8 @@ class ExternalLogger { constructor ({ ddsource, hostname, service, apiKey, site = 'datadoghq.com', interval = 10000, timeout = 2000, limit = 1000 }) { + this.enabled = !!apiKey + this.ddsource = ddsource this.hostname = hostname this.service = service @@ -38,6 +40,8 @@ class ExternalLogger { // Parses and enqueues a log log (log, span, tags) { + if (!this.enabled) return + const logTags = ExternalLogger.tagString(tags) if (span) { diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 0c125bea1be..2d0e80f9433 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -56,6 +56,7 @@ module.exports = { get 'net' () { return require('../../../datadog-plugin-net/src') }, get 'next' () { return require('../../../datadog-plugin-next/src') }, get 'oracledb' () { return require('../../../datadog-plugin-oracledb/src') }, + get 'openai' () { return require('../../../datadog-plugin-openai/src') }, get 'paperplane' () { return require('../../../datadog-plugin-paperplane/src') }, get 'pg' () { return require('../../../datadog-plugin-pg/src') }, get 'pino' () { return require('../../../datadog-plugin-pino/src') }, From d59cd306f54a41389f9d29566de0c1bf7720937a Mon Sep 17 00:00:00 2001 From: Laplie Anderson Date: Fri, 23 Jun 2023 14:36:36 -0400 Subject: [PATCH 15/20] Fix s3 package upload (#3218) --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e7755544d3..600cdd34312 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,6 +29,7 @@ package: stage: deploy variables: PRODUCT_NAME: auto_inject-node + PACKAGE_FILTER: js # product name is "node" but package name ends "js" deploy_to_reliability_env: stage: deploy From c253e3a300aa485b30a29a8f5276d766e71047a1 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 23 Jun 2023 13:10:27 -0700 Subject: [PATCH 16/20] Telemetry metrics for OTel (#3259) --- integration-tests/helpers.js | 50 ++ integration-tests/opentelemetry.spec.js | 56 +- integration-tests/opentelemetry/basic.js | 5 + packages/dd-trace/src/opentelemetry/span.js | 1 + packages/dd-trace/src/opentracing/span.js | 32 ++ packages/dd-trace/src/opentracing/tracer.js | 3 +- packages/dd-trace/src/plugins/tracing.js | 3 +- packages/dd-trace/src/telemetry/index.js | 3 + packages/dd-trace/src/telemetry/metrics.js | 225 ++++++++ .../dd-trace/test/opentracing/tracer.spec.js | 9 +- .../dd-trace/test/telemetry/metrics.spec.js | 531 ++++++++++++++++++ 11 files changed, 911 insertions(+), 7 deletions(-) create mode 100644 packages/dd-trace/src/telemetry/metrics.js create mode 100644 packages/dd-trace/test/telemetry/metrics.spec.js diff --git a/integration-tests/helpers.js b/integration-tests/helpers.js index 08caa4e7dae..acbb2630f26 100644 --- a/integration-tests/helpers.js +++ b/integration-tests/helpers.js @@ -27,6 +27,7 @@ class FakeAgent extends EventEmitter { async start () { const app = express() app.use(bodyParser.raw({ limit: Infinity, type: 'application/msgpack' })) + app.use(bodyParser.json({ limit: Infinity, type: 'application/json' })) app.put('/v0.4/traces', (req, res) => { if (req.body.length === 0) return res.status(200).send() res.status(200).send({ rate_by_service: { 'service:,env:': 1 } }) @@ -43,6 +44,13 @@ class FakeAgent extends EventEmitter { files: req.files }) }) + app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => { + res.status(200).send() + this.emit('telemetry', { + headers: req.headers, + payload: req.body + }) + }) return new Promise((resolve, reject) => { const timeoutObj = setTimeout(() => { @@ -103,6 +111,48 @@ class FakeAgent extends EventEmitter { return resultPromise } + + assertTelemetryReceived (fn, timeout, requestType, expectedMessageCount = 1) { + timeout = timeout || 5000 + let resultResolve + let resultReject + let msgCount = 0 + const errors = [] + + const timeoutObj = setTimeout(() => { + resultReject([...errors, new Error('timeout')]) + }, timeout) + + const resultPromise = new Promise((resolve, reject) => { + resultResolve = () => { + clearTimeout(timeoutObj) + resolve() + } + resultReject = (e) => { + clearTimeout(timeoutObj) + reject(e) + } + }) + + const messageHandler = msg => { + if (msg.payload.request_type !== requestType) return + msgCount += 1 + try { + fn(msg) + if (msgCount === expectedMessageCount) { + resultResolve() + } + } catch (e) { + errors.push(e) + } + if (msgCount === expectedMessageCount) { + this.removeListener('telemetry', messageHandler) + } + } + this.on('telemetry', messageHandler) + + return resultPromise + } } function spawnProc (filename, options = {}) { diff --git a/integration-tests/opentelemetry.spec.js b/integration-tests/opentelemetry.spec.js index 0b7d01ceba3..57750934fc1 100644 --- a/integration-tests/opentelemetry.spec.js +++ b/integration-tests/opentelemetry.spec.js @@ -6,9 +6,13 @@ const { join } = require('path') const { assert } = require('chai') const { satisfies } = require('semver') -function check (agent, proc, timeout, onMessage = () => { }) { +function check (agent, proc, timeout, onMessage = () => { }, isMetrics) { + const messageReceiver = isMetrics + ? agent.assertTelemetryReceived(onMessage, timeout, 'generate-metrics') + : agent.assertMessageReceived(onMessage, timeout) + return Promise.all([ - agent.assertMessageReceived(onMessage, timeout), + messageReceiver, new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Process timed out')) @@ -38,6 +42,11 @@ function eachEqual (spans, expected, fn) { return spans.every((span, i) => fn(span) === expected[i]) } +function nearNow (ts, now = Date.now(), range = 1000) { + const delta = Math.abs(now - ts) + return delta < range && delta >= 0 +} + describe('opentelemetry', () => { let agent let proc @@ -84,6 +93,49 @@ describe('opentelemetry', () => { }) }) + it('should capture telemetry', () => { + proc = fork(join(cwd, 'opentelemetry/basic.js'), { + cwd, + env: { + DD_TRACE_AGENT_PORT: agent.port, + DD_TRACE_OTEL_ENABLED: 1, + DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, + TIMEOUT: 1500 + } + }) + + return check(agent, proc, timeout, ({ payload }) => { + assert.strictEqual(payload.request_type, 'generate-metrics') + + const metrics = payload.payload + assert.strictEqual(metrics.namespace, 'tracers') + + const spanCreated = metrics.series.find(({ metric }) => metric === 'span_created') + const spanFinished = metrics.series.find(({ metric }) => metric === 'span_finished') + + // Validate common fields between start and finish + for (const series of [spanCreated, spanFinished]) { + assert.ok(series) + + assert.strictEqual(series.points.length, 1) + assert.strictEqual(series.points[0].length, 2) + + const [ts, value] = series.points[0] + assert.ok(nearNow(ts, Date.now() / 1e3)) + assert.strictEqual(value, 1) + + assert.strictEqual(series.type, 'count') + assert.strictEqual(series.common, true) + assert.deepStrictEqual(series.tags, [ + 'integration_name:otel', + 'otel_enabled:true', + 'lib_language:nodejs', + `version:${process.version}` + ]) + } + }, true) + }) + it('should work within existing datadog-traced http request', async () => { proc = fork(join(cwd, 'opentelemetry/server.js'), { cwd, diff --git a/integration-tests/opentelemetry/basic.js b/integration-tests/opentelemetry/basic.js index 024c4395e3d..f3397103084 100644 --- a/integration-tests/opentelemetry/basic.js +++ b/integration-tests/opentelemetry/basic.js @@ -1,5 +1,7 @@ 'use strict' +const TIMEOUT = Number(process.env.TIMEOUT || 0) + const tracer = require('dd-trace').init() const { TracerProvider } = tracer @@ -16,5 +18,8 @@ const otelTracer = ot.trace.getTracer( otelTracer.startActiveSpan('otel-sub', otelSpan => { setImmediate(() => { otelSpan.end() + + // Allow the process to be held open to gather telemetry metrics + setTimeout(() => {}, TIMEOUT) }) }) diff --git a/packages/dd-trace/src/opentelemetry/span.js b/packages/dd-trace/src/opentelemetry/span.js index ddfe391298b..f2c3e277c6a 100644 --- a/packages/dd-trace/src/opentelemetry/span.js +++ b/packages/dd-trace/src/opentelemetry/span.js @@ -38,6 +38,7 @@ class Span { context: spanContext._ddContext, startTime, hostname: _tracer._hostname, + integrationName: 'otel', tags: { 'service.name': _tracer._service } diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index 7da3b6ff089..ca77ad11232 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -11,6 +11,9 @@ const tagger = require('../tagger') const metrics = require('../metrics') const log = require('../log') const { storage } = require('../../../datadog-core') +const telemetryMetrics = require('../telemetry/metrics') + +const tracerMetrics = telemetryMetrics.manager.namespace('tracers') const { DD_TRACE_EXPERIMENTAL_STATE_TRACKING, @@ -20,6 +23,30 @@ const { const unfinishedRegistry = createRegistry('unfinished') const finishedRegistry = createRegistry('finished') +const OTEL_ENABLED = !!process.env.DD_TRACE_OTEL_ENABLED + +const integrationCounters = { + span_created: {}, + span_finished: {} +} + +function getIntegrationCounter (event, integration) { + const counters = integrationCounters[event] + + if (integration in counters) { + return counters[integration] + } + + const counter = tracerMetrics.count(event, [ + `integration_name:${integration.toLowerCase()}`, + `otel_enabled:${OTEL_ENABLED}` + ]) + + integrationCounters[event][integration] = counter + + return counter +} + class DatadogSpan { constructor (tracer, processor, prioritySampler, fields, debug) { const operationName = fields.operationName @@ -38,6 +65,9 @@ class DatadogSpan { // This name property is not updated when the span name changes. // This is necessary for span count metrics. this._name = operationName + this._integrationName = fields.integrationName || 'opentracing' + + getIntegrationCounter('span_created', this._integrationName).inc() this._spanContext = this._createContext(parent, fields) this._spanContext._name = operationName @@ -126,6 +156,8 @@ class DatadogSpan { } } + getIntegrationCounter('span_finished', this._integrationName).inc() + if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) { metrics.decrement('runtime.node.spans.unfinished') metrics.decrement('runtime.node.spans.unfinished.by.name', `span_name:${this._name}`) diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index 8ff74151145..1cb3ad959ed 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -61,7 +61,8 @@ class DatadogTracer { tags, startTime: options.startTime, hostname: this._hostname, - traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled + traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled, + integrationName: options.integrationName }, this._debug) span.addTags(this._tags) diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index c0ad2e3d0f5..30704f308da 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -91,7 +91,8 @@ class TracingPlugin extends Plugin { 'span.type': type, ...meta, ...metrics - } + }, + integrationName: type }) analyticsSampler.sample(span, this.config.measured) diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index 20705f49efc..ce54f140dec 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -6,6 +6,8 @@ const os = require('os') const dependencies = require('./dependencies') const { sendData } = require('./send-data') +const { manager: metricsManager } = require('./metrics') + const telemetryStartChannel = dc.channel('datadog:telemetry:start') const telemetryStopChannel = dc.channel('datadog:telemetry:stop') @@ -121,6 +123,7 @@ function start (aConfig, thePluginManager) { dependencies.start(config, application, host) sendData(config, application, host, 'app-started', appStarted()) interval = setInterval(() => { + metricsManager.send(config, application, host) sendData(config, application, host, 'app-heartbeat') }, heartbeatInterval) interval.unref() diff --git a/packages/dd-trace/src/telemetry/metrics.js b/packages/dd-trace/src/telemetry/metrics.js new file mode 100644 index 00000000000..9669bf9ba82 --- /dev/null +++ b/packages/dd-trace/src/telemetry/metrics.js @@ -0,0 +1,225 @@ +'use strict' + +const { version } = require('../../../../package.json') + +const { sendData } = require('./send-data') + +function getId (type, namespace, name, tags) { + return `${type}:${namespace}.${name}:${tagArray(tags).sort().join(',')}` +} + +function tagArray (tags = {}) { + if (Array.isArray(tags)) return tags + const list = [] + for (const [key, value] of Object.entries(tags)) { + list.push(`${key}:${value}`.toLowerCase()) + } + return list +} + +function now () { + return Date.now() / 1e3 +} + +function mapToJsonArray (map) { + return Array.from(map.values()).map(v => v.toJSON()) +} + +class Metric { + constructor (namespace, metric, common, tags) { + this.namespace = namespace.toString() + this.metric = common ? metric : `nodejs.${metric}` + this.tags = tagArray(tags) + if (common) { + this.tags.push('lib_language:nodejs') + this.tags.push(`version:${process.version}`) + } else { + this.tags.push(`lib_version:${version}`) + } + this.common = common + + this.points = [] + } + + toString () { + const { namespace, metric } = this + return `${namespace}.${metric}` + } + + reset () { + this.points = [] + } + + track () { + throw new Error('not implemented') + } + + toJSON () { + const { metric, points, interval, type, tags, common } = this + return { + metric, + points, + interval, + type, + tags, + common + } + } +} + +class CountMetric extends Metric { + get type () { + return 'count' + } + + inc (value) { + return this.track(value) + } + + dec (value = -1) { + return this.track(value) + } + + track (value = 1) { + if (this.points.length) { + this.points[0][1] += value + } else { + this.points.push([now(), value]) + } + } +} + +class GaugeMetric extends Metric { + get type () { + return 'gauge' + } + + mark (value) { + return this.track(value) + } + + track (value = 1) { + this.points.push([now(), value]) + } +} + +class RateMetric extends Metric { + constructor (namespace, metric, common, tags, interval) { + super(namespace, metric, common, tags) + + this.interval = interval + this.rate = 0 + } + + get type () { + return 'rate' + } + + reset () { + super.reset() + this.rate = 0 + } + + track (value = 1) { + this.rate += value + const rate = this.interval ? (this.rate / this.interval) : 0.0 + this.points = [[now(), rate]] + } +} + +const metricsTypes = { + count: CountMetric, + gauge: GaugeMetric, + rate: RateMetric +} + +class Namespace extends Map { + constructor (namespace) { + super() + this.namespace = namespace + } + + reset () { + for (const metric of this.values()) { + metric.reset() + } + } + + toString () { + return `dd.instrumentation_telemetry_data.${this.namespace}` + } + + getMetric (type, name, tags, interval) { + const metricId = getId(type, this, name, tags) + + let metric = this.get(metricId) + if (metric) return metric + + const Factory = metricsTypes[type] + if (!Factory) { + throw new Error(`Unknown metric type ${type}`) + } + + metric = new Factory(this, name, true, tags, interval) + this.set(metricId, metric) + + return metric + } + + count (name, tags) { + return this.getMetric('count', name, tags) + } + + gauge (name, tags) { + return this.getMetric('gauge', name, tags) + } + + rate (name, interval, tags) { + return this.getMetric('rate', name, tags, interval) + } + + toJSON () { + const { namespace } = this + return { + namespace, + series: mapToJsonArray(this) + } + } +} + +class NamespaceManager extends Map { + namespace (name) { + let ns = this.get(name) + if (ns) return ns + + ns = new Namespace(name) + this.set(name, ns) + return ns + } + + toJSON () { + return mapToJsonArray(this) + } + + send (config, application, host) { + for (const namespace of this.values()) { + sendData(config, application, host, 'generate-metrics', namespace.toJSON()) + + // TODO: This could also be clear() but then it'd have to rebuild all + // metric instances on every send. This may be desirable if we want tags + // with high cardinality and variability over time. + namespace.reset() + } + } +} + +const manager = new NamespaceManager() + +module.exports = { + CountMetric, + GaugeMetric, + RateMetric, + Namespace, + NamespaceManager, + manager +} diff --git a/packages/dd-trace/test/opentracing/tracer.spec.js b/packages/dd-trace/test/opentracing/tracer.spec.js index d2b96e5729d..0c3fb37fbf3 100644 --- a/packages/dd-trace/test/opentracing/tracer.spec.js +++ b/packages/dd-trace/test/opentracing/tracer.spec.js @@ -119,7 +119,8 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, - traceId128BitGenerationEnabled: undefined + traceId128BitGenerationEnabled: undefined, + integrationName: undefined }, true) expect(span.addTags).to.have.been.calledWith({ @@ -176,7 +177,8 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: os.hostname(), - traceId128BitGenerationEnabled: undefined + traceId128BitGenerationEnabled: undefined, + integrationName: undefined }) expect(testSpan).to.equal(span) @@ -246,7 +248,8 @@ describe('Tracer', () => { }, startTime: fields.startTime, hostname: undefined, - traceId128BitGenerationEnabled: true + traceId128BitGenerationEnabled: true, + integrationName: undefined }) expect(testSpan).to.equal(span) diff --git a/packages/dd-trace/test/telemetry/metrics.spec.js b/packages/dd-trace/test/telemetry/metrics.spec.js new file mode 100644 index 00000000000..81c9faf7616 --- /dev/null +++ b/packages/dd-trace/test/telemetry/metrics.spec.js @@ -0,0 +1,531 @@ +'use strict' + +require('../setup/tap') + +const proxyquire = require('proxyquire') + +describe('metrics', () => { + let metrics + let sendData + let now + + beforeEach(() => { + now = Date.now() + sinon.stub(Date, 'now').returns(now) + + sendData = sinon.stub() + metrics = proxyquire('../../src/telemetry/metrics', { + './send-data': { + sendData + } + }) + }) + + afterEach(() => { + Date.now.restore() + }) + + describe('NamespaceManager', () => { + it('should export singleton manager', () => { + expect(metrics.manager).to.be.instanceOf(metrics.NamespaceManager) + }) + + it('should make namespaces', () => { + const manager = new metrics.NamespaceManager() + const ns = manager.namespace('test') + expect(ns).to.be.instanceOf(metrics.Namespace) + expect(ns.namespace).to.equal('test') + expect(ns.toString()).to.equal('dd.instrumentation_telemetry_data.test') + }) + + it('should reuse namespace instances with the same name', () => { + const manager = new metrics.NamespaceManager() + const ns = manager.namespace('test') + expect(manager.namespace('test')).to.equal(ns) + }) + + it('should convert to json', () => { + const manager = new metrics.NamespaceManager() + + const test1 = manager.namespace('test1') + const test2 = manager.namespace('test2') + + test1.count('metric1', { bar: 'baz' }).inc() + test2.count('metric2', { bux: 'bax' }).inc() + + expect(manager.toJSON()).to.deep.equal([ + { + namespace: 'test1', + series: [ + { + metric: 'metric1', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }, + { + namespace: 'test2', + series: [ + { + metric: 'metric2', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + } + ]) + }) + + it('should send data', () => { + const manager = new metrics.NamespaceManager() + + const test1 = manager.namespace('test1') + const test2 = manager.namespace('test2') + + test1.count('metric1', { bar: 'baz' }).inc() + test2.count('metric2', { bux: 'bax' }).inc() + + const config = { + hostname: 'localhost', + port: 12345, + tags: { + 'runtime-id': 'abc123' + } + } + const application = { + language_name: 'nodejs', + tracer_version: '1.2.3' + } + const host = {} + + manager.send(config, application, host) + + expect(sendData).to.have.been + .calledWith(config, application, host, 'generate-metrics', { + namespace: 'test1', + series: [ + { + metric: 'metric1', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }) + expect(sendData).to.have.been + .calledWith(config, application, host, 'generate-metrics', { + namespace: 'test2', + series: [ + { + metric: 'metric2', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }) + }) + }) + + describe('Namespace', () => { + it('should store namespace name', () => { + const ns = new metrics.Namespace('name') + expect(ns).to.have.property('namespace', 'name') + }) + + it('should convert to string', () => { + const ns = new metrics.Namespace('name') + expect(ns.toString()).to.equal('dd.instrumentation_telemetry_data.name') + }) + + it('should get count metric', () => { + const ns = new metrics.Namespace('name') + expect(ns.count('name')).to.be.instanceOf(metrics.CountMetric) + }) + + it('should get gauge metric', () => { + const ns = new metrics.Namespace('name') + expect(ns.gauge('name')).to.be.instanceOf(metrics.GaugeMetric) + }) + + it('should get rate metric', () => { + const ns = new metrics.Namespace('name') + expect(ns.rate('name')).to.be.instanceOf(metrics.RateMetric) + }) + + it('should get metric by type', () => { + const ns = new metrics.Namespace('name') + expect(ns.getMetric('count', 'name')).to.be.instanceOf(metrics.CountMetric) + expect(ns.getMetric('gauge', 'name')).to.be.instanceOf(metrics.GaugeMetric) + expect(ns.getMetric('rate', 'name')).to.be.instanceOf(metrics.RateMetric) + + expect(() => ns.getMetric('non-existent', 'name')) + .to.throw(Error, 'Unknown metric type non-existent') + }) + + it('should have unique metrics per unique tag set', () => { + const ns = new metrics.Namespace('test') + ns.count('foo', { bar: 'baz' }).inc() + ns.count('foo', { bar: 'baz' }).inc() // not unique + ns.count('foo', { bux: 'bax' }).inc() + expect(ns).to.have.lengthOf(2) + }) + + it('should reset metrics', () => { + const ns = new metrics.Namespace('test') + const metric = ns.count('foo', { bar: 'baz' }) + metric.inc() + + metric.reset = sinon.spy(metric.reset) + + expect(metric.points).to.have.lengthOf(1) + ns.reset() + expect(metric.points).to.have.lengthOf(0) + + expect(metric.reset).to.have.been.called + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('test') + ns.count('foo', { bar: 'baz' }).inc() + ns.count('foo', { bux: 'bax' }).inc() + + expect(ns.toJSON()).to.deep.equal({ + namespace: 'test', + series: [ + { + metric: 'foo', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }, + { + metric: 'foo', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + } + ] + }) + }) + }) + + describe('CountMetric', () => { + it('should expose input data', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name', { + foo: 'bar', + baz: 'buz' + }) + + expect(metric.type).to.equal('count') + expect(metric).to.deep.equal({ + namespace: 'dd.instrumentation_telemetry_data.tracers', + metric: 'name', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true, + points: [] + }) + }) + + it('should increment', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.track = sinon.spy(metric.track) + + metric.inc() + + expect(metric.track).to.be.called + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1] + ]) + + metric.inc() + + expect(metric.points).to.deep.equal([ + [now / 1e3, 2] + ]) + }) + + it('should decrement', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.inc() + metric.inc() + + metric.track = sinon.spy(metric.track) + + metric.dec() + + expect(metric.track).to.be.calledWith(-1) + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1] + ]) + }) + + it('should retain timestamp of first change', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.inc() + + Date.now.restore() + const newNow = Date.now() + sinon.stub(Date, 'now').returns(newNow) + + metric.inc() + + expect(metric.points).to.deep.equal([ + [now / 1e3, 2] + ]) + }) + + it('should reset state', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name') + + metric.inc() + metric.reset() + + expect(metric.points).to.deep.equal([]) + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.count('name', { + foo: 'bar', + baz: 'buz' + }) + + metric.inc() + + expect(metric.toJSON()).to.deep.equal({ + metric: 'name', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }) + }) + }) + + describe('GaugeMetric', () => { + it('should expose input data', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name', { + foo: 'bar', + baz: 'buz' + }) + + expect(metric.type).to.equal('gauge') + expect(metric).to.deep.equal({ + namespace: 'dd.instrumentation_telemetry_data.tracers', + metric: 'name', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true, + points: [] + }) + }) + + it('should mark', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name') + + metric.track = sinon.spy(metric.track) + + metric.mark(1) + + expect(metric.track).to.be.called + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1] + ]) + + Date.now.restore() + const newNow = Date.now() + sinon.stub(Date, 'now').returns(newNow) + + metric.mark(2) + + expect(metric.points).to.deep.equal([ + [now / 1e3, 1], + [newNow / 1e3, 2] + ]) + }) + + it('should reset state', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name') + + metric.mark(1) + metric.reset() + + expect(metric.points).to.deep.equal([]) + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.gauge('name', { + foo: 'bar', + baz: 'buz' + }) + + metric.mark(1) + + Date.now.restore() + const newNow = Date.now() + sinon.stub(Date, 'now').returns(newNow) + + metric.mark(2) + + expect(metric.toJSON()).to.deep.equal({ + metric: 'name', + points: [ + [now / 1e3, 1], + [newNow / 1e3, 2] + ], + interval: undefined, + type: 'gauge', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }) + }) + }) + + describe('RateMetric', () => { + it('should expose input data', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000, { + foo: 'bar', + baz: 'buz' + }) + + expect(metric.type).to.equal('rate') + expect(metric).to.deep.equal({ + namespace: 'dd.instrumentation_telemetry_data.tracers', + metric: 'name', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true, + points: [], + interval: 1000, + rate: 0 + }) + }) + + it('should track', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000) + + metric.track(100) + + expect(metric.points).to.deep.equal([ + [now / 1e3, 0.1] + ]) + }) + + it('should reset state', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000) + + metric.track(1) + metric.reset() + + expect(metric.points).to.deep.equal([]) + }) + + it('should convert to json', () => { + const ns = new metrics.Namespace('tracers') + const metric = ns.rate('name', 1000, { + foo: 'bar', + baz: 'buz' + }) + + metric.track(123) + + expect(metric.toJSON()).to.deep.equal({ + metric: 'name', + points: [ + [now / 1e3, 0.123] + ], + interval: 1000, + type: 'rate', + tags: [ + 'foo:bar', + 'baz:buz', + 'lib_language:nodejs', + `version:${process.version}` + ], + common: true + }) + }) + }) +}) From 0fb796c728a4c9318eb96f6da5765f892ba1cf48 Mon Sep 17 00:00:00 2001 From: Nicolas Savoire Date: Mon, 26 Jun 2023 11:19:04 +0200 Subject: [PATCH 17/20] Fix job name (#3280) --- .github/workflows/release-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 75e751ab1a1..f080443f35e 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -30,7 +30,7 @@ jobs: injection-image-publish: runs-on: ubuntu-latest - needs: ['publish'] + needs: ['dev_release'] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From e8dd7c7b2493764ae3a8d070546c36f95ea989b4 Mon Sep 17 00:00:00 2001 From: Nicolas Savoire Date: Mon, 26 Jun 2023 12:49:32 +0200 Subject: [PATCH 18/20] Enable OOM monitoring by default (#3292) --- packages/dd-trace/src/profiling/config.js | 4 ++-- packages/dd-trace/test/profiling/config.spec.js | 16 +--------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 1cdf23e44c2..a361e2f2d45 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -53,7 +53,7 @@ class Config { const endpointCollection = coalesce(options.endpointCollection, DD_PROFILING_ENDPOINT_COLLECTION_ENABLED, false) const pprofPrefix = coalesce(options.pprofPrefix, - DD_PROFILING_PPROF_PREFIX) + DD_PROFILING_PPROF_PREFIX, '') this.enabled = enabled this.service = service @@ -88,7 +88,7 @@ class Config { ], this) const oomMonitoringEnabled = isTrue(coalesce(options.oomMonitoring, - DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, false)) + DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, true)) const heapLimitExtensionSize = coalesce(options.oomHeapLimitExtensionSize, Number(DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE), 0) const maxHeapExtensionCount = coalesce(options.oomMaxHeapExtensionCount, diff --git a/packages/dd-trace/test/profiling/config.spec.js b/packages/dd-trace/test/profiling/config.spec.js index 446b20492d8..209c7540838 100644 --- a/packages/dd-trace/test/profiling/config.spec.js +++ b/packages/dd-trace/test/profiling/config.spec.js @@ -145,17 +145,6 @@ describe('config', () => { expect(exporterUrl).to.equal(expectedUrl) }) - it('should disable OOM heap profiler by default', () => { - const config = new Config() - expect(config.oomMonitoring).to.deep.equal({ - enabled: false, - heapLimitExtensionSize: 0, - maxHeapExtensionCount: 0, - exportStrategies: [], - exportCommand: undefined - }) - }) - it('should support OOM heap profiler configuration', () => { process.env = { DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED: 'false' @@ -171,10 +160,7 @@ describe('config', () => { }) }) - it('should use process as default strategy for OOM heap profiler', () => { - process.env = { - DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED: 'true' - } + it('should enable OOM heap profiler by default and use process as default strategy', () => { const config = new Config() expect(config.oomMonitoring).to.deep.equal({ From 39bb21ecd2aab5ee0ca87412a9d864cd3268dc0e Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 26 Jun 2023 20:36:58 +0200 Subject: [PATCH 19/20] moleculer - service naming (#3281) --- .../datadog-plugin-moleculer/src/client.js | 4 +- .../datadog-plugin-moleculer/src/server.js | 4 +- .../test/index.spec.js | 48 ++++++++++++++----- .../datadog-plugin-moleculer/test/naming.js | 24 ++++++++++ packages/dd-trace/src/plugins/client.js | 1 + packages/dd-trace/src/plugins/server.js | 2 + .../src/service-naming/schemas/v0/index.js | 3 +- .../src/service-naming/schemas/v0/web.js | 18 +++++++ .../src/service-naming/schemas/v1/index.js | 3 +- .../src/service-naming/schemas/v1/web.js | 18 +++++++ 10 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 packages/datadog-plugin-moleculer/test/naming.js create mode 100644 packages/dd-trace/src/service-naming/schemas/v0/web.js create mode 100644 packages/dd-trace/src/service-naming/schemas/v1/web.js diff --git a/packages/datadog-plugin-moleculer/src/client.js b/packages/datadog-plugin-moleculer/src/client.js index 33fa799fb88..228b423d10d 100644 --- a/packages/datadog-plugin-moleculer/src/client.js +++ b/packages/datadog-plugin-moleculer/src/client.js @@ -8,8 +8,8 @@ class MoleculerClientPlugin extends ClientPlugin { static get operation () { return 'call' } start ({ actionName, opts }) { - const span = this.startSpan('moleculer.call', { - service: this.config.service, + const span = this.startSpan(this.operationName(), { + service: this.config.service || this.serviceName(), resource: actionName, kind: 'client' }) diff --git a/packages/datadog-plugin-moleculer/src/server.js b/packages/datadog-plugin-moleculer/src/server.js index 070e8fd9cbe..98a667b4cc1 100644 --- a/packages/datadog-plugin-moleculer/src/server.js +++ b/packages/datadog-plugin-moleculer/src/server.js @@ -10,9 +10,9 @@ class MoleculerServerPlugin extends ServerPlugin { start ({ action, ctx, broker }) { const followsFrom = this.tracer.extract('text_map', ctx.meta) - this.startSpan('moleculer.action', { + this.startSpan(this.operationName(), { childOf: followsFrom || this.activeSpan, - service: this.config.service, + service: this.config.service || this.serviceName(), resource: action.name, kind: 'server', type: 'web', diff --git a/packages/datadog-plugin-moleculer/test/index.spec.js b/packages/datadog-plugin-moleculer/test/index.spec.js index 8d5957ad8fc..64cc5a9c76f 100644 --- a/packages/datadog-plugin-moleculer/test/index.spec.js +++ b/packages/datadog-plugin-moleculer/test/index.spec.js @@ -4,6 +4,7 @@ const { expect } = require('chai') const getPort = require('get-port') const os = require('os') const agent = require('../../dd-trace/test/plugins/agent') +const namingSchema = require('./naming') const sort = trace => trace.sort((a, b) => a.start.toNumber() - b.start.toNumber()) @@ -54,8 +55,8 @@ describe('Plugin', () => { agent.use(traces => { const spans = sort(traces[0]) - expect(spans[0]).to.have.property('name', 'moleculer.action') - expect(spans[0]).to.have.property('service', 'test') + expect(spans[0]).to.have.property('name', namingSchema.server.opName) + expect(spans[0]).to.have.property('service', namingSchema.server.serviceName) expect(spans[0]).to.have.property('type', 'web') expect(spans[0]).to.have.property('resource', 'math.add') expect(spans[0].meta).to.have.property('span.kind', 'server') @@ -67,8 +68,8 @@ describe('Plugin', () => { expect(spans[0].meta).to.have.property('moleculer.node_id', `server-${process.pid}`) expect(spans[0].meta).to.have.property('component', 'moleculer') - expect(spans[1]).to.have.property('name', 'moleculer.action') - expect(spans[1]).to.have.property('service', 'test') + expect(spans[1]).to.have.property('name', namingSchema.server.opName) + expect(spans[1]).to.have.property('service', namingSchema.server.serviceName) expect(spans[1]).to.have.property('type', 'web') expect(spans[1]).to.have.property('resource', 'math.numerify') expect(spans[1].meta).to.have.property('span.kind', 'server') @@ -83,6 +84,11 @@ describe('Plugin', () => { broker.call('math.add', { a: 5, b: 3 }).catch(done) }) + withNamingSchema( + (done) => broker.call('math.add', { a: 5, b: 3 }).then(done, done), + () => namingSchema.server.opName, + () => namingSchema.server.serviceName + ) }) describe('with configuration', () => { @@ -103,6 +109,12 @@ describe('Plugin', () => { broker.call('math.add', { a: 5, b: 3 }).catch(done) }) + + withNamingSchema( + (done) => broker.call('math.add', { a: 5, b: 3 }).then(done, done), + () => namingSchema.server.opName, + () => 'custom' + ) }) }) @@ -135,8 +147,8 @@ describe('Plugin', () => { agent.use(traces => { const spans = sort(traces[0]) - expect(spans[0]).to.have.property('name', 'moleculer.call') - expect(spans[0]).to.have.property('service', 'test') + expect(spans[0]).to.have.property('name', namingSchema.client.opName) + expect(spans[0]).to.have.property('service', namingSchema.client.serviceName) expect(spans[0]).to.have.property('resource', 'math.add') expect(spans[0].meta).to.have.property('span.kind', 'client') expect(spans[0].meta).to.have.property('out.host', hostname) @@ -151,6 +163,12 @@ describe('Plugin', () => { broker.call('math.add', { a: 5, b: 3 }).catch(done) }) + + withNamingSchema( + (done) => broker.call('math.add', { a: 5, b: 3 }).then(done, done), + () => namingSchema.client.opName, + () => namingSchema.client.serviceName + ) }) describe('with configuration', () => { @@ -171,6 +189,12 @@ describe('Plugin', () => { broker.call('math.add', { a: 5, b: 3 }).catch(done) }) + + withNamingSchema( + (done) => broker.call('math.add', { a: 5, b: 3 }).then(done, done), + () => namingSchema.client.opName, + () => 'custom' + ) }) }) @@ -187,7 +211,7 @@ describe('Plugin', () => { const clientPromise = agent.use(traces => { const spans = sort(traces[0]) - expect(spans[0]).to.have.property('name', 'moleculer.call') + expect(spans[0]).to.have.property('name', namingSchema.client.opName) spanId = spans[0].span_id }) @@ -195,8 +219,8 @@ describe('Plugin', () => { const serverPromise = agent.use(traces => { const spans = sort(traces[0]) - expect(spans[0]).to.have.property('name', 'moleculer.action') - expect(spans[1]).to.have.property('name', 'moleculer.action') + expect(spans[0]).to.have.property('name', namingSchema.server.opName) + expect(spans[1]).to.have.property('name', namingSchema.server.opName) parentId = spans[0].parent_id }) @@ -252,7 +276,7 @@ describe('Plugin', () => { const clientPromise = agent.use(traces => { const spans = sort(traces[0]) - expect(spans[0]).to.have.property('name', 'moleculer.call') + expect(spans[0]).to.have.property('name', namingSchema.client.opName) expect(spans[0].meta).to.have.property('moleculer.context.node_id', `server-${process.pid}`) expect(spans[0].meta).to.have.property('moleculer.node_id', `client-${process.pid}`) @@ -262,8 +286,8 @@ describe('Plugin', () => { const serverPromise = agent.use(traces => { const spans = sort(traces[0]) - expect(spans[0]).to.have.property('name', 'moleculer.action') - expect(spans[1]).to.have.property('name', 'moleculer.action') + expect(spans[0]).to.have.property('name', namingSchema.server.opName) + expect(spans[1]).to.have.property('name', namingSchema.server.opName) parentId = spans[0].parent_id }) diff --git a/packages/datadog-plugin-moleculer/test/naming.js b/packages/datadog-plugin-moleculer/test/naming.js new file mode 100644 index 00000000000..2dd635d2445 --- /dev/null +++ b/packages/datadog-plugin-moleculer/test/naming.js @@ -0,0 +1,24 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + client: { + v0: { + opName: 'moleculer.call', + serviceName: 'test' + }, + v1: { + opName: 'moleculer.client.request', + serviceName: 'test' + } + }, + server: { + v0: { + opName: 'moleculer.action', + serviceName: 'test' + }, + v1: { + opName: 'moleculer.server.request', + serviceName: 'test' + } + } +}) diff --git a/packages/dd-trace/src/plugins/client.js b/packages/dd-trace/src/plugins/client.js index e0fe539ca4a..eba4c8732a7 100644 --- a/packages/dd-trace/src/plugins/client.js +++ b/packages/dd-trace/src/plugins/client.js @@ -5,6 +5,7 @@ const OutboundPlugin = require('./outbound') class ClientPlugin extends OutboundPlugin { static get operation () { return 'request' } static get kind () { return 'client' } + static get type () { return 'web' } // overridden by storage and other client type plugins } module.exports = ClientPlugin diff --git a/packages/dd-trace/src/plugins/server.js b/packages/dd-trace/src/plugins/server.js index eaf8b5b002b..9571b6c578f 100644 --- a/packages/dd-trace/src/plugins/server.js +++ b/packages/dd-trace/src/plugins/server.js @@ -4,6 +4,8 @@ const InboundPlugin = require('./inbound') class ServerPlugin extends InboundPlugin { static get operation () { return 'request' } + static get kind () { return 'server' } + static get type () { return 'web' } // a default that may eventually be overriden by nonweb servers } module.exports = ServerPlugin diff --git a/packages/dd-trace/src/service-naming/schemas/v0/index.js b/packages/dd-trace/src/service-naming/schemas/v0/index.js index faccf370851..e2ee3f60217 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/index.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/index.js @@ -1,5 +1,6 @@ const SchemaDefinition = require('../definition') const messaging = require('./messaging') const storage = require('./storage') +const web = require('./web') -module.exports = new SchemaDefinition({ messaging, storage }) +module.exports = new SchemaDefinition({ messaging, storage, web }) diff --git a/packages/dd-trace/src/service-naming/schemas/v0/web.js b/packages/dd-trace/src/service-naming/schemas/v0/web.js new file mode 100644 index 00000000000..0e40038ddc4 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/v0/web.js @@ -0,0 +1,18 @@ +const { identityService } = require('../util') + +const web = { + client: { + moleculer: { + opName: () => 'moleculer.call', + serviceName: identityService + } + }, + server: { + moleculer: { + opName: () => 'moleculer.action', + serviceName: identityService + } + } +} + +module.exports = web diff --git a/packages/dd-trace/src/service-naming/schemas/v1/index.js b/packages/dd-trace/src/service-naming/schemas/v1/index.js index faccf370851..e2ee3f60217 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/index.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/index.js @@ -1,5 +1,6 @@ const SchemaDefinition = require('../definition') const messaging = require('./messaging') const storage = require('./storage') +const web = require('./web') -module.exports = new SchemaDefinition({ messaging, storage }) +module.exports = new SchemaDefinition({ messaging, storage, web }) diff --git a/packages/dd-trace/src/service-naming/schemas/v1/web.js b/packages/dd-trace/src/service-naming/schemas/v1/web.js new file mode 100644 index 00000000000..15d86ae7463 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/v1/web.js @@ -0,0 +1,18 @@ +const { identityService } = require('../util') + +const web = { + client: { + moleculer: { + opName: () => 'moleculer.client.request', + serviceName: identityService + } + }, + server: { + moleculer: { + opName: () => 'moleculer.server.request', + serviceName: identityService + } + } +} + +module.exports = web From eeda4f8990f9ef70e7d3bf80da856cf134f04763 Mon Sep 17 00:00:00 2001 From: Thomas Hunter II Date: Mon, 26 Jun 2023 12:04:13 -0700 Subject: [PATCH 20/20] openai: use a public domain test image (#3294) --- packages/datadog-plugin-openai/test/hal.png | Bin 45350 -> 0 bytes .../datadog-plugin-openai/test/index.spec.js | 18 +++++++++--------- packages/datadog-plugin-openai/test/ntsc.png | Bin 0 -> 1680 bytes 3 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 packages/datadog-plugin-openai/test/hal.png create mode 100644 packages/datadog-plugin-openai/test/ntsc.png diff --git a/packages/datadog-plugin-openai/test/hal.png b/packages/datadog-plugin-openai/test/hal.png deleted file mode 100644 index abbd7997c3db71a7f56d9993c16be1f4a18d85d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45350 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 diff --git a/packages/datadog-plugin-openai/test/index.spec.js b/packages/datadog-plugin-openai/test/index.spec.js index 2fca8f14dc2..9e97f6bbb9e 100644 --- a/packages/datadog-plugin-openai/test/index.spec.js +++ b/packages/datadog-plugin-openai/test/index.spec.js @@ -1680,8 +1680,8 @@ describe('Plugin', () => { expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/images/edits') - expect(traces[0][0].meta).to.have.property('openai.request.mask', 'hal.png') - expect(traces[0][0].meta).to.have.property('openai.request.image', 'hal.png') + expect(traces[0][0].meta).to.have.property('openai.request.mask', 'ntsc.png') + expect(traces[0][0].meta).to.have.property('openai.request.image', 'ntsc.png') expect(traces[0][0].meta).to.have.property('openai.request.prompt', 'Change all red to blue') expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'url') expect(traces[0][0].meta).to.have.property('openai.request.size', '256x256') @@ -1695,9 +1695,9 @@ describe('Plugin', () => { }) const result = await openai.createImageEdit( - fs.createReadStream(Path.join(__dirname, 'hal.png')), + fs.createReadStream(Path.join(__dirname, 'ntsc.png')), 'Change all red to blue', - fs.createReadStream(Path.join(__dirname, 'hal.png')), + fs.createReadStream(Path.join(__dirname, 'ntsc.png')), 1, '256x256', 'url', @@ -1710,8 +1710,8 @@ describe('Plugin', () => { status: 'info', message: 'sampled createImageEdit', prompt: 'Change all red to blue', - file: 'hal.png', - mask: 'hal.png' + file: 'ntsc.png', + mask: 'ntsc.png' }) await checkTraces @@ -1757,7 +1757,7 @@ describe('Plugin', () => { expect(traces[0][0].meta).to.have.property('openai.request.method', 'POST') expect(traces[0][0].meta).to.have.property('openai.request.endpoint', '/v1/images/variations') - expect(traces[0][0].meta).to.have.property('openai.request.image', 'hal.png') + expect(traces[0][0].meta).to.have.property('openai.request.image', 'ntsc.png') expect(traces[0][0].meta).to.have.property('openai.request.response_format', 'url') expect(traces[0][0].meta).to.have.property('openai.request.size', '256x256') expect(traces[0][0].meta).to.have.property('openai.request.user', 'hunter2') @@ -1770,14 +1770,14 @@ describe('Plugin', () => { }) const result = await openai.createImageVariation( - fs.createReadStream(Path.join(__dirname, 'hal.png')), 1, '256x256', 'url', 'hunter2') + fs.createReadStream(Path.join(__dirname, 'ntsc.png')), 1, '256x256', 'url', 'hunter2') expect(result.data.data[0].url.startsWith('https://')).to.be.true expect(externalLoggerStub).to.have.been.calledWith({ status: 'info', message: 'sampled createImageVariation', - file: 'hal.png' + file: 'ntsc.png' }) await checkTraces diff --git a/packages/datadog-plugin-openai/test/ntsc.png b/packages/datadog-plugin-openai/test/ntsc.png new file mode 100644 index 0000000000000000000000000000000000000000..a23b928c3ba5e10d77c076fbff6ee226c7b9082b GIT binary patch literal 1680 zcmd^=`BTyf7{yj z+Tw|4N;Tb{Pl$Htn_cEmW$O$<0^iCI zZfA_JbEusOp6F$kOAP?Bqm*ZpY3bC>A8>C%?8_7E9u zQxpR6s2o~ZQ(_uHmj#;^RQ%?Mo}jQ2zF!)Co2u0$72u{O!dqF|`J&92cBvP;Mm;<7 zLOrX&iItv)PyGho3;Qy3U`kwSfe)2PPQKIg_DM|Rnsan{X%ZT_qN00&Ju2eseutOR zp3<3)HrzOh(y8iQU!YfGC&}~sif|k06?U`?s1z1fR(cvt?5B1#4UEe!yVZGEUNyB? zt2g|ynuSB4S})>y9@2xKcZ&z3$a>- z&!GWtqF^aYCDNxWH0P`_Hd_z!-&Qs6~t5fq;aj;}r@`!x*Ar>|v9mK^rUt5>+tz;!9`Fh{6NZeqX|$ z$ej%BTlWiOR;-HKUE(SU0^M**7Mf*-m&j}F=ql2;FK5|FX*3#-P!F8VquV<;=5g)Q>bSmM#9z_73)Wou z=lS