Skip to content

Commit

Permalink
Shell execution integration (#3608)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Ugaitz Urien <ugaitz.urien@datadoghq.com>
Co-authored-by: Igor Unanua <igor.unanua@datadoghq.com>
  • Loading branch information
3 people authored Jan 30, 2024
1 parent 7542248 commit 7c3157e
Show file tree
Hide file tree
Showing 19 changed files with 1,450 additions and 69 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@ jobs:
uses: ./.github/actions/testagent/logs
- uses: codecov/codecov-action@v3

child_process:
runs-on: ubuntu-latest
env:
PLUGINS: child_process
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/node/setup
- run: yarn install
- uses: ./.github/actions/node/oldest
- run: yarn test:plugins:ci
- uses: ./.github/actions/node/20
- run: yarn test:plugins:ci
- uses: ./.github/actions/node/latest
- run: yarn test:plugins:ci
- uses: codecov/codecov-action@v2

couchbase:
runs-on: ubuntu-latest
services:
Expand Down
1 change: 1 addition & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require,protobufjs,BSD-3-Clause,Copyright 2016 Daniel Wirtz
require,tlhunter-sorted-set,MIT,Copyright (c) 2023 Datadog Inc.
require,retry,MIT,Copyright 2011 Tim Koschützki Felix Geisendörfer
require,semver,ISC,Copyright Isaac Z. Schlueter and Contributors
require,shell-quote,mit,Copyright (c) 2013 James Halliday
dev,@types/node,MIT,Copyright Authors
dev,autocannon,MIT,Copyright 2016 Matteo Collina
dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Expand Down
1 change: 1 addition & 0 deletions ci/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ if (isJestWorker) {
if (shouldInit) {
tracer.init(options)
tracer.use('fs', false)
tracer.use('child_process', false)
}

module.exports = tracer
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"protobufjs": "^7.2.5",
"retry": "^0.13.1",
"semver": "^7.5.4",
"shell-quote": "^1.8.1",
"tlhunter-sorted-set": "^0.1.0"
},
"devDependencies": {
Expand Down
29 changes: 0 additions & 29 deletions packages/datadog-instrumentations/src/child-process.js

This file was deleted.

150 changes: 150 additions & 0 deletions packages/datadog-instrumentations/src/child_process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict'

const util = require('util')

const {
addHook,
AsyncResource
} = require('./helpers/instrument')
const shimmer = require('../../datadog-shimmer')
const dc = require('dc-polyfill')

const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')

// ignored exec method because it calls to execFile directly
const execAsyncMethods = ['execFile', 'spawn']
const execSyncMethods = ['execFileSync', 'spawnSync']

const names = ['child_process', 'node:child_process']

// child_process and node:child_process returns the same object instance, we only want to add hooks once
let patched = false
names.forEach(name => {
addHook({ name }, childProcess => {
if (!patched) {
patched = true
shimmer.massWrap(childProcess, execAsyncMethods, wrapChildProcessAsyncMethod())
shimmer.massWrap(childProcess, execSyncMethods, wrapChildProcessSyncMethod())
shimmer.wrap(childProcess, 'execSync', wrapChildProcessSyncMethod(true))
}

return childProcess
})
})

function normalizeArgs (args, shell) {
const childProcessInfo = {
command: args[0]
}

if (Array.isArray(args[1])) {
childProcessInfo.command = childProcessInfo.command + ' ' + args[1].join(' ')
if (args[2] != null && typeof args[2] === 'object') {
childProcessInfo.options = args[2]
}
} else if (args[1] != null && typeof args[1] === 'object') {
childProcessInfo.options = args[1]
}
childProcessInfo.shell = shell ||
childProcessInfo.options?.shell === true ||
typeof childProcessInfo.options?.shell === 'string'

return childProcessInfo
}

function wrapChildProcessSyncMethod (shell = false) {
return function wrapMethod (childProcessMethod) {
return function () {
if (!childProcessChannel.start.hasSubscribers || arguments.length === 0) {
return childProcessMethod.apply(this, arguments)
}

const childProcessInfo = normalizeArgs(arguments, shell)

return childProcessChannel.traceSync(
childProcessMethod,
{
command: childProcessInfo.command,
shell: childProcessInfo.shell
},
this,
...arguments)
}
}
}

function wrapChildProcessCustomPromisifyMethod (customPromisifyMethod, shell) {
return function () {
if (!childProcessChannel.start.hasSubscribers || arguments.length === 0) {
return customPromisifyMethod.apply(this, arguments)
}

const childProcessInfo = normalizeArgs(arguments, shell)

return childProcessChannel.tracePromise(
customPromisifyMethod,
{
command: childProcessInfo.command,
shell: childProcessInfo.shell
},
this,
...arguments)
}
}

function wrapChildProcessAsyncMethod (shell = false) {
return function wrapMethod (childProcessMethod) {
function wrappedChildProcessMethod () {
if (!childProcessChannel.start.hasSubscribers || arguments.length === 0) {
return childProcessMethod.apply(this, arguments)
}

const childProcessInfo = normalizeArgs(arguments, shell)

const innerResource = new AsyncResource('bound-anonymous-fn')
return innerResource.runInAsyncScope(() => {
childProcessChannel.start.publish({ command: childProcessInfo.command, shell: childProcessInfo.shell })

const childProcess = childProcessMethod.apply(this, arguments)
if (childProcess) {
let errorExecuted = false

childProcess.on('error', (e) => {
errorExecuted = true
childProcessChannel.error.publish(e)
})

childProcess.on('close', (code) => {
code = code || 0
if (!errorExecuted && code !== 0) {
childProcessChannel.error.publish()
}
childProcessChannel.asyncEnd.publish({
command: childProcessInfo.command,
shell: childProcessInfo.shell,
result: code
})
})
}

return childProcess
})
}

if (childProcessMethod[util.promisify.custom]) {
const wrapedChildProcessCustomPromisifyMethod =
shimmer.wrap(childProcessMethod[util.promisify.custom],
wrapChildProcessCustomPromisifyMethod(childProcessMethod[util.promisify.custom]), shell)

// should do it in this way because the original property is readonly
const descriptor = Object.getOwnPropertyDescriptor(childProcessMethod, util.promisify.custom)
Object.defineProperty(wrappedChildProcessMethod,
util.promisify.custom,
{
...descriptor,
value: wrapedChildProcessCustomPromisifyMethod
})
}
return wrappedChildProcessMethod
}
}
4 changes: 2 additions & 2 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module.exports = {
'body-parser': () => require('../body-parser'),
'bunyan': () => require('../bunyan'),
'cassandra-driver': () => require('../cassandra-driver'),
'child_process': () => require('../child-process'),
'child_process': () => require('../child_process'),
'connect': () => require('../connect'),
'cookie': () => require('../cookie'),
'cookie-parser': () => require('../cookie-parser'),
Expand Down Expand Up @@ -78,7 +78,7 @@ module.exports = {
'mysql2': () => require('../mysql2'),
'net': () => require('../net'),
'next': () => require('../next'),
'node:child_process': () => require('../child-process'),
'node:child_process': () => require('../child_process'),
'node:crypto': () => require('../crypto'),
'node:dns': () => require('../dns'),
'node:http': () => require('../http'),
Expand Down
Loading

0 comments on commit 7c3157e

Please sign in to comment.