Skip to content

Commit

Permalink
add crashtracker
Browse files Browse the repository at this point in the history
  • Loading branch information
rochdev committed Sep 17, 2024
1 parent 421f3d4 commit 24dfa4c
Show file tree
Hide file tree
Showing 12 changed files with 417 additions and 4 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"node": ">=18"
},
"dependencies": {
"@datadog/libdatadog": "^0.1.4",
"@datadog/native-appsec": "8.1.1",
"@datadog/native-iast-rewriter": "2.4.1",
"@datadog/native-iast-taint-tracking": "3.1.0",
Expand Down Expand Up @@ -119,11 +120,11 @@
"dotenv": "16.3.1",
"esbuild": "0.16.12",
"eslint": "^8.57.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-mocha": "^10.4.3",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.4.0",
"eslint-config-standard": "^17.1.0",
"express": "^4.18.2",
"get-port": "^3.2.0",
"glob": "^7.1.6",
Expand Down
3 changes: 3 additions & 0 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ class Config {
this._setValue(defaults, 'appsec.wafTimeout', 5e3) // µs
this._setValue(defaults, 'clientIpEnabled', false)
this._setValue(defaults, 'clientIpHeader', null)
this._setValue(defaults, 'crashtracking.enabled', false)
this._setValue(defaults, 'dbmPropagationMode', 'disabled')
this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1')
this._setValue(defaults, 'dogstatsd.port', '8125')
Expand Down Expand Up @@ -570,6 +571,7 @@ class Config {
DD_APPSEC_RASP_ENABLED,
DD_APPSEC_TRACE_RATE_LIMIT,
DD_APPSEC_WAF_TIMEOUT,
DD_CRASHTRACKING_ENABLED,
DD_DATA_STREAMS_ENABLED,
DD_DBM_PROPAGATION_MODE,
DD_DOGSTATSD_HOSTNAME,
Expand Down Expand Up @@ -699,6 +701,7 @@ class Config {
this._envUnprocessed['appsec.wafTimeout'] = DD_APPSEC_WAF_TIMEOUT
this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED)
this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER)
this._setBoolean(env, 'crashtracking.enabled', coalesce(DD_CRASHTRACKING_ENABLED, !!DD_INJECTION_ENABLED))
this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE)
this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME)
this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT)
Expand Down
95 changes: 95 additions & 0 deletions packages/dd-trace/src/crashtracking/crashtracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict'

const log = require('../log')
const { URL } = require('url')
const libdatadog = require('@datadog/libdatadog')
const binding = libdatadog.load('crashtracker')
const pkg = require('../../../../package.json')

class Crashtracker {
constructor () {
this._started = false
}

configure (config) {
if (!this._started) return

try {
binding.updateConfig(this._getConfig(config))
binding.updateMetadata(this._getMetadata(config))
} catch (e) {
log.error(e)
}
}

start (config) {
if (this._started) return this.configure(config)

this._started = true

try {
binding.initWithReceiver(
this._getConfig(config),
this._getReceiverConfig(config),
this._getMetadata(config),
)
} catch (e) {
log.error(e)
}
}

// TODO: Send only configured values when defaults are fixed.
_getConfig (config) {
const { hostname = '127.0.0.1', port = 8126 } = config
const url = config.url || new URL(`http://${hostname}:${port}`)

return {
additional_files: [],
create_alt_stack: false,
endpoint: {
// TODO: Use the string directly when deserialization is fixed.
url: {
scheme: url.protocol.slice(0, -1),
authority: url.protocol === 'unix'
? Buffer.from(url.pathname).toString('hex')
: url.host,
path_and_query: ''
},
timeout_ms: 3000
},
// TODO: Use `EnabledWithSymbolsInReceiver` instead for Linux when fixed.
resolve_frames: 'EnabledWithInprocessSymbols',
wait_for_receiver: false
}
}

_getMetadata (config) {
const tags = Object.keys(config.tags).map(key => `${key}:${config.tags[key]}`)

return {
library_name: pkg.name,
library_version: pkg.version,
family: 'nodejs',
tags: [
...tags,
'is_crash:true',
'language:javascript',
`library_version:${pkg.version}`,
'runtime:nodejs',
'severity:crash'
]
}
}

_getReceiverConfig () {
return {
args: [],
env: [],
path_to_receiver_binary: libdatadog.find('crashtracker-receiver', true),
stderr_filename: null,
stdout_filename: null,
}
}
}

module.exports = new Crashtracker()
15 changes: 15 additions & 0 deletions packages/dd-trace/src/crashtracking/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict'

const { isMainThread } = require('worker_threads')
const log = require('../log')

if (isMainThread) {
try {
module.exports = require('./crashtracker')
} catch (e) {
log.warn(e.message)
module.exports = require('./noop')
}
} else {
module.exports = require('./noop')
}
8 changes: 8 additions & 0 deletions packages/dd-trace/src/crashtracking/noop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict'

class NoopCrashtracker {
configure () {}
start () {}
}

module.exports = new NoopCrashtracker()
3 changes: 3 additions & 0 deletions packages/dd-trace/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class Tracer extends NoopProxy {

try {
const config = new Config(options) // TODO: support dynamic code config

require('./crashtracking').start(config) // TODO: start earlier in process

telemetry.start(config, this._pluginManager)

if (config.dogstatsd) {
Expand Down
12 changes: 12 additions & 0 deletions packages/dd-trace/test/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ describe('Config', () => {
expect(config).to.have.property('queryStringObfuscation').with.length(626)
expect(config).to.have.property('clientIpEnabled', false)
expect(config).to.have.property('clientIpHeader', null)
expect(config).to.have.nested.property('crashtracking.enabled', false)
expect(config).to.have.property('sampleRate', undefined)
expect(config).to.have.property('runtimeMetrics', false)
expect(config.tags).to.have.property('service', 'node')
Expand Down Expand Up @@ -422,6 +423,7 @@ describe('Config', () => {
process.env.DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP = '.*'
process.env.DD_TRACE_CLIENT_IP_ENABLED = 'true'
process.env.DD_TRACE_CLIENT_IP_HEADER = 'x-true-client-ip'
process.env.DD_CRASHTRACKING_ENABLED = 'true'
process.env.DD_RUNTIME_METRICS_ENABLED = 'true'
process.env.DD_TRACE_REPORT_HOSTNAME = 'true'
process.env.DD_ENV = 'test'
Expand Down Expand Up @@ -504,6 +506,7 @@ describe('Config', () => {
expect(config).to.have.property('queryStringObfuscation', '.*')
expect(config).to.have.property('clientIpEnabled', true)
expect(config).to.have.property('clientIpHeader', 'x-true-client-ip')
expect(config).to.have.nested.property('crashtracking.enabled', true)
expect(config).to.have.property('runtimeMetrics', true)
expect(config).to.have.property('reportHostname', true)
expect(config).to.have.property('env', 'test')
Expand Down Expand Up @@ -600,6 +603,7 @@ describe('Config', () => {
{ name: 'appsec.wafTimeout', value: '42', origin: 'env_var' },
{ name: 'clientIpEnabled', value: true, origin: 'env_var' },
{ name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'env_var' },
{ name: 'crashtracking.enabled', value: true, origin: 'env_var' },
{ name: 'dogstatsd.hostname', value: 'dsd-agent', origin: 'env_var' },
{ name: 'dogstatsd.port', value: '5218', origin: 'env_var' },
{ name: 'env', value: 'test', origin: 'env_var' },
Expand Down Expand Up @@ -700,6 +704,14 @@ describe('Config', () => {
expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['tracecontext'])
})

it('should enable crash tracking for SSI', () => {
process.env.DD_INJECTION_ENABLED = 'tracer'

const config = new Config()

expect(config).to.have.nested.deep.property('crashtracking.enabled', true)
})

it('should initialize from the options', () => {
const logger = {}
const tags = {
Expand Down
134 changes: 134 additions & 0 deletions packages/dd-trace/test/crashtracking/crashtracker.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use strict'

const { expect } = require('chai')
const sinon = require('sinon')
const pkg = require('../../../../package.json')
const proxyquire = require('proxyquire').noCallThru()

require('../setup/tap')

describe('crashtracking', () => {
describe('crashtracker', () => {
let crashtracker
let binding
let config
let crashtrackerConfig
let crashtrackerMetadata
let crashtrackerReceiverConfig
let libdatadog

beforeEach(() => {
config = {
port: 7357,
tags: {
foo: 'bar'
}
}

crashtrackerConfig = {
endpoint: {
url: {
scheme: 'http',
authority: '127.0.0.1:7357',
path_and_query: ''
}
},
resolve_frames: 'EnabledWithInprocessSymbols'
}

crashtrackerReceiverConfig = {
path_to_receiver_binary: '/test/receiver'
}

crashtrackerMetadata = {
tags: [
'foo:bar',
'is_crash:true',
'language:javascript',
`library_version:${pkg.version}`,
'runtime:nodejs',
'severity:crash'
]
}

binding = {
initWithReceiver: sinon.stub(),
updateConfig: sinon.stub(),
updateMetadata: sinon.stub()
}

libdatadog = {
find: sinon.stub(),
load: sinon.stub()
}
libdatadog.find.withArgs('crashtracker-receiver', true).returns('/test/receiver')
libdatadog.load.withArgs('crashtracker').returns(binding)

crashtracker = proxyquire('../../src/crashtracking/crashtracker', {
'@datadog/libdatadog': libdatadog
})
})

describe('start', () => {
it('should initialize the binding', () => {
crashtracker.start(config)

expect(binding.initWithReceiver).to.have.been.calledWithMatch(
crashtrackerConfig,
crashtrackerReceiverConfig,
crashtrackerMetadata
)
})

it('should initialize the binding only once', () => {
crashtracker.start(config)
crashtracker.start(config)

expect(binding.initWithReceiver).to.have.been.calledOnce
})

it('should reconfigure when started multiple times', () => {
crashtracker.start(config)
crashtracker.start(config)

expect(binding.updateConfig).to.have.been.calledWithMatch(crashtrackerConfig)
expect(binding.updateMetadata).to.have.been.calledWithMatch(crashtrackerMetadata)
})

it('should handle errors', () => {
binding.initWithReceiver.throws(new Error('boom'))

crashtracker.start(config)

expect(() => crashtracker.start(config)).to.not.throw()
})
})

describe('configure', () => {
it('should reconfigure the binding when started', () => {
crashtracker.start(config)
crashtracker.configure(config)

expect(binding.updateConfig).to.have.been.calledWithMatch(crashtrackerConfig)
expect(binding.updateMetadata).to.have.been.calledWithMatch(crashtrackerMetadata)
})

it('should reconfigure the binding only when started', () => {
crashtracker.configure(config)

expect(binding.updateConfig).to.not.have.been.called
expect(binding.updateMetadata).to.not.have.been.called
})

it('should handle errors', () => {
binding.updateConfig.throws(new Error('boom'))
binding.updateMetadata.throws(new Error('boom'))

crashtracker.start(config)
crashtracker.configure(config)

expect(() => crashtracker.configure(config)).to.not.throw()
})
})
})
})
Loading

0 comments on commit 24dfa4c

Please sign in to comment.