aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 13:06:20 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 13:06:20 +0200
commit6ec883a7fd40a14f2882076bd1a377bbbd0438ad (patch)
tree2d199226b29ada6dbe74b2ce278b000c296a1b3a
parent522512197de372aa9b9c5584711619a76a5546d8 (diff)
Add local Pyroscope server and Go continuous profiling
Add a docker-compose service running the Grafana Pyroscope backend on port 4040 with a persistent data volume, and instrument the Go app via github.com/grafana/pyroscope-go. The app pushes CPU, memory, and goroutine profiles every 10s when pyroscope.enabled is set in config.yaml. Config is extended with application_name and server_address (defaults to http://localhost:4040). Document usage in the README.
-rw-r--r--README.md16
-rw-r--r--config.example.yaml5
-rw-r--r--config/config.go17
-rw-r--r--docker-compose.yml16
-rw-r--r--go.mod9
-rw-r--r--go.sum16
-rw-r--r--main.go36
7 files changed, 103 insertions, 12 deletions
diff --git a/README.md b/README.md
index 9c042d4..35bb1c9 100644
--- a/README.md
+++ b/README.md
@@ -26,8 +26,15 @@ go run . -extract-osm france-latest.osm.pbf # Extract mountain passes from a Fra
go run . -backfill # Recompute and persist all ride computed values
go run . -backtest # Benchmark computed vs stored ride values
go run . -pprof # Expose pprof endpoints at `/debug/pprof`
+docker compose up -d pyroscope # Start the local Pyroscope server on http://localhost:4040
```
+Continuous profiling with Pyroscope: the app pushes CPU, memory, and goroutine
+profiles every 10s when `pyroscope.enabled` is set in `config.yaml`. Run
+`docker compose up -d pyroscope` to start the ingest server, then browse
+`http://localhost:4040` to inspect the `biking_home` application profile.
+
+
Configuration is stored in `config.yaml`. Start from `config.example.yaml` and set the Strava credentials before launching the server.
## Strava API
@@ -149,17 +156,8 @@ Other endpoints:
go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap # allocs
```
-Optional continuous profiling with Pyroscope (local docker, ~1-2% overhead):
-
-```bash
-docker run --rm -p 4040:4040 grafana/pyroscope
-```
-
-Add `github.com/grafana/pyroscope-go` and call `pyroscope.Start` with `ServerAddress: "http://localhost:4040"` and `route` label — gives the same flamegraphs but over time. Keep it only if pprof snapshots are not enough.
-
## TODO
-- pyroscope
- screaming archi + hexagon
- Compute estimated power, speed
- For a ride, scatter, one point per 100m segment. Slope vs speed
diff --git a/config.example.yaml b/config.example.yaml
index a4f099d..c4b22a2 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -11,6 +11,11 @@ storage:
official_climb:
match_radius_m: 100
+pyroscope:
+ enabled: false
+ application_name: biking_home
+ server_address: http://localhost:4040
+
strava:
client_id: ""
client_secret: ""
diff --git a/config/config.go b/config/config.go
index 6fe9048..f95d3f8 100644
--- a/config/config.go
+++ b/config/config.go
@@ -16,6 +16,7 @@ type Config struct {
Storage StorageConfig `yaml:"storage"`
Strava StravaConfig `yaml:"strava"`
OfficialClimb OfficialClimbConfig `yaml:"official_climb"`
+ Pyroscope PyroscopeConfig `yaml:"pyroscope"`
}
type DatabaseConfig struct {
@@ -43,6 +44,12 @@ type OfficialClimbConfig struct {
MatchRadiusM float64 `yaml:"match_radius_m"`
}
+type PyroscopeConfig struct {
+ Enabled bool `yaml:"enabled"`
+ ApplicationName string `yaml:"application_name"`
+ ServerAddress string `yaml:"server_address"`
+}
+
func Default() Config {
return Config{
Database: DatabaseConfig{Path: "biking_home.db"},
@@ -52,6 +59,10 @@ func Default() Config {
},
Storage: StorageConfig{GPXDir: "data/rides"},
OfficialClimb: OfficialClimbConfig{MatchRadiusM: official_climb.DefaultMatchRadiusM},
+ Pyroscope: PyroscopeConfig{
+ ApplicationName: "biking_home",
+ ServerAddress: "http://localhost:4040",
+ },
}
}
@@ -73,6 +84,12 @@ func Load(filename string) (Config, error) {
if config.Storage.GPXDir == "" {
return Config{}, fmt.Errorf("config %q: storage.gpx_dir is required", filename)
}
+ if config.Pyroscope.Enabled && config.Pyroscope.ApplicationName == "" {
+ return Config{}, fmt.Errorf("config %q: pyroscope.application_name is required when pyroscope is enabled", filename)
+ }
+ if config.Pyroscope.Enabled && config.Pyroscope.ServerAddress == "" {
+ return Config{}, fmt.Errorf("config %q: pyroscope.server_address is required when pyroscope is enabled", filename)
+ }
if math.IsNaN(config.OfficialClimb.MatchRadiusM) || math.IsInf(config.OfficialClimb.MatchRadiusM, 0) || config.OfficialClimb.MatchRadiusM <= 0 {
return Config{}, fmt.Errorf("config %q: official_climb.match_radius_m must be greater than zero", filename)
}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..6605075
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,16 @@
+services:
+ pyroscope:
+ image: grafana/pyroscope:latest
+ container_name: pyroscope
+ ports:
+ - "4040:4040"
+ command:
+ - server
+ - --storage-path
+ - /home/pyroscope
+ volumes:
+ - pyroscope-data:/home/pyroscope
+ restart: unless-stopped
+
+volumes:
+ pyroscope-data:
diff --git a/go.mod b/go.mod
index f700691..114b215 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.16
github.com/paulmach/osm v0.9.0
github.com/schollz/progressbar/v3 v3.18.0
- github.com/stretchr/testify v1.10.0
+ github.com/stretchr/testify v1.11.1
github.com/tkrajina/gpxgo v1.4.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,9 +33,12 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
+ github.com/grafana/pyroscope-go v1.4.2 // indirect
+ github.com/grafana/pyroscope-go/godeltaprof v0.1.11 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/kataras/i18n v0.0.8 // indirect
- github.com/kr/pretty v0.1.0 // indirect
+ github.com/klauspost/compress v1.18.7 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -66,7 +69,7 @@ require (
golang.org/x/text v0.41.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
- gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
diff --git a/go.sum b/go.sum
index 3f6c2a0..c1513d2 100644
--- a/go.sum
+++ b/go.sum
@@ -30,6 +30,7 @@ github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -47,6 +48,10 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/grafana/pyroscope-go v1.4.2 h1:0LW5HrUJXgGr9zF5gITP/HaFXN9/LsMiwlgVJAK75l0=
+github.com/grafana/pyroscope-go v1.4.2/go.mod h1:Ej13Jr05rRJrjWvrrFhfh6gGYXtfibuukOs3Tl3Y7QQ=
+github.com/grafana/pyroscope-go/godeltaprof v0.1.11 h1:el5LYpXissAiCKZ5/6yjlr6mhYVV6Cp5lahTocxraXM=
+github.com/grafana/pyroscope-go/godeltaprof v0.1.11/go.mod h1:jl1V8M4cWsXciROCPIDDG7CtjSjT/ECbp6eLVuMxYRI=
github.com/jftuga/geodist v1.0.0 h1:PFPQlZtj10u8ETAYTyxE0DWMl1bwA+Xzrqb4+oLkkC0=
github.com/jftuga/geodist v1.0.0/go.mod h1:BohEDxpZ8S5ADAxW/9EKPSKWOVl0+3wHENIT40m4UO4=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
@@ -54,8 +59,13 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kataras/i18n v0.0.8 h1:thiDRqq4fN2sQOK5CwR5Yh1yT7swP6B3wnadILhqjhk=
github.com/kataras/i18n v0.0.8/go.mod h1:M/yRAqQ3Y7z2oSpotxG/+nPrgsJLas6t0kEJfIJk98E=
+github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
+github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -87,10 +97,12 @@ github.com/paulmach/protoscan v0.2.1 h1:rM0FpcTjUMvPUNk2BhPJrreDKetq43ChnL+x1sRg
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
@@ -104,6 +116,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tkrajina/gpxgo v1.4.0 h1:cSD5uSwy3VZuNFieTEZLyRnuIwhonQEkGPkPGW4XNag=
github.com/tkrajina/gpxgo v1.4.0/go.mod h1:BXSMfUAvKiEhMEXAFM2NvNsbjsSvp394mOvdcNjettg=
github.com/urfave/cli/v2 v2.24.2 h1:q1VA+ofZ8SWfEKB9xXHUD4QZaeI9e+ItEqSbfH2JBXk=
@@ -199,6 +213,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index 4404acf..8971433 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,9 @@ package main
import (
"database/sql"
"flag"
+ "log/slog"
+ "github.com/grafana/pyroscope-go"
"github.com/martinlehoux/biking_home/cli"
"github.com/martinlehoux/biking_home/config"
"github.com/martinlehoux/kagamigo/kcore"
@@ -18,8 +20,42 @@ func main() {
flag.Parse()
appConfig, err := config.Load(*configFile)
kcore.Expect(err, "failed to load configuration")
+ stopProfiler, err := startPyroscope(appConfig.Pyroscope)
+ if err != nil {
+ kcore.Expect(err, "failed to start pyroscope")
+ }
+ if stopProfiler != nil {
+ defer stopProfiler()
+ }
db, err := sql.Open("sqlite3", appConfig.Database.Path)
kcore.Expect(err, "failed to open database")
defer db.Close()
cli.Run(db, *configFile, appConfig)
}
+
+// startPyroscope starts the continuous profiler when enabled in the config and
+// returns a stop function to flush and detach the profiler on shutdown, or nil
+// when pyroscope is disabled.
+func startPyroscope(appConfig config.PyroscopeConfig) (func(), error) {
+ if !appConfig.Enabled {
+ return nil, nil
+ }
+ profiler, err := pyroscope.Start(pyroscope.Config{
+ ApplicationName: appConfig.ApplicationName,
+ ServerAddress: appConfig.ServerAddress,
+ Logger: pyroscope.StandardLogger,
+ ProfileTypes: []pyroscope.ProfileType{
+ pyroscope.ProfileCPU,
+ pyroscope.ProfileAllocSpace,
+ pyroscope.ProfileAllocObjects,
+ pyroscope.ProfileInuseSpace,
+ pyroscope.ProfileInuseObjects,
+ pyroscope.ProfileGoroutines,
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ slog.Info("pyroscope enabled", "server", appConfig.ServerAddress, "application", appConfig.ApplicationName)
+ return func() { _ = profiler.Stop() }, nil
+}