dd-trace 5.70.0 → 5.72.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 (72) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/index.d.ts +110 -1
  3. package/initialize.mjs +7 -1
  4. package/package.json +21 -2
  5. package/packages/datadog-instrumentations/src/anthropic.js +115 -0
  6. package/packages/datadog-instrumentations/src/azure-event-hubs.js +37 -0
  7. package/packages/datadog-instrumentations/src/azure-functions.js +3 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +7 -7
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  10. package/packages/datadog-instrumentations/src/jest.js +29 -36
  11. package/packages/datadog-instrumentations/src/mocha/main.js +8 -9
  12. package/packages/datadog-instrumentations/src/mocha/utils.js +1 -1
  13. package/packages/datadog-instrumentations/src/mocha/worker.js +2 -2
  14. package/packages/datadog-instrumentations/src/pg.js +1 -1
  15. package/packages/datadog-instrumentations/src/playwright.js +5 -5
  16. package/packages/datadog-instrumentations/src/vitest.js +8 -8
  17. package/packages/datadog-plugin-anthropic/src/index.js +17 -0
  18. package/packages/datadog-plugin-anthropic/src/tracing.js +30 -0
  19. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +73 -27
  20. package/packages/datadog-plugin-azure-event-hubs/src/index.js +15 -0
  21. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +82 -0
  22. package/packages/datadog-plugin-azure-functions/src/index.js +37 -0
  23. package/packages/datadog-plugin-cucumber/src/index.js +3 -3
  24. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -9
  25. package/packages/datadog-plugin-jest/src/util.js +10 -2
  26. package/packages/datadog-plugin-mocha/src/index.js +2 -2
  27. package/packages/datadog-plugin-playwright/src/index.js +2 -2
  28. package/packages/datadog-plugin-vitest/src/index.js +2 -2
  29. package/packages/datadog-plugin-ws/src/server.js +5 -3
  30. package/packages/dd-trace/src/appsec/reporter.js +70 -21
  31. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
  32. package/packages/dd-trace/src/config.js +110 -26
  33. package/packages/dd-trace/src/config_defaults.js +14 -0
  34. package/packages/dd-trace/src/git_properties.js +90 -5
  35. package/packages/dd-trace/src/llmobs/plugins/anthropic.js +282 -0
  36. package/packages/dd-trace/src/llmobs/tagger.js +35 -0
  37. package/packages/dd-trace/src/noop/proxy.js +3 -0
  38. package/packages/dd-trace/src/openfeature/constants/constants.js +51 -0
  39. package/packages/dd-trace/src/openfeature/flagging_provider.js +45 -0
  40. package/packages/dd-trace/src/openfeature/index.js +77 -0
  41. package/packages/dd-trace/src/openfeature/noop.js +101 -0
  42. package/packages/dd-trace/src/openfeature/writers/base.js +181 -0
  43. package/packages/dd-trace/src/openfeature/writers/exposures.js +173 -0
  44. package/packages/dd-trace/src/openfeature/writers/util.js +43 -0
  45. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +100 -0
  46. package/packages/dd-trace/src/opentelemetry/logs/index.js +87 -0
  47. package/packages/dd-trace/src/opentelemetry/logs/logger.js +77 -0
  48. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +126 -0
  49. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +173 -0
  50. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +367 -0
  51. package/packages/dd-trace/src/opentelemetry/protos/common.proto +116 -0
  52. package/packages/dd-trace/src/opentelemetry/protos/logs.proto +226 -0
  53. package/packages/dd-trace/src/opentelemetry/protos/logs_service.proto +78 -0
  54. package/packages/dd-trace/src/opentelemetry/protos/protobuf_loader.js +48 -0
  55. package/packages/dd-trace/src/opentelemetry/protos/resource.proto +45 -0
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +7 -6
  57. package/packages/dd-trace/src/plugins/index.js +2 -0
  58. package/packages/dd-trace/src/plugins/util/test.js +6 -5
  59. package/packages/dd-trace/src/profiling/config.js +21 -1
  60. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -2
  61. package/packages/dd-trace/src/profiling/profiler.js +44 -22
  62. package/packages/dd-trace/src/profiling/profilers/events.js +12 -3
  63. package/packages/dd-trace/src/profiling/profilers/space.js +35 -24
  64. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -6
  65. package/packages/dd-trace/src/proxy.js +22 -1
  66. package/packages/dd-trace/src/remote_config/capabilities.js +2 -0
  67. package/packages/dd-trace/src/remote_config/index.js +3 -0
  68. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +4 -0
  69. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +8 -0
  70. package/packages/dd-trace/src/supported-configurations.json +18 -0
  71. package/packages/dd-trace/src/telemetry/telemetry.js +13 -1
  72. package/register.js +9 -1
@@ -9,7 +9,8 @@ const tagger = require('./tagger')
9
9
  const set = require('../../datadog-core/src/utils/src/set')
10
10
  const { isTrue, isFalse, normalizeProfilingEnabledValue } = require('./util')
11
11
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('./plugins/util/tags')
12
- const { getGitMetadataFromGitProperties, removeUserSensitiveInfo } = require('./git_properties')
12
+ const { getGitMetadataFromGitProperties, removeUserSensitiveInfo, getRemoteOriginURL, resolveGitHeadSHA } =
13
+ require('./git_properties')
13
14
  const { updateConfig } = require('./telemetry')
14
15
  const telemetryMetrics = require('./telemetry/metrics')
15
16
  const { isInServerlessEnvironment, getIsGCPFunction, getIsAzureFunction } = require('./serverless')
@@ -17,6 +18,7 @@ const { ORIGIN_KEY } = require('./constants')
17
18
  const { appendRules } = require('./payload-tagging/config')
18
19
  const { getEnvironmentVariable, getEnvironmentVariables } = require('./config-helper')
19
20
  const defaults = require('./config_defaults')
21
+ const path = require('path')
20
22
 
21
23
  const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
22
24
 
@@ -67,6 +69,8 @@ const VALID_PROPAGATION_BEHAVIOR_EXTRACT = new Set(['continue', 'restart', 'igno
67
69
 
68
70
  const VALID_LOG_LEVELS = new Set(['debug', 'info', 'warn', 'error'])
69
71
 
72
+ const DEFAULT_OTLP_PORT = 4318
73
+
70
74
  function getFromOtelSamplerMap (otelTracesSampler, otelTracesSamplerArg) {
71
75
  const OTEL_TRACES_SAMPLER_MAPPING = {
72
76
  always_on: '1.0',
@@ -380,30 +384,7 @@ class Config {
380
384
  }
381
385
 
382
386
  if (this.gitMetadataEnabled) {
383
- this.repositoryUrl = removeUserSensitiveInfo(
384
- getEnvironmentVariable('DD_GIT_REPOSITORY_URL') ??
385
- this.tags[GIT_REPOSITORY_URL]
386
- )
387
- this.commitSHA = getEnvironmentVariable('DD_GIT_COMMIT_SHA') ??
388
- this.tags[GIT_COMMIT_SHA]
389
- if (!this.repositoryUrl || !this.commitSHA) {
390
- const DD_GIT_PROPERTIES_FILE = getEnvironmentVariable('DD_GIT_PROPERTIES_FILE') ??
391
- `${process.cwd()}/git.properties`
392
- let gitPropertiesString
393
- try {
394
- gitPropertiesString = fs.readFileSync(DD_GIT_PROPERTIES_FILE, 'utf8')
395
- } catch (e) {
396
- // Only log error if the user has set a git.properties path
397
- if (getEnvironmentVariable('DD_GIT_PROPERTIES_FILE')) {
398
- log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', DD_GIT_PROPERTIES_FILE, e)
399
- }
400
- }
401
- if (gitPropertiesString) {
402
- const { commitSHA, repositoryUrl } = getGitMetadataFromGitProperties(gitPropertiesString)
403
- this.commitSHA = this.commitSHA || commitSHA
404
- this.repositoryUrl = this.repositoryUrl || repositoryUrl
405
- }
406
- }
387
+ this._loadGitMetadata()
407
388
  }
408
389
  }
409
390
 
@@ -554,6 +535,7 @@ class Config {
554
535
  DD_INSTRUMENTATION_TELEMETRY_ENABLED,
555
536
  DD_INSTRUMENTATION_CONFIG_ID,
556
537
  DD_LOGS_INJECTION,
538
+ DD_LOGS_OTEL_ENABLED,
557
539
  DD_LANGCHAIN_SPAN_CHAR_LIMIT,
558
540
  DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE,
559
541
  DD_LLMOBS_AGENTLESS_ENABLED,
@@ -635,7 +617,18 @@ class Config {
635
617
  OTEL_RESOURCE_ATTRIBUTES,
636
618
  OTEL_SERVICE_NAME,
637
619
  OTEL_TRACES_SAMPLER,
638
- OTEL_TRACES_SAMPLER_ARG
620
+ OTEL_TRACES_SAMPLER_ARG,
621
+ DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED,
622
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
623
+ OTEL_EXPORTER_OTLP_LOGS_HEADERS,
624
+ OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
625
+ OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
626
+ OTEL_EXPORTER_OTLP_PROTOCOL,
627
+ OTEL_EXPORTER_OTLP_ENDPOINT,
628
+ OTEL_EXPORTER_OTLP_HEADERS,
629
+ OTEL_EXPORTER_OTLP_TIMEOUT,
630
+ OTEL_BSP_SCHEDULE_DELAY,
631
+ OTEL_BSP_MAX_EXPORT_BATCH_SIZE
639
632
  } = getEnvironmentVariables()
640
633
 
641
634
  const tags = {}
@@ -649,6 +642,23 @@ class Config {
649
642
  tagger.add(tags, DD_TRACE_TAGS)
650
643
  tagger.add(tags, DD_TRACE_GLOBAL_TAGS)
651
644
 
645
+ this._setBoolean(env, 'otelLogsEnabled', isTrue(DD_LOGS_OTEL_ENABLED))
646
+ // Set OpenTelemetry logs configuration with specific _LOGS_ vars taking precedence over generic _EXPORTERS_ vars
647
+ if (OTEL_EXPORTER_OTLP_ENDPOINT) {
648
+ // Only set if there's a custom URL, otherwise let calc phase handle the default
649
+ this._setString(env, 'otelUrl', OTEL_EXPORTER_OTLP_ENDPOINT)
650
+ }
651
+ if (OTEL_EXPORTER_OTLP_ENDPOINT || OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
652
+ this._setString(env, 'otelLogsUrl', OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || env.otelUrl)
653
+ }
654
+ this._setString(env, 'otelHeaders', OTEL_EXPORTER_OTLP_HEADERS)
655
+ this._setString(env, 'otelLogsHeaders', OTEL_EXPORTER_OTLP_LOGS_HEADERS || env.otelHeaders)
656
+ this._setString(env, 'otelProtocol', OTEL_EXPORTER_OTLP_PROTOCOL)
657
+ this._setString(env, 'otelLogsProtocol', OTEL_EXPORTER_OTLP_LOGS_PROTOCOL || env.otelProtocol)
658
+ env.otelTimeout = maybeInt(OTEL_EXPORTER_OTLP_TIMEOUT)
659
+ env.otelLogsTimeout = maybeInt(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) || env.otelTimeout
660
+ env.otelLogsBatchTimeout = maybeInt(OTEL_BSP_SCHEDULE_DELAY)
661
+ env.otelLogsMaxExportBatchSize = maybeInt(OTEL_BSP_MAX_EXPORT_BATCH_SIZE)
652
662
  this._setBoolean(
653
663
  env,
654
664
  'apmTracingEnabled',
@@ -668,6 +678,7 @@ class Config {
668
678
  this._envUnprocessed['appsec.blockedTemplateJson'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON
669
679
  this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED)
670
680
  this._setString(env, 'appsec.eventTracking.mode', DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE)
681
+ // TODO appsec.extendedHeadersCollection are deprecated, to delete in a major
671
682
  this._setBoolean(env, 'appsec.extendedHeadersCollection.enabled', DD_APPSEC_COLLECT_ALL_HEADERS)
672
683
  this._setBoolean(
673
684
  env,
@@ -679,6 +690,7 @@ class Config {
679
690
  this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP)
680
691
  this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP)
681
692
  this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED)
693
+ // TODO Deprecated, to delete in a major
682
694
  this._setBoolean(env, 'appsec.rasp.bodyCollection', DD_APPSEC_RASP_COLLECT_REQUEST_BODY)
683
695
  env['appsec.rateLimit'] = maybeInt(DD_APPSEC_TRACE_RATE_LIMIT)
684
696
  this._envUnprocessed['appsec.rateLimit'] = DD_APPSEC_TRACE_RATE_LIMIT
@@ -719,6 +731,7 @@ class Config {
719
731
  env['dynamicInstrumentation.uploadIntervalSeconds'] = maybeFloat(DD_DYNAMIC_INSTRUMENTATION_UPLOAD_INTERVAL_SECONDS)
720
732
  this._envUnprocessed['dynamicInstrumentation.uploadInterval'] = DD_DYNAMIC_INSTRUMENTATION_UPLOAD_INTERVAL_SECONDS
721
733
  this._setString(env, 'env', DD_ENV || tags.env)
734
+ this._setBoolean(env, 'experimental.flaggingProvider.enabled', DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED)
722
735
  this._setBoolean(env, 'traceEnabled', DD_TRACE_ENABLED)
723
736
  this._setBoolean(env, 'experimental.aiguard.enabled', DD_AI_GUARD_ENABLED)
724
737
  this._setString(env, 'experimental.aiguard.endpoint', DD_AI_GUARD_ENDPOINT)
@@ -968,6 +981,7 @@ class Config {
968
981
  this._optsUnprocessed['experimental.aiguard.timeout'] = options.experimental?.aiguard?.timeout
969
982
  this._setBoolean(opts, 'experimental.enableGetRumData', options.experimental?.enableGetRumData)
970
983
  this._setString(opts, 'experimental.exporter', options.experimental?.exporter)
984
+ this._setBoolean(opts, 'experimental.flaggingProvider.enabled', options.experimental?.flaggingProvider?.enabled)
971
985
  opts.flushInterval = maybeInt(options.flushInterval)
972
986
  this._optsUnprocessed.flushInterval = options.flushInterval
973
987
  opts.flushMinSpans = maybeInt(options.flushMinSpans)
@@ -1154,7 +1168,20 @@ class Config {
1154
1168
  calc.testManagementAttemptToFixRetries = maybeInt(DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES) ?? 20
1155
1169
  this._setBoolean(calc, 'isImpactedTestsEnabled', !isFalse(DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED))
1156
1170
  }
1171
+
1172
+ // Disable log injection when OTEL logs are enabled
1173
+ // OTEL logs and DD log injection are mutually exclusive
1174
+ if (this._env.otelLogsEnabled) {
1175
+ this._setBoolean(calc, 'logInjection', false)
1176
+ }
1177
+
1157
1178
  calc['dogstatsd.hostname'] = this._getHostname()
1179
+
1180
+ // Compute OTLP logs URL to send payloads to the active Datadog Agent
1181
+ const agentHostname = this._getHostname()
1182
+ calc.otelLogsUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}`
1183
+ calc.otelUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}`
1184
+
1158
1185
  this._setBoolean(calc, 'isGitUploadEnabled',
1159
1186
  calc.isIntelligentTestRunnerEnabled && !isFalse(this._isCiVisibilityGitUploadEnabled()))
1160
1187
  this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService())
@@ -1364,6 +1391,63 @@ class Config {
1364
1391
  }
1365
1392
  }
1366
1393
  }
1394
+
1395
+ _loadGitMetadata () {
1396
+ // try to read Git metadata from the environment variables
1397
+ this.repositoryUrl = removeUserSensitiveInfo(
1398
+ getEnvironmentVariable('DD_GIT_REPOSITORY_URL') ??
1399
+ this.tags[GIT_REPOSITORY_URL]
1400
+ )
1401
+ this.commitSHA = getEnvironmentVariable('DD_GIT_COMMIT_SHA') ??
1402
+ this.tags[GIT_COMMIT_SHA]
1403
+
1404
+ // otherwise, try to read Git metadata from the git.properties file
1405
+ if (!this.repositoryUrl || !this.commitSHA) {
1406
+ const DD_GIT_PROPERTIES_FILE = getEnvironmentVariable('DD_GIT_PROPERTIES_FILE') ??
1407
+ `${process.cwd()}/git.properties`
1408
+ let gitPropertiesString
1409
+ try {
1410
+ gitPropertiesString = fs.readFileSync(DD_GIT_PROPERTIES_FILE, 'utf8')
1411
+ } catch (e) {
1412
+ // Only log error if the user has set a git.properties path
1413
+ if (getEnvironmentVariable('DD_GIT_PROPERTIES_FILE')) {
1414
+ log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', DD_GIT_PROPERTIES_FILE, e)
1415
+ }
1416
+ }
1417
+ if (gitPropertiesString) {
1418
+ const { commitSHA, repositoryUrl } = getGitMetadataFromGitProperties(gitPropertiesString)
1419
+ this.commitSHA = this.commitSHA || commitSHA
1420
+ this.repositoryUrl = this.repositoryUrl || repositoryUrl
1421
+ }
1422
+ }
1423
+ // otherwise, try to read Git metadata from the .git/ folder
1424
+ if (!this.repositoryUrl || !this.commitSHA) {
1425
+ const DD_GIT_FOLDER_PATH = getEnvironmentVariable('DD_GIT_FOLDER_PATH') ??
1426
+ path.join(process.cwd(), '.git')
1427
+ if (!this.repositoryUrl) {
1428
+ // try to read git config (repository URL)
1429
+ const gitConfigPath = path.join(DD_GIT_FOLDER_PATH, 'config')
1430
+ try {
1431
+ const gitConfigContent = fs.readFileSync(gitConfigPath, 'utf8')
1432
+ if (gitConfigContent) {
1433
+ this.repositoryUrl = getRemoteOriginURL(gitConfigContent)
1434
+ }
1435
+ } catch (e) {
1436
+ // Only log error if the user has set a .git/ path
1437
+ if (getEnvironmentVariable('DD_GIT_FOLDER_PATH')) {
1438
+ log.error('Error reading git config: %s', gitConfigPath, e)
1439
+ }
1440
+ }
1441
+ }
1442
+ if (!this.commitSHA) {
1443
+ // try to read git HEAD (commit SHA)
1444
+ const gitHeadSha = resolveGitHeadSHA(DD_GIT_FOLDER_PATH)
1445
+ if (gitHeadSha) {
1446
+ this.commitSHA = gitHeadSha
1447
+ }
1448
+ }
1449
+ }
1450
+ }
1367
1451
  }
1368
1452
 
1369
1453
  function handleOtel (tagString) {
@@ -36,12 +36,14 @@ module.exports = {
36
36
  'appsec.blockedTemplateJson': undefined,
37
37
  'appsec.enabled': undefined,
38
38
  'appsec.eventTracking.mode': 'identification',
39
+ // TODO appsec.extendedHeadersCollection is deprecated, to delete in a major
39
40
  'appsec.extendedHeadersCollection.enabled': false,
40
41
  'appsec.extendedHeadersCollection.redaction': true,
41
42
  'appsec.extendedHeadersCollection.maxHeaders': 50,
42
43
  'appsec.obfuscatorKeyRegex': defaultWafObfuscatorKeyRegex,
43
44
  'appsec.obfuscatorValueRegex': defaultWafObfuscatorValueRegex,
44
45
  'appsec.rasp.enabled': true,
46
+ // TODO Deprecated, to delete in a major
45
47
  'appsec.rasp.bodyCollection': false,
46
48
  'appsec.rateLimit': 100,
47
49
  'appsec.rules': undefined,
@@ -75,6 +77,7 @@ module.exports = {
75
77
  'experimental.aiguard.timeout': 10_000, // ms
76
78
  'experimental.enableGetRumData': false,
77
79
  'experimental.exporter': undefined,
80
+ 'experimental.flaggingProvider.enabled': false,
78
81
  flushInterval: 2000,
79
82
  flushMinSpans: 1000,
80
83
  gitMetadataEnabled: true,
@@ -124,6 +127,17 @@ module.exports = {
124
127
  isTestManagementEnabled: false,
125
128
  isImpactedTestsEnabled: false,
126
129
  logInjection: true,
130
+ otelLogsEnabled: false,
131
+ otelUrl: undefined,
132
+ otelLogsUrl: undefined, // Will be computed using agent host
133
+ otelHeaders: undefined,
134
+ otelLogsHeaders: '',
135
+ otelProtocol: 'http/protobuf',
136
+ otelLogsProtocol: 'http/protobuf',
137
+ otelLogsTimeout: 10_000,
138
+ otelTimeout: 10_000,
139
+ otelLogsBatchTimeout: 5000,
140
+ otelLogsMaxExportBatchSize: 512,
127
141
  lookup: undefined,
128
142
  inferredProxyServicesEnabled: false,
129
143
  memcachedCommandEnabled: false,
@@ -1,7 +1,14 @@
1
1
  'use strict'
2
2
 
3
- const commitSHARegex = /git\.commit\.sha=([a-f\d]{40})/
4
- const repositoryUrlRegex = /git\.repository_url=([\w\d:@/.-]+)/
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const gitPropertiesCommitSHARegex = /git\.commit\.sha=([a-f\d]{40})/
7
+ const gitPropertiesRepositoryUrlRegex = /git\.repository_url=([\w\d:@/.-]+)/
8
+ const repositoryUrlRegex = /^([\w\d:@/.-]+)$/
9
+ const remoteOriginRegex = /^\[remote\s+"origin"\]/i
10
+ const gitHeadRefRegex = /ref:\s+(refs\/[A-Za-z0-9._/-]+)/
11
+ const commitSHARegex = /^[0-9a-f]{40}$/
5
12
 
6
13
  function removeUserSensitiveInfo (repositoryUrl) {
7
14
  try {
@@ -21,8 +28,8 @@ function getGitMetadataFromGitProperties (gitPropertiesString) {
21
28
  if (!gitPropertiesString) {
22
29
  return {}
23
30
  }
24
- const commitSHAMatch = gitPropertiesString.match(commitSHARegex)
25
- const repositoryUrlMatch = gitPropertiesString.match(repositoryUrlRegex)
31
+ const commitSHAMatch = gitPropertiesString.match(gitPropertiesCommitSHARegex)
32
+ const repositoryUrlMatch = gitPropertiesString.match(gitPropertiesRepositoryUrlRegex)
26
33
 
27
34
  const repositoryUrl = repositoryUrlMatch ? repositoryUrlMatch[1] : undefined
28
35
 
@@ -32,4 +39,82 @@ function getGitMetadataFromGitProperties (gitPropertiesString) {
32
39
  }
33
40
  }
34
41
 
35
- module.exports = { getGitMetadataFromGitProperties, removeUserSensitiveInfo }
42
+ function getRemoteOriginURL (gitConfigContent) {
43
+ if (!gitConfigContent) {
44
+ return
45
+ }
46
+ const lines = gitConfigContent.split('\n')
47
+ let index = 0
48
+
49
+ // find the remote origin section
50
+ for (; index < lines.length; index++) {
51
+ const line = lines[index]
52
+ if (line[0] !== '[') continue // fast path
53
+ if (remoteOriginRegex.test(line)) break
54
+ }
55
+
56
+ // find the url key/value in the [remote "origin"] section
57
+ index++
58
+ for (; index < lines.length; index++) {
59
+ const line = lines[index]
60
+ if (line[0] === '[') return // abort, section didn't contain a url
61
+ const splitAt = line.indexOf('=')
62
+ if (splitAt === -1) continue
63
+ const key = line.slice(0, splitAt).trim().toLowerCase()
64
+ if (key !== 'url') continue
65
+ const repositoryUrlValue = line.slice(splitAt + 1).trim()
66
+ const repositoryUrlMatch = repositoryUrlValue.match(repositoryUrlRegex)
67
+ if (!repositoryUrlMatch) continue
68
+ return removeUserSensitiveInfo(repositoryUrlMatch[0])
69
+ }
70
+ }
71
+
72
+ function getGitHeadRef (gitHeadContent) {
73
+ if (!gitHeadContent) {
74
+ return
75
+ }
76
+
77
+ // Extract the ref after 'ref: '
78
+ const gitRefMatch = gitHeadContent.match(gitHeadRefRegex)
79
+ return gitRefMatch?.[1]
80
+ }
81
+
82
+ function resolveGitHeadSHA (DD_GIT_FOLDER_PATH) {
83
+ const gitHeadPath = path.join(DD_GIT_FOLDER_PATH, 'HEAD')
84
+
85
+ try {
86
+ const gitHeadContent = fs.readFileSync(gitHeadPath, 'utf8')
87
+ if (!gitHeadContent) {
88
+ return
89
+ }
90
+
91
+ const headContent = gitHeadContent.trim()
92
+
93
+ // Handle detached head case
94
+ if (commitSHARegex.test(headContent)) {
95
+ return headContent
96
+ }
97
+
98
+ // Handle ref case - extract the ref and read the SHA from the ref file
99
+ const gitHeadRef = getGitHeadRef(headContent)
100
+ if (!gitHeadRef) {
101
+ return
102
+ }
103
+ const gitHeadRefPath = path.join(DD_GIT_FOLDER_PATH, gitHeadRef)
104
+ const gitHeadRefContent = fs.readFileSync(gitHeadRefPath, 'utf8')
105
+ if (gitHeadRefContent) {
106
+ const headRefContent = gitHeadRefContent.trim()
107
+ if (commitSHARegex.test(headRefContent)) {
108
+ return headRefContent
109
+ }
110
+ }
111
+ } catch {}
112
+ }
113
+
114
+ module.exports = {
115
+ getGitMetadataFromGitProperties,
116
+ removeUserSensitiveInfo,
117
+ getGitHeadRef,
118
+ getRemoteOriginURL,
119
+ resolveGitHeadSHA,
120
+ }
@@ -0,0 +1,282 @@
1
+ 'use strict'
2
+
3
+ const LLMObsPlugin = require('./base')
4
+
5
+ const ALLOWED_METADATA_KEYS = new Set([
6
+ 'max_tokens',
7
+ 'stop_sequences',
8
+ 'temperature',
9
+ 'top_k',
10
+ 'top_p',
11
+ ])
12
+
13
+ class AnthropicLLMObsPlugin extends LLMObsPlugin {
14
+ static integration = 'anthropic' // used for llmobs telemetry
15
+ static id = 'anthropic'
16
+ static prefix = 'tracing:apm:anthropic:request'
17
+
18
+ constructor () {
19
+ super(...arguments)
20
+
21
+ this.addSub('apm:anthropic:request:chunk', ({ ctx, chunk, done }) => {
22
+ ctx.chunks ??= []
23
+ const chunks = ctx.chunks
24
+ if (chunk) chunks.push(chunk)
25
+
26
+ if (!done) return
27
+
28
+ const response = { content: [] }
29
+
30
+ for (const chunk of chunks) {
31
+ switch (chunk.type) {
32
+ case 'message_start': {
33
+ const { message } = chunk
34
+ if (!message) continue
35
+
36
+ const { role, usage } = message
37
+ if (role) response.role = role
38
+ if (usage) response.usage = usage
39
+ break
40
+ }
41
+ case 'content_block_start': {
42
+ const contentBlock = chunk.content_block
43
+ if (!contentBlock) continue
44
+
45
+ const { type } = contentBlock
46
+ if (type === 'text') {
47
+ response.content.push({ type, text: contentBlock.text })
48
+ } else if (type === 'tool_use') {
49
+ response.content.push({ type, name: contentBlock.name, input: '', id: contentBlock.id })
50
+ }
51
+ break
52
+ }
53
+ case 'content_block_delta': {
54
+ const { delta } = chunk
55
+ if (!delta) continue
56
+
57
+ const { text } = delta
58
+ if (text) response.content[response.content.length - 1].text += text
59
+
60
+ const partialJson = delta.partial_json
61
+ if (partialJson && delta.type === 'input_json_delta') {
62
+ response.content[response.content.length - 1].input += partialJson
63
+ }
64
+ break
65
+ }
66
+ case 'content_block_stop': {
67
+ const type = response.content[response.content.length - 1].type
68
+ if (type === 'tool_use') {
69
+ const input = response.content[response.content.length - 1].input ?? '{}'
70
+ response.content[response.content.length - 1].input = JSON.parse(input)
71
+ }
72
+ break
73
+ }
74
+ case 'message_delta': {
75
+ const { delta } = chunk
76
+
77
+ const finishReason = delta?.stop_reason
78
+ if (finishReason) response.finish_reason = finishReason
79
+
80
+ const { usage } = chunk
81
+ if (usage) {
82
+ const responseUsage = response.usage ?? (response.usage = { input_tokens: 0, output_tokens: 0 })
83
+ responseUsage.output_tokens = usage.output_tokens
84
+
85
+ const cacheCreationTokens = usage.cache_creation_input_tokens
86
+ const cacheReadTokens = usage.cache_read_input_tokens
87
+ if (cacheCreationTokens) responseUsage.cache_creation_input_tokens = cacheCreationTokens
88
+ if (cacheReadTokens) responseUsage.cache_read_input_tokens = cacheReadTokens
89
+ }
90
+
91
+ break
92
+ }
93
+ case 'error': {
94
+ const { error } = chunk
95
+ if (!error) continue
96
+
97
+ response.error = {}
98
+ if (error.type) response.error.type = error.type
99
+ if (error.message) response.error.message = error.message
100
+
101
+ break
102
+ }
103
+ }
104
+
105
+ ctx.result = response
106
+ }
107
+ })
108
+ }
109
+
110
+ getLLMObsSpanRegisterOptions (ctx) {
111
+ const { options } = ctx
112
+ const { model } = options
113
+
114
+ return {
115
+ kind: 'llm',
116
+ modelName: model,
117
+ modelProvider: 'anthropic'
118
+ }
119
+ }
120
+
121
+ setLLMObsTags (ctx) {
122
+ const span = ctx.currentStore?.span
123
+ if (!span) return
124
+
125
+ const { options, result } = ctx
126
+
127
+ this.#tagAnthropicInputMessages(span, options)
128
+ this.#tagAnthropicOutputMessages(span, result)
129
+ this.#tagAnthropicMetadata(span, options)
130
+ this.#tagAnthropicUsage(span, result)
131
+ }
132
+
133
+ #tagAnthropicInputMessages (span, options) {
134
+ const { system, messages } = options
135
+ const inputMessages = []
136
+
137
+ if (system) {
138
+ messages.unshift({ content: system, role: 'system' })
139
+ }
140
+
141
+ for (const message of messages) {
142
+ const { content, role } = message
143
+
144
+ if (typeof content === 'string') {
145
+ inputMessages.push({ content, role })
146
+ continue
147
+ }
148
+
149
+ for (const block of content) {
150
+ if (block.type === 'text') {
151
+ inputMessages.push({ content: block.text, role })
152
+ } else if (block.type === 'image') {
153
+ inputMessages.push({ content: '([IMAGE DETECTED])', role })
154
+ } else if (block.type === 'tool_use') {
155
+ const { text, name, id, type } = block
156
+ let input = block.input
157
+ if (typeof input === 'string') {
158
+ input = JSON.parse(input)
159
+ }
160
+
161
+ const toolCall = {
162
+ name,
163
+ arguments: input,
164
+ toolId: id,
165
+ type
166
+ }
167
+
168
+ inputMessages.push({ content: text ?? '', role, toolCalls: [toolCall] })
169
+ } else if (block.type === 'tool_result') {
170
+ const { content } = block
171
+ const formattedContent = this.#formatAnthropicToolResultContent(content)
172
+ const toolResult = {
173
+ result: formattedContent,
174
+ toolId: block.tool_use_id,
175
+ type: 'tool_result'
176
+ }
177
+
178
+ inputMessages.push({ content: '', role, toolResults: [toolResult] })
179
+ } else {
180
+ inputMessages.push({ content: JSON.stringify(block), role })
181
+ }
182
+ }
183
+ }
184
+
185
+ this._tagger.tagLLMIO(span, inputMessages)
186
+ }
187
+
188
+ #tagAnthropicOutputMessages (span, result) {
189
+ if (!result) return
190
+
191
+ const { content, role } = result
192
+
193
+ if (typeof content === 'string') {
194
+ this._tagger.tagLLMIO(span, null, [{ content, role }])
195
+ return
196
+ }
197
+
198
+ const outputMessages = []
199
+ for (const block of content) {
200
+ const { text } = block
201
+ if (typeof text === 'string') {
202
+ outputMessages.push({ content: text, role })
203
+ } else if (block.type === 'tool_use') {
204
+ let input = block.input
205
+ if (typeof input === 'string') {
206
+ input = JSON.parse(input)
207
+ }
208
+
209
+ const toolCall = {
210
+ name: block.name,
211
+ arguments: input,
212
+ toolId: block.id,
213
+ type: block.type
214
+ }
215
+
216
+ outputMessages.push({ content: text ?? '', role, toolCalls: [toolCall] })
217
+ }
218
+ }
219
+
220
+ this._tagger.tagLLMIO(span, null, outputMessages)
221
+ }
222
+
223
+ #tagAnthropicMetadata (span, options) {
224
+ const metadata = {}
225
+ for (const [key, value] of Object.entries(options)) {
226
+ if (ALLOWED_METADATA_KEYS.has(key)) {
227
+ metadata[key] = value
228
+ }
229
+ }
230
+
231
+ this._tagger.tagMetadata(span, metadata)
232
+ }
233
+
234
+ #tagAnthropicUsage (span, result) {
235
+ if (!result) return
236
+
237
+ const { usage } = result
238
+ if (!usage) return
239
+
240
+ const inputTokens = usage.input_tokens
241
+ const outputTokens = usage.output_tokens
242
+ const cacheWriteTokens = usage.cache_creation_input_tokens
243
+ const cacheReadTokens = usage.cache_read_input_tokens
244
+
245
+ const metrics = {}
246
+
247
+ metrics.inputTokens =
248
+ (inputTokens ?? 0) +
249
+ (cacheWriteTokens ?? 0) +
250
+ (cacheReadTokens ?? 0)
251
+
252
+ if (outputTokens) metrics.outputTokens = outputTokens
253
+ const totalTokens = metrics.inputTokens + (outputTokens ?? 0)
254
+ if (totalTokens) metrics.totalTokens = totalTokens
255
+
256
+ if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
257
+ if (cacheReadTokens != null) metrics.cacheReadTokens = cacheReadTokens
258
+
259
+ this._tagger.tagMetrics(span, metrics)
260
+ }
261
+
262
+ // maybe can make into a util file
263
+ #formatAnthropicToolResultContent (content) {
264
+ if (typeof content === 'string') {
265
+ return content
266
+ } else if (Array.isArray(content)) {
267
+ const formattedContent = []
268
+ for (const toolResultBlock of content) {
269
+ if (toolResultBlock.text) {
270
+ formattedContent.push(toolResultBlock.text)
271
+ } else if (toolResultBlock.type === 'image') {
272
+ formattedContent.push('([IMAGE DETECTED])')
273
+ }
274
+ }
275
+
276
+ return formattedContent.join(',')
277
+ }
278
+ return JSON.stringify(content)
279
+ }
280
+ }
281
+
282
+ module.exports = AnthropicLLMObsPlugin