Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add crashtracking with libdatadog native binding #4692

Merged
merged 16 commits into from
Nov 13, 2024
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 2018 Datadog Inc.
rochdev marked this conversation as resolved.
Show resolved Hide resolved
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": ">=18"
},
"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
Loading