Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: span compression #2623

Merged
merged 31 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
astorm marked this conversation as resolved.
Show resolved Hide resolved

[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({
]
})
----

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Note: docs taken from java agent's docs of the same field.

[[span-compression-enabled]]
==== `spanCompressionEnabled`
* *Type:* Boolean
* *Default:* `false`
* *Env:* `ELASTIC_SPAN_COMPRESSION_ENABLED`
astorm marked this conversation as resolved.
Show resolved Hide resolved

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_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION`
astorm marked this conversation as resolved.
Show resolved Hide resolved

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.
astorm marked this conversation as resolved.
Show resolved Hide resolved

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_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION`
astorm marked this conversation as resolved.
Show resolved Hide resolved

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_SPAN_COMPRESSION_ENABLED',
spanCompressionExactMatchMaxDuration: 'ELASTIC_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION',
spanCompressionSameKindMaxDuration: 'ELASTIC_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION',
astorm marked this conversation as resolved.
Show resolved Hide resolved
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',
astorm marked this conversation as resolved.
Show resolved Hide resolved
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()
}
31 changes: 31 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,32 @@ Instrumentation.prototype.addEndedSpan = function (span) {
return
}

if (!this._agent._conf.spanCompressionEnabled) {
astorm marked this conversation as resolved.
Show resolved Hide resolved
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.isCompressionEligible()) {
// span is not compressible -- send
this.encodeAndSendSpan(span)
astorm marked this conversation as resolved.
Show resolved Hide resolved
} 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) {
astorm marked this conversation as resolved.
Show resolved Hide resolved
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