Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,11 @@
"source-map": "^0.7.6"
},
"devDependencies": {
"@types/busboy": "^1.5.4",
"@types/debug": "^4.1.12",
"@types/node": "^24",
"@types/source-map": "^0.5.7",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"busboy": "^1.6.0",
"eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
Expand Down
25 changes: 14 additions & 11 deletions src/pyroscope-api-exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class PyroscopeApiExporter implements ProfileExporter {
private buildEndpointUrl(profileExport: ProfileExport): URL {
const endpointUrl: URL = new URL(`${this.serverAddress}/ingest`);

endpointUrl.searchParams.append('format', 'pprof');
endpointUrl.searchParams.append(
'from',
dateToUnixTimestamp(profileExport.startedAt).toString()
Expand Down Expand Up @@ -93,34 +94,36 @@ export class PyroscopeApiExporter implements ProfileExporter {
return arrayBuffer;
}

private async buildUploadProfileFormData(
/**
* Serializes the profile into the upload request body: the gzipped pprof
* bytes, sent raw.
*/
private async buildUploadProfileBody(
profile: Profile
): Promise<FormData> {
): Promise<Uint8Array<ArrayBuffer>> {
const processedProfile: Profile = processProfile(profile, {
stripFilenames: this.config.stripFilenames,
shortenPaths: this.config.shortenPaths,
});
const profileBuffer: Buffer = await encode(processedProfile);
const arrayBuffer: Uint8Array<ArrayBuffer> =
this.buildArrayBuffer(profileBuffer);

const formData: FormData = new FormData();
formData.append('profile', new Blob([arrayBuffer]), 'profile');

return formData;
return this.buildArrayBuffer(profileBuffer);
}

private async uploadProfile(profileExport: ProfileExport): Promise<void> {
const formData: FormData = await this.buildUploadProfileFormData(
const body: Uint8Array<ArrayBuffer> = await this.buildUploadProfileBody(
profileExport.profile
);

const headers: Headers = this.buildRequestHeaders();
headers.set('content-type', 'application/octet-stream');

try {
const response = await fetch(
this.buildEndpointUrl(profileExport).toString(),
{
body: formData,
headers: this.buildRequestHeaders(),
body,
headers,
method: 'POST',
}
);
Expand Down
22 changes: 10 additions & 12 deletions test/profiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import process from 'node:process';
import Pyroscope from '../src/index.js';
import { VERSION } from '../src/version.js';
import express from 'express';
import busboy from 'busboy';
import { Profile } from 'pprof-format';
import zlib from 'zlib';

Expand Down Expand Up @@ -51,23 +50,18 @@ const createBackend = (
};
type Numeric = number | bigint;

// Uploads are raw gzipped pprof bytes (`/ingest?format=pprof`).
const extractProfile = (
req: express.Request,
res: express.Response,
callback: (p: Profile, name: string) => void
callback: (p: Profile) => void
) => {
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file) => {
file
.toArray()
.then((values) =>
callback(Profile.decode(zlib.gunzipSync(values[0])), name)
);
});
bb.on('close', () => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => chunks.push(chunk));
req.on('end', () => {
callback(Profile.decode(zlib.gunzipSync(Buffer.concat(chunks))));
res.send('ok');
});
req.pipe(bb);
};

const doWork = (d: number): void => {
Expand Down Expand Up @@ -106,6 +100,7 @@ describe('common behaviour of profilers', () => {
const req = await firstRequest;
await Pyroscope.stopWallProfiling();
assert.strictEqual(req.query.spyName, 'nodespy');
assert.strictEqual(req.query.format, 'pprof');
assertAppNameIncludes(req.query.name, 'nodejs{', ...defaultSemconvTags);
});

Expand Down Expand Up @@ -157,6 +152,7 @@ describe('common behaviour of profilers', () => {
const req = await firstRequest;
await Pyroscope.stopHeapProfiling();
assert.strictEqual(req.query['spyName'], 'nodespy');
assert.strictEqual(req.query['format'], 'pprof');
assertAppNameIncludes(
req.query['name'],
'nodejs{',
Expand Down Expand Up @@ -269,6 +265,7 @@ describe('common behaviour of profilers', () => {
await Pyroscope.stopWallProfiling();

assert.strictEqual(req.query['spyName'], 'nodespy');
assert.strictEqual(req.query['format'], 'pprof');
assertAppNameIncludes(req.query['name'], 'nodejs{', ...defaultSemconvTags);

// ensure we contain everything expected
Expand Down Expand Up @@ -329,6 +326,7 @@ describe('common behaviour of profilers', () => {
await Pyroscope.stopWallProfiling();

assert.strictEqual(req.query['spyName'], 'nodespy');
assert.strictEqual(req.query['format'], 'pprof');
assertAppNameIncludes(req.query['name'], 'nodejs{', ...defaultSemconvTags);
// expect sample, wall and cpu types
assert.deepStrictEqual(sampleType, [
Expand Down
46 changes: 10 additions & 36 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,11 @@ __metadata:
resolution: "@pyroscope/nodejs@workspace:."
dependencies:
"@datadog/pprof": "npm:5.14.4"
"@types/busboy": "npm:^1.5.4"
"@types/debug": "npm:^4.1.12"
"@types/node": "npm:^24"
"@types/source-map": "npm:^0.5.7"
"@typescript-eslint/eslint-plugin": "npm:^8.60.1"
"@typescript-eslint/parser": "npm:^8.60.1"
busboy: "npm:^1.6.0"
debug: "npm:^4.4.3"
eslint: "npm:^10.4.1"
eslint-config-prettier: "npm:^10.1.8"
Expand All @@ -267,15 +266,6 @@ __metadata:
languageName: unknown
linkType: soft

"@types/busboy@npm:^1.5.4":
version: 1.5.4
resolution: "@types/busboy@npm:1.5.4"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/0bdd209069a445d5a71277e558978ec0db1b1a3d108335da96477d017177ae349c4d81d604db0bea19e3c99c10af2d60dbac20e36cef7cb7a517e5a4760738bd
languageName: node
linkType: hard

"@types/debug@npm:^4.1.12":
version: 4.1.12
resolution: "@types/debug@npm:4.1.12"
Expand Down Expand Up @@ -313,12 +303,12 @@ __metadata:
languageName: node
linkType: hard

"@types/node@npm:*":
version: 25.2.3
resolution: "@types/node@npm:25.2.3"
"@types/node@npm:^24":
version: 24.13.3
resolution: "@types/node@npm:24.13.3"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/925833029ce0bb4a72c36f90b93287184d3511aeb0fa60a994ae94b5430c22f9be6693d67d210df79267cb54c6f6978caaefb149d99ab5f83af5827ba7cb9822
undici-types: "npm:~7.18.0"
checksum: 10c0/a5bc08f49b9581dcdca90e02cd77197a3c799840807664942e5cd5161a553d4d2ae8064f7e3294410d748d7d098b5c1848be7fa50c06f29c4193ab16be4e59eb
languageName: node
linkType: hard

Expand Down Expand Up @@ -605,15 +595,6 @@ __metadata:
languageName: node
linkType: hard

"busboy@npm:^1.6.0":
version: 1.6.0
resolution: "busboy@npm:1.6.0"
dependencies:
streamsearch: "npm:^1.1.0"
checksum: 10c0/fa7e836a2b82699b6e074393428b91ae579d4f9e21f5ac468e1b459a244341d722d2d22d10920cdd849743dbece6dca11d72de939fb75a7448825cf2babfba1f
languageName: node
linkType: hard

"bytes@npm:^3.1.2, bytes@npm:~3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
Expand Down Expand Up @@ -2275,13 +2256,6 @@ __metadata:
languageName: node
linkType: hard

"streamsearch@npm:^1.1.0":
version: 1.1.0
resolution: "streamsearch@npm:1.1.0"
checksum: 10c0/fbd9aecc2621364384d157f7e59426f4bfd385e8b424b5aaa79c83a6f5a1c8fd2e4e3289e95de1eb3511cb96bb333d6281a9919fafce760e4edb35b2cd2facab
languageName: node
linkType: hard

"synckit@npm:^0.11.12":
version: 0.11.12
resolution: "synckit@npm:0.11.12"
Expand Down Expand Up @@ -2386,10 +2360,10 @@ __metadata:
languageName: node
linkType: hard

"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
"undici-types@npm:~7.18.0":
version: 7.18.2
resolution: "undici-types@npm:7.18.2"
checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d
languageName: node
linkType: hard

Expand Down
Loading