dd-trace
Version:
Datadog APM tracing client for JavaScript
184 lines (163 loc) • 5.3 kB
JavaScript
const http = require('http')
const { URL } = require('url')
const log = require('../../log')
const telemetryMetrics = require('../../telemetry/metrics')
const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
/**
* Base class for OTLP HTTP exporters.
*
* This implementation follows the OTLP HTTP v1.7.0 specification:
* https://opentelemetry.io/docs/specs/otlp/#otlphttp
*
* @class OtlpHttpExporterBase
*/
class OtlpHttpExporterBase {
/**
* Creates a new OtlpHttpExporterBase instance.
*
* @param {string} url - OTLP endpoint URL
* @param {string|undefined} headers - Additional HTTP headers as comma-separated key=value string
* @param {number} timeout - Request timeout in milliseconds
* @param {string} protocol - OTLP protocol (http/protobuf or http/json)
* @param {string} defaultPath - Default path to use if URL has no path
* @param {string} signalType - Signal type for error messages (e.g., 'logs', 'metrics')
*/
constructor (url, headers, timeout, protocol, defaultPath, signalType) {
const parsedUrl = new URL(url)
this.protocol = protocol
this.signalType = signalType
// If no path is provided, use default path
const path = parsedUrl.pathname === '/' ? defaultPath : parsedUrl.pathname
const isJson = protocol === 'http/json'
this.options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: path + parsedUrl.search,
method: 'POST',
timeout,
headers: {
'Content-Type': isJson ? 'application/json' : 'application/x-protobuf',
...this.#parseAdditionalHeaders(headers),
},
}
this.telemetryTags = [
'protocol:http',
`encoding:${isJson ? 'json' : 'protobuf'}`,
]
}
/**
* Records telemetry metrics for exported data.
* @param {string} metricName - Name of the metric to record
* @param {number} count - Count to increment
* @param {Array<string>} [additionalTags] - Optional custom tags (defaults to this exporter's tags)
* @protected
*/
recordTelemetry (metricName, count, additionalTags) {
if (additionalTags?.length > 0) {
tracerMetrics.count(metricName, [...this.telemetryTags, ...additionalTags || []]).inc(count)
} else {
tracerMetrics.count(metricName, this.telemetryTags).inc(count)
}
}
/**
* Sends the payload via HTTP request.
* @param {Buffer|string} payload - The payload to send
* @param {Function} resultCallback - Callback for the result
* @protected
*/
sendPayload (payload, resultCallback) {
const options = {
...this.options,
headers: {
...this.options.headers,
'Content-Length': payload.length,
},
}
const req = http.request(options, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.once('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resultCallback({ code: 0 })
} else {
const error = new Error(`HTTP ${res.statusCode}: ${data}`)
resultCallback({ code: 1, error })
}
})
})
req.on('error', (error) => {
log.error('Error sending OTLP %s:', this.signalType, error)
resultCallback({ code: 1, error })
})
req.once('timeout', () => {
req.destroy()
const error = new Error('Request timeout')
resultCallback({ code: 1, error })
})
req.write(payload)
req.end()
}
/**
* Parses additional HTTP headers from a comma-separated string or pre-parsed map.
* @param {string|Record<string, string>} [headersString=''] - Comma-separated key=value pairs or map
* @returns {Record<string, string>} Parsed headers object
*/
#parseAdditionalHeaders (headersString = '') {
if (headersString !== null && typeof headersString === 'object') {
// The config MAP parser uses tagger.add (which splits on ':'), so OTEL-format
// headers ('key=value') arrive with the full 'key=value' string as the map key
// and an empty string as the value. Re-split on '=' to get the correct pairs.
const result = {}
for (const [k, v] of Object.entries(headersString)) {
if (v === '' && k.includes('=')) {
const idx = k.indexOf('=')
result[k.slice(0, idx).trim()] = k.slice(idx + 1).trim()
} else {
result[k] = v
}
}
return result
}
const headers = {}
let key = ''
let value = ''
let readingKey = true
for (const char of headersString) {
if (readingKey) {
if (char === '=') {
readingKey = false
key = key.trim()
} else {
key += char
}
} else if (char === ',') {
value = value.trim()
if (key && value) {
headers[key] = value
}
key = ''
value = ''
readingKey = true
} else {
value += char
}
}
// Add the last pair if present
if (!readingKey) {
value = value.trim()
if (value) {
headers[key] = value
}
}
return headers
}
/**
* Shuts down the exporter.
* Subclasses can override to add cleanup logic.
*/
shutdown () {}
}
module.exports = OtlpHttpExporterBase