dd-trace
Version:
Datadog APM tracing client for JavaScript
215 lines (183 loc) • 4.6 kB
JavaScript
'use strict'
const os = require('os')
const { version } = require('./pkg')
const pkg = require('../../../package.json')
const { LogCollapsingLowestDenseDDSketch } = require('@datadog/sketches-js')
const { ORIGIN_KEY, TOP_LEVEL_KEY } = require('./constants')
const {
MEASURED,
HTTP_STATUS_CODE
} = require('../../../ext/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.errors = 0
this.duration = 0
this.okDistribution = new LogCollapsingLowestDenseDDSketch()
this.errorDistribution = new LogCollapsingLowestDenseDDSketch()
}
record (span) {
const durationNs = span.duration
this.hits++
this.duration += durationNs
if (span.metrics[TOP_LEVEL_KEY]) {
this.topLevelHits++
}
if (span.error) {
this.errors++
this.errorDistribution.accept(durationNs)
} else {
this.okDistribution.accept(durationNs)
}
}
toJSON () {
const {
name,
service,
resource,
type,
statusCode,
synthetics
} = this.aggKey
return {
Name: name,
Service: service,
Resource: resource,
Type: type,
HTTPStatusCode: statusCode,
Synthetics: synthetics,
Hits: this.hits,
TopLevelHits: this.topLevelHits,
Errors: this.errors,
Duration: this.duration,
OkSummary: this.okDistribution.toProto(), // TODO: custom proto encoding
ErrorSummary: this.errorDistribution.toProto() // TODO: custom proto encoding
}
}
}
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'
}
toString () {
return [
this.name,
this.service,
this.resource,
this.type,
this.statusCode,
this.synthetics
].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: {
enabled = false,
interval = 10
},
hostname,
port,
url,
env,
tags,
version
} = {}) {
this.exporter = new SpanStatsExporter({
hostname,
port,
tags,
url
})
this.interval = interval
this.bucketSizeNs = interval * 1e9
this.buckets = new TimeBuckets()
this.hostname = os.hostname()
this.enabled = enabled
this.env = env
this.tags = tags || {}
this.sequence = 0
this.version = version
if (this.enabled) {
this.timer = setInterval(this.onInterval.bind(this), interval * 1e3)
this.timer.unref()
}
}
onInterval () {
const serialized = this._serializeBuckets()
if (!serialized) return
this.exporter.export({
Hostname: this.hostname,
Env: this.env,
Version: this.version || version,
Stats: serialized,
Lang: 'javascript',
TracerVersion: pkg.version,
RuntimeID: this.tags['runtime-id'],
Sequence: ++this.sequence
})
}
onSpanFinished (span) {
if (!this.enabled) return
if (!span.metrics[TOP_LEVEL_KEY] && !span.metrics[MEASURED]) return
const spanEndNs = span.startTime + span.duration
const bucketTime = spanEndNs - (spanEndNs % this.bucketSizeNs)
this.buckets.forTime(bucketTime)
.forSpan(span)
.record(span)
}
_serializeBuckets () {
const { bucketSizeNs } = this
const serializedBuckets = []
for (const [timeNs, bucket] of this.buckets.entries()) {
const bucketAggStats = []
for (const stats of bucket.values()) {
bucketAggStats.push(stats.toJSON())
}
serializedBuckets.push({
Start: timeNs,
Duration: bucketSizeNs,
Stats: bucketAggStats
})
}
this.buckets.clear()
return serializedBuckets
}
}
module.exports = {
SpanAggStats,
SpanAggKey,
SpanBuckets,
TimeBuckets,
SpanStatsProcessor
}