Repository: DataDog/dd-trace-js
Pull request #5375: dbm: tedious (sql server) service mode
Author: login: tlhunter
Labels: name: semver-minor
Created: 2025-03-06T23:02:24Z | Merged: 2025-03-19T19:12:28Z

### What does this PR do?
- adds DBM service mode tracing to tedious (MS SQL Server)

### Motivation
- users want it

### Plugin Checklist
<!-- Fill this section if adding or updating a plugin. Remove otherwise. -->

- [ ] Unit tests.
- [ ] TypeScript [definitions][1].
- [ ] TypeScript [tests][2].
- [ ] API [documentation][3].
- [ ] CircleCI [jobs/workflows][4].
- [ ] Plugin is [exported][5].

[1]: https://github.com/DataDog/dd-trace-js/blob/master/index.d.ts
[2]: https://github.com/DataDog/dd-trace-js/blob/master/docs/test.ts
[3]: https://github.com/DataDog/documentation/blob/master/content/en/tracing/trace_collection/library_config/nodejs.md
[4]: https://github.com/DataDog/dd-trace-js/blob/master/.circleci/config.yml
[5]: https://github.com/DataDog/dd-trace-js/blob/master/packages/dd-trace/src/plugins/index.js

### Additional Notes
<!-- Anything else we should know when reviewing? -->





Diff:
diff --git a/packages/datadog-instrumentations/src/tedious.js b/packages/datadog-instrumentations/src/tedious.js
index 3a80b03ad7..36a21b3c23 100644
--- a/packages/datadog-instrumentations/src/tedious.js
+++ b/packages/datadog-instrumentations/src/tedious.js
@@ -16,7 +16,7 @@ addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => {
       return makeRequest.apply(this, arguments)
     }
 
-    const queryOrProcedure = getQueryOrProcedure(request)
+    const [queryOrProcedure, queryParent, queryField] = getQueryOrProcedure(request)
 
     if (!queryOrProcedure) {
       return makeRequest.apply(this, arguments)
@@ -28,7 +28,9 @@ addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => {
     const connectionConfig = this.config
 
     return asyncResource.runInAsyncScope(() => {
-      startCh.publish({ queryOrProcedure, connectionConfig })
+      const payload = { queryOrProcedure, connectionConfig }
+      startCh.publish(payload)
+      queryParent[queryField] = payload.sql
 
       const cb = callbackResource.bind(request.callback, request)
       request.callback = asyncResource.bind(function (error) {
@@ -53,14 +55,15 @@ addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => {
   return tedious
 })
 
+// returns [queryOrProcedure, parentObjectToSet, propertyNameToSet]
 function getQueryOrProcedure (request) {
-  if (!request.parameters) return
-
-  const statement = request.parametersByName.statement || request.parametersByName.stmt
-
-  if (!statement) {
-    return request.sqlTextOrProcedure
+  if (!request.parameters) return [null]
+
+  if (request.parametersByName.statement) {
+    return [request.parametersByName.statement.value, request.parametersByName.statement, 'value']
+  } else if (request.parametersByName.stmt) {
+    return [request.parametersByName.stmt.value, request.parametersByName.stmt, 'value']
+  } else {
+    return [request.sqlTextOrProcedure, request, 'sqlTextOrProcedure']
   }
-
-  return statement.value
 }
diff --git a/packages/datadog-plugin-tedious/src/index.js b/packages/datadog-plugin-tedious/src/index.js
index 97eab3617d..7fba730c9d 100644
--- a/packages/datadog-plugin-tedious/src/index.js
+++ b/packages/datadog-plugin-tedious/src/index.js
@@ -8,22 +8,27 @@ class TediousPlugin extends DatabasePlugin {
   static get operation () { return 'request' } // TODO: change to match other database plugins
   static get system () { return 'mssql' }
 
-  start ({ queryOrProcedure, connectionConfig }) {
-    this.startSpan(this.operationName(), {
-      service: this.serviceName({ pluginConfig: this.config, system: this.system }),
-      resource: queryOrProcedure,
+  start (payload) {
+    const service = this.serviceName({ pluginConfig: this.config, system: this.system })
+    const span = this.startSpan(this.operationName(), {
+      service,
+      resource: payload.queryOrProcedure,
       type: 'sql',
       kind: 'client',
       meta: {
         'db.type': 'mssql',
         component: 'tedious',
-        'out.host': connectionConfig.server,
-        [CLIENT_PORT_KEY]: connectionConfig.options.port,
-        'db.user': connectionConfig.userName || connectionConfig.authentication.options.userName,
-        'db.name': connectionConfig.options.database,
-        'db.instance': connectionConfig.options.instanceName
+        'out.host': payload.connectionConfig.server,
+        [CLIENT_PORT_KEY]: payload.connectionConfig.options.port,
+        'db.user': payload.connectionConfig.userName || payload.connectionConfig.authentication.options.userName,
+        'db.name': payload.connectionConfig.options.database,
+        'db.instance': payload.connectionConfig.options.instanceName
       }
     })
+
+    // SQL Server includes comments when caching queries
+    // For that reason we allow service mode but not full mode
+    payload.sql = this.injectDbmQuery(span, payload.queryOrProcedure, service, true)
   }
 }
 
diff --git a/packages/datadog-plugin-tedious/test/index.spec.js b/packages/datadog-plugin-tedious/test/index.spec.js
index e3fc9479b7..702714b2c2 100644
--- a/packages/datadog-plugin-tedious/test/index.spec.js
+++ b/packages/datadog-plugin-tedious/test/index.spec.js
@@ -12,7 +12,6 @@ describe('Plugin', () => {
   let tds
   let tracer
   let connection
-  let connectionIsClosed
 
   withVersions('tedious', 'tedious', version => {
     beforeEach(() => {
@@ -53,7 +52,6 @@ describe('Plugin', () => {
           config.password = MSSQL_PASSWORD
         }
 
-        connectionIsClosed = false
         connection = new tds.Connection(config)
           .on('connect', done)
 
@@ -64,12 +62,8 @@ describe('Plugin', () => {
       })
 
       afterEach(function (done) {
-        if (connectionIsClosed) {
-          done()
-        } else {
-          connection.on('end', () => done())
-          connection.close()
-        }
+        connection.on('end', () => done())
+        connection.close()
       })
 
       withNamingSchema(
@@ -405,5 +399,68 @@ describe('Plugin', () => {
         })
       }
     })
+
+    // it's a pretty old version with a different enough API that I don't think it's worth supporting
+    const testDbm = semver.intersects(version, '<10') ? describe.skip : describe
+    testDbm('with configuration and DBM enabled', () => {
+      let config
+      let tds
+      let connection
+
+      beforeEach(() => {
+        return agent.load('tedious', { dbmPropagationMode: 'service', service: 'custom' }).then(() => {
+          tds = require(`../../../versions/tedious@${version}`).get()
+        })
+      })
+
+      afterEach(() => {
+        return agent.close({ ritmReset: false })
+      })
+
+      beforeEach((done) => {
+        config = {
+          server: 'localhost',
+          options: {
+            database: 'master',
+            trustServerCertificate: true
+          },
+          authentication: {
+            options: {
+              userName: MSSQL_USERNAME,
+              password: MSSQL_PASSWORD
+            },
+            type: 'default'
+          }
+        }
+
+        connection = new tds.Connection(config)
+          .on('connect', done)
+
+        connection.connect()
+      })
+
+      afterEach(function (done) {
+        connection.on('end', () => done())
+        connection.close()
+      })
+
+      it('should inject the correct DBM comment into query but not into trace', done => {
+        const query = 'SELECT 1 + 1 AS solution'
+
+        const request = new tds.Request(query, (err) => {
+          if (err) return done(err)
+          promise.then(done, done)
+        })
+
+        const promise = agent
+          .use(traces => {
+            expect(traces[0][0]).to.have.property('resource', 'SELECT 1 + 1 AS solution')
+            expect(request.sqlTextOrProcedure).to.equal("/*dddb='master',dddbs='custom',dde='tester'," +
+              "ddh='localhost',ddps='test',ddpv='10.8.2'*/ SELECT 1 + 1 AS solution")
+          })
+
+        connection.execSql(request)
+      })
+    })
   })
 })
diff --git a/packages/dd-trace/src/plugins/database.js b/packages/dd-trace/src/plugins/database.js
index cd68813376..dc0c53c982 100644
--- a/packages/dd-trace/src/plugins/database.js
+++ b/packages/dd-trace/src/plugins/database.js
@@ -63,7 +63,7 @@ class DatabasePlugin extends StoragePlugin {
     return tracerService
   }
 
-  createDbmComment (span, serviceName, isPreparedStatement = false) {
+  createDbmComment (span, serviceName, disableFullMode = false) {
     const mode = this.config.dbmPropagationMode
     const dbmService = this.getDbmServiceName(span, serviceName)
 
@@ -73,7 +73,7 @@ class DatabasePlugin extends StoragePlugin {
 
     const servicePropagation = this.createDBMPropagationCommentService(dbmService, span)
 
-    if (isPreparedStatement || mode === 'service') {
+    if (disableFullMode || mode === 'service') {
       return servicePropagation
     } else if (mode === 'full') {
       span.setTag('_dd.dbm_trace_injected', 'true')
@@ -82,8 +82,8 @@ class DatabasePlugin extends StoragePlugin {
     }
   }
 
-  injectDbmQuery (span, query, serviceName, isPreparedStatement = false) {
-    const dbmTraceComment = this.createDbmComment(span, serviceName, isPreparedStatement)
+  injectDbmQuery (span, query, serviceName, disableFullMode = false) {
+    const dbmTraceComment = this.createDbmComment(span, serviceName, disableFullMode)
 
     if (!dbmTraceComment) {
       return query


Reviews:
user: login: tlhunter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T22:26:07Z
user: login: tlhunter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T22:26:23Z
user: login: sabrenner
body: logic here lgtm, might be worth getting a look from DBM folks too to see if the content of the modified query looks ok!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T19:12:25Z
user: login: tlhunter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T19:58:04Z

Review comments:
diff_hunk: @@ -53,7 +52,6 @@ describe('Plugin', () => {
           config.password = MSSQL_PASSWORD
         }
 
-        connectionIsClosed = false
path: packages/datadog-plugin-tedious/test/index.spec.js
user: login: tlhunter
body: This was never written to
created_at: 2025-03-14T22:26:07Z
author_association: MEMBER
diff_hunk: @@ -63,7 +63,7 @@ class DatabasePlugin extends StoragePlugin {
     return tracerService
   }
 
-  createDbmComment (span, serviceName, isPreparedStatement = false) {
+  createDbmComment (span, serviceName, disableFullMode = false) {
path: packages/dd-trace/src/plugins/database.js
user: login: tlhunter
body: Renaming this to be more universal
created_at: 2025-03-14T22:26:23Z
author_association: MEMBER
diff_hunk: @@ -53,14 +55,15 @@ addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => {
   return tedious
 })
 
+// returns [queryOrProcedure, parentObjectToSet, propertyNameToSet]
 function getQueryOrProcedure (request) {
-  if (!request.parameters) return
-
-  const statement = request.parametersByName.statement || request.parametersByName.stmt
-
-  if (!statement) {
-    return request.sqlTextOrProcedure
+  if (!request.parameters) return [null]
+
+  if (request.parametersByName.statement) {
+    return [request.parametersByName.statement.value, request.parametersByName.statement, 'value']
path: packages/datadog-instrumentations/src/tedious.js
user: login: sabrenner
body: not too familiar with `request.parametersByName.statement` or `request.parametersByName.stmt`, but is it possible they don't have a `value` attr? should we `?.` it?
created_at: 2025-03-19T18:57:41Z
author_association: COLLABORATOR
diff_hunk: @@ -28,7 +28,9 @@ addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => {
     const connectionConfig = this.config
 
     return asyncResource.runInAsyncScope(() => {
-      startCh.publish({ queryOrProcedure, connectionConfig })
+      const payload = { queryOrProcedure, connectionConfig }
+      startCh.publish(payload)
+      queryParent[queryField] = payload.sql
path: packages/datadog-instrumentations/src/tedious.js
user: login: sabrenner
body: maybe we could pass `queryParent` and `queryField` on the payload to the start event so we can do this on the subscriber side when doing `injectDbmQuery`, instead of the instrumentation having to know about it and set it here (i think it should be fine, since they're all references, but correct me if i'm wrong). i don't mind much either way. wdyt?
created_at: 2025-03-19T19:10:42Z
author_association: COLLABORATOR
diff_hunk: @@ -28,7 +28,9 @@ addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => {
     const connectionConfig = this.config
 
     return asyncResource.runInAsyncScope(() => {
-      startCh.publish({ queryOrProcedure, connectionConfig })
+      const payload = { queryOrProcedure, connectionConfig }
+      startCh.publish(payload)
+      queryParent[queryField] = payload.sql
path: packages/datadog-instrumentations/src/tedious.js
user: login: tlhunter
body: You might be onto something. Ideally `getQueryOrProcedure` could do the mutation but the publish expects the original object. 
created_at: 2025-03-19T19:58:04Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-06T23:03:20Z
body: 
# Overall package size

Self size: 8.97 MB
Deduped: 101.5 MB
No deduping: 102.01 MB

<details><summary>Dependency sizes</summary>
| name | version | self size | total size |
|------|---------|-----------|------------|
| @datadog/libdatadog | 0.5.0 | 29.83 MB | 29.83 MB |
| @datadog/native-appsec | 8.5.0 | 19.26 MB | 19.26 MB |
| @datadog/native-iast-taint-tracking | 3.3.0 | 13.77 MB | 13.78 MB |
| @datadog/pprof | 5.5.1 | 9.79 MB | 10.17 MB |
| @opentelemetry/core | 1.30.1 | 908.66 kB | 7.16 MB |
| protobufjs | 7.4.0 | 2.77 MB | 5.42 MB |
| @datadog/native-iast-rewriter | 2.8.0 | 2.6 MB | 2.74 MB |
| @datadog/native-metrics | 3.1.0 | 1.06 MB | 1.46 MB |
| @opentelemetry/api | 1.8.0 | 1.21 MB | 1.21 MB |
| import-in-the-middle | 1.13.1 | 117.64 kB | 839.26 kB |
| source-map | 0.7.4 | 226 kB | 226 kB |
| opentracing | 0.14.7 | 194.81 kB | 194.81 kB |
| lru-cache | 7.18.3 | 133.92 kB | 133.92 kB |
| pprof-format | 2.1.0 | 111.69 kB | 111.69 kB |
| @datadog/sketches-js | 2.1.1 | 109.9 kB | 109.9 kB |
| lodash.sortby | 4.7.0 | 75.76 kB | 75.76 kB |
| ignore | 5.3.2 | 53.63 kB | 53.63 kB |
| istanbul-lib-coverage | 3.2.0 | 29.34 kB | 29.34 kB |
| rfdc | 1.4.1 | 27.15 kB | 27.15 kB |
| @isaacs/ttlcache | 1.4.1 | 25.2 kB | 25.2 kB |
| tlhunter-sorted-set | 0.1.0 | 24.94 kB | 24.94 kB |
| dc-polyfill | 0.1.6 | 24.56 kB | 24.56 kB |
| shell-quote | 1.8.2 | 23.54 kB | 23.54 kB |
| limiter | 1.1.5 | 23.17 kB | 23.17 kB |
| retry | 0.13.1 | 18.85 kB | 18.85 kB |
| semifies | 1.0.0 | 15.84 kB | 15.84 kB |
| jest-docblock | 29.7.0 | 8.99 kB | 12.76 kB |
| crypto-randomuuid | 1.0.0 | 11.18 kB | 11.18 kB |
| ttl-set | 1.0.0 | 4.61 kB | 9.69 kB |
| path-to-regexp | 0.1.12 | 6.6 kB | 6.6 kB |
| koalas | 1.0.2 | 6.47 kB | 6.47 kB |
| module-details-from-path | 1.0.3 | 4.47 kB | 4.47 kB |


</details>



<sub>🤖 This report was automatically generated by [heaviest-objects-in-the-universe](https://github.com/qard/heaviest-objects-in-the-universe/)<sub>
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-06T23:03:53Z
body: ## [Codecov](https://app.codecov.io/gh/DataDog/dd-trace-js/pull/5375?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 79.47%. Comparing base [(`4f0481e`)](https://app.codecov.io/gh/DataDog/dd-trace-js/commit/4f0481edd2934ed1e8b8991ef8483686078d3fac?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog) to head [(`54853c1`)](https://app.codecov.io/gh/DataDog/dd-trace-js/commit/54853c177b9c8e6484e5221ec31cfa020bba541f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).
> Report is 12 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5375      +/-   ##
==========================================
- Coverage   79.47%   79.47%   -0.01%     
==========================================
  Files         510      509       -1     
  Lines       22819    22811       -8     
==========================================
- Hits        18136    18128       -8     
  Misses       4683     4683              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/DataDog/dd-trace-js/pull/5375?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DataDog).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: datadog-datadog-prod-us1[bot]
created_at: 2025-03-06T23:08:36Z
body: 
## Datadog Report

Branch report: [`tlhunter/mssql-dbm-service-mode`](https://app.datadoghq.com/ci/test-branch/github.com%2FDataDog%2Fdd-trace-js/dd-trace-js-integration-tests/tlhunter%2Fmssql-dbm-service-mode?env=none&index=citest&_user_click_origin_=github)
Commit report: [`c985ead`](https://app.datadoghq.com/ci/test-commit/github.com%2FDataDog%2Fdd-trace-js/dd-trace-js-integration-tests/tlhunter%2Fmssql-dbm-service-mode/c985ead9da0a71be5b4bc9bd55e01fb36e673fbe?env=none&index=citest&_user_click_origin_=github)
Test service: `dd-trace-js-integration-tests`

:white_check_mark: 0 Failed, 807 Passed, 0 Skipped, 18m 43.03s Total Time




author_association: NONE
user: login: pr-commenter[bot]
created_at: 2025-03-06T23:09:24Z
body: ## Benchmarks

Benchmark execution time: 2025-03-14 22:17:40

Comparing candidate commit 54853c177b9c8e6484e5221ec31cfa020bba541f in PR branch `tlhunter/mssql-dbm-service-mode` with baseline commit 4f0481edd2934ed1e8b8991ef8483686078d3fac in branch `master`.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 916 metrics, 17 unstable metrics.


author_association: NONE