dd-trace
Version:
Datadog APM tracing client for JavaScript
252 lines (205 loc) • 7.05 kB
JavaScript
'use strict'
const { URL, format } = require('node:url')
const path = require('node:path')
const request = require('../../exporters/common/request')
const { getEnvironmentVariable } = require('../../config/helper')
const logger = require('../../log')
const { encodeUnicode } = require('../util')
const telemetry = require('../telemetry')
const log = require('../../log')
const {
EVP_SUBDOMAIN_HEADER_NAME,
EVP_PROXY_AGENT_BASE_PATH,
} = require('../constants/writers')
const { getAgentUrl } = require('../../agent/url')
const { parseResponseAndLog } = require('./util')
class LLMObsBuffer {
constructor ({ events, size, routing = {}, isDefault = false, limit = 1000 }) {
this.events = events
this.size = size
this.routing = routing
this.isDefault = isDefault
this.limit = limit
}
clear () {
this.events = []
this.size = 0
}
}
class BaseLLMObsWriter {
#destroyer
/** @type {Map<string, LLMObsBuffer>} */
#multiTenantBuffers = new Map()
constructor ({ interval, timeout, eventType, config, endpoint, intake }) {
this._interval = interval ?? getEnvironmentVariable('_DD_LLMOBS_FLUSH_INTERVAL') ?? 1000 // 1s
this._timeout = timeout ?? getEnvironmentVariable('_DD_LLMOBS_TIMEOUT') ?? 5000 // 5s
this._eventType = eventType
/** @type {LLMObsBuffer} */
this._buffer = new LLMObsBuffer({ events: [], size: 0, isDefault: true })
/** @type {import('../../config/config-base')} */
this._config = config
this._endpoint = endpoint
this._baseEndpoint = endpoint // should not be unset
this._intake = intake
this._periodic = setInterval(() => {
this.flush()
}, this._interval)
this._periodic.unref?.()
const destroyer = this.destroy.bind(this)
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(destroyer)
this.#destroyer = destroyer
}
// Split on protocol separator to preserve it
// path.join will remove some slashes unnecessarily
#buildUrl (baseUrl, endpoint) {
const [protocol, rest] = baseUrl.split('://')
return protocol + '://' + path.join(rest, endpoint)
}
get url () {
if (this._agentless == null) return null
return this.#buildUrl(this._baseUrl.href, this._endpoint)
}
_getBuffer (routing) {
if (!routing?.apiKey) {
return this._buffer
}
const apiKey = routing.apiKey
let buffer = this.#multiTenantBuffers.get(apiKey)
if (!buffer) {
buffer = new LLMObsBuffer({ events: [], size: 0, routing })
this.#multiTenantBuffers.set(apiKey, buffer)
}
return buffer
}
/**
* @returns {boolean} `true` if the event was buffered, `false` if it was dropped
* (e.g. the per-routing buffer was full). Callers that depend on the event
* actually being submitted should check this value.
*/
append (event, routing, byteLength) {
const buffer = this._getBuffer(routing)
if (buffer.events.length >= buffer.limit) {
logger.warn(`${this.constructor.name} event buffer full (limit is ${buffer.limit}), dropping event`)
telemetry.recordDroppedPayload(1, this._eventType, 'buffer_full')
return false
}
const eventSize = byteLength || Buffer.byteLength(JSON.stringify(event))
buffer.size += eventSize
buffer.events.push(event)
return true
}
flush () {
if (this._agentless == null) {
return
}
// Flush default buffer
if (this._buffer.events.length > 0) {
const events = this._buffer.events
this._buffer.clear()
const payload = this._encode(this.makePayload(events))
log.debug('Encoded LLMObs payload: %s', payload)
const options = this._getOptions()
request(payload, options, (err, resp, code) => {
parseResponseAndLog(err, code, events.length, this.url, this._eventType)
})
}
// Flush multi-tenant buffers
for (const [apiKey, buffer] of this.#multiTenantBuffers) {
if (buffer.events.length === 0) continue
const events = buffer.events
buffer.clear()
const payload = this._encode(this.makePayload(events))
const site = buffer.routing.site || this._config.site
const options = {
headers: {
'Content-Type': 'application/json',
'DD-API-KEY': apiKey,
},
method: 'POST',
timeout: this._timeout,
url: new URL(format({
protocol: 'https:',
hostname: `${this._intake}.${site}`,
})),
path: this._baseEndpoint,
}
const url = this.#buildUrl(options.url.href, options.path)
const maskedApiKey = apiKey ? `****${apiKey.slice(-4)}` : ''
log.debug('Encoding and flushing multi-tenant buffer for %s', maskedApiKey)
log.debug('Encoded LLMObs payload: %s', payload)
request(payload, options, (err, resp, code) => {
parseResponseAndLog(err, code, events.length, url, this._eventType)
})
}
this.#cleanupEmptyBuffers()
}
#cleanupEmptyBuffers () {
for (const [key, buffer] of this.#multiTenantBuffers) {
if (buffer.events.length === 0) {
this.#multiTenantBuffers.delete(key)
}
}
}
makePayload (events) {}
destroy () {
if (this.#destroyer) {
logger.debug(`Stopping ${this.constructor.name}`)
clearInterval(this._periodic)
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.delete(this.#destroyer)
this.flush()
this.#destroyer = undefined
}
}
setAgentless (agentless) {
this._agentless = agentless
const { url, endpoint } = this._getUrlAndPath()
this._baseUrl = url
this._endpoint = endpoint
logger.debug(`Configuring ${this.constructor.name} to ${this.url}`)
}
_getUrlAndPath () {
if (this._agentless) {
const site = this._config.site
return {
url: new URL(format({
protocol: 'https:',
hostname: `${this._intake}.${site}`,
})),
endpoint: this._endpoint,
}
}
const overrideOriginEnv = getEnvironmentVariable('_DD_LLMOBS_OVERRIDE_ORIGIN')
const overrideOriginUrl = overrideOriginEnv && new URL(overrideOriginEnv)
const base = overrideOriginUrl ?? getAgentUrl(this._config)
return {
url: base,
endpoint: path.join(EVP_PROXY_AGENT_BASE_PATH, this._endpoint),
}
}
_getOptions () {
const options = {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
timeout: this._timeout,
url: this._baseUrl,
path: this._endpoint,
}
if (this._agentless) {
options.headers['DD-API-KEY'] = this._config.apiKey || ''
} else {
options.headers[EVP_SUBDOMAIN_HEADER_NAME] = this._intake
}
return options
}
_encode (payload) {
return JSON.stringify(payload, (key, value) => {
if (typeof value === 'string') {
return encodeUnicode(value) // serialize unicode characters
}
return value
}).replaceAll(String.raw`\\u`, String.raw`\u`) // remove double escaping
}
}
module.exports = BaseLLMObsWriter