Improved trace functionality & added filters support (#817)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
2
go.mod
2
go.mod
@@ -18,7 +18,7 @@ require (
|
||||
github.com/minio/cli v1.22.0
|
||||
github.com/minio/direct-csi v1.3.5-0.20210601185811-f7776f7961bf
|
||||
github.com/minio/kes v0.11.0
|
||||
github.com/minio/madmin-go v1.0.8
|
||||
github.com/minio/madmin-go v1.0.12
|
||||
github.com/minio/mc v0.0.0-20210531030240-fbbae711bdb4
|
||||
github.com/minio/minio-go/v7 v7.0.11-0.20210517200026-f0518ca447d6
|
||||
github.com/minio/operator v0.0.0-20210604224119-7e256f98cf90
|
||||
|
||||
@@ -174,6 +174,11 @@ export const searchField = {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
padding: "0 16px",
|
||||
"& label, & label.MuiInputLabel-shrink": {
|
||||
fontSize: 10,
|
||||
transform: "translate(5px, 2px)",
|
||||
transformOrigin: "top left",
|
||||
},
|
||||
"& input": {
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
@@ -700,3 +705,10 @@ export const inputFieldStyles = {
|
||||
color: "#b53b4b",
|
||||
},
|
||||
};
|
||||
|
||||
export const inlineCheckboxes = {
|
||||
inlineCheckboxes: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
actionsTray,
|
||||
containerForHeader,
|
||||
searchField,
|
||||
inlineCheckboxes
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import CheckboxWrapper from "../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
@@ -65,10 +66,7 @@ const styles = (theme: Theme) =>
|
||||
scanData: {
|
||||
fontSize: 13,
|
||||
},
|
||||
inlineCheckboxes: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
...inlineCheckboxes,
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
|
||||
@@ -14,23 +14,35 @@
|
||||
// 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/>.
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useState, Fragment } from "react";
|
||||
import { Grid, Button, TextField } from "@material-ui/core";
|
||||
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
|
||||
import { AppState } from "../../../store";
|
||||
import { connect } from "react-redux";
|
||||
import { traceMessageReceived, traceResetMessages } from "./actions";
|
||||
import { traceMessageReceived, traceResetMessages, setTraceStarted } from "./actions";
|
||||
import { TraceMessage } from "./types";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { niceBytes, timeFromDate } from "../../../common/utils";
|
||||
import { wsProtocol } from "../../../utils/wsUtils";
|
||||
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
|
||||
import { Grid } from "@material-ui/core";
|
||||
import {
|
||||
containerForHeader,
|
||||
searchField,
|
||||
actionsTray,
|
||||
hrClass,
|
||||
inlineCheckboxes,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
import CheckboxWrapper from "../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
|
||||
import moment from "moment/moment";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
paperContainer: {
|
||||
padding: 15,
|
||||
paddingLeft: 50,
|
||||
display: "flex",
|
||||
},
|
||||
logList: {
|
||||
background: "white",
|
||||
height: "400px",
|
||||
@@ -52,6 +64,31 @@ const styles = (theme: Theme) =>
|
||||
timeItem: {
|
||||
width: 100,
|
||||
},
|
||||
labelCheckboxes: {
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
paddingTop: 19,
|
||||
},
|
||||
startButton: {
|
||||
textAlign: "right",
|
||||
},
|
||||
...actionsTray,
|
||||
...searchField,
|
||||
...hrClass,
|
||||
...inlineCheckboxes,
|
||||
searchField: {
|
||||
...searchField.searchField,
|
||||
margin: "0 5px",
|
||||
"&:first-of-type": {
|
||||
marginLeft: 0,
|
||||
},
|
||||
"&:last-of-type": {
|
||||
marginRight: 0,
|
||||
},
|
||||
},
|
||||
tableWrapper: {
|
||||
height: "calc(100vh - 292px)",
|
||||
},
|
||||
...containerForHeader(theme.spacing(4)),
|
||||
});
|
||||
|
||||
@@ -59,30 +96,63 @@ interface ITrace {
|
||||
classes: any;
|
||||
traceMessageReceived: typeof traceMessageReceived;
|
||||
traceResetMessages: typeof traceResetMessages;
|
||||
setTraceStarted: typeof setTraceStarted;
|
||||
messages: TraceMessage[];
|
||||
namespace: string;
|
||||
tenant: string;
|
||||
traceStarted: boolean;
|
||||
}
|
||||
|
||||
var c: any = null;
|
||||
|
||||
const Trace = ({
|
||||
classes,
|
||||
traceMessageReceived,
|
||||
traceResetMessages,
|
||||
setTraceStarted,
|
||||
traceStarted,
|
||||
messages,
|
||||
}: ITrace) => {
|
||||
useEffect(() => {
|
||||
const [statusCode, setStatusCode] = useState<string>("");
|
||||
const [method, setMethod] = useState<string>("");
|
||||
const [func, setFunc] = useState<string>("");
|
||||
const [path, setPath] = useState<string>("");
|
||||
const [threshold, setThreshold] = useState<number>(0);
|
||||
const [all, setAll] = useState<boolean>(false);
|
||||
const [s3, setS3] = useState<boolean>(true);
|
||||
const [internal, setInternal] = useState<boolean>(false);
|
||||
const [storage, setStorage] = useState<boolean>(false);
|
||||
const [os, setOS] = useState<boolean>(false);
|
||||
const [errors, setErrors] = useState<boolean>(false);
|
||||
|
||||
const startTrace = () => {
|
||||
traceResetMessages();
|
||||
const url = new URL(window.location.toString());
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const port = isDev ? "9090" : url.port;
|
||||
|
||||
let calls = `${s3 ? "s3," : ""}${internal ? "internal," : ""}${
|
||||
storage ? "storage," : ""
|
||||
}${os ? "os," : ""}`;
|
||||
|
||||
if (all) {
|
||||
calls = "all";
|
||||
}
|
||||
|
||||
const wsProt = wsProtocol(url.protocol);
|
||||
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/trace`);
|
||||
c = new W3CWebSocket(
|
||||
`${wsProt}://${
|
||||
url.hostname
|
||||
}:${port}/ws/trace?calls=${calls}&threshold=${threshold}&onlyErrors=${
|
||||
errors ? "yes" : "no"
|
||||
}&statusCode=${statusCode}&method=${method}&funcname=${func}&path=${path}`
|
||||
);
|
||||
|
||||
let interval: any | null = null;
|
||||
if (c !== null) {
|
||||
c.onopen = () => {
|
||||
console.log("WebSocket Client Connected");
|
||||
setTraceStarted(true);
|
||||
c.send("ok");
|
||||
interval = setInterval(() => {
|
||||
c.send("ok");
|
||||
@@ -97,20 +167,202 @@ const Trace = ({
|
||||
c.onclose = () => {
|
||||
clearInterval(interval);
|
||||
console.log("connection closed by server");
|
||||
setTraceStarted(false);
|
||||
};
|
||||
return () => {
|
||||
c.close(1000);
|
||||
clearInterval(interval);
|
||||
console.log("closing websockets");
|
||||
setTraceStarted(false);
|
||||
};
|
||||
}
|
||||
}, [traceMessageReceived, traceResetMessages]);
|
||||
};
|
||||
|
||||
const stopTrace = () => {
|
||||
c.close(1000);
|
||||
setTraceStarted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Fragment>
|
||||
<PageHeader label={"Trace"} />
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.container}>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Status Code"
|
||||
className={classes.searchField}
|
||||
id="status-code"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
value={statusCode}
|
||||
onChange={(e) => {
|
||||
setStatusCode(e.target.value);
|
||||
}}
|
||||
disabled={traceStarted}
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Method"
|
||||
className={classes.searchField}
|
||||
id="method"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
value={method}
|
||||
onChange={(e) => {
|
||||
setMethod(e.target.value);
|
||||
}}
|
||||
disabled={traceStarted}
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Function Name"
|
||||
className={classes.searchField}
|
||||
id="func-name"
|
||||
label=""
|
||||
disabled={traceStarted}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
value={func}
|
||||
onChange={(e) => {
|
||||
setFunc(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
placeholder="Path"
|
||||
className={classes.searchField}
|
||||
id="path"
|
||||
label=""
|
||||
disabled={traceStarted}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
value={path}
|
||||
onChange={(e) => {
|
||||
setPath(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
className={classes.searchField}
|
||||
id="fthreshold"
|
||||
label="Response Threshold"
|
||||
disabled={traceStarted}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
}}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
}}
|
||||
value={threshold}
|
||||
onChange={(e) => {
|
||||
setThreshold(parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.inlineCheckboxes}>
|
||||
<span className={classes.labelCheckboxes}>Calls to trace:</span>
|
||||
<CheckboxWrapper
|
||||
checked={all}
|
||||
id={"all_calls"}
|
||||
name={"all_calls"}
|
||||
label={"All"}
|
||||
onChange={(item) => {
|
||||
setAll(item.target.checked);
|
||||
}}
|
||||
value={"all"}
|
||||
disabled={traceStarted}
|
||||
/>
|
||||
<CheckboxWrapper
|
||||
checked={s3 || all}
|
||||
id={"s3_calls"}
|
||||
name={"s3_calls"}
|
||||
label={"S3"}
|
||||
onChange={(item) => {
|
||||
setS3(item.target.checked);
|
||||
}}
|
||||
value={"s3"}
|
||||
disabled={all || traceStarted}
|
||||
/>
|
||||
<CheckboxWrapper
|
||||
checked={internal || all}
|
||||
id={"internal_calls"}
|
||||
name={"internal_calls"}
|
||||
label={"Internal"}
|
||||
onChange={(item) => {
|
||||
setInternal(item.target.checked);
|
||||
}}
|
||||
value={"internal"}
|
||||
disabled={all || traceStarted}
|
||||
/>
|
||||
<CheckboxWrapper
|
||||
checked={storage || all}
|
||||
id={"storage_calls"}
|
||||
name={"storage_calls"}
|
||||
label={"Storage"}
|
||||
onChange={(item) => {
|
||||
setStorage(item.target.checked);
|
||||
}}
|
||||
value={"storage"}
|
||||
disabled={all || traceStarted}
|
||||
/>
|
||||
<CheckboxWrapper
|
||||
checked={os || all}
|
||||
id={"os_calls"}
|
||||
name={"os_calls"}
|
||||
label={"OS"}
|
||||
onChange={(item) => {
|
||||
setOS(item.target.checked);
|
||||
}}
|
||||
value={"os"}
|
||||
disabled={all || traceStarted}
|
||||
/>
|
||||
<span className={classes.labelCheckboxes}>
|
||||
|
|
||||
</span>
|
||||
<CheckboxWrapper
|
||||
checked={errors}
|
||||
id={"only_errors"}
|
||||
name={"only_errors"}
|
||||
label={"Display only Errors"}
|
||||
onChange={(item) => {
|
||||
setErrors(item.target.checked);
|
||||
}}
|
||||
value={"only_errors"}
|
||||
disabled={traceStarted}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.startButton}>
|
||||
{!traceStarted && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={traceStarted}
|
||||
onClick={startTrace}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
)}
|
||||
{traceStarted && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={stopTrace}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
|
||||
<TableWrapper
|
||||
itemActions={[]}
|
||||
columns={[
|
||||
@@ -160,22 +412,29 @@ const Trace = ({
|
||||
records={messages}
|
||||
entityName="Traces"
|
||||
idField="api"
|
||||
customEmptyMessage="There are no traced Elements yet"
|
||||
customEmptyMessage={
|
||||
traceStarted
|
||||
? "No Traced elements received yet"
|
||||
: "Trace is not started yet"
|
||||
}
|
||||
customPaperHeight={classes.tableWrapper}
|
||||
autoScrollToBottom
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
messages: state.trace.messages,
|
||||
traceStarted: state.trace.traceStarted,
|
||||
});
|
||||
|
||||
const connector = connect(mapState, {
|
||||
traceMessageReceived: traceMessageReceived,
|
||||
traceResetMessages: traceResetMessages,
|
||||
setTraceStarted,
|
||||
});
|
||||
|
||||
export default connector(withStyles(styles)(Trace));
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
|
||||
import { TraceMessage } from "./types";
|
||||
|
||||
export const TRACE_MESSAGE_RECEIVED = "TRACE_MESSAGE_RECEIVED";
|
||||
export const TRACE_RESET_MESSAGES = "TRACE_RESET_MESSAGES";
|
||||
export const TRACE_MESSAGE_RECEIVED = "TRACE/MESSAGE_RECEIVED";
|
||||
export const TRACE_RESET_MESSAGES = "TRACE/RESET_MESSAGES";
|
||||
export const TRACE_SET_STARTED = "TRACE/SET_STARTED";
|
||||
|
||||
interface TraceMessageReceivedAction {
|
||||
type: typeof TRACE_MESSAGE_RECEIVED;
|
||||
@@ -28,9 +29,15 @@ interface TraceResetMessagesAction {
|
||||
type: typeof TRACE_RESET_MESSAGES;
|
||||
}
|
||||
|
||||
interface TraceSetStarted {
|
||||
type: typeof TRACE_SET_STARTED;
|
||||
status: boolean;
|
||||
}
|
||||
|
||||
export type TraceActionTypes =
|
||||
| TraceMessageReceivedAction
|
||||
| TraceResetMessagesAction;
|
||||
| TraceResetMessagesAction
|
||||
| TraceSetStarted;
|
||||
|
||||
export function traceMessageReceived(message: TraceMessage) {
|
||||
return {
|
||||
@@ -44,3 +51,10 @@ export function traceResetMessages() {
|
||||
type: TRACE_RESET_MESSAGES,
|
||||
};
|
||||
}
|
||||
|
||||
export function setTraceStarted(status: boolean) {
|
||||
return {
|
||||
type: TRACE_SET_STARTED,
|
||||
status
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,16 +17,19 @@
|
||||
import {
|
||||
TRACE_MESSAGE_RECEIVED,
|
||||
TRACE_RESET_MESSAGES,
|
||||
TRACE_SET_STARTED,
|
||||
TraceActionTypes,
|
||||
} from "./actions";
|
||||
import { TraceMessage } from "./types";
|
||||
|
||||
export interface TraceState {
|
||||
messages: TraceMessage[];
|
||||
traceStarted: boolean;
|
||||
}
|
||||
|
||||
const initialState: TraceState = {
|
||||
messages: [],
|
||||
traceStarted: false,
|
||||
};
|
||||
|
||||
export function traceReducer(
|
||||
@@ -44,6 +47,11 @@ export function traceReducer(
|
||||
...state,
|
||||
messages: [],
|
||||
};
|
||||
case TRACE_SET_STARTED:
|
||||
return {
|
||||
...state,
|
||||
traceStarted: action.status,
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -47,15 +47,54 @@ type callStats struct {
|
||||
Ttfb string `json:"timeToFirstByte"`
|
||||
}
|
||||
|
||||
type serviceTraceOpts struct {
|
||||
AllTraffic bool
|
||||
ErrOnly bool
|
||||
// trace filters
|
||||
func matchTrace(opts TraceRequest, traceInfo madmin.ServiceTraceInfo) bool {
|
||||
statusCode := int(opts.statusCode)
|
||||
method := opts.method
|
||||
funcName := opts.funcName
|
||||
apiPath := opts.path
|
||||
|
||||
if statusCode == 0 && method == "" && funcName == "" && apiPath == "" {
|
||||
// no specific filtering found trace all the requests
|
||||
return true
|
||||
}
|
||||
|
||||
// Filter request path if passed by the user
|
||||
if apiPath != "" {
|
||||
pathToLookup := strings.ToLower(apiPath)
|
||||
pathFromTrace := strings.ToLower(traceInfo.Trace.ReqInfo.Path)
|
||||
|
||||
return strings.Contains(pathFromTrace, pathToLookup)
|
||||
}
|
||||
|
||||
// Filter response status codes if passed by the user
|
||||
if statusCode > 0 {
|
||||
statusCodeFromTrace := traceInfo.Trace.RespInfo.StatusCode
|
||||
|
||||
return statusCodeFromTrace == statusCode
|
||||
}
|
||||
|
||||
// Filter request method if passed by the user
|
||||
if method != "" {
|
||||
methodFromTrace := traceInfo.Trace.ReqInfo.Method
|
||||
|
||||
return methodFromTrace == method
|
||||
}
|
||||
|
||||
if funcName != "" {
|
||||
funcToLookup := strings.ToLower(funcName)
|
||||
funcFromTrace := strings.ToLower(traceInfo.Trace.FuncName)
|
||||
|
||||
return strings.Contains(funcFromTrace, funcToLookup)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// startTraceInfo starts trace of the servers
|
||||
func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin, opts serviceTraceOpts) error {
|
||||
func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin, opts TraceRequest) error {
|
||||
// Start listening on all trace activity.
|
||||
traceCh := client.serviceTrace(ctx, opts.AllTraffic, opts.ErrOnly)
|
||||
traceCh := client.serviceTrace(ctx, opts.threshold, opts.s3, opts.internal, opts.storage, opts.os, opts.onlyErrors)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -69,6 +108,7 @@ func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin, opts se
|
||||
LogError("error on serviceTrace: %v", traceInfo.Err)
|
||||
return traceInfo.Err
|
||||
}
|
||||
if matchTrace(opts, traceInfo) {
|
||||
// Serialize message to be sent
|
||||
traceInfoBytes, err := json.Marshal(shortTrace(&traceInfo))
|
||||
if err != nil {
|
||||
@@ -83,6 +123,7 @@ func startTraceInfo(ctx context.Context, conn WSConn, client MinioAdmin, opts se
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shortTrace creates a shorter Trace Info message.
|
||||
@@ -100,6 +141,7 @@ func shortTrace(info *madmin.ServiceTraceInfo) shortTraceMsg {
|
||||
s.CallStats.Duration = t.CallStats.Latency.String()
|
||||
s.CallStats.Rx = t.CallStats.InputBytes
|
||||
s.CallStats.Tx = t.CallStats.OutputBytes
|
||||
s.CallStats.Ttfb = t.CallStats.TimeToFirstByte.String()
|
||||
|
||||
if host, ok := t.ReqInfo.Headers["Host"]; ok {
|
||||
s.Host = strings.Join(host, "")
|
||||
|
||||
@@ -27,11 +27,11 @@ import (
|
||||
)
|
||||
|
||||
// assigning mock at runtime instead of compile time
|
||||
var minioServiceTraceMock func(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
var minioServiceTraceMock func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
|
||||
// mock function of listPolicies()
|
||||
func (ac adminClientMock) serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
return minioServiceTraceMock(ctx, allTrace, errTrace)
|
||||
func (ac adminClientMock) serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
return minioServiceTraceMock(ctx, threshold, s3, internal, storage, os, errTrace)
|
||||
}
|
||||
|
||||
func TestAdminTrace(t *testing.T) {
|
||||
@@ -48,7 +48,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
|
||||
// Test-1: Serve Trace with no errors until trace finishes sending
|
||||
// define mock function behavior for minio server Trace
|
||||
minioServiceTraceMock = func(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
minioServiceTraceMock = func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
ch := make(chan madmin.ServiceTraceInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.ServiceTraceInfo) {
|
||||
@@ -82,7 +82,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
writesCount++
|
||||
return nil
|
||||
}
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, serviceTraceOpts{AllTraffic: true, ErrOnly: false}); err != nil {
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, TraceRequest{s3: true, internal: true, storage: true, os: true, onlyErrors: false}); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// check that the TestReceiver got the same number of data from trace.
|
||||
@@ -94,13 +94,13 @@ func TestAdminTrace(t *testing.T) {
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return fmt.Errorf("error on write")
|
||||
}
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, serviceTraceOpts{}); assert.Error(err) {
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, TraceRequest{}); assert.Error(err) {
|
||||
assert.Equal("error on write", err.Error())
|
||||
}
|
||||
|
||||
// Test-3: error happens on serviceTrace Minio, trace should stop
|
||||
// and error shall be returned.
|
||||
minioServiceTraceMock = func(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
minioServiceTraceMock = func(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
ch := make(chan madmin.ServiceTraceInfo)
|
||||
// Only success, start a routine to start reading line by line.
|
||||
go func(ch chan<- madmin.ServiceTraceInfo) {
|
||||
@@ -120,7 +120,7 @@ func TestAdminTrace(t *testing.T) {
|
||||
connWriteMessageMock = func(messageType int, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, serviceTraceOpts{}); assert.Error(err) {
|
||||
if err := startTraceInfo(ctx, mockWSConn, adminClient, TraceRequest{}); assert.Error(err) {
|
||||
assert.Equal("error on trace", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ type MinioAdmin interface {
|
||||
serverInfo(ctx context.Context) (madmin.InfoMessage, error)
|
||||
startProfiling(ctx context.Context, profiler madmin.ProfilerType) ([]madmin.StartProfilingResult, error)
|
||||
stopProfiling(ctx context.Context) (io.ReadCloser, error)
|
||||
serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo
|
||||
getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo
|
||||
accountInfo(ctx context.Context) (madmin.AccountInfo, error)
|
||||
heal(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
|
||||
@@ -253,13 +253,16 @@ func (ac adminClient) stopProfiling(ctx context.Context) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// implements madmin.ServiceTrace()
|
||||
func (ac adminClient) serviceTrace(ctx context.Context, allTrace, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
func (ac adminClient) serviceTrace(ctx context.Context, threshold int64, s3, internal, storage, os, errTrace bool) <-chan madmin.ServiceTraceInfo {
|
||||
thresholdT := time.Duration(threshold)
|
||||
|
||||
tracingOptions := madmin.ServiceTraceOpts{
|
||||
S3: true,
|
||||
OnlyErrors: errTrace,
|
||||
Internal: allTrace,
|
||||
Storage: allTrace,
|
||||
OS: allTrace,
|
||||
Internal: internal,
|
||||
Storage: storage,
|
||||
OS: os,
|
||||
Threshold: thresholdT,
|
||||
}
|
||||
|
||||
return ac.client.ServiceTrace(ctx, tracingOptions)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -81,6 +82,20 @@ type wsConn struct {
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
// Types for trace request. this adds support for calls, threshold, status and extra filters
|
||||
type TraceRequest struct {
|
||||
s3 bool
|
||||
internal bool
|
||||
storage bool
|
||||
os bool
|
||||
threshold int64
|
||||
onlyErrors bool
|
||||
statusCode int64
|
||||
method string
|
||||
funcName string
|
||||
path string
|
||||
}
|
||||
|
||||
func (c wsConn) writeMessage(messageType int, data []byte) error {
|
||||
return c.conn.WriteMessage(messageType, data)
|
||||
}
|
||||
@@ -122,7 +137,35 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
|
||||
closeWsConn(conn)
|
||||
return
|
||||
}
|
||||
go wsAdminClient.trace()
|
||||
|
||||
calls := req.URL.Query().Get("calls")
|
||||
threshold, _ := strconv.ParseInt(req.URL.Query().Get("threshold"), 10, 64)
|
||||
onlyErrors := req.URL.Query().Get("onlyErrors")
|
||||
stCode, errorStCode := strconv.ParseInt(req.URL.Query().Get("statusCode"), 10, 64)
|
||||
method := req.URL.Query().Get("method")
|
||||
funcName := req.URL.Query().Get("funcname")
|
||||
path := req.URL.Query().Get("path")
|
||||
|
||||
statusCode := int64(0)
|
||||
|
||||
if errorStCode == nil {
|
||||
statusCode = stCode
|
||||
}
|
||||
|
||||
traceRequestItem := TraceRequest{
|
||||
s3: strings.Contains(calls, "s3") || strings.Contains(calls, "all"),
|
||||
internal: strings.Contains(calls, "internal") || strings.Contains(calls, "all"),
|
||||
storage: strings.Contains(calls, "storage") || strings.Contains(calls, "all"),
|
||||
os: strings.Contains(calls, "os") || strings.Contains(calls, "all"),
|
||||
onlyErrors: onlyErrors == "yes",
|
||||
threshold: threshold,
|
||||
statusCode: statusCode,
|
||||
method: method,
|
||||
funcName: funcName,
|
||||
path: path,
|
||||
}
|
||||
|
||||
go wsAdminClient.trace(traceRequestItem)
|
||||
case strings.HasPrefix(wsPath, `/console`):
|
||||
wsAdminClient, err := newWebSocketAdminClient(conn, session)
|
||||
if err != nil {
|
||||
@@ -254,7 +297,7 @@ func closeWsConn(conn *websocket.Conn) {
|
||||
|
||||
// trace serves madmin.ServiceTraceInfo
|
||||
// on a Websocket connection.
|
||||
func (wsc *wsAdminClient) trace() {
|
||||
func (wsc *wsAdminClient) trace(traceRequestItem TraceRequest) {
|
||||
defer func() {
|
||||
LogInfo("trace stopped")
|
||||
// close connection after return
|
||||
@@ -264,10 +307,7 @@ func (wsc *wsAdminClient) trace() {
|
||||
|
||||
ctx := wsReadClientCtx(wsc.conn)
|
||||
|
||||
err := startTraceInfo(ctx, wsc.conn, wsc.client, serviceTraceOpts{
|
||||
AllTraffic: false,
|
||||
ErrOnly: false,
|
||||
})
|
||||
err := startTraceInfo(ctx, wsc.conn, wsc.client, traceRequestItem)
|
||||
|
||||
sendWsCloseMessage(wsc.conn, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user