dd-trace 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE-3rdparty.csv +4 -3
  2. package/index.d.ts +27 -0
  3. package/package.json +4 -4
  4. package/packages/datadog-instrumentations/src/aws-sdk.js +5 -0
  5. package/packages/datadog-instrumentations/src/cassandra-driver.js +6 -3
  6. package/packages/datadog-instrumentations/src/elasticsearch.js +39 -1
  7. package/packages/datadog-instrumentations/src/express.js +23 -0
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  9. package/packages/datadog-instrumentations/src/kafkajs.js +2 -2
  10. package/packages/datadog-instrumentations/src/openai.js +50 -0
  11. package/packages/datadog-instrumentations/src/opensearch.js +2 -1
  12. package/packages/datadog-instrumentations/src/passport-http.js +22 -0
  13. package/packages/datadog-instrumentations/src/passport-local.js +22 -0
  14. package/packages/datadog-instrumentations/src/passport-utils.js +36 -0
  15. package/packages/datadog-instrumentations/src/pg.js +17 -4
  16. package/packages/datadog-plugin-aws-sdk/src/base.js +3 -3
  17. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -0
  19. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -0
  20. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +1 -0
  21. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -0
  22. package/packages/datadog-plugin-cassandra-driver/src/index.js +6 -6
  23. package/packages/datadog-plugin-dns/src/lookup.js +1 -1
  24. package/packages/datadog-plugin-elasticsearch/src/index.js +2 -2
  25. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -1
  26. package/packages/datadog-plugin-graphql/src/execute.js +1 -1
  27. package/packages/datadog-plugin-graphql/src/parse.js +1 -1
  28. package/packages/datadog-plugin-graphql/src/resolve.js +0 -5
  29. package/packages/datadog-plugin-graphql/src/validate.js +1 -1
  30. package/packages/datadog-plugin-grpc/src/client.js +9 -3
  31. package/packages/datadog-plugin-grpc/src/server.js +3 -3
  32. package/packages/datadog-plugin-http/src/client.js +1 -1
  33. package/packages/datadog-plugin-http/src/server.js +38 -34
  34. package/packages/datadog-plugin-http2/src/client.js +0 -5
  35. package/packages/datadog-plugin-http2/src/server.js +23 -23
  36. package/packages/datadog-plugin-kafkajs/src/consumer.js +6 -1
  37. package/packages/datadog-plugin-kafkajs/src/producer.js +8 -1
  38. package/packages/datadog-plugin-mocha/src/index.js +3 -3
  39. package/packages/datadog-plugin-moleculer/src/client.js +3 -3
  40. package/packages/datadog-plugin-moleculer/src/server.js +2 -2
  41. package/packages/datadog-plugin-mongodb-core/src/index.js +15 -4
  42. package/packages/datadog-plugin-next/src/index.js +50 -52
  43. package/packages/datadog-plugin-openai/src/index.js +685 -0
  44. package/packages/datadog-plugin-openai/src/services.js +43 -0
  45. package/packages/datadog-plugin-oracledb/src/index.js +3 -10
  46. package/packages/datadog-plugin-pg/src/index.js +3 -11
  47. package/packages/datadog-plugin-sharedb/src/index.js +1 -1
  48. package/packages/dd-trace/src/appsec/channels.js +1 -0
  49. package/packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js +3 -2
  50. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +12 -2
  51. package/packages/dd-trace/src/appsec/index.js +20 -0
  52. package/packages/dd-trace/src/appsec/passport.js +110 -0
  53. package/packages/dd-trace/src/appsec/sdk/track_event.js +14 -5
  54. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -4
  55. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +45 -0
  56. package/packages/dd-trace/src/config.js +38 -1
  57. package/packages/dd-trace/src/constants.js +2 -0
  58. package/packages/dd-trace/src/data_streams_context.js +15 -0
  59. package/packages/dd-trace/src/datastreams/pathway.js +58 -0
  60. package/packages/dd-trace/src/datastreams/processor.js +194 -0
  61. package/packages/dd-trace/src/datastreams/writer.js +66 -0
  62. package/packages/dd-trace/src/dogstatsd.js +12 -4
  63. package/packages/dd-trace/src/external-logger/src/index.js +4 -0
  64. package/packages/dd-trace/src/opentelemetry/span.js +1 -0
  65. package/packages/dd-trace/src/opentracing/span.js +32 -0
  66. package/packages/dd-trace/src/opentracing/tracer.js +3 -1
  67. package/packages/dd-trace/src/plugin_manager.js +7 -2
  68. package/packages/dd-trace/src/plugins/client.js +1 -0
  69. package/packages/dd-trace/src/plugins/database.js +2 -1
  70. package/packages/dd-trace/src/plugins/index.js +2 -0
  71. package/packages/dd-trace/src/plugins/outbound.js +59 -1
  72. package/packages/dd-trace/src/plugins/server.js +2 -0
  73. package/packages/dd-trace/src/plugins/tracing.js +5 -1
  74. package/packages/dd-trace/src/plugins/util/exec.js +2 -0
  75. package/packages/dd-trace/src/plugins/util/git.js +38 -10
  76. package/packages/dd-trace/src/plugins/util/user-provided-git.js +36 -2
  77. package/packages/dd-trace/src/profiling/config.js +34 -7
  78. package/packages/dd-trace/src/proxy.js +6 -0
  79. package/packages/dd-trace/src/service-naming/index.js +13 -1
  80. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  81. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +34 -1
  82. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +27 -0
  83. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  84. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +31 -0
  85. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +26 -0
  86. package/packages/dd-trace/src/telemetry/index.js +3 -0
  87. package/packages/dd-trace/src/telemetry/metrics.js +281 -0
  88. package/packages/dd-trace/src/tracer.js +19 -1
@@ -13,6 +13,8 @@ const {
13
13
  } = require('./tags')
14
14
 
15
15
  const { normalizeRef } = require('./ci')
16
+ const log = require('../../log')
17
+ const { URL } = require('url')
16
18
 
17
19
  function removeEmptyValues (tags) {
18
20
  return Object.keys(tags).reduce((filteredTags, tag) => {
@@ -39,6 +41,37 @@ function filterSensitiveInfoFromRepository (repositoryUrl) {
39
41
  }
40
42
  }
41
43
 
44
+ // The regex is extracted from
45
+ // https://github.com/jonschlinkert/is-git-url/blob/396965ffabf2f46656c8af4c47bef1d69f09292e/index.js#L9C15-L9C87
46
+ function validateGitRepositoryUrl (repoUrl) {
47
+ return /(?:git|ssh|https?|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/.test(repoUrl)
48
+ }
49
+
50
+ function validateGitCommitSha (gitCommitSha) {
51
+ const isValidSha1 = /^[0-9a-f]{40}$/.test(gitCommitSha)
52
+ const isValidSha256 = /^[0-9a-f]{64}$/.test(gitCommitSha)
53
+ return isValidSha1 || isValidSha256
54
+ }
55
+
56
+ function removeInvalidGitMetadata (metadata) {
57
+ return Object.keys(metadata).reduce((filteredTags, tag) => {
58
+ if (tag === GIT_REPOSITORY_URL) {
59
+ if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) {
60
+ log.error('DD_GIT_REPOSITORY_URL must be a valid URL')
61
+ return filteredTags
62
+ }
63
+ }
64
+ if (tag === GIT_COMMIT_SHA) {
65
+ if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) {
66
+ log.error('DD_GIT_COMMIT_SHA must be a full-length git SHA')
67
+ return filteredTags
68
+ }
69
+ }
70
+ filteredTags[tag] = metadata[tag]
71
+ return filteredTags
72
+ }, {})
73
+ }
74
+
42
75
  function getUserProviderGitMetadata () {
43
76
  const {
44
77
  DD_GIT_COMMIT_SHA,
@@ -62,7 +95,7 @@ function getUserProviderGitMetadata () {
62
95
  tag = normalizeRef(DD_GIT_BRANCH)
63
96
  }
64
97
 
65
- return removeEmptyValues({
98
+ const metadata = removeEmptyValues({
66
99
  [GIT_COMMIT_SHA]: DD_GIT_COMMIT_SHA,
67
100
  [GIT_BRANCH]: branch,
68
101
  [GIT_REPOSITORY_URL]: filterSensitiveInfoFromRepository(DD_GIT_REPOSITORY_URL),
@@ -75,6 +108,7 @@ function getUserProviderGitMetadata () {
75
108
  [GIT_COMMIT_AUTHOR_EMAIL]: DD_GIT_COMMIT_AUTHOR_EMAIL,
76
109
  [GIT_COMMIT_AUTHOR_DATE]: DD_GIT_COMMIT_AUTHOR_DATE
77
110
  })
111
+ return removeInvalidGitMetadata(metadata)
78
112
  }
79
113
 
80
- module.exports = { getUserProviderGitMetadata }
114
+ module.exports = { getUserProviderGitMetadata, validateGitRepositoryUrl, validateGitCommitSha }
@@ -12,7 +12,7 @@ const WallProfiler = require('./profilers/wall')
12
12
  const SpaceProfiler = require('./profilers/space')
13
13
  const { oomExportStrategies, snapshotKinds } = require('./constants')
14
14
  const { tagger } = require('./tagger')
15
- const { isTrue } = require('../util')
15
+ const { isFalse, isTrue } = require('../util')
16
16
 
17
17
  class Config {
18
18
  constructor (options = {}) {
@@ -32,6 +32,8 @@ class Config {
32
32
  DD_PROFILING_SOURCE_MAP,
33
33
  DD_PROFILING_UPLOAD_PERIOD,
34
34
  DD_PROFILING_PPROF_PREFIX,
35
+ DD_PROFILING_HEAP_ENABLED,
36
+ DD_PROFILING_WALLTIME_ENABLED,
35
37
  DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED,
36
38
  DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE,
37
39
  DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT,
@@ -53,7 +55,7 @@ class Config {
53
55
  const endpointCollection = coalesce(options.endpointCollection,
54
56
  DD_PROFILING_ENDPOINT_COLLECTION_ENABLED, false)
55
57
  const pprofPrefix = coalesce(options.pprofPrefix,
56
- DD_PROFILING_PPROF_PREFIX)
58
+ DD_PROFILING_PPROF_PREFIX, '')
57
59
 
58
60
  this.enabled = enabled
59
61
  this.service = service
@@ -88,7 +90,7 @@ class Config {
88
90
  ], this)
89
91
 
90
92
  const oomMonitoringEnabled = isTrue(coalesce(options.oomMonitoring,
91
- DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, false))
93
+ DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, true))
92
94
  const heapLimitExtensionSize = coalesce(options.oomHeapLimitExtensionSize,
93
95
  Number(DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE), 0)
94
96
  const maxHeapExtensionCount = coalesce(options.oomMaxHeapExtensionCount,
@@ -106,10 +108,9 @@ class Config {
106
108
  exportCommand
107
109
  }
108
110
 
109
- const profilers = coalesce(options.profilers, DD_PROFILING_PROFILERS, [
110
- new WallProfiler(this),
111
- new SpaceProfiler(this)
112
- ])
111
+ const profilers = options.profilers
112
+ ? options.profilers
113
+ : getProfilers({ DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED, DD_PROFILING_PROFILERS })
113
114
 
114
115
  this.profilers = ensureProfilers(profilers, this)
115
116
  }
@@ -117,6 +118,32 @@ class Config {
117
118
 
118
119
  module.exports = { Config }
119
120
 
121
+ function getProfilers ({ DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED, DD_PROFILING_PROFILERS }) {
122
+ // First consider "legacy" DD_PROFILING_PROFILERS env variable, defaulting to wall + space
123
+ // Use a Set to avoid duplicates
124
+ const profilers = new Set(coalesce(DD_PROFILING_PROFILERS, 'wall,space').split(','))
125
+
126
+ // Add/remove wall depending on the value of DD_PROFILING_WALLTIME_ENABLED
127
+ if (DD_PROFILING_WALLTIME_ENABLED != null) {
128
+ if (isTrue(DD_PROFILING_WALLTIME_ENABLED)) {
129
+ profilers.add('wall')
130
+ } else if (isFalse(DD_PROFILING_WALLTIME_ENABLED)) {
131
+ profilers.delete('wall')
132
+ }
133
+ }
134
+
135
+ // Add/remove wall depending on the value of DD_PROFILING_HEAP_ENABLED
136
+ if (DD_PROFILING_HEAP_ENABLED != null) {
137
+ if (isTrue(DD_PROFILING_HEAP_ENABLED)) {
138
+ profilers.add('space')
139
+ } else if (isFalse(DD_PROFILING_HEAP_ENABLED)) {
140
+ profilers.delete('space')
141
+ }
142
+ }
143
+
144
+ return [...profilers]
145
+ }
146
+
120
147
  function getExportStrategy (name, options) {
121
148
  const strategy = Object.values(oomExportStrategies).find(value => value === name)
122
149
  if (strategy === undefined) {
@@ -64,6 +64,12 @@ class Tracer extends NoopProxy {
64
64
  this._pluginManager.configure(config)
65
65
  setStartupLogPluginManager(this._pluginManager)
66
66
  telemetry.start(config, this._pluginManager)
67
+
68
+ if (config.isManualApiEnabled) {
69
+ const TestApiManualPlugin = require('./ci-visibility/test-api-manual/test-api-manual-plugin')
70
+ this._testApiManualPlugin = new TestApiManualPlugin(this)
71
+ this._testApiManualPlugin.configure({ ...config, enabled: true })
72
+ }
67
73
  }
68
74
  } catch (e) {
69
75
  log.error(e)
@@ -3,7 +3,7 @@ const { schemaDefinitions } = require('./schemas')
3
3
  class SchemaManager {
4
4
  constructor () {
5
5
  this.schemas = schemaDefinitions
6
- this.config = { spanAttributeSchema: 'v0' }
6
+ this.config = { spanAttributeSchema: 'v0', traceRemoveIntegrationServiceNamesEnabled: false }
7
7
  }
8
8
 
9
9
  get schema () {
@@ -14,6 +14,10 @@ class SchemaManager {
14
14
  return this.config.spanAttributeSchema
15
15
  }
16
16
 
17
+ get shouldUseConsistentServiceNaming () {
18
+ return this.config.traceRemoveIntegrationServiceNamesEnabled && this.version === 'v0'
19
+ }
20
+
17
21
  opName (type, kind, plugin, ...opNameArgs) {
18
22
  return this.schema.getOpName(type, kind, plugin, ...opNameArgs)
19
23
  }
@@ -22,6 +26,14 @@ class SchemaManager {
22
26
  return this.schema.getServiceName(type, kind, plugin, this.config.service, ...serviceNameArgs)
23
27
  }
24
28
 
29
+ shortCircuitServiceName (pluginConfig, ...args) {
30
+ // We're short-circuiting, so we do not obey custom service functions
31
+ if (typeof pluginConfig.service === 'function') {
32
+ return this.config.service
33
+ }
34
+ return pluginConfig.service || this.config.service
35
+ }
36
+
25
37
  configure (config = {}) {
26
38
  this.config = config
27
39
  }
@@ -1,5 +1,6 @@
1
1
  const SchemaDefinition = require('../definition')
2
2
  const messaging = require('./messaging')
3
3
  const storage = require('./storage')
4
+ const web = require('./web')
4
5
 
5
- module.exports = new SchemaDefinition({ messaging, storage })
6
+ module.exports = new SchemaDefinition({ messaging, storage, web })
@@ -16,7 +16,16 @@ function mysqlServiceName (service, config, dbConfig, system) {
16
16
  if (typeof config.service === 'function') {
17
17
  return config.service(dbConfig)
18
18
  }
19
- return config.service ? config.service : fromSystem(service, system)
19
+ return config.service || fromSystem(service, system)
20
+ }
21
+
22
+ function withSuffixFunction (suffix) {
23
+ return (service, config, params) => {
24
+ if (typeof config.service === 'function') {
25
+ return config.service(params)
26
+ }
27
+ return config.service || `${service}-${suffix}`
28
+ }
20
29
  }
21
30
 
22
31
  const redisConfig = {
@@ -28,6 +37,14 @@ const redisConfig = {
28
37
 
29
38
  const storage = {
30
39
  client: {
40
+ 'cassandra-driver': {
41
+ opName: () => 'cassandra.query',
42
+ serviceName: (service, config, system) => config.service || fromSystem(service, system)
43
+ },
44
+ elasticsearch: {
45
+ opName: () => 'elasticsearch.query',
46
+ serviceName: (service, config) => config.service || `${service}-elasticsearch`
47
+ },
31
48
  ioredis: redisConfig,
32
49
  mariadb: {
33
50
  opName: () => 'mariadb.query',
@@ -37,6 +54,10 @@ const storage = {
37
54
  opName: () => 'memcached.command',
38
55
  serviceName: (service, config, system) => config.service || fromSystem(service, system)
39
56
  },
57
+ 'mongodb-core': {
58
+ opName: () => 'mongodb.query',
59
+ serviceName: (service, config) => config.service || `${service}-mongodb`
60
+ },
40
61
  mysql: {
41
62
  opName: () => 'mysql.query',
42
63
  serviceName: mysqlServiceName
@@ -45,6 +66,18 @@ const storage = {
45
66
  opName: () => 'mysql.query',
46
67
  serviceName: mysqlServiceName
47
68
  },
69
+ opensearch: {
70
+ opName: () => 'opensearch.query',
71
+ serviceName: (service, config) => config.service || `${service}-opensearch`
72
+ },
73
+ oracledb: {
74
+ opName: () => 'oracle.query',
75
+ serviceName: withSuffixFunction('oracle')
76
+ },
77
+ pg: {
78
+ opName: () => 'pg.query',
79
+ serviceName: withSuffixFunction('postgres')
80
+ },
48
81
  redis: redisConfig,
49
82
  tedious: {
50
83
  opName: () => 'tedious.request',
@@ -0,0 +1,27 @@
1
+ const { identityService } = require('../util')
2
+ const { DD_MAJOR } = require('../../../../../../version')
3
+
4
+ const web = {
5
+ client: {
6
+ grpc: {
7
+ opName: () => DD_MAJOR <= 2 ? 'grpc.request' : 'grpc.client',
8
+ serviceName: identityService
9
+ },
10
+ moleculer: {
11
+ opName: () => 'moleculer.call',
12
+ serviceName: identityService
13
+ }
14
+ },
15
+ server: {
16
+ grpc: {
17
+ opName: () => DD_MAJOR <= 2 ? 'grpc.request' : 'grpc.server',
18
+ serviceName: identityService
19
+ },
20
+ moleculer: {
21
+ opName: () => 'moleculer.action',
22
+ serviceName: identityService
23
+ }
24
+ }
25
+ }
26
+
27
+ module.exports = web
@@ -1,5 +1,6 @@
1
1
  const SchemaDefinition = require('../definition')
2
2
  const messaging = require('./messaging')
3
3
  const storage = require('./storage')
4
+ const web = require('./web')
4
5
 
5
- module.exports = new SchemaDefinition({ messaging, storage })
6
+ module.exports = new SchemaDefinition({ messaging, storage, web })
@@ -14,8 +14,23 @@ const mySQLNaming = {
14
14
  serviceName: identityService
15
15
  }
16
16
 
17
+ function withFunction (service, config, params) {
18
+ if (typeof config.service === 'function') {
19
+ return config.service(params)
20
+ }
21
+ return configWithFallback(service, config)
22
+ }
23
+
17
24
  const storage = {
18
25
  client: {
26
+ 'cassandra-driver': {
27
+ opName: () => 'cassandra.query',
28
+ serviceName: configWithFallback
29
+ },
30
+ elasticsearch: {
31
+ opName: () => 'elasticsearch.query',
32
+ serviceName: configWithFallback
33
+ },
19
34
  ioredis: redisNaming,
20
35
  mariadb: {
21
36
  opName: () => 'mariadb.query',
@@ -25,8 +40,24 @@ const storage = {
25
40
  opName: () => 'memcached.command',
26
41
  serviceName: configWithFallback
27
42
  },
43
+ 'mongodb-core': {
44
+ opName: () => 'mongodb.query',
45
+ serviceName: configWithFallback
46
+ },
28
47
  mysql: mySQLNaming,
29
48
  mysql2: mySQLNaming,
49
+ opensearch: {
50
+ opName: () => 'opensearch.query',
51
+ serviceName: configWithFallback
52
+ },
53
+ oracledb: {
54
+ opName: () => 'oracle.query',
55
+ serviceName: withFunction
56
+ },
57
+ pg: {
58
+ opName: () => 'postgresql.query',
59
+ serviceName: withFunction
60
+ },
30
61
  redis: redisNaming,
31
62
  tedious: {
32
63
  opName: () => 'mssql.query',
@@ -0,0 +1,26 @@
1
+ const { identityService } = require('../util')
2
+
3
+ const web = {
4
+ client: {
5
+ grpc: {
6
+ opName: () => 'grpc.client.request',
7
+ serviceName: identityService
8
+ },
9
+ moleculer: {
10
+ opName: () => 'moleculer.client.request',
11
+ serviceName: identityService
12
+ }
13
+ },
14
+ server: {
15
+ grpc: {
16
+ opName: () => 'grpc.server.request',
17
+ serviceName: identityService
18
+ },
19
+ moleculer: {
20
+ opName: () => 'moleculer.server.request',
21
+ serviceName: identityService
22
+ }
23
+ }
24
+ }
25
+
26
+ module.exports = web
@@ -6,6 +6,8 @@ const os = require('os')
6
6
  const dependencies = require('./dependencies')
7
7
  const { sendData } = require('./send-data')
8
8
 
9
+ const { manager: metricsManager } = require('./metrics')
10
+
9
11
  const telemetryStartChannel = dc.channel('datadog:telemetry:start')
10
12
  const telemetryStopChannel = dc.channel('datadog:telemetry:stop')
11
13
 
@@ -121,6 +123,7 @@ function start (aConfig, thePluginManager) {
121
123
  dependencies.start(config, application, host)
122
124
  sendData(config, application, host, 'app-started', appStarted())
123
125
  interval = setInterval(() => {
126
+ metricsManager.send(config, application, host)
124
127
  sendData(config, application, host, 'app-heartbeat')
125
128
  }, heartbeatInterval)
126
129
  interval.unref()
@@ -0,0 +1,281 @@
1
+ 'use strict'
2
+
3
+ const { version } = require('../../../../package.json')
4
+
5
+ const { sendData } = require('./send-data')
6
+
7
+ function getId (type, namespace, name, tags) {
8
+ return `${type}:${namespace}.${name}:${tagArray(tags).sort().join(',')}`
9
+ }
10
+
11
+ function tagArray (tags = {}) {
12
+ if (Array.isArray(tags)) return tags
13
+ const list = []
14
+ for (const [key, value] of Object.entries(tags)) {
15
+ list.push(`${key}:${value}`.toLowerCase())
16
+ }
17
+ return list
18
+ }
19
+
20
+ function now () {
21
+ return Date.now() / 1e3
22
+ }
23
+
24
+ function mapToJsonArray (map) {
25
+ return Array.from(map.values()).map(v => v.toJSON())
26
+ }
27
+
28
+ class Metric {
29
+ constructor (namespace, metric, common, tags) {
30
+ this.namespace = namespace.toString()
31
+ this.metric = common ? metric : `nodejs.${metric}`
32
+ this.tags = tagArray(tags)
33
+ if (common) {
34
+ this.tags.push('lib_language:nodejs')
35
+ this.tags.push(`version:${process.version}`)
36
+ } else {
37
+ this.tags.push(`lib_version:${version}`)
38
+ }
39
+ this.common = common
40
+
41
+ this.points = []
42
+ }
43
+
44
+ toString () {
45
+ const { namespace, metric } = this
46
+ return `${namespace}.${metric}`
47
+ }
48
+
49
+ reset () {
50
+ this.points = []
51
+ }
52
+
53
+ track () {
54
+ throw new Error('not implemented')
55
+ }
56
+
57
+ toJSON () {
58
+ const { metric, points, interval, type, tags, common } = this
59
+ return {
60
+ metric,
61
+ points,
62
+ interval,
63
+ type,
64
+ tags,
65
+ common
66
+ }
67
+ }
68
+ }
69
+
70
+ class CountMetric extends Metric {
71
+ get type () {
72
+ return 'count'
73
+ }
74
+
75
+ inc (value) {
76
+ return this.track(value)
77
+ }
78
+
79
+ dec (value = -1) {
80
+ return this.track(value)
81
+ }
82
+
83
+ track (value = 1) {
84
+ if (this.points.length) {
85
+ this.points[0][1] += value
86
+ } else {
87
+ this.points.push([now(), value])
88
+ }
89
+ }
90
+ }
91
+
92
+ class DistributionMetric extends Metric {
93
+ get type () {
94
+ return 'distribution'
95
+ }
96
+
97
+ track (value = 1) {
98
+ this.points.push(value)
99
+ }
100
+
101
+ toJSON () {
102
+ const { metric, points, tags, common } = this
103
+ return {
104
+ metric,
105
+ points,
106
+ common,
107
+ tags
108
+ }
109
+ }
110
+ }
111
+
112
+ class GaugeMetric extends Metric {
113
+ get type () {
114
+ return 'gauge'
115
+ }
116
+
117
+ mark (value) {
118
+ return this.track(value)
119
+ }
120
+
121
+ track (value = 1) {
122
+ this.points.push([now(), value])
123
+ }
124
+ }
125
+
126
+ class RateMetric extends Metric {
127
+ constructor (namespace, metric, common, tags, interval) {
128
+ super(namespace, metric, common, tags)
129
+
130
+ this.interval = interval
131
+ this.rate = 0
132
+ }
133
+
134
+ get type () {
135
+ return 'rate'
136
+ }
137
+
138
+ reset () {
139
+ super.reset()
140
+ this.rate = 0
141
+ }
142
+
143
+ track (value = 1) {
144
+ this.rate += value
145
+ const rate = this.interval ? (this.rate / this.interval) : 0.0
146
+ this.points = [[now(), rate]]
147
+ }
148
+ }
149
+
150
+ const metricsTypes = {
151
+ count: CountMetric,
152
+ distribution: DistributionMetric,
153
+ gauge: GaugeMetric,
154
+ rate: RateMetric
155
+ }
156
+
157
+ class MetricsCollection extends Map {
158
+ constructor (namespace) {
159
+ super()
160
+ this.namespace = namespace
161
+ }
162
+
163
+ reset () {
164
+ for (const metric of this.values()) {
165
+ metric.reset()
166
+ }
167
+ }
168
+
169
+ toString () {
170
+ return this.namespace
171
+ }
172
+
173
+ toJSON () {
174
+ if (!this.size) return
175
+ const { namespace } = this
176
+ return {
177
+ namespace,
178
+ series: mapToJsonArray(this)
179
+ }
180
+ }
181
+ }
182
+
183
+ function getMetric (collection, type, name, tags, interval) {
184
+ const metricId = getId(type, collection, name, tags)
185
+
186
+ let metric = collection.get(metricId)
187
+ if (metric) return metric
188
+
189
+ const Factory = metricsTypes[type]
190
+ if (!Factory) {
191
+ throw new Error(`Unknown metric type ${type}`)
192
+ }
193
+
194
+ metric = new Factory(collection, name, true, tags, interval)
195
+ collection.set(metricId, metric)
196
+
197
+ return metric
198
+ }
199
+
200
+ class Namespace {
201
+ constructor (namespace) {
202
+ this.distributions = new MetricsCollection(namespace)
203
+ this.metrics = new MetricsCollection(namespace)
204
+ }
205
+
206
+ reset () {
207
+ this.metrics.reset()
208
+ this.distributions.reset()
209
+ }
210
+
211
+ count (name, tags) {
212
+ return getMetric(this.metrics, 'count', name, tags)
213
+ }
214
+
215
+ gauge (name, tags) {
216
+ return getMetric(this.metrics, 'gauge', name, tags)
217
+ }
218
+
219
+ rate (name, interval, tags) {
220
+ return getMetric(this.metrics, 'rate', name, tags, interval)
221
+ }
222
+
223
+ distribution (name, tags) {
224
+ return getMetric(this.distributions, 'distribution', name, tags)
225
+ }
226
+
227
+ toJSON () {
228
+ const { distributions, metrics } = this
229
+ return {
230
+ distributions: distributions.toJSON(),
231
+ metrics: metrics.toJSON()
232
+ }
233
+ }
234
+ }
235
+
236
+ class NamespaceManager extends Map {
237
+ namespace (name) {
238
+ let ns = this.get(name)
239
+ if (ns) return ns
240
+
241
+ ns = new Namespace(name)
242
+ this.set(name, ns)
243
+ return ns
244
+ }
245
+
246
+ toJSON () {
247
+ return mapToJsonArray(this)
248
+ }
249
+
250
+ send (config, application, host) {
251
+ for (const namespace of this.values()) {
252
+ const { metrics, distributions } = namespace.toJSON()
253
+
254
+ if (metrics) {
255
+ sendData(config, application, host, 'generate-metrics', metrics)
256
+ }
257
+
258
+ if (distributions) {
259
+ sendData(config, application, host, 'distributions', distributions)
260
+ }
261
+
262
+ // TODO: This could also be clear() but then it'd have to rebuild all
263
+ // metric instances on every send. This may be desirable if we want tags
264
+ // with high cardinality and variability over time.
265
+ namespace.reset()
266
+ }
267
+ }
268
+ }
269
+
270
+ const manager = new NamespaceManager()
271
+
272
+ module.exports = {
273
+ CountMetric,
274
+ DistributionMetric,
275
+ GaugeMetric,
276
+ RateMetric,
277
+ MetricsCollection,
278
+ Namespace,
279
+ NamespaceManager,
280
+ manager
281
+ }