-
Notifications
You must be signed in to change notification settings - Fork 31
feat: add profiling middleware fastify #173
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
Merged
jake-kramer
merged 16 commits into
grafana:main
from
monwolf:feature/add-fastify-support
Nov 13, 2025
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
76905b3
feat: add profiling middleware for Express and Fastify
monwolf 5b17fce
Merge branch 'main' into feature/add-fastify-support
jake-kramer 18eba87
Update yarn.lock after main merge
jake-kramer f090243
Remove package-lock.json
jake-kramer 19580cf
Make `express` and `fastify` peerDependencies
jake-kramer a2e5dab
Add `express` and `fastify` back as devDependencies
jake-kramer 89b70fe
yarn lint --fix
jake-kramer b79a811
Fix remaining lint issues
jake-kramer 9607b0e
Update comments
jake-kramer a053801
Merge branch 'main' into feature/add-fastify-support
jake-kramer ca9f9df
fix race condition in module loading
jake-kramer 88cb2e4
tmp fix
jake-kramer 378d39c
Revert "tmp fix"
jake-kramer faa3d33
Revert "fix race condition in module loading"
jake-kramer c3c8068
revert async module loading
jake-kramer a6cbaae
Update package.json
jake-kramer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import debug from 'debug'; | ||
| import type { | ||
| FastifyRequest, | ||
| FastifyReply, | ||
| FastifyPluginCallback, | ||
| } from 'fastify'; | ||
| import { Profile } from 'pprof-format'; | ||
| import { Profiler } from '../profilers/profiler.js'; | ||
| import { PyroscopeProfiler } from '../profilers/pyroscope-profiler.js'; | ||
| import { WallProfilerStartArgs } from '../profilers/wall-profiler.js'; | ||
| import { getProfiler } from '../utils/pyroscope-profiler.js'; | ||
| import { encode } from '@datadog/pprof'; | ||
| import { HeapProfilerStartArgs } from '../profilers/heap-profiler.js'; | ||
|
|
||
| const log = debug('pyroscope'); | ||
|
|
||
| async function collectProfile<TStartArgs>( | ||
| profiler: Profiler<TStartArgs> | ||
| ): Promise<Buffer> { | ||
| const profile: Profile = profiler.profile().profile; | ||
|
|
||
| profiler.stop(); | ||
|
|
||
| return encode(profile); | ||
| } | ||
|
|
||
| async function collectProfileAfterMs<TStartArgs>( | ||
| profiler: Profiler<TStartArgs>, | ||
| args: TStartArgs, | ||
| delayMs: number | ||
| ): Promise<Buffer> { | ||
| profiler.start(args); | ||
|
|
||
| if (delayMs === 0) { | ||
| return collectProfile(profiler); | ||
| } | ||
|
|
||
| return new Promise( | ||
| (resolve: (buffer: Buffer | PromiseLike<Buffer>) => void) => { | ||
| setTimeout(() => { | ||
| resolve(collectProfile(profiler)); | ||
| }, delayMs); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| function collectHeap(): Promise<Buffer> { | ||
| const profiler: PyroscopeProfiler = getProfiler(); | ||
|
|
||
| const heapProfilerArgs: HeapProfilerStartArgs = | ||
| profiler.heapProfiler.startArgs; | ||
| const heapProfiler: Profiler<HeapProfilerStartArgs> = | ||
| profiler.heapProfiler.profiler; | ||
|
|
||
| return collectProfileAfterMs(heapProfiler, heapProfilerArgs, 0); | ||
| } | ||
|
|
||
| function collectWall(ms: number): Promise<Buffer> { | ||
| const profiler: PyroscopeProfiler = getProfiler(); | ||
|
|
||
| const wallProfilerArgs: WallProfilerStartArgs = | ||
| profiler.wallProfiler.startArgs; | ||
| const wallProfiler: Profiler<WallProfilerStartArgs> = | ||
| profiler.wallProfiler.profiler; | ||
|
|
||
| return collectProfileAfterMs(wallProfiler, wallProfilerArgs, ms); | ||
| } | ||
|
|
||
| async function heapHandler( | ||
| request: FastifyRequest, | ||
| reply: FastifyReply | ||
| ): Promise<void> { | ||
| log('Fetching Heap Profile'); | ||
| try { | ||
| const profileBuffer = await collectHeap(); | ||
| reply.status(200).type('application/octet-stream').send(profileBuffer); | ||
| } catch (error: unknown) { | ||
| log('Error collecting Heap', error); | ||
| reply.status(500).send({ error: 'Internal Server Error' }); | ||
| } | ||
| } | ||
|
|
||
| async function wallHandler( | ||
| request: FastifyRequest<{ Querystring: { seconds?: string } }>, | ||
| reply: FastifyReply | ||
| ): Promise<void> { | ||
| log('Fetching Wall Profile'); | ||
| try { | ||
| const seconds = Number(request.query.seconds || 1); | ||
| const profileBuffer = await collectWall(1000 * seconds); | ||
| reply.status(200).type('application/octet-stream').send(profileBuffer); | ||
| } catch (error: unknown) { | ||
| log('Error collecting Wall', error); | ||
| reply.status(500).send({ error: 'Internal Server Error' }); | ||
| } | ||
| } | ||
|
|
||
| const fastifyMiddleware = (): FastifyPluginCallback => { | ||
| const plugin: FastifyPluginCallback = (fastify, _options, done) => { | ||
| // Register route for heap profiling | ||
| fastify.get('/debug/pprof/heap', heapHandler); | ||
|
|
||
| // Register route for wall/CPU profiling | ||
| fastify.get('/debug/pprof/profile', wallHandler); | ||
|
|
||
| done(); | ||
| }; | ||
| return plugin; | ||
| }; | ||
|
|
||
| export default fastifyMiddleware; | ||
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
|
|
||
| import Pyroscope from '../src/index.js'; | ||
| import Fastify from 'fastify'; | ||
|
|
||
| // You only need appName for the pull mode | ||
| Pyroscope.init(); | ||
|
|
||
| describe('fastify middleware', () => { | ||
| it('should be a function', () => { | ||
| expect(typeof Pyroscope.fastifyMiddleware).toBe('function'); | ||
| }); | ||
| it('should respond to cpu calls', async () => { | ||
| const app = Fastify(); | ||
| await app.register(Pyroscope.fastifyMiddleware()); | ||
| const response = await app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/profile?seconds=1', | ||
| }); | ||
| expect(response.statusCode).toBe(200); | ||
| }); | ||
| it('should respond to repetitive cpu calls', async () => { | ||
| const app = Fastify(); | ||
| await app.register(Pyroscope.fastifyMiddleware()); | ||
| const response = await app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/profile?seconds=1', | ||
| }); | ||
| expect(response.statusCode).toBe(200); | ||
| }); | ||
|
|
||
| // it('should respond to simultaneous cpu calls', async () => { | ||
| // const app = Fastify() | ||
| // await app.register(Pyroscope.fastifyMiddleware()) | ||
| // console.log('0', Date.now()/1000); | ||
jake-kramer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // const [response1, response2] = await Promise.all([ | ||
| // app.inject({ | ||
| // method: 'GET', | ||
| // url: '/debug/pprof/profile?seconds=1' | ||
| // }), | ||
| // app.inject({ | ||
| // method: 'GET', | ||
| // url: '/debug/pprof/profile?seconds=1' | ||
| // }), | ||
| // ]) | ||
| // expect(response1.statusCode).toBe(200) | ||
| // expect(response2.statusCode).toBe(200) | ||
| // }) | ||
| it('should respond to heap profiling calls', async () => { | ||
| const app = Fastify(); | ||
| await app.register(Pyroscope.fastifyMiddleware()); | ||
| const response = await app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/heap', | ||
| }); | ||
| expect(response.statusCode).toBe(200); | ||
| }); | ||
| it('should respond to repetitive heap profiling calls', async () => { | ||
| const app = Fastify(); | ||
| await app.register(Pyroscope.fastifyMiddleware()); | ||
| const response = await app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/heap', | ||
| }); | ||
| expect(response.statusCode).toBe(200); | ||
| }); | ||
|
|
||
| it('should respond to simultaneous heap profiling calls', async () => { | ||
| const app = Fastify(); | ||
| await app.register(Pyroscope.fastifyMiddleware()); | ||
| const [response1, response2] = await Promise.all([ | ||
| app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/heap?seconds=1', | ||
| }), | ||
| app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/heap?seconds=1', | ||
| }), | ||
| ]); | ||
| expect(response1.statusCode).toBe(200); | ||
| expect(response2.statusCode).toBe(200); | ||
| }); | ||
|
|
||
| it('should be fine using two middlewares at the same time', async () => { | ||
| const app = Fastify(); | ||
| await app.register(Pyroscope.fastifyMiddleware()); | ||
|
|
||
| const app2 = Fastify(); | ||
| await app2.register(Pyroscope.fastifyMiddleware()); | ||
|
|
||
| const response1 = await app.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/heap', | ||
| }); | ||
| expect(response1.statusCode).toBe(200); | ||
|
|
||
| const response2 = await app2.inject({ | ||
| method: 'GET', | ||
| url: '/debug/pprof/heap', | ||
| }); | ||
| expect(response2.statusCode).toBe(200); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.