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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
os-version: [ 22.04, 24.04 ]
go-version: [ 1.17, 1.18, 1.19 ]
go-version: [ '1.19', '1.20', '1.21', '1.22' ]
runs-on: ubuntu-${{ matrix.os-version }}
steps:
- uses: actions/checkout@v1
Expand All @@ -31,7 +31,7 @@ jobs:
strategy:
matrix:
os-version: [ 13, 14, 15 ]
go-version: [ 1.17, 1.18, 1.19 ]
go-version: [ '1.19', '1.20', '1.21', '1.22' ]
runs-on: macos-${{ matrix.os-version }}
steps:
- uses: actions/checkout@v1
Expand Down
179 changes: 169 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ e.g. Slack).
## Installation

```bash
go get -u github.com/daangn/autopprof
go get -u github.com/daangn/autopprof/v2
```

## Usage
Expand All @@ -33,17 +33,17 @@ import (
"errors"
"log"

"github.com/daangn/autopprof"
"github.com/daangn/autopprof/report"
"github.com/daangn/autopprof/v2"
"github.com/daangn/autopprof/v2/report"
)

func main() {
err := autopprof.Start(autopprof.Option{
App: "YOUR_APP_NAME",
CPUThreshold: 0.8, // Default: 0.75.
MemThreshold: 0.8, // Default: 0.75.
Reporter: report.NewSlackReporter(
&report.SlackReporterOption{
App: "YOUR_APP_NAME",
Token: "YOUR_TOKEN_HERE",
ChannelID: "REPORT_CHANNEL_ID",
},
Expand All @@ -63,6 +63,161 @@ func main() {

> You can create a custom reporter by implementing the `report.Reporter` interface.

## Custom metrics

Beyond the built-in CPU / memory / goroutine watchers, you can register your own
`Metric` — useful for domain signals that only the owning struct knows about
(connection-pool usage, queue backlog, cache hit ratio, …).

```go
type Pool struct { /* ... */ }

func (p *Pool) Name() string { return "db_pool" }
func (p *Pool) Threshold() float64 { return 0.9 }
func (p *Pool) Interval() time.Duration { return 10 * time.Second } // 0 → global watchInterval
func (p *Pool) Query() (float64, error) { return p.Usage(), nil }
func (p *Pool) Collect(v float64) (autopprof.CollectResult, error) {
snap, err := p.Snapshot()
if err != nil {
return autopprof.CollectResult{}, err
}
return autopprof.CollectResult{
Reader: bytes.NewReader(snap),
Filename: fmt.Sprintf("db_pool_%d.dump", time.Now().Unix()),
Comment: fmt.Sprintf(":rotating_light:[pool] %.2f ≥ 0.90", v),
}, nil
}

// After autopprof.Start(...) — typically inside the struct's constructor:
_ = autopprof.Register(pool)
defer autopprof.Unregister("db_pool")
```

For one-off hooks you can skip the custom type entirely:

```go
_ = autopprof.Register(autopprof.NewMetric(
"goroutine_blocked", 100, 5*time.Second,
func() (float64, error) { return float64(runtime.NumGoroutine()), nil },
func(v float64) (autopprof.CollectResult, error) { /* ... */ },
))
```

The names `cpu`, `mem`, and `goroutine` are reserved for the built-in metrics.
User metrics do **not** participate in the built-in cascade.

A built-in breach reports every other enabled built-in in addition to the
triggering one. Set `DisableCPUProf`, `DisableMemProf`, or
`DisableGoroutineProf` to opt a built-in out — it leaves the watcher and
the cascade in one step.

## Migrating from v1 to v2

v2 unifies CPU / Mem / Goroutine / Custom under a single `Metric` interface
and narrows the `Reporter` surface. This section lists every change a v1
caller needs to make.

### 1. Update the module path

```diff
- import "github.com/daangn/autopprof"
+ import "github.com/daangn/autopprof/v2"
```

Update `go.mod`:

```bash
go get github.com/daangn/autopprof/v2
```

### 2. `Option` changes

| v1 | v2 |
|---|---|
| `ReportBoth bool` | **Removed.** Cascade is always on for enabled built-ins. |
| `ReportAll bool` | **Removed.** Cascade is always on for enabled built-ins. |
| *(n/a)* | `App string` — the `"<app>"` segment of built-in filenames. Defaults to `"autopprof"` when empty. |
| *(n/a)* | `Metrics []Metric` — user-defined metrics to register at `Start`. |

All other fields (`CPUThreshold`, `MemThreshold`, `GoroutineThreshold`,
`Disable*Prof`, `Reporter`) are unchanged. Disable individual built-ins
via `Disable*Prof` — they're excluded from the cascade as well.

```diff
autopprof.Start(autopprof.Option{
+ App: "YOUR_APP_NAME",
CPUThreshold: 0.8,
- ReportBoth: true,
Reporter: myReporter,
})
```

### 3. `SlackReporterOption` changes

```diff
report.NewSlackReporter(&report.SlackReporterOption{
- App: "YOUR_APP_NAME", // moved to Option.App
Token: "YOUR_TOKEN_HERE",
- Channel: "old-channel-name", // removed; Slack API dropped channel-name uploads
ChannelID: "REPORT_CHANNEL_ID",
})
```

### 4. `Reporter` interface (4 methods → 1)

```diff
-type Reporter interface {
- ReportCPUProfile(ctx context.Context, r io.Reader, ci CPUInfo) error
- ReportHeapProfile(ctx context.Context, r io.Reader, mi MemInfo) error
- ReportGoroutineProfile(ctx context.Context, r io.Reader, gi GoroutineInfo) error
-}
+type Reporter interface {
+ Report(ctx context.Context, r io.Reader, info ReportInfo) error
+}
+
+type ReportInfo struct {
+ MetricName string // "cpu", "mem", "goroutine", or user-defined name
+ Filename string
+ Comment string
+ Value float64
+ Threshold float64
+}
```

Custom `Reporter` implementations should route on `info.MetricName`:

```go
func (r *MyReporter) Report(ctx context.Context, reader io.Reader, info report.ReportInfo) error {
switch info.MetricName {
case "cpu":
return r.sendCPU(ctx, reader, info.Value*100, info.Threshold*100)
case "mem":
return r.sendMem(ctx, reader, info.Value*100, info.Threshold*100)
case "goroutine":
return r.sendGoroutine(ctx, reader, int(info.Value), int(info.Threshold))
default:
return r.sendCustom(ctx, reader, info)
}
}
```

Removed types from the `report` package: `CPUInfo`, `MemInfo`, `GoroutineInfo`,
`CPUProfileFilenameFmt`, `HeapProfileFilenameFmt`, `GoroutineProfileFilenameFmt`.

### 5. Bug fixes carried in v2

- `Option.DisableGoroutineProf` was silently ignored in v1 (the value
wasn't assigned to the internal struct at `Start` time). It now takes
effect correctly, which may change observed behavior for callers
relying on the flag.
- Cascade (the v1 `ReportAll: true` behavior) is now unconditional for
enabled built-ins; use `Disable*Prof` to opt specific metrics out.

### 6. New: custom metrics

v2 lets you register your own `Metric`. See the **Custom metrics** section
above — this is the main reason to migrate.

## Benchmark

Benchmark the overhead of watching the CPU and memory utilization. The overhead is very
Expand All @@ -76,12 +231,16 @@ small, so we don't have to worry about the performance degradation.
>

```
BenchmarkLightJob-5 49444164 245.6 ns/op 0 B/op 0 allocs/op
BenchmarkLightJobWithWatchCPUUsage-5 48884026 250.1 ns/op 0 B/op 0 allocs/op
BenchmarkLightJobWithWatchMemUsage-5 49036617 246.3 ns/op 0 B/op 0 allocs/op
BenchmarkHeavyJob-5 59010 203759 ns/op 0 B/op 0 allocs/op
BenchmarkHeavyJobWithWatchCPUUsage-5 58915 204054 ns/op 2 B/op 0 allocs/op
BenchmarkHeavyJobWithWatchMemUsage-5 58850 204764 ns/op 2 B/op 0 allocs/op
BenchmarkLightJob-12 98078731 134.5 ns/op 0 B/op 0 allocs/op
BenchmarkLightJobWithWatchCPUUsage-12 92799849 133.0 ns/op 0 B/op 0 allocs/op
BenchmarkLightJobWithWatchMemUsage-12 96778594 128.3 ns/op 0 B/op 0 allocs/op
BenchmarkHeavyJob-12 105848 106606 ns/op 0 B/op 0 allocs/op
BenchmarkHeavyJobWithWatchCPUUsage-12 113047 112734 ns/op 1 B/op 0 allocs/op
BenchmarkHeavyJobWithWatchMemUsage-12 101102 133426 ns/op 1 B/op 0 allocs/op
BenchmarkLightAsyncJob-12 399696 29953 ns/op 7040 B/op 352 allocs/op
BenchmarkLightAsyncJobWithWatchGoroutineCount-12 347266 34259 ns/op 7040 B/op 352 allocs/op
BenchmarkHeavyAsyncJob-12 452 26689023 ns/op 6002064 B/op 300097 allocs/op
BenchmarkHeavyAsyncJobWithWatchGoroutineCount-12 414 27350318 ns/op 5973015 B/op 298647 allocs/op
```

## License
Expand Down
Loading
Loading