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.1",
"@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.initWithReceiver(
rochdev marked this conversation as resolved.
Show resolved Hide resolved
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
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
Loading