Skip to content

Commit

Permalink
add crashtracking with libdatadog native binding (#4692)
Browse files Browse the repository at this point in the history
  • Loading branch information
rochdev committed Nov 13, 2024
1 parent b6eebfc commit cae20c0
Show file tree
Hide file tree
Showing 13 changed files with 385 additions and 1 deletion.
1 change: 1 addition & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Component,Origin,License,Copyright
require,@datadog/libdatadog,Apache license 2.0,Copyright 2024 Datadog Inc.
require,@datadog/native-appsec,Apache license 2.0,Copyright 2018 Datadog Inc.
require,@datadog/native-metrics,Apache license 2.0,Copyright 2018 Datadog Inc.
require,@datadog/native-iast-rewriter,Apache license 2.0,Copyright 2018 Datadog Inc.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"node": ">=16"
},
"dependencies": {
"@datadog/libdatadog": "^0.2.2",
"@datadog/native-appsec": "8.2.1",
"@datadog/native-iast-rewriter": "2.5.0",
"@datadog/native-iast-taint-tracking": "3.2.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ class Config {
this._setValue(defaults, 'ciVisibilityTestSessionName', '')
this._setValue(defaults, 'clientIpEnabled', false)
this._setValue(defaults, 'clientIpHeader', null)
this._setValue(defaults, 'crashtracking.enabled', false)
this._setValue(defaults, 'codeOriginForSpans.enabled', false)
this._setValue(defaults, 'dbmPropagationMode', 'disabled')
this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1')
Expand Down Expand Up @@ -586,6 +587,7 @@ class Config {
DD_APPSEC_RASP_ENABLED,
DD_APPSEC_TRACE_RATE_LIMIT,
DD_APPSEC_WAF_TIMEOUT,
DD_CRASHTRACKING_ENABLED,
DD_CODE_ORIGIN_FOR_SPANS_ENABLED,
DD_DATA_STREAMS_ENABLED,
DD_DBM_PROPAGATION_MODE,
Expand Down Expand Up @@ -730,6 +732,7 @@ class Config {
this._setValue(env, 'baggageMaxItems', DD_TRACE_BAGGAGE_MAX_ITEMS)
this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED)
this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER)
this._setBoolean(env, 'crashtracking.enabled', DD_CRASHTRACKING_ENABLED)
this._setBoolean(env, 'codeOriginForSpans.enabled', DD_CODE_ORIGIN_FOR_SPANS_ENABLED)
this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE)
this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME)
Expand Down Expand Up @@ -1138,6 +1141,9 @@ class Config {
if (iastEnabled || ['auto', 'true'].includes(profilingEnabled) || injectionIncludesProfiler) {
this._setBoolean(calc, 'telemetry.logCollection', true)
}
if (this._env.injectionEnabled?.length > 0) {
this._setBoolean(calc, 'crashtracking.enabled', true)
}
}

_applyRemote (options) {
Expand Down
98 changes: 98 additions & 0 deletions packages/dd-trace/src/crashtracking/crashtracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'

// Load binding first to not import other modules if it throws
const libdatadog = require('@datadog/libdatadog')
const binding = libdatadog.load('crashtracker')

const log = require('../log')
const { URL } = require('url')
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.init(
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: true,
use_alt_stack: true,
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
},
timeout_ms: 0,
// TODO: Use `EnabledWithSymbolsInReceiver` instead for Linux when fixed.
resolve_frames: 'EnabledWithInprocessSymbols'
}
}

_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()
5 changes: 5 additions & 0 deletions packages/dd-trace/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class Tracer extends NoopProxy {

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

if (config.crashtracking.enabled) {
require('./crashtracking').start(config)
}

telemetry.start(config, this._pluginManager)

if (config.dogstatsd) {
Expand Down
21 changes: 21 additions & 0 deletions packages/dd-trace/test/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,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 @@ -440,6 +441,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 @@ -529,6 +531,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.grpc.client.error.statuses).to.deep.equal([3, 13, 400, 401, 402, 403])
expect(config.grpc.server.error.statuses).to.deep.equal([3, 13, 400, 401, 402, 403])
expect(config).to.have.property('runtimeMetrics', true)
Expand Down Expand Up @@ -633,6 +636,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: 'codeOriginForSpans.enabled', value: true, origin: 'env_var' },
{ name: 'dogstatsd.hostname', value: 'dsd-agent', origin: 'env_var' },
{ name: 'dogstatsd.port', value: '5218', origin: 'env_var' },
Expand Down Expand Up @@ -738,6 +742,23 @@ describe('Config', () => {
expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['tracecontext'])
})

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

const config = new Config()

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

it('should disable crash tracking for SSI when configured', () => {
process.env.DD_CRASHTRACKING_ENABLED = 'false'
process.env.DD_INJECTION_ENABLED = 'tracer'

const config = new Config()

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

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

const { expect } = require('chai')
const sinon = require('sinon')
const proxyquire = require('proxyquire').noCallThru()

require('../setup/tap')

describe('crashtracking', () => {
describe('crashtracker', () => {
let crashtracker
let binding
let config
let libdatadog
let log

beforeEach(() => {
libdatadog = require('@datadog/libdatadog')

binding = libdatadog.load('crashtracker')

config = {
port: 7357,
tags: {
foo: 'bar'
}
}

log = {
error: sinon.stub()
}

sinon.spy(binding, 'init')
sinon.spy(binding, 'updateConfig')
sinon.spy(binding, 'updateMetadata')

crashtracker = proxyquire('../../src/crashtracking/crashtracker', {
'../log': log
})
})

afterEach(() => {
binding.init.restore()
binding.updateConfig.restore()
binding.updateMetadata.restore()
})

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

expect(binding.init).to.have.been.called
expect(log.error).to.not.have.been.called
})

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

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

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

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

it('should handle errors', () => {
crashtracker.start(null)

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.called
expect(binding.updateMetadata).to.have.been.called
})

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', () => {
crashtracker.start(config)
crashtracker.configure(null)

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

0 comments on commit cae20c0

Please sign in to comment.