-
Notifications
You must be signed in to change notification settings - Fork 39
PMM-5492 Add Pprof data to logs.zip #1117
base: main
Are you sure you want to change the base?
Changes from 15 commits
e92bca5
882f9aa
366d963
a3c438d
80e5d2d
683b4db
214a656
b8ddc92
ea5c617
2f36941
f8aa75a
b0e5163
a28413e
28e4406
291869e
22c5b95
27856c8
d988223
3d5d295
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,6 +95,7 @@ import ( | |
| "github.com/percona/pmm-managed/utils/clean" | ||
| "github.com/percona/pmm-managed/utils/interceptors" | ||
| "github.com/percona/pmm-managed/utils/logger" | ||
| "github.com/percona/pmm-managed/utils/pprof" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -107,14 +108,30 @@ const ( | |
|
|
||
| cleanInterval = 10 * time.Minute | ||
| cleanOlderThan = 30 * time.Minute | ||
|
|
||
| defaultContextTimeout = 10 * time.Second | ||
| pProfProfileDuration = 30 * time.Second | ||
| pProfTraceDuration = 10 * time.Second | ||
| ) | ||
|
|
||
| func addLogsHandler(mux *http.ServeMux, logs *supervisord.Logs) { | ||
| l := logrus.WithField("component", "logs.zip") | ||
|
|
||
| mux.HandleFunc("/logs.zip", func(rw http.ResponseWriter, req *http.Request) { | ||
| contextTimeout := defaultContextTimeout | ||
| // increase context timeout if pprof query parameter exist in request | ||
| pprofQueryParameter, _ := strconv.Atoi(req.FormValue("pprof")) | ||
| var pprofConfig *pprof.Config | ||
| if pprofQueryParameter > 0 { | ||
| contextTimeout += pProfProfileDuration + pProfTraceDuration | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess nginx will terminate connections longer than 10 minutes. Can you please check it? If so we should document this limitation or change nginx configuration. |
||
| pprofConfig = &pprof.Config{ | ||
| ProfileDuration: pProfProfileDuration, | ||
| TraceDuration: pProfTraceDuration, | ||
| } | ||
| } | ||
|
|
||
| // fail-safe | ||
| ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second) | ||
| ctx, cancel := context.WithTimeout(req.Context(), contextTimeout) | ||
| defer cancel() | ||
|
|
||
| filename := fmt.Sprintf("pmm-server_%s.zip", time.Now().UTC().Format("2006-01-02_15-04")) | ||
|
|
@@ -123,7 +140,7 @@ func addLogsHandler(mux *http.ServeMux, logs *supervisord.Logs) { | |
| rw.Header().Set(`Content-Disposition`, `attachment; filename="`+filename+`"`) | ||
|
|
||
| ctx = logger.Set(ctx, "logs") | ||
| if err := logs.Zip(ctx, rw); err != nil { | ||
| if err := logs.Zip(ctx, rw, pprofConfig); err != nil { | ||
| l.Errorf("%+v", err) | ||
| } | ||
| }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ import ( | |
|
|
||
| "github.com/percona/pmm-managed/services/platform" | ||
| "github.com/percona/pmm-managed/services/telemetry" | ||
| "github.com/percona/pmm-managed/utils/pprof" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -49,6 +50,7 @@ type Config struct { | |
| Services struct { | ||
| Platform platform.Config `yaml:"platform"` | ||
| Telemetry telemetry.ServiceConfig `yaml:"telemetry"` | ||
| Pprof pprof.Config `yaml:"pprof"` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please remove it from the config, it will simplify the code a lot.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| } `yaml:"services"` | ||
| } | ||
|
|
||
|
|
@@ -94,7 +96,7 @@ func (s *Service) Load() error { | |
| if err := cfg.Services.Telemetry.Init(s.l); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| cfg.Services.Pprof.Init() | ||
| s.Config = cfg | ||
|
|
||
| return nil | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ import ( | |
| "golang.org/x/sys/unix" | ||
|
|
||
| "github.com/percona/pmm-managed/utils/logger" | ||
| pprofUtils "github.com/percona/pmm-managed/utils/pprof" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -69,7 +70,7 @@ func NewLogs(pmmVersion string, pmmUpdateChecker *PMMUpdateChecker) *Logs { | |
| } | ||
|
|
||
| // Zip creates .zip archive with all logs. | ||
| func (l *Logs) Zip(ctx context.Context, w io.Writer) error { | ||
| func (l *Logs) Zip(ctx context.Context, w io.Writer, pprofConfig *pprofUtils.Config) error { | ||
| start := time.Now() | ||
| log := logger.Get(ctx).WithField("component", "logs") | ||
| log.WithField("d", time.Since(start).Seconds()).Info("Starting...") | ||
|
|
@@ -80,7 +81,7 @@ func (l *Logs) Zip(ctx context.Context, w io.Writer) error { | |
| zw := zip.NewWriter(w) | ||
| now := time.Now().UTC() | ||
|
|
||
| files := l.files(ctx) | ||
| files := l.files(ctx, pprofConfig) | ||
| log.WithField("d", time.Since(start).Seconds()).Infof("Collected %d files.", len(files)) | ||
|
|
||
| for _, file := range files { | ||
|
|
@@ -127,8 +128,8 @@ func (l *Logs) Zip(ctx context.Context, w io.Writer) error { | |
| return nil | ||
| } | ||
|
|
||
| // files reads log/config files and returns content. | ||
| func (l *Logs) files(ctx context.Context) []fileContent { | ||
| // files reads log/config/pprof files and returns content. | ||
| func (l *Logs) files(ctx context.Context, pprofConfig *pprofUtils.Config) []fileContent { | ||
| files := make([]fileContent, 0, 20) | ||
|
|
||
| // add logs | ||
|
|
@@ -214,6 +215,30 @@ func (l *Logs) files(ctx context.Context) []fileContent { | |
| Err: err, | ||
| }) | ||
|
|
||
| // add pprof | ||
| if pprofConfig != nil { | ||
| traceBytes, err := pprofUtils.Trace(pprofConfig.TraceDuration) | ||
| files = append(files, fileContent{ | ||
| Name: "pprof/trace.out", | ||
| Data: traceBytes, | ||
| Err: err, | ||
| }) | ||
|
|
||
| profileBytes, err := pprofUtils.Profile(pprofConfig.ProfileDuration) | ||
| files = append(files, fileContent{ | ||
| Name: "pprof/profile.pb.gz", | ||
| Data: profileBytes, | ||
| Err: err, | ||
| }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we run them in parallel to speed up?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. Fixed. |
||
|
|
||
| heapBytes, err := pprofUtils.Heap(true) | ||
| files = append(files, fileContent{ | ||
| Name: "pprof/heap.pb.gz", | ||
| Data: heapBytes, | ||
| Err: err, | ||
| }) | ||
| } | ||
|
|
||
| sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name }) | ||
| return files | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // pmm-managed | ||
| // Copyright (C) 2017 Percona LLC | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
|
|
||
| package pprof | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "runtime" | ||
| "runtime/pprof" | ||
| "runtime/trace" | ||
| "time" | ||
| ) | ||
|
|
||
| // Profile responds with the pprof-formatted cpu profile. | ||
| // Profiling lasts for duration specified in seconds. | ||
| func Profile(duration time.Duration) ([]byte, error) { | ||
| var profileBuf bytes.Buffer | ||
| if err := pprof.StartCPUProfile(&profileBuf); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| time.Sleep(duration) | ||
| pprof.StopCPUProfile() | ||
|
|
||
| return profileBuf.Bytes(), nil | ||
| } | ||
|
|
||
| // Trace responds with the execution trace in binary form. | ||
| // Tracing lasts for duration specified in seconds. | ||
| func Trace(duration time.Duration) ([]byte, error) { | ||
| var traceBuf bytes.Buffer | ||
| if err := trace.Start(&traceBuf); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| time.Sleep(duration) | ||
| trace.Stop() | ||
|
|
||
| return traceBuf.Bytes(), nil | ||
| } | ||
|
|
||
| // Heap responds with the pprof-formatted profile named "heap". | ||
| // listing the available profiles. | ||
| // You can specify the gc parameter to run gc before taking the heap sample. | ||
| func Heap(gc bool) ([]byte, error) { | ||
| var heapBuf bytes.Buffer | ||
| debug := 0 | ||
| profile := "heap" | ||
|
|
||
| p := pprof.Lookup(profile) | ||
| if p == nil { | ||
| return nil, fmt.Errorf("profile cannot be found: %s", profile) | ||
| } | ||
|
|
||
| if gc { | ||
| runtime.GC() | ||
| } | ||
|
|
||
| err := p.WriteTo(&heapBuf, debug) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return heapBuf.Bytes(), nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // pmm-managed | ||
| // Copyright (C) 2017 Percona LLC | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
|
|
||
| package pprof | ||
|
|
||
| import ( | ||
| "time" | ||
| ) | ||
|
|
||
| // Config pprof settings. | ||
| type Config struct { | ||
| ProfileDuration time.Duration `yaml:"profile_duration"` //nolint:tagliatelle | ||
| TraceDuration time.Duration `yaml:"trace_duration"` //nolint:tagliatelle | ||
| } | ||
|
|
||
| // Init pprof config init. | ||
| func (c *Config) Init() { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // pmm-managed | ||
| // Copyright (C) 2017 Percona LLC | ||
| // | ||
| // This program is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU Affero General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // This program is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU Affero General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU Affero General Public License | ||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
|
|
||
| package pprof | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "compress/gzip" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestHeap(t *testing.T) { | ||
| t.Parallel() | ||
| t.Run("Heap test", func(t *testing.T) { | ||
| heapBytes, err := Heap(true) | ||
|
|
||
| // read gzip | ||
| reader, err := gzip.NewReader(bytes.NewBuffer(heapBytes)) | ||
| assert.NoError(t, err) | ||
|
|
||
| var resB bytes.Buffer | ||
| _, err = resB.ReadFrom(reader) | ||
| assert.NoError(t, err) | ||
| assert.NotEmpty(t, resB.Bytes()) | ||
| }) | ||
| } | ||
|
|
||
| func TestProfile(t *testing.T) { | ||
| t.Parallel() | ||
| t.Run("Profile test", func(t *testing.T) { | ||
| profileBytes, err := Profile(1 * time.Second) | ||
|
|
||
| assert.NoError(t, err) | ||
| assert.NotEmpty(t, profileBytes) | ||
|
|
||
| // read gzip | ||
| reader, err := gzip.NewReader(bytes.NewBuffer(profileBytes)) | ||
| assert.NoError(t, err) | ||
|
|
||
| var resB bytes.Buffer | ||
| _, err = resB.ReadFrom(reader) | ||
| assert.NoError(t, err) | ||
|
|
||
| assert.NotEmpty(t, resB.Bytes()) | ||
| }) | ||
| } | ||
|
|
||
| func TestTrace(t *testing.T) { | ||
| t.Parallel() | ||
| t.Run("Trace test", func(t *testing.T) { | ||
| traceBytes, err := Trace(1 * time.Second) | ||
|
|
||
| assert.NoError(t, err) | ||
| assert.NotEmpty(t, traceBytes) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest using
strconv.ParseBoolinstead ofstrconv.Atoi.Do not ignore err, let's at least log the message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed.