Profiling endpoint fixes (#1707)

- Added support to download all profile tests
- profile.zip file was corrupted after download
- Add suspension warning
- Add Checkbox support
- Support for running multiple profiling types at the same time
- fix profiling test

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Lenin Alevski
2022-03-18 12:52:42 -07:00
committed by GitHub
parent 3a09361899
commit d7fef8d89e
11 changed files with 241 additions and 212 deletions

View File

@@ -1,110 +0,0 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// 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 <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
)
// ProfilerType profiler type
//
// swagger:model profilerType
type ProfilerType string
func NewProfilerType(value ProfilerType) *ProfilerType {
return &value
}
// Pointer returns a pointer to a freshly-allocated ProfilerType.
func (m ProfilerType) Pointer() *ProfilerType {
return &m
}
const (
// ProfilerTypeCPU captures enum value "cpu"
ProfilerTypeCPU ProfilerType = "cpu"
// ProfilerTypeMem captures enum value "mem"
ProfilerTypeMem ProfilerType = "mem"
// ProfilerTypeBlock captures enum value "block"
ProfilerTypeBlock ProfilerType = "block"
// ProfilerTypeMutex captures enum value "mutex"
ProfilerTypeMutex ProfilerType = "mutex"
// ProfilerTypeTrace captures enum value "trace"
ProfilerTypeTrace ProfilerType = "trace"
// ProfilerTypeThreads captures enum value "threads"
ProfilerTypeThreads ProfilerType = "threads"
// ProfilerTypeGoroutines captures enum value "goroutines"
ProfilerTypeGoroutines ProfilerType = "goroutines"
)
// for schema
var profilerTypeEnum []interface{}
func init() {
var res []ProfilerType
if err := json.Unmarshal([]byte(`["cpu","mem","block","mutex","trace","threads","goroutines"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
profilerTypeEnum = append(profilerTypeEnum, v)
}
}
func (m ProfilerType) validateProfilerTypeEnum(path, location string, value ProfilerType) error {
if err := validate.EnumCase(path, location, value, profilerTypeEnum, true); err != nil {
return err
}
return nil
}
// Validate validates this profiler type
func (m ProfilerType) Validate(formats strfmt.Registry) error {
var res []error
// value enum
if err := m.validateProfilerTypeEnum("", "body", m); err != nil {
return err
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// ContextValidate validates this profiler type based on context it is used
func (m ProfilerType) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}

View File

@@ -38,7 +38,7 @@ type ProfilingStartRequest struct {
// type
// Required: true
Type *ProfilerType `json:"type"`
Type *string `json:"type"`
}
// Validate validates this profiling start request
@@ -61,51 +61,11 @@ func (m *ProfilingStartRequest) validateType(formats strfmt.Registry) error {
return err
}
if err := validate.Required("type", "body", m.Type); err != nil {
return err
}
if m.Type != nil {
if err := m.Type.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("type")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("type")
}
return err
}
}
return nil
}
// ContextValidate validate this profiling start request based on the context it is used
// ContextValidate validates this profiling start request based on context it is used
func (m *ProfilingStartRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateType(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *ProfilingStartRequest) contextValidateType(ctx context.Context, formats strfmt.Registry) error {
if m.Type != nil {
if err := m.Type.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("type")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("type")
}
return err
}
}
return nil
}

View File

@@ -0,0 +1,199 @@
import React, { Fragment, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Button, Grid } from "@mui/material";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import CheckboxWrapper from "../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import api from "../../../common/api";
import { ErrorResponseHandler } from "../../../common/types";
import {
actionsTray,
containerForHeader,
inlineCheckboxes,
} from "../Common/FormComponents/common/styleLibrary";
import HelpBox from "../../../common/HelpBox";
import WarnIcon from "../../../icons/WarnIcon";
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
marginTop: 24,
textAlign: "right",
"& .MuiButton-root": {
marginLeft: 8,
},
},
dropdown: {
marginBottom: 24,
},
checkboxLabel: {
marginTop: 12,
marginRight: 4,
fontSize: 16,
fontWeight: 500,
},
checkboxDisabled: {
opacity: 0.5,
},
inlineCheckboxes: {
...inlineCheckboxes.inlineCheckboxes,
alignItems: "center",
"@media (max-width: 900px)": {
flexFlow: "column",
alignItems: "flex-start",
},
},
...actionsTray,
...containerForHeader(theme.spacing(4)),
});
interface IProfileProps {
classes: any;
}
const Profile = ({ classes }: IProfileProps) => {
const [profilingStarted, setProfilingStarted] = useState<boolean>(false);
const [types, setTypes] = useState<string[]>([
"cpu",
"mem",
"block",
"mutex",
"trace",
"threads",
"goroutines",
]);
const typesList = [
{ label: "cpu", value: "cpu" },
{ label: "mem", value: "mem" },
{ label: "block", value: "block" },
{ label: "mutex", value: "mutex" },
{ label: "trace", value: "trace" },
{ label: "threads", value: "threads" },
{ label: "goroutines", value: "goroutines" },
];
const onCheckboxClick = (e: React.ChangeEvent<HTMLInputElement>) => {
let newArr: string[] = [];
if (types.indexOf(e.target.value) > -1) {
newArr = types.filter((type) => type !== e.target.value);
} else {
newArr = [...types, e.target.value];
}
setTypes(newArr);
};
const startProfiling = () => {
if (!profilingStarted) {
const typeString = types.join(",");
setProfilingStarted(true);
api
.invoke("POST", `/api/v1/profiling/start`, {
type: typeString,
})
.then(() => {})
.catch((err: ErrorResponseHandler) => {
console.log(err);
setProfilingStarted(false);
});
}
};
const stopProfiling = () => {
if (profilingStarted) {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
let path = "/api/v1/profiling/stop";
var req = new XMLHttpRequest();
req.open("POST", path, true);
req.responseType = "blob";
req.onreadystatechange = () => {
if (req.readyState === 4 && req.status === 200) {
let filename = "profile.zip";
setProfilingStarted(false);
var link = document.createElement("a");
link.href = window.URL.createObjectURL(req.response);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
req.send();
}
};
return (
<Fragment>
<PageHeader label="Profile" />
<PageLayout>
<Grid item xs={12} className={classes.boxy}>
<Grid item xs={12} className={classes.dropdown}>
<Grid
item
xs={12}
className={`${classes.inlineCheckboxes} ${
profilingStarted && classes.checkboxDisabled
}`}
>
<div className={classes.checkboxLabel}>Types to profile:</div>
{typesList.map((t) => (
<CheckboxWrapper
checked={types.indexOf(t.value) > -1}
disabled={profilingStarted}
key={`checkbox-${t.label}`}
id={`checkbox-${t.label}`}
label={t.label}
name={`checkbox-${t.label}`}
onChange={onCheckboxClick}
value={t.value}
/>
))}
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={profilingStarted || types.length < 1}
onClick={() => {
startProfiling();
}}
>
Start Profiling
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={!profilingStarted}
onClick={() => {
stopProfiling();
}}
>
Stop Profiling
</Button>
</Grid>
</Grid>
{!profilingStarted && (
<Fragment>
<br />
<HelpBox
title={
"During the profiling run all production traffic will be suspended."
}
iconComponent={<WarnIcon />}
help={<Fragment />}
/>
</Fragment>
)}
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(Profile);

View File

@@ -27,6 +27,7 @@ import withSuspense from "../Common/Components/withSuspense";
const Inspect = withSuspense(React.lazy(() => import("./Inspect")));
const Register = withSuspense(React.lazy(() => import("../Support/Register")));
const Profile = withSuspense(React.lazy(() => import("../Support/Profile")));
const Tools = () => {
return (
@@ -34,6 +35,7 @@ const Tools = () => {
<Switch>
<Route path={IAM_PAGES.TOOLS} exact component={ToolsList} />
<Route path={IAM_PAGES.REGISTER_SUPPORT} exact component={Register} />
<Route path={IAM_PAGES.PROFILE} exact component={Profile} />
<Route
path={IAM_PAGES.CALL_HOME}
exact

View File

@@ -37,6 +37,7 @@ import {
MetricsMenuIcon,
MonitoringMenuIcon,
PerformanceMenuIcon,
ProfileMenuIcon,
SupportMenuIcon,
TraceMenuIcon,
UsersMenuIcon,
@@ -186,6 +187,13 @@ export const validRoutes = (
icon: PerformanceMenuIcon,
to: IAM_PAGES.TOOLS_SPEEDTEST,
},
{
name: "Profile",
id: "profile",
component: NavLink,
icon: ProfileMenuIcon,
to: IAM_PAGES.PROFILE,
},
// {
// name: "Call Home",
@@ -201,13 +209,6 @@ export const validRoutes = (
icon: InspectMenuIcon,
component: NavLink,
},
// {
// name: "Profile",
// id: "profile",
// component: NavLink,
// icon: ProfileMenuIcon,
// to: IAM_PAGES.PROFILE,
// },
],
},
{
@@ -315,9 +316,9 @@ export const validRoutes = (
((childItem.customPermissionFnc
? childItem.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[childItem.to ?? ""]
)) ||
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[childItem.to ?? ""]
)) ||
childItem.forceDisplay) &&
!childItem.fsHidden
);
@@ -329,9 +330,9 @@ export const validRoutes = (
((item.customPermissionFnc
? item.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[item.to ?? ""]
)) ||
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[item.to ?? ""]
)) ||
item.forceDisplay) &&
!item.fsHidden;
return res;

View File

@@ -48,8 +48,7 @@ func registerProfilingHandler(api *operations.ConsoleAPI) {
// HTTP client the name and extension of the file we are returning
return middleware.ResponderFunc(func(w http.ResponseWriter, _ runtime.Producer) {
defer profilingStopResponse.Close()
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=profile.zip")
io.Copy(w, profilingStopResponse)
})
@@ -66,7 +65,7 @@ func registerProfilingHandler(api *operations.ConsoleAPI) {
// "nodeName": "127.0.0.1:9000"
// "error": ""
// }
func startProfiling(ctx context.Context, client MinioAdmin, profilerType models.ProfilerType) ([]*models.StartProfilingItem, error) {
func startProfiling(ctx context.Context, client MinioAdmin, profilerType string) ([]*models.StartProfilingItem, error) {
profilingResults, err := client.startProfiling(ctx, madmin.ProfilerType(profilerType))
if err != nil {
return nil, err
@@ -109,11 +108,11 @@ func getProfilingStartResponse(session *models.Principal, params *models.Profili
// stopProfiling() stop the profiling on the Minio server and returns
// the generated Zip file as io.ReadCloser
func stopProfiling(ctx context.Context, client MinioAdmin) (io.ReadCloser, error) {
profilingData, err := client.stopProfiling(ctx)
zippedData, err := client.stopProfiling(ctx)
if err != nil {
return nil, err
}
return profilingData, nil
return zippedData, nil
}
// getProfilingStopResponse() performs setPolicy() and serializes it to the handler's output

View File

@@ -24,7 +24,6 @@ import (
"errors"
"github.com/minio/console/models"
"github.com/minio/madmin-go"
"github.com/stretchr/testify/assert"
)
@@ -63,7 +62,7 @@ func TestStartProfiling(t *testing.T) {
}, nil
}
function := "startProfiling()"
cpuProfiler := models.ProfilerType("cpu")
cpuProfiler := "cpu"
startProfilingResults, err := startProfiling(ctx, adminClient, cpuProfiler)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())

View File

@@ -30,6 +30,7 @@
// - multipart/form-data
//
// Produces:
// - application/zip
// - application/octet-stream
// - application/json
//

View File

@@ -2984,7 +2984,7 @@ func init() {
"/profiling/stop": {
"post": {
"produces": [
"application/octet-stream"
"application/zip"
],
"tags": [
"AdminAPI"
@@ -5519,18 +5519,6 @@ func init() {
}
}
},
"profilerType": {
"type": "string",
"enum": [
"cpu",
"mem",
"block",
"mutex",
"trace",
"threads",
"goroutines"
]
},
"profilingStartRequest": {
"type": "object",
"required": [
@@ -5538,7 +5526,7 @@ func init() {
],
"properties": {
"type": {
"$ref": "#/definitions/profilerType"
"type": "string"
}
}
},
@@ -9480,7 +9468,7 @@ func init() {
"/profiling/stop": {
"post": {
"produces": [
"application/octet-stream"
"application/zip"
],
"tags": [
"AdminAPI"
@@ -12141,18 +12129,6 @@ func init() {
}
}
},
"profilerType": {
"type": "string",
"enum": [
"cpu",
"mem",
"block",
"mutex",
"trace",
"threads",
"goroutines"
]
},
"profilingStartRequest": {
"type": "object",
"required": [
@@ -12160,7 +12136,7 @@ func init() {
],
"properties": {
"type": {
"$ref": "#/definitions/profilerType"
"type": "string"
}
}
},

View File

@@ -24,6 +24,7 @@ package operations
import (
"fmt"
"io"
"net/http"
"strings"
@@ -62,6 +63,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
JSONConsumer: runtime.JSONConsumer(),
MultipartformConsumer: runtime.DiscardConsumer,
ApplicationZipProducer: runtime.ProducerFunc(func(w io.Writer, data interface{}) error {
return errors.NotImplemented("applicationZip producer has not yet been implemented")
}),
BinProducer: runtime.ByteStreamProducer(),
JSONProducer: runtime.JSONProducer(),
@@ -448,6 +452,9 @@ type ConsoleAPI struct {
// - multipart/form-data
MultipartformConsumer runtime.Consumer
// ApplicationZipProducer registers a producer for the following mime types:
// - application/zip
ApplicationZipProducer runtime.Producer
// BinProducer registers a producer for the following mime types:
// - application/octet-stream
BinProducer runtime.Producer
@@ -766,6 +773,9 @@ func (o *ConsoleAPI) Validate() error {
unregistered = append(unregistered, "MultipartformConsumer")
}
if o.ApplicationZipProducer == nil {
unregistered = append(unregistered, "ApplicationZipProducer")
}
if o.BinProducer == nil {
unregistered = append(unregistered, "BinProducer")
}
@@ -1177,6 +1187,8 @@ func (o *ConsoleAPI) ProducersFor(mediaTypes []string) map[string]runtime.Produc
result := make(map[string]runtime.Producer, len(mediaTypes))
for _, mt := range mediaTypes {
switch mt {
case "application/zip":
result["application/zip"] = o.ApplicationZipProducer
case "application/octet-stream":
result["application/octet-stream"] = o.BinProducer
case "application/json":

View File

@@ -2186,7 +2186,7 @@ paths:
summary: Stop and download profile data
operationId: ProfilingStop
produces:
- application/octet-stream
- application/zip
responses:
201:
description: A successful response.
@@ -3469,23 +3469,13 @@ definitions:
type: array
items:
$ref: "#/definitions/startProfilingItem"
profilerType:
type: string
enum:
- cpu
- mem
- block
- mutex
- trace
- threads
- goroutines
profilingStartRequest:
type: object
required:
- type
properties:
type:
$ref: "#/definitions/profilerType"
type: string
sessionResponse:
type: object
properties: