Skip to content

Commit 5da0a22

Browse files
authored
RPC: continuous profiling for Python/JS/Go subprocesses (#7558)
Each language's RPC server now starts the Pyroscope SDK when PYROSCOPE_SERVER_ADDRESS is set in the environment, with PYROSCOPE_TAGS forwarded verbatim and a runtime=python/node/go tag added so flame graphs in a shared `modcli` Pyroscope application can be sliced by which subprocess produced them. Env vars propagate from the parent JVM via ProcessBuilder.environment() inheritance, so the saas-side CliProcessEnvContributor that already drives the JVM agent transparently reaches all four runtimes once these inits land. Python: pyroscope-io is a new optional dep ([profiling] extra) so local dev and CI without the package don't break — _init_pyroscope() warns and no-ops on ImportError. JS: @pyroscope/nodejs is a new optionalDependency so npm install doesn't fail when the binding's native components aren't available; initPyroscope() catches the require and warns. Go: github.com/grafana/pyroscope-go is a regular dep (Go has no optional deps; the SDK is small and pure-Go via stdlib pprof). initPyroscope() short-circuits on missing env so non-profiled deployments pay only one os.Getenv at startup. C# requires no rewrite-side change: CoreCLR loads the Pyroscope native profiler purely from CORECLR_ENABLE_PROFILING / CORECLR_PROFILER / CORECLR_PROFILER_PATH env vars, which the same saas-side contributor sets alongside PYROSCOPE_*.
1 parent bbe3b20 commit 5da0a22

8 files changed

Lines changed: 284 additions & 1 deletion

File tree

rewrite-go/cmd/rpc/main.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"time"
3737

3838
"github.com/google/uuid"
39+
"github.com/grafana/pyroscope-go"
3940

4041
goparser "github.com/openrewrite/rewrite/rewrite-go/pkg/parser"
4142
"github.com/openrewrite/rewrite/rewrite-go/pkg/printer"
@@ -257,7 +258,34 @@ func parseFlags() serverConfig {
257258
return cfg
258259
}
259260

261+
// initPyroscope starts continuous profiling when PYROSCOPE_SERVER_ADDRESS is
262+
// set. Tags inherited via PYROSCOPE_TAGS (k=v,k=v) are forwarded verbatim; a
263+
// runtime=go tag is added so flame graphs in the shared modcli application
264+
// can be sliced by which RPC subprocess produced them.
265+
func initPyroscope() {
266+
server := os.Getenv("PYROSCOPE_SERVER_ADDRESS")
267+
if server == "" {
268+
return
269+
}
270+
appName := os.Getenv("PYROSCOPE_APPLICATION_NAME")
271+
if appName == "" {
272+
appName = "modcli"
273+
}
274+
tags := map[string]string{"runtime": "go"}
275+
for _, pair := range strings.Split(os.Getenv("PYROSCOPE_TAGS"), ",") {
276+
if i := strings.Index(pair, "="); i > 0 {
277+
tags[strings.TrimSpace(pair[:i])] = strings.TrimSpace(pair[i+1:])
278+
}
279+
}
280+
_, _ = pyroscope.Start(pyroscope.Config{
281+
ApplicationName: appName,
282+
ServerAddress: server,
283+
Tags: tags,
284+
})
285+
}
286+
260287
func main() {
288+
initPyroscope()
261289
cfg := parseFlags()
262290
s := newServer(cfg)
263291
s.logger.Println("Go RPC server starting...")

rewrite-go/go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ go 1.25.0
44

55
require github.com/google/uuid v1.6.0
66

7-
require golang.org/x/mod v0.35.0 // indirect
7+
require (
8+
github.com/grafana/pyroscope-go v1.2.8 // indirect
9+
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
10+
github.com/klauspost/compress v1.17.8 // indirect
11+
golang.org/x/mod v0.35.0 // indirect
12+
)

rewrite-go/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
22
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
3+
github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M=
4+
github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8=
5+
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
6+
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
7+
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
8+
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
39
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
410
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=

rewrite-javascript/rewrite/package-lock.json

Lines changed: 180 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rewrite-javascript/rewrite/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"vitest": "^4.0.18"
8787
},
8888
"optionalDependencies": {
89+
"@pyroscope/nodejs": "^0.4.0",
8990
"prettier": "^3.7.4"
9091
},
9192
"bin": {

rewrite-javascript/rewrite/src/rpc/server.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ import "../javascript";
3131
// Not possible to set the stack size when executing from npx for security reasons
3232
require('v8').setFlagsFromString('--stack-size=8000');
3333

34+
function initPyroscope(logger: rpc.Logger): void {
35+
const server = process.env.PYROSCOPE_SERVER_ADDRESS;
36+
if (!server) {
37+
return;
38+
}
39+
let Pyroscope: any;
40+
try {
41+
Pyroscope = require('@pyroscope/nodejs');
42+
} catch {
43+
logger.warn('PYROSCOPE_SERVER_ADDRESS set but @pyroscope/nodejs not installed; profiling disabled');
44+
return;
45+
}
46+
const tags: Record<string, string> = {runtime: 'node'};
47+
for (const pair of (process.env.PYROSCOPE_TAGS || '').split(',')) {
48+
const eq = pair.indexOf('=');
49+
if (eq > 0) {
50+
tags[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
51+
}
52+
}
53+
Pyroscope.init({
54+
appName: process.env.PYROSCOPE_APPLICATION_NAME || 'modcli',
55+
serverAddress: server,
56+
tags,
57+
});
58+
Pyroscope.start();
59+
}
60+
3461
interface ProgramOptions {
3562
logFile?: string;
3663
metricsCsv?: string;
@@ -97,6 +124,8 @@ async function main() {
97124
log: (msg: string) => log && options.traceRpcMessages && log.write(`[js trace] ${msg}\n`)
98125
};
99126

127+
initPyroscope(logger);
128+
100129
// Create the connection with the custom logger
101130
const connection = rpc.createMessageConnection(
102131
new rpc.StreamMessageReader(process.stdin),

rewrite-python/rewrite/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ publish = [
4141
"build>=1.0.0",
4242
"twine>=5.0.0",
4343
]
44+
# Continuous profiling for production recipe-worker subprocesses; the RPC
45+
# server's _init_pyroscope() is a no-op when this isn't installed.
46+
profiling = [
47+
"pyroscope-io>=0.8.0",
48+
]
4449

4550
[project.urls]
4651
Homepage = "https://github.com/openrewrite/rewrite"

0 commit comments

Comments
 (0)