Skip to content

Commit

Permalink
feat: span compression (#2623)
Browse files Browse the repository at this point in the history
* feat: Implement Span Compression Algorithm 

#2600
  • Loading branch information
astorm authored Apr 11, 2022
1 parent 1ce50f3 commit ee7ae7c
Show file tree
Hide file tree
Showing 20 changed files with 804 additions and 12 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ Notes:
[[release-notes-3.x]]
=== Node.js Agent version 3.x
==== Unreleased
[float]
===== Breaking changes
[float]
===== Features
* Adds initial implementaion of
https://github.com/elastic/apm/blob/main/specs/agents/handling-huge-traces/tracing-spans-compress.md[span compression]. To enable, set the `spanCompressionEnabled` configuration field to `true`. ({issues}2100[#2100])
[float]
===== Bug fixes
[[release-notes-3.31.0]]
==== 3.31.0 2022/03/23
Expand Down
54 changes: 54 additions & 0 deletions docs/configuration.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1181,3 +1181,57 @@ require('elastic-apm-node').start({
]
})
----

[[span-compression-enabled]]
==== `spanCompressionEnabled`
* *Type:* Boolean
* *Default:* `false`
* *Env:* `ELASTIC_APM_SPAN_COMPRESSION_ENABLED`

Setting this option to true will enable span compression feature. Span compression reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that some information such as DB statements of all the compressed spans will not be collected.

Example usage:

[source,js]
----
require('elastic-apm-node').start({
spanCompressionEnabled: true
})
----

[[span-compression-exact-match-max-duration]]
==== `spanCompressionExactMatchMaxDuration`
* *Type:* String
* *Default:* `50ms`
* *Env:* `ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION`

Consecutive spans that are exact match and that are under this threshold will be compressed into a single composite span. This option does not apply to composite spans. This reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that the DB statements of all the compressed spans will not be collected.

Supports the duration suffixes ms (milliseconds), s (seconds) and m (minutes).

Example usage:

[source,js]
----
require('elastic-apm-node').start({
spanCompressionExactMatchMaxDuration:'100ms'
})
----

[[span-compression-same-kind-max-duration]]
==== `spanCompressionSameKindMaxDuration`
* *Type:* String
* *Default:* `0ms`
* *Env:* `ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION`

Consecutive spans to the same destination that are under this threshold will be compressed into a single composite span. This option does not apply to composite spans. This reduces the collection, processing, and storage overhead, and removes clutter from the UI. The tradeoff is that the DB statements of all the compressed spans will not be collected.

Example usage:

[source,js]
----
require('elastic-apm-node').start({
spanCompressionSameKindMaxDuration:'0ms'
})
----

3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ declare namespace apm {
sourceLinesErrorLibraryFrames?: number;
sourceLinesSpanAppFrames?: number;
sourceLinesSpanLibraryFrames?: number;
spanCompressionEnabled?: boolean;
spanCompressionExactMatchMaxDuration?: string;
spanCompressionSameKindMaxDuration?: string;
/**
* @deprecated Use `spanStackTraceMinDuration`.
*/
Expand Down
20 changes: 19 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ var DEFAULTS = {
sourceLinesErrorLibraryFrames: 5,
sourceLinesSpanAppFrames: 0,
sourceLinesSpanLibraryFrames: 0,
spanCompressionEnabled: false,
spanCompressionExactMatchMaxDuration: '50ms',
spanCompressionSameKindMaxDuration: '0ms',
// 'spanStackTraceMinDuration' is explicitly *not* included in DEFAULTS
// because normalizeSpanStackTraceMinDuration() needs to know if a value
// was provided by the user.
Expand Down Expand Up @@ -136,6 +139,9 @@ var ENV_TABLE = {
sourceLinesErrorLibraryFrames: 'ELASTIC_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES',
sourceLinesSpanAppFrames: 'ELASTIC_APM_SOURCE_LINES_SPAN_APP_FRAMES',
sourceLinesSpanLibraryFrames: 'ELASTIC_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES',
spanCompressionEnabled: 'ELASTIC_APM_SPAN_COMPRESSION_ENABLED',
spanCompressionExactMatchMaxDuration: 'ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION',
spanCompressionSameKindMaxDuration: 'ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION',
spanStackTraceMinDuration: 'ELASTIC_APM_SPAN_STACK_TRACE_MIN_DURATION',
spanFramesMinDuration: 'ELASTIC_APM_SPAN_FRAMES_MIN_DURATION',
stackTraceLimit: 'ELASTIC_APM_STACK_TRACE_LIMIT',
Expand Down Expand Up @@ -173,6 +179,7 @@ var BOOL_OPTS = [
'instrument',
'instrumentIncomingHTTPRequests',
'logUncaughtExceptions',
'spanCompressionEnabled',
'usePathAsTransactionName',
'verifyServerCert'
]
Expand Down Expand Up @@ -215,7 +222,18 @@ var DURATION_OPTS = [
allowedUnits: ['ms', 's', 'm'],
allowNegative: false
},

{
name: 'spanCompressionExactMatchMaxDuration',
defaultUnit: 'ms',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false
},
{
name: 'spanCompressionSameKindMaxDuration',
defaultUnit: 'ms',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false
},
{
// Deprecated: use `spanStackTraceMinDuration`.
name: 'spanFramesMinDuration',
Expand Down
34 changes: 34 additions & 0 deletions lib/instrumentation/generic-span.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const config = require('../config')
const constants = require('../constants')
const Timer = require('./timer')
const TraceContext = require('../tracecontext')
const { SpanCompression } = require('./span-compression')

module.exports = GenericSpan

function GenericSpan (agent, ...args) {
Expand All @@ -17,13 +19,21 @@ function GenericSpan (agent, ...args) {

this._context = TraceContext.startOrResume(opts.childOf, agent._conf, opts.tracestate)

this._parentSpan = null
if (opts.childOf instanceof GenericSpan) {
this.setParentSpan(opts.childOf)
}
this._compression = new SpanCompression(agent)
this._compression.setBufferedSpan(null)

this._agent = agent
this._labels = null
this._ids = null // Populated by sub-types of GenericSpan

this.timestamp = this._timer.start
this.ended = false
this._duration = null // Duration in milliseconds. Set on `.end()`.
this._endTimestamp = null

this.outcome = constants.OUTCOME_UNKNOWN

Expand Down Expand Up @@ -141,3 +151,27 @@ GenericSpan.prototype._isValidOutcome = function (outcome) {
outcome === constants.OUTCOME_SUCCESS ||
outcome === constants.OUTCOME_UNKNOWN
}

GenericSpan.prototype.setParentSpan = function (span) {
this._parentSpan = span
}

GenericSpan.prototype.getParentSpan = function (span) {
return this._parentSpan
}

GenericSpan.prototype.getBufferedSpan = function () {
return this._compression.getBufferedSpan()
}

GenericSpan.prototype.setBufferedSpan = function (span) {
return this._compression.setBufferedSpan(span)
}

GenericSpan.prototype.isCompositeSameKind = function () {
return this._compression.isCompositeSameKind()
}

GenericSpan.prototype.isComposite = function () {
return this._compression.isComposite()
}
35 changes: 35 additions & 0 deletions lib/instrumentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ Instrumentation.prototype.addEndedTransaction = function (transaction) {
return
}

// if I have ended and I have something buffered, send that buffered thing
if (transaction.getBufferedSpan()) {
this._encodeAndSendSpan(transaction.getBufferedSpan())
}

var payload = agent._transactionFilters.process(transaction._encode())
if (!payload) {
agent.logger.debug('transaction ignored by filter %o', { trans: transaction.id, trace: transaction.traceId })
Expand Down Expand Up @@ -345,6 +350,36 @@ Instrumentation.prototype.addEndedSpan = function (span) {
return
}

if (!this._agent._conf.spanCompressionEnabled) {
this._encodeAndSendSpan(span)
} else {
// if I have ended and I have something buffered, send that buffered thing
if (span.getBufferedSpan()) {
this._encodeAndSendSpan(span.getBufferedSpan())
}

if (span.getParentSpan().ended || !span.isCompressionEligible()) {
const buffered = span.getBufferedSpan()
if (buffered) {
this._encodeAndSendSpan(buffered)
span.setBufferedSpan(null)
}
this._encodeAndSendSpan(span)
} else if (!span.getParentSpan().getBufferedSpan()) {
// span is compressible and there's nothing buffered
// add to buffer, move on
span.getParentSpan().setBufferedSpan(span)
} else if (!span.getParentSpan().getBufferedSpan().tryToCompress(span)) {
// we could not compress span so SEND bufferend span
// and buffer the span we could not compress
this._encodeAndSendSpan(span.getParentSpan().getBufferedSpan())
span.getParentSpan().setBufferedSpan(span)
}
}
}

Instrumentation.prototype._encodeAndSendSpan = function (span) {
const agent = this._agent
// Note this error as an "inflight" event. See Agent#flush().
const inflightEvents = agent._inflightEvents
inflightEvents.add(span.id)
Expand Down
Loading

0 comments on commit ee7ae7c

Please sign in to comment.