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: dropped span stats #2707

Merged
merged 40 commits into from
Jun 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b2edc03
feat: in progress
May 13, 2022
7a1cf0e
feat: remove logging
May 13, 2022
4d886b0
feat: make max span test fit
May 15, 2022
84fa951
fix: lint
May 15, 2022
f6a5afa
fix: lint
May 15, 2022
6736d5b
fix: lint
May 15, 2022
7169ef3
fix: correcting comment
May 15, 2022
e0825e5
replace hit-transaction-max-spans.js OTel Bridge test case with creat…
trentm May 17, 2022
e0e961c
fix lint and remove debugging printf
trentm May 17, 2022
f57cc51
test: fix running benchmarks (#2693)
trentm May 13, 2022
0b15274
test: skip [email protected] in TAV tests to workaround npm v6 install iss…
trentm May 16, 2022
f6bb9e6
add traceContinuationStrategy config option; add Span Links API (#2692)
trentm May 16, 2022
9261276
feat: add "nodejs16.x" lambda runtime to our compatible runtimes (#2697)
trentm May 17, 2022
44e56dd
synchronize json schema specs (#2702)
apmmachine May 18, 2022
d31e33c
refactor: RunContext.exitSpan() -> .leaveSpan() (#2703)
trentm May 18, 2022
3e76c3b
chore: deprecate 'hapi' package instrumentation (#2698)
trentm May 18, 2022
ac19339
chore(deps-dev): bump pug from 2.0.4 to 3.0.1 (#2690)
dependabot[bot] May 18, 2022
eec972b
synchronize json schema specs (#2704)
apmmachine May 19, 2022
152df0c
fix: move isRecoded check
May 19, 2022
3e6c99e
Merge branch 'main' into astorm/span-stats-create-dropped-span
May 19, 2022
863d6ad
feat: dropped span stats object
May 19, 2022
ebd452f
feat: added to transaction
May 19, 2022
30ea8a3
fix: lint
May 19, 2022
a714f81
fix: tests passing
May 20, 2022
88d7298
fix: changelog entry
May 20, 2022
32d8901
Update CHANGELOG.asciidoc
astorm May 24, 2022
4fa68f8
Update lib/instrumentation/dropped-span-stats.js
astorm May 24, 2022
e455ac7
Update lib/instrumentation/index.js
astorm May 24, 2022
a3a823f
fix: add constant for stats limit
May 24, 2022
2b34124
fix: only send dropped_spans_stats when there's data to send
May 24, 2022
9a2d1a3
fix: test file name
May 24, 2022
8437013
test: add test for full dropped case
May 24, 2022
94279d2
Merge branch 'main' into astorm/span-stats-dropped-span-stats
May 24, 2022
33b5ebc
fix: feedback suggetsions
May 25, 2022
ec1a896
fix: feedback
May 25, 2022
5a7f406
Update lib/instrumentation/dropped-span-stats.js
astorm May 31, 2022
15ecd2a
Update test/instrumentation/transaction.test.js
astorm May 31, 2022
b1cf6b8
fix: comment
Jun 1, 2022
66566e1
Merge branch 'main' into astorm/span-stats-dropped-span-stats
Jun 1, 2022
a5241c7
fix: don't block spans already added to map
Jun 1, 2022
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
2 changes: 2 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ specifying links during span creation (with the limitation that span link
Lambda layers now that
https://aws.amazon.com/blogs/compute/node-js-16-x-runtime-now-available-in-aws-lambda/[this runtime is available on AWS].

- Adds [dropped span statistics](https://github.com/elastic/apm/blob/main/specs/agents/handling-huge-traces/tracing-spans-dropped-stats.md) to transaction payloads allowing APM Server to calculate more accurate throughput metrics.

[float]
===== Bug fixes

Expand Down
58 changes: 58 additions & 0 deletions lib/instrumentation/dropped-span-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict'
const LIMIT_STATS = 128
class DroppedSpanStats {
constructor () {
this.statsMap = new Map()
}

captureDroppedSpan (span) {
const resource = span && span._destination && span._destination.service && span._destination.service.resource
if (!resource || !span._exitSpan) {
return
}

const stats = this.getOrCreateStats(resource, span.outcome)
if (!stats) {
return
}
stats.duration.count++
stats.duration.sum.us += (span._duration * 1000)
return true
astorm marked this conversation as resolved.
Show resolved Hide resolved
}

getOrCreateStats (resource, outcome) {
const key = [resource, outcome].join('')
let stats = this.statsMap.get(key)
if (stats) {
return stats
}

if (this.statsMap.size >= LIMIT_STATS) {
return
}
stats = {
duration: {
count: 0,
sum: {
us: 0
}
},
destination_service_resource: resource,
outcome: outcome
}
this.statsMap.set(key, stats)
return stats
}

encode () {
return Array.from(this.statsMap.values())
}

size () {
return this.statsMap.size
}
}

module.exports = {
DroppedSpanStats
}
1 change: 1 addition & 0 deletions lib/instrumentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ Instrumentation.prototype.addEndedSpan = function (span) {
}

if (!span.isRecorded()) {
span.transaction.captureDroppedSpan(span)
return
astorm marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
9 changes: 9 additions & 0 deletions lib/instrumentation/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var util = require('util')
var ObjectIdentityMap = require('object-identity-map')

const constants = require('../constants')
const { DroppedSpanStats } = require('./dropped-span-stats')
var getPathFromRequest = require('./express-utils').getPathFromRequest
var GenericSpan = require('./generic-span')
var parsers = require('../parsers')
Expand Down Expand Up @@ -87,6 +88,7 @@ function Transaction (agent, name, ...args) {
this._service = undefined
this._message = undefined
this._cloud = undefined
this._droppedSpanStats = new DroppedSpanStats()
this.outcome = constants.OUTCOME_UNKNOWN
}

Expand Down Expand Up @@ -280,6 +282,9 @@ Transaction.prototype.toJSON = function () {
payload.links = this._links
}

if (this._droppedSpanStats.size() > 0) {
payload.dropped_spans_stats = this._droppedSpanStats.encode()
}
return payload
}

Expand Down Expand Up @@ -433,6 +438,10 @@ Transaction.prototype._captureBreakdown = function (span) {
}
}

Transaction.prototype.captureDroppedSpan = function (span) {
return this._droppedSpanStats.captureDroppedSpan(span)
}

function transactionBreakdownDetails ({ name, type } = {}) {
return {
name,
Expand Down
141 changes: 141 additions & 0 deletions test/instrumentation/dropped-span-stats.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use strict'
const agent = require('../..').start({
serviceName: 'test-span-stats',
captureExceptions: false,
metricsInterval: 0,
centralConfig: false,
cloudProvider: 'none',
spanCompressionEnabled: true,
spanCompressionExactMatchMaxDuration: '60ms',
spanCompressionSameKindMaxDuration: '50ms'
})

const tape = require('tape')
const { OUTCOME_FAILURE, OUTCOME_SUCCESS } = require('../../lib/constants')

const destinationContext = {
service: {
resource: 'foo'
}
}
tape.test(function (suite) {
suite.test('test DroppedSpanStats invalid cases', function (test) {
const transaction = agent.startTransaction('trans')
const span = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span.setDestinationContext(destinationContext)
span.setOutcome(OUTCOME_SUCCESS)
span.end()

test.ok(transaction.captureDroppedSpan(span))

test.ok(!transaction.captureDroppedSpan(null))

span.setOutcome(OUTCOME_SUCCESS)
span.setDestinationContext({
service: {}
})
test.ok(!transaction.captureDroppedSpan(span))

transaction.end()
test.end()
})

suite.test('test DroppedSpanStats objects', function (test) {
const transaction = agent.startTransaction('trans')
for (let i = 0; i < 2; i++) {
const span = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span.setDestinationContext(destinationContext)
span.setOutcome(OUTCOME_SUCCESS)
span.end()
test.ok(
transaction.captureDroppedSpan(span)
)
}

for (let i = 0; i < 3; i++) {
const span = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span.setDestinationContext(destinationContext)
span.setOutcome(OUTCOME_FAILURE)
span.end()
test.ok(
transaction.captureDroppedSpan(span)
)
}

for (let i = 0; i < 4; i++) {
const span = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span.setDestinationContext({
service: {
resource: 'bar'
}
})
span.setOutcome(OUTCOME_SUCCESS)
span.end()
span._duration = 1000 // override duration so we can test the sum
test.ok(
transaction.captureDroppedSpan(span)
)
}
transaction.end()

// three distinct resource/outcome pairs captured
test.equals(transaction._droppedSpanStats.statsMap.size, 3)

const payload = transaction._encode()
const stats = payload.dropped_spans_stats
test.equals(stats[0].duration.count, 2)
test.equals(stats[0].destination_service_resource, 'foo')
test.equals(stats[0].outcome, OUTCOME_SUCCESS)

test.equals(stats[1].duration.count, 3)
test.equals(stats[1].destination_service_resource, 'foo')
test.equals(stats[1].outcome, OUTCOME_FAILURE)

test.equals(stats[2].duration.count, 4)
test.equals(stats[2].destination_service_resource, 'bar')
test.equals(stats[2].duration.sum.us, 4000000)
test.equals(stats[2].outcome, OUTCOME_SUCCESS)

test.end()
})

suite.test('test DroppedSpanStats max items', function (test) {
const transaction = agent.startTransaction('trans')
for (let i = 0; i < 128; i++) {
const destinationContext = {
service: {
resource: 'foo' + i
}
}
const span = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span.setDestinationContext(destinationContext)
span.setOutcome(OUTCOME_FAILURE)
span.end()
test.ok(transaction.captureDroppedSpan(span))
}

// one too many
const span = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span.setDestinationContext(destinationContext)
span.setOutcome(OUTCOME_FAILURE)
span.end()
test.ok(!transaction.captureDroppedSpan(span))

// and we're still able to increment spans that fit the previous profile
const span2 = agent.startSpan('foo', 'baz', 'bar', { exitSpan: true })
span2.setDestinationContext({
service: {
resource: 'foo0'
}
})
span2.setOutcome(OUTCOME_FAILURE)
span2.end()
test.ok(transaction.captureDroppedSpan(span2))

transaction.end()
test.equals(transaction._droppedSpanStats.statsMap.size, 128)
test.end()
})

suite.end()
})
10 changes: 8 additions & 2 deletions test/instrumentation/transaction.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,17 @@ test('#_encode() - dropped spans', function (t) {
trans.result = 'result'
var span0 = trans.startSpan('s0', 'type0')
trans.startSpan('s1', 'type1')
var span2 = trans.startSpan()
var span2 = trans.startSpan('s2', { exitSpan: true })
span2.setDestinationContext({
service: {
resource: 'foo'
}
})
if (span2.isRecorded()) {
t.fail('should have dropped the span')
}
span0.end()
span2.end()
trans.end()

agent.flush(function () {
Expand All @@ -527,7 +533,7 @@ test('#_encode() - dropped spans', function (t) {
started: 2,
dropped: 1
})

t.equals(payload.dropped_spans_stats.length, 1)
agent._conf.transactionMaxSpans = oldTransactionMaxSpans
t.end()
})
Expand Down