Skip to content

Commit 738045c

Browse files
authored
feat: Added ProfilingAggregator that will be used to collect and send pprof_data telemetry (newrelic#3732)
1 parent 2b4d7d8 commit 738045c

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const logger = require('../logger').child({ component: 'pprof_aggregator' })
9+
const BaseAggregator = require('./base-aggregator')
10+
11+
/**
12+
* Serves as a means for transmitting pprof data as it is being collected.
13+
* This doesn't really work as an "aggregator". It instead leverages the `BaseAggregator`
14+
* to integrate with the `Harvester` and `Collector`
15+
*
16+
* The `ProfilingAggregator` aggregator overrides the base `start` method.
17+
* It sets up the interval based on configuration but instead of calling `send`,
18+
* it calls `collectData` which instructs the Profiler to collect profiling data
19+
* for every register profiler(cpu, heap at the moment if enabled)
20+
*
21+
* @private
22+
* @class
23+
*/
24+
class ProfilingAggregator extends BaseAggregator {
25+
constructor(opts = {}, agent) {
26+
const { collector, harvester } = agent
27+
opts.method = opts.method || 'pprof_data'
28+
super(opts, collector, harvester)
29+
this.agent = agent
30+
this.profiler = opts.profiler
31+
this.pprofData = null
32+
}
33+
34+
// simply returns what was given from the profiler collect method
35+
_toPayloadSync() {
36+
return this.pprofData
37+
}
38+
39+
/**
40+
* This overrides the default `start` method
41+
* as we want to collect profiling data for `cpu` and/or `heap`
42+
* and send the gzipped binary encoded data for each profiler
43+
*/
44+
start() {
45+
logger.trace(`${this.method} aggregator started.`)
46+
47+
if (!this.sendTimer) {
48+
this.sendTimer = setInterval(this.collectData.bind(this), this.periodMs)
49+
this.sendTimer.unref()
50+
}
51+
}
52+
53+
/**
54+
* Called on an interval. Iterates over all registered profilers
55+
* and collects data for the given time period. Then asynchronously
56+
* calls send which takes care of sending the data to the collector
57+
*/
58+
collectData() {
59+
for (const profiler of this.profiler.profilers) {
60+
this.pprofData = profiler.collect()
61+
this.send()
62+
}
63+
}
64+
65+
// `pprof_data` is not retained/merged in any way to just return null
66+
_getMergeData() {
67+
return null
68+
}
69+
70+
// this implies the transmission fails
71+
// we log this error in `lib/collector/api.js#_handleResponseCode`
72+
_merge() {
73+
return null
74+
}
75+
76+
// clears the `this.pprofData` for the next iteration of profiling collection
77+
clear() {
78+
this.pprofData = null
79+
}
80+
}
81+
82+
module.exports = ProfilingAggregator

lib/collector/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ function CollectorAPI(agent) {
103103
'sql_trace_data',
104104
'error_event_data',
105105
'span_event_data',
106-
'log_event_data'
106+
'log_event_data',
107+
'pprof_data'
107108
]) {
108109
const method = new RemoteMethod(name, agent, initialEndpoint)
109110
this._methods[name] = method
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const test = require('node:test')
9+
const assert = require('node:assert')
10+
const sinon = require('sinon')
11+
const ProfilingAggregator = require('#agentlib/aggregators/profiling-aggregator.js')
12+
const helper = require('#testlib/agent_helper.js')
13+
const RUN_ID = 1337
14+
15+
test.beforeEach((ctx) => {
16+
const agent = helper.loadMockedAgent()
17+
const cpuProfiler = {
18+
collect() {
19+
return 'cpu profile data'
20+
}
21+
}
22+
23+
const clock = sinon.useFakeTimers()
24+
const heapProfiler = {
25+
collect() {
26+
return 'heap profile data'
27+
}
28+
}
29+
const profiler = {
30+
profilers: [cpuProfiler, heapProfiler]
31+
}
32+
sinon.spy(agent.collector, 'send')
33+
const profilingAggregator = new ProfilingAggregator({ runId: RUN_ID, periodMs: 100, profiler }, agent)
34+
ctx.nr = {
35+
agent,
36+
clock,
37+
profilingAggregator,
38+
profiler
39+
}
40+
})
41+
42+
test.afterEach((ctx) => {
43+
helper.unloadAgent(ctx.nr.agent)
44+
ctx.nr.clock.restore()
45+
ctx.nr.agent.collector.send.restore()
46+
})
47+
48+
test('should set the correct default method', (t) => {
49+
const { profilingAggregator } = t.nr
50+
const method = profilingAggregator.method
51+
assert.equal(method, 'pprof_data')
52+
})
53+
54+
test('should intialize pprofData and profiler', (t) => {
55+
const { profilingAggregator, profiler } = t.nr
56+
assert.deepEqual(profilingAggregator.profiler, profiler)
57+
assert.equal(profilingAggregator.pprofData, null)
58+
})
59+
60+
test('should send 2 messages per interval', (t) => {
61+
const { profilingAggregator, clock, agent } = t.nr
62+
profilingAggregator.start()
63+
assert.equal(agent.collector.send.callCount, 0)
64+
clock.tick(100)
65+
assert.equal(agent.collector.send.callCount, 2)
66+
const [cpuCall, heapCall] = agent.collector.send.args
67+
assert.equal(cpuCall[0], 'pprof_data')
68+
assert.equal(cpuCall[1], 'cpu profile data')
69+
assert.equal(heapCall[0], 'pprof_data')
70+
assert.equal(heapCall[1], 'heap profile data')
71+
assert.equal(profilingAggregator.pprofData, null)
72+
})

0 commit comments

Comments
 (0)