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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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 @@ -76,6 +76,7 @@
"node": ">=18"
},
"dependencies": {
"@datadog/libdatadog": "^0.1.6",
"@datadog/native-appsec": "8.1.1",
"@datadog/native-iast-rewriter": "2.4.1",
"@datadog/native-iast-taint-tracking": "3.1.0",
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 @@ -571,6 +572,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 @@ -701,6 +703,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))
rochdev marked this conversation as resolved.
Show resolved Hide resolved
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
97 changes: 97 additions & 0 deletions packages/dd-trace/src/crashtracking/crashtracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'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(
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()
5 changes: 5 additions & 0 deletions packages/dd-trace/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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 @@ -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 @@ -424,6 +425,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 @@ -507,6 +509,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('dynamicInstrumentationEnabled', true)
Expand Down Expand Up @@ -604,6 +607,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: 'dynamicInstrumentationEnabled', value: true, origin: 'env_var' },
Expand Down Expand Up @@ -705,6 +709,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