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 tracer flare #4351

Merged
merged 2 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions packages/dd-trace/src/flare/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict'

const { Writable } = require('stream')

const INITIAL_SIZE = 64 * 1024

class FlareFile extends Writable {
constructor () {
super()

this.length = 0

this._buffer = Buffer.alloc(INITIAL_SIZE)
}

get data () {
return this._buffer.subarray(0, this.length)
}

_write (chunk, encoding, callback) {
const length = Buffer.byteLength(chunk)

this._reserve(length)

if (Buffer.isBuffer(chunk)) {
this.length += chunk.copy(this._buffer, this.length)
} else {
this.length += this._buffer.write(chunk, encoding)
}

callback()
}

_reserve (length) {
while (this.length + length > this._buffer.length) {
const buffer = Buffer.alloc(this.length * 2)

this._buffer.copy(buffer)
this._buffer = buffer
}
}
}

module.exports = FlareFile
98 changes: 98 additions & 0 deletions packages/dd-trace/src/flare/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use strict'

const log = require('../log')
const startupLog = require('../startup-log')
const FlareFile = require('./file')
const { LogChannel } = require('../log/channels')
const request = require('../exporters/common/request')
const FormData = require('../exporters/common/form-data')

const MAX_LOG_SIZE = 12 * 1024 * 1024 // 12MB soft limit
const TIMEOUT = 20 * 1000 * 60

let logChannel = null
let tracerLogs = null
let timer
let tracerConfig = null

const logger = {
debug: (msg) => recordLog(msg),
info: (msg) => recordLog(msg),
warn: (msg) => recordLog(msg),
error: (err) => recordLog(err.stack)
}

const flare = {
enable (tracerConfig_) {
tracerConfig = tracerConfig_
},

disable () {
tracerConfig = null

flare.cleanup()
},

prepare (logLevel) {
if (!tracerConfig) return

logChannel?.unsubscribe(logger)
logChannel = new LogChannel(logLevel)
logChannel.subscribe(logger)
tracerLogs = tracerLogs || new FlareFile()
timer = timer || setTimeout(flare.cleanup, TIMEOUT)
},

send (task) {
if (!tracerConfig) return

const tracerInfo = new FlareFile()

tracerInfo.write(JSON.stringify(startupLog.tracerInfo(), null, 2))

flare._sendFile(task, tracerInfo, 'tracer_info.txt')
flare._sendFile(task, tracerLogs, 'tracer_logs.txt')

flare.cleanup()
},

cleanup () {
logChannel?.unsubscribe(logger)
timer = clearTimeout(timer)
logChannel = null
tracerLogs = null
},

_sendFile (task, file, filename) {
if (!file) return

const form = new FormData()

form.append('case_id', task.case_id)
form.append('hostname', task.hostname)
form.append('email', task.user_handle)
form.append('source', 'tracer_nodejs')
form.append('flare_file', file.data, { filename })

request(form, {
url: tracerConfig.url,
hostname: tracerConfig.hostname,
port: tracerConfig.port,
method: 'POST',
path: '/tracer_flare/v1',
headers: form.getHeaders()
}, (err) => {
if (err) {
log.error(err)
}
})
}
}

function recordLog (msg) {
if (tracerLogs.length > MAX_LOG_SIZE) return

tracerLogs.write(`${msg}\n`) // TODO: gzip
}

module.exports = flare
83 changes: 54 additions & 29 deletions packages/dd-trace/src/log/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,69 @@
const { channel } = require('dc-polyfill')

const Level = {
Debug: 'debug',
Info: 'info',
Warn: 'warn',
Error: 'error'
trace: 20,
debug: 20,
info: 30,
warn: 40,
error: 50,
critical: 50,
off: 100
}

const defaultLevel = Level.Debug
const debugChannel = channel('datadog:log:debug')
const infoChannel = channel('datadog:log:info')
const warnChannel = channel('datadog:log:warn')
const errorChannel = channel('datadog:log:error')

// based on: https://github.com/trentm/node-bunyan#levels
const logChannels = {
[Level.Debug]: createLogChannel(Level.Debug, 20),
[Level.Info]: createLogChannel(Level.Info, 30),
[Level.Warn]: createLogChannel(Level.Warn, 40),
[Level.Error]: createLogChannel(Level.Error, 50)
}
const defaultLevel = Level.debug

function createLogChannel (name, logLevel) {
const logChannel = channel(`datadog:log:${name}`)
logChannel.logLevel = logLevel
return logChannel
function getChannelLogLevel (level) {
return level && typeof level === 'string'
? Level[level.toLowerCase().trim()] || defaultLevel
: defaultLevel
}

function getChannelLogLevel (level) {
let logChannel
if (level && typeof level === 'string') {
logChannel = logChannels[level.toLowerCase().trim()] || logChannels[defaultLevel]
} else {
logChannel = logChannels[defaultLevel]
class LogChannel {
constructor (level) {
this._level = getChannelLogLevel(level)
}

subscribe (logger) {
if (Level.debug >= this._level) {
debugChannel.subscribe(logger.debug)
}
if (Level.info >= this._level) {
infoChannel.subscribe(logger.info)
}
if (Level.warn >= this._level) {
warnChannel.subscribe(logger.warn)
}
if (Level.error >= this._level) {
errorChannel.subscribe(logger.error)
}
}

unsubscribe (logger) {
if (debugChannel.hasSubscribers) {
debugChannel.unsubscribe(logger.debug)
}
if (infoChannel.hasSubscribers) {
infoChannel.unsubscribe(logger.info)
}
if (warnChannel.hasSubscribers) {
warnChannel.unsubscribe(logger.warn)
}
if (errorChannel.hasSubscribers) {
errorChannel.unsubscribe(logger.error)
}
}
return logChannel.logLevel
}

module.exports = {
Level,
getChannelLogLevel,
LogChannel,

debugChannel: logChannels[Level.Debug],
infoChannel: logChannels[Level.Info],
warnChannel: logChannels[Level.Warn],
errorChannel: logChannels[Level.Error]
debugChannel,
infoChannel,
warnChannel,
errorChannel
}
56 changes: 7 additions & 49 deletions packages/dd-trace/src/log/writer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use strict'

const { storage } = require('../../../datadog-core')
const { getChannelLogLevel, debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels')

const { LogChannel } = require('./channels')
const defaultLogger = {
debug: msg => console.debug(msg), /* eslint-disable-line no-console */
info: msg => console.info(msg), /* eslint-disable-line no-console */
Expand All @@ -12,7 +11,7 @@ const defaultLogger = {

let enabled = false
let logger = defaultLogger
let logLevel = getChannelLogLevel()
let logChannel = new LogChannel()

function withNoop (fn) {
const store = storage.getStore()
Expand All @@ -23,45 +22,21 @@ function withNoop (fn) {
}

function unsubscribeAll () {
if (debugChannel.hasSubscribers) {
debugChannel.unsubscribe(onDebug)
}
if (infoChannel.hasSubscribers) {
infoChannel.unsubscribe(onInfo)
}
if (warnChannel.hasSubscribers) {
warnChannel.unsubscribe(onWarn)
}
if (errorChannel.hasSubscribers) {
errorChannel.unsubscribe(onError)
}
logChannel.unsubscribe({ debug, info, warn, error })
}

function toggleSubscription (enable) {
function toggleSubscription (enable, level) {
unsubscribeAll()

if (enable) {
if (debugChannel.logLevel >= logLevel) {
debugChannel.subscribe(onDebug)
}
if (infoChannel.logLevel >= logLevel) {
infoChannel.subscribe(onInfo)
}
if (warnChannel.logLevel >= logLevel) {
warnChannel.subscribe(onWarn)
}
if (errorChannel.logLevel >= logLevel) {
errorChannel.subscribe(onError)
}
logChannel = new LogChannel(level)
logChannel.subscribe({ debug, info, warn, error })
}
}

function toggle (enable, level) {
if (level !== undefined) {
logLevel = getChannelLogLevel(level)
}
enabled = enable
toggleSubscription(enabled)
toggleSubscription(enabled, level)
}

function use (newLogger) {
Expand All @@ -73,26 +48,9 @@ function use (newLogger) {
function reset () {
logger = defaultLogger
enabled = false
logLevel = getChannelLogLevel()
toggleSubscription(false)
}

function onError (err) {
if (enabled) error(err)
}

function onWarn (message) {
if (enabled) warn(message)
}

function onInfo (message) {
if (enabled) info(message)
}

function onDebug (message) {
if (enabled) debug(message)
}

function error (err) {
if (typeof err !== 'object' || !err) {
err = String(err)
Expand Down
20 changes: 20 additions & 0 deletions packages/dd-trace/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Tracer extends NoopProxy {
this._pluginManager = new PluginManager(this)
this.dogstatsd = new NoopDogStatsDClient()
this._tracingInitialized = false
this._flare = new LazyModule(() => require('./flare'))

// these requires must work with esm bundler
this._modules = {
Expand Down Expand Up @@ -90,6 +91,25 @@ class Tracer extends NoopProxy {
}
this._enableOrDisableTracing(config)
})

rc.on('AGENT_CONFIG', (action, conf) => {
if (!conf?.name?.startsWith('flare-log-level.')) return

if (action === 'unapply') {
this._flare.disable()
} else if (conf.config?.log_level) {
this._flare.enable(config)
this._flare.module.prepare(conf.config.log_level)
}
})

rc.on('AGENT_TASK', (action, conf) => {
if (action === 'unapply' || !conf) return
if (conf.task_type !== 'tracer_flare' || !conf.args) return

this._flare.enable(config)
this._flare.module.send(conf.args)
})
}

if (config.isGCPFunction || config.isAzureFunction) {
Expand Down
Loading
Loading