dd-trace
Version:
Datadog APM tracing client for JavaScript
182 lines (154 loc) • 5.34 kB
JavaScript
'use strict'
const request = require('../../exporters/common/request')
const { safeJSONStringify } = require('../../exporters/common/util')
const { URL, format } = require('node:url')
const log = require('../../log')
/**
* @typedef {Object} BaseFFEWriterOptions
* @property {number} [interval] - Flush interval in milliseconds
* @property {number} [timeout] - Request timeout in milliseconds
* @property {Object} config - Tracer configuration object
* @property {string} endpoint - API endpoint path
* @property {URL} [agentUrl] - Base URL for the agent
* @property {number} [payloadSizeLimit] - Maximum payload size in bytes
* @property {number} [eventSizeLimit] - Maximum individual event size in bytes
* @property {Object} [headers] - Additional HTTP headers
*/
/**
* BaseFFEWriter is the base class for sending Feature Flagging & Exposure Events payloads to the Datadog Agent.
* @class BaseFFEWriter
*/
class BaseFFEWriter {
/**
* @param {BaseFFEWriterOptions} options - Writer configuration options
*/
constructor ({ interval, timeout, config, endpoint, agentUrl, payloadSizeLimit, eventSizeLimit, headers }) {
this._interval = interval ?? 1000
this._timeout = timeout ?? 5000
this._buffer = []
this._bufferLimit = 1000
this._bufferSize = 0
this._config = config
this._endpoint = endpoint
this._baseUrl = agentUrl ?? this._getAgentUrl()
this._payloadSizeLimit = payloadSizeLimit
this._eventSizeLimit = eventSizeLimit
this._headers = headers || {}
this._requestOptions = {
headers: {
...this._headers,
'Content-Type': 'application/json'
},
method: 'POST',
timeout: this._timeout,
url: this._baseUrl,
path: this._endpoint
}
this._periodic = setInterval(() => {
this.flush()
}, this._interval).unref()
this._beforeExitHandler = () => {
this.destroy()
}
process.once('beforeExit', this._beforeExitHandler)
this._destroyed = false
this._droppedEvents = 0
}
/**
* Appends an event array to the buffer
* @param {Array|Object} events - Event object(s) to append to buffer
*/
append (events) {
const eventArray = Array.isArray(events) ? events : [events]
for (const event of eventArray) {
if (this._buffer.length >= this._bufferLimit) {
log.warn(`${this.constructor.name} event buffer full (limit is ${this._bufferLimit}), dropping event`)
this._droppedEvents++
continue
}
const eventSizeBytes = Buffer.byteLength(JSON.stringify(event))
// Check individual event size limit if configured
if (this._eventSizeLimit && eventSizeBytes > this._eventSizeLimit) {
log.warn(`${this.constructor.name} event size
${eventSizeBytes} bytes exceeds limit ${this._eventSizeLimit}, dropping event`)
this._droppedEvents++
continue
}
// Check if adding this event would exceed payload size limit if configured
if (this._payloadSizeLimit && this._bufferSize + eventSizeBytes > this._payloadSizeLimit) {
log.debug(() => `${this.constructor.name}
buffer size would exceed ${this._payloadSizeLimit} bytes, flushing first`)
this.flush()
}
this._bufferSize += eventSizeBytes
this._buffer.push(event)
}
}
/**
* Flushes all buffered events to the agent
*/
flush () {
if (this._buffer.length === 0) {
return
}
const events = this._buffer
this._buffer = []
this._bufferSize = 0
const payload = this._encode(this.makePayload(events))
log.debug(() => `${this.constructor.name} flushing payload: ${safeJSONStringify(payload)}`)
request(payload, this._requestOptions, (err, resp, code) => {
if (err) {
log.error(`Failed to send events to ${this._baseUrl.href}${this._endpoint}: ${err.message}`)
} else if (code >= 200 && code < 300) {
log.debug(() => `Successfully sent ${events.length} events`)
} else {
log.warn(`Events request returned status ${code}`)
}
})
}
/**
* Override in subclass to customize payload structure
* @param {Array} events - Array of events to be sent
* @returns {object} Formatted payload
*/
makePayload (events) {
// Override in subclass
return events
}
/**
* Cleans up resources and flushes remaining events
*/
destroy () {
if (!this._destroyed) {
log.debug(() => `Stopping ${this.constructor.name}`)
clearInterval(this._periodic)
process.removeListener('beforeExit', this._beforeExitHandler)
this.flush()
this._destroyed = true
if (this._droppedEvents > 0) {
log.warn(`${this.constructor.name} dropped ${this._droppedEvents} events due to buffer overflow`)
}
}
}
/**
* @private
* @returns {URL} Constructs agent URL from config
*/
_getAgentUrl () {
const { hostname, port } = this._config
return this._config.url ?? new URL(format({
protocol: 'http:',
hostname: hostname || 'localhost',
port: port || 8126
}))
}
/**
* @private
* @param {Array<object>} payload - Payload to encode
* @returns {string} JSON-stringified payload
*/
_encode (payload) {
return JSON.stringify(payload)
}
}
module.exports = BaseFFEWriter