dd-trace
Version:
Datadog APM tracing client for JavaScript
250 lines (225 loc) • 7.09 kB
JavaScript
'use strict'
const os = require('node:os')
const pkg = require('../../../package.json')
const { LogCollapsingLowestDenseDDSketch } = require('../../../vendor/dist/@datadog/sketches-js')
const {
MEASURED,
HTTP_STATUS_CODE,
HTTP_ENDPOINT,
HTTP_ROUTE,
HTTP_METHOD,
SPAN_KIND,
GRPC_STATUS_CODE,
} = require('../../../ext/tags')
const { ORIGIN_KEY, TOP_LEVEL_KEY, SVC_SRC_KEY, GRPC_STATUS_NAMES } = require('./constants')
const { version } = require('./pkg')
const processTags = require('./process-tags')
const { SpanStatsExporter } = require('./exporters/span-stats')
const {
DEFAULT_SPAN_NAME,
DEFAULT_SERVICE_NAME,
} = require('./encode/tags-processors')
class SpanAggStats {
constructor (aggKey) {
this.aggKey = aggKey
this.hits = 0
this.topLevelHits = 0
this.topLevelOkDistribution = new LogCollapsingLowestDenseDDSketch()
this.topLevelErrorDistribution = new LogCollapsingLowestDenseDDSketch()
this.nonTopLevelOkDistribution = new LogCollapsingLowestDenseDDSketch()
this.nonTopLevelErrorDistribution = new LogCollapsingLowestDenseDDSketch()
}
record (span) {
const durationNs = span.duration
this.hits++
const isTopLevel = Boolean(span.metrics[TOP_LEVEL_KEY])
if (isTopLevel) this.topLevelHits++
if (span.error) {
if (isTopLevel) this.topLevelErrorDistribution.accept(durationNs)
else this.nonTopLevelErrorDistribution.accept(durationNs)
} else {
if (isTopLevel) this.topLevelOkDistribution.accept(durationNs)
else this.nonTopLevelOkDistribution.accept(durationNs)
}
}
toJSON () {
const {
name, service, resource, type, statusCode, synthetics, method, endpoint, srvSrc,
spanKind, rpcStatusCode,
} = this.aggKey
const base = {
Name: name,
Service: service,
Resource: resource,
Type: type,
HTTPStatusCode: statusCode,
Synthetics: synthetics,
HTTPMethod: method,
HTTPEndpoint: endpoint,
srv_src: srvSrc,
SpanKind: spanKind,
GRPCStatusCode: rpcStatusCode,
}
const rows = []
if (this.topLevelHits > 0) {
rows.push({
...base,
Hits: this.topLevelHits,
TopLevelHits: this.topLevelHits,
Errors: this.topLevelErrorDistribution.count,
Duration: this.topLevelOkDistribution.sum + this.topLevelErrorDistribution.sum,
OkSummary: this.topLevelOkDistribution.toProto(),
ErrorSummary: this.topLevelErrorDistribution.toProto(),
})
}
const nonTopLevelHits = this.hits - this.topLevelHits
if (nonTopLevelHits > 0) {
rows.push({
...base,
Hits: nonTopLevelHits,
TopLevelHits: 0,
Errors: this.nonTopLevelErrorDistribution.count,
Duration: this.nonTopLevelOkDistribution.sum + this.nonTopLevelErrorDistribution.sum,
OkSummary: this.nonTopLevelOkDistribution.toProto(), // TODO: custom proto encoding
ErrorSummary: this.nonTopLevelErrorDistribution.toProto(), // TODO: custom proto encoding
})
}
return rows
}
}
class SpanAggKey {
constructor (span) {
this.name = span.name || DEFAULT_SPAN_NAME
this.service = span.service || DEFAULT_SERVICE_NAME
this.resource = span.resource || ''
this.type = span.type || ''
this.statusCode = span.meta[HTTP_STATUS_CODE] || 0
this.synthetics = span.meta[ORIGIN_KEY] === 'synthetics'
this.endpoint = span.meta[HTTP_ROUTE] || span.meta[HTTP_ENDPOINT] || ''
this.method = span.meta[HTTP_METHOD] || ''
this.srvSrc = span.meta[SVC_SRC_KEY] || ''
this.spanKind = span.meta[SPAN_KIND] || ''
// dd gRPC plugin sets a numeric code via setTag; OTel/manual sets a string name via meta.
const grpcCode = span.meta[GRPC_STATUS_CODE] ?? span.metrics?.[GRPC_STATUS_CODE]
this.rpcStatusCode = typeof grpcCode === 'number'
? (GRPC_STATUS_NAMES[grpcCode] ?? String(grpcCode))
: (grpcCode ?? '')
}
toString () {
return [
this.name,
this.service,
this.resource,
this.type,
this.statusCode,
this.synthetics,
this.method,
this.endpoint,
this.srvSrc,
this.spanKind,
this.rpcStatusCode,
].join(',')
}
}
class SpanBuckets extends Map {
forSpan (span) {
const aggKey = new SpanAggKey(span)
const key = aggKey.toString()
if (!this.has(key)) {
this.set(key, new SpanAggStats(aggKey))
}
return this.get(key)
}
}
class TimeBuckets extends Map {
forTime (time) {
if (!this.has(time)) {
this.set(time, new SpanBuckets())
}
return this.get(time)
}
}
class SpanStatsProcessor {
constructor ({
stats: {
DD_TRACE_STATS_COMPUTATION_ENABLED: enabled = false,
interval = 10,
} = {},
hostname,
port,
url,
env,
tags,
version: appVersion,
_DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL: flushIntervalMs,
} = {}, otlpExporter) {
if (!otlpExporter) {
this.exporter = new SpanStatsExporter({ hostname, port, tags, url })
}
const intervalMs = otlpExporter ? (flushIntervalMs ?? 10_000) : interval * 1e3
this.interval = intervalMs / 1e3
this.bucketSizeNs = intervalMs * 1e6
this.buckets = new TimeBuckets()
this.hostname = os.hostname()
this.enabled = enabled
this.otlpExporter = otlpExporter || null
this.env = env
this.tags = tags || {}
this.sequence = 0
this.version = appVersion
if (this.enabled || this.otlpExporter) {
this.timer = setInterval(this.onInterval.bind(this), intervalMs)
this.timer.unref?.()
}
}
onInterval () {
const drained = this.#drainBuckets()
if (this.enabled && !this.otlpExporter) {
this.exporter.export({
Hostname: this.hostname,
Env: this.env,
Version: this.version || version,
Stats: this.#toV06Payload(drained),
Lang: 'javascript',
TracerVersion: pkg.version,
RuntimeID: this.tags['runtime-id'],
Sequence: ++this.sequence,
ProcessTags: processTags.serialized,
})
} else if (this.otlpExporter && drained.length > 0) {
this.otlpExporter.export(drained, this.bucketSizeNs)
}
}
onSpanFinished (span) {
if (!this.enabled && !this.otlpExporter) return
if (!span.metrics[TOP_LEVEL_KEY] && !span.metrics[MEASURED]) return
const spanEndNs = span.start + span.duration
const bucketTime = spanEndNs - (spanEndNs % this.bucketSizeNs)
this.buckets.forTime(bucketTime)
.forSpan(span)
.record(span)
}
#drainBuckets () {
const drained = []
for (const [timeNs, bucket] of this.buckets.entries()) {
drained.push({ timeNs, bucket })
}
this.buckets.clear()
return drained
}
#toV06Payload (drained) {
const { bucketSizeNs } = this
return drained.map(({ timeNs, bucket }) => ({
Start: timeNs,
Duration: bucketSizeNs,
Stats: [...bucket.values()].flatMap(stats => stats.toJSON()),
}))
}
}
module.exports = {
SpanAggStats,
SpanAggKey,
SpanBuckets,
TimeBuckets,
SpanStatsProcessor,
}