// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// 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 cmd

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"hash/fnv"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"sort"
	"strings"
	"sync"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/dustin/go-humanize"
	"github.com/fatih/color"
	"github.com/klauspost/compress/zstd"
	"github.com/minio/cli"
	json "github.com/minio/colorjson"
	"github.com/minio/madmin-go/v3"
	"github.com/minio/mc/pkg/probe"
	"github.com/minio/pkg/v3/console"
)

var adminTraceFlags = []cli.Flag{
	cli.BoolFlag{
		Name:  "verbose, v",
		Usage: "print verbose trace",
	},
	cli.BoolFlag{
		Name:  "all, a",
		Usage: "trace all call types",
	},
	cli.StringSliceFlag{
		Name:  "call",
		Usage: "trace only matching call types. See CALL TYPES below for list. (default: s3)",
	},
	cli.IntSliceFlag{
		Name:  "status-code",
		Usage: "trace only matching status code",
	},
	cli.StringSliceFlag{
		Name:  "method",
		Usage: "trace only matching HTTP method",
	},
	cli.StringSliceFlag{
		Name:  "funcname",
		Usage: "trace only matching func name",
	},
	cli.StringSliceFlag{
		Name:  "path",
		Usage: "trace only matching path",
	},
	cli.StringSliceFlag{
		Name:  "node",
		Usage: "trace only matching servers",
	},
	cli.StringSliceFlag{
		Name:  "request-header",
		Usage: "trace only matching request headers",
	},
	cli.StringSliceFlag{
		Name:  "request-query",
		Usage: "trace only matching request queries",
	},
	cli.BoolFlag{
		Name:  "errors, e",
		Usage: "trace only failed requests",
	},
	cli.BoolFlag{
		Name:  "stats",
		Usage: "print statistical summary of all the traced calls",
	},
	cli.IntFlag{
		Name:   "stats-n",
		Usage:  "maximum number of stat entries",
		Value:  20,
		Hidden: true,
	},
	cli.BoolFlag{
		Name:  "filter-request",
		Usage: "trace calls only with request bytes greater than this threshold, use with filter-size",
	},
	cli.BoolFlag{
		Name:  "filter-response",
		Usage: "trace calls only with response bytes greater than this threshold, use with filter-size",
	},
	cli.DurationFlag{
		Name:  "response-duration",
		Usage: "trace calls only with response duration greater than this threshold (e.g. `5ms`)",
	},
	cli.StringFlag{
		Name:  "filter-size",
		Usage: "filter size, use with filter (see UNITS)",
	},
	cli.StringFlag{
		Name:  "in",
		Usage: "read previously saved json from file and replay",
	},
}

// traceCallTypes contains all call types and flags to apply when selected.
var traceCallTypes = map[string]func(o *madmin.ServiceTraceOpts) (help string){
	"storage":   func(o *madmin.ServiceTraceOpts) string { o.Storage = true; return "Trace Storage calls" },
	"internal":  func(o *madmin.ServiceTraceOpts) string { o.Internal = true; return "Trace Internal RPC calls" },
	"s3":        func(o *madmin.ServiceTraceOpts) string { o.S3 = true; return "Trace S3 API calls" },
	"os":        func(o *madmin.ServiceTraceOpts) string { o.OS = true; return "Trace Operating System calls" },
	"scanner":   func(o *madmin.ServiceTraceOpts) string { o.Scanner = true; return "Trace Scanner calls" },
	"bootstrap": func(o *madmin.ServiceTraceOpts) string { o.Bootstrap = true; return "Trace Bootstrap operations" },
	"ilm":       func(o *madmin.ServiceTraceOpts) string { o.ILM = true; return "Trace ILM operations" },
	"ftp":       func(o *madmin.ServiceTraceOpts) string { o.FTP = true; return "Trace FTP operations" },

	"healing": func(o *madmin.ServiceTraceOpts) string {
		o.Healing = true
		return "Trace Healing operations (alias: heal)"
	},
	"batch-replication": func(o *madmin.ServiceTraceOpts) string {
		o.BatchReplication = true
		return "Trace Batch Replication (alias: brep)"
	},
	"batch-keyrotation": func(o *madmin.ServiceTraceOpts) string {
		o.BatchKeyRotation = true
		return "Trace Batch KeyRotation (alias: brot)"
	},
	"batch-expiration": func(o *madmin.ServiceTraceOpts) string {
		o.BatchExpire = true
		return "Trace Batch Expiration (alias: bexp)"
	},

	"decommission": func(o *madmin.ServiceTraceOpts) string {
		o.Decommission = true
		return "Trace Decommission operations (alias: decom)"
	},
	"rebalance": func(o *madmin.ServiceTraceOpts) string {
		o.Rebalance = true
		return "Trace Server Pool Rebalancing operations"
	},
	"replication-resync": func(o *madmin.ServiceTraceOpts) string {
		o.ReplicationResync = true
		return "Trace Replication Resync operations (alias: resync)"
	},
}

// traceCallTypes contains aliases (short versions) of
var traceCallTypeAliases = map[string]func(o *madmin.ServiceTraceOpts) string{
	"heal":   traceCallTypes["healing"],
	"decom":  traceCallTypes["decommission"],
	"resync": traceCallTypes["replication-resync"],
	"brep":   traceCallTypes["batch-replication"],
	"brot":   traceCallTypes["batch-keyrotation"],
	"bexp":   traceCallTypes["batch-expiration"],
}

func traceCallsHelp() string {
	var help []string
	o := madmin.ServiceTraceOpts{}
	const padkeyLen = 19
	for k, fn := range traceCallTypes {
		pad := ""
		if len(k) < padkeyLen {
			pad = strings.Repeat(" ", padkeyLen-len(k))
		}
		help = append(help, fmt.Sprintf("  %s: %s%s", k, pad, fn(&o)))
	}
	sort.Strings(help)
	return strings.Join(help, "\n")
}

var adminTraceCmd = cli.Command{
	Name:            "trace",
	Usage:           "Show HTTP call trace for all incoming and internode on MinIO",
	Action:          mainAdminTrace,
	OnUsageError:    onUsageError,
	Before:          setGlobalsFromContext,
	Flags:           append(adminTraceFlags, globalFlags...),
	HideHelpCommand: true,
	CustomHelpTemplate: `NAME:
  {{.HelpName}} - {{.Usage}}

USAGE:
  {{.HelpName}} [FLAGS] TARGET

FLAGS:
  {{range .VisibleFlags}}{{.}}
  {{end}}
CALL TYPES:
` + traceCallsHelp() + `

UNITS
  --filter-size flags use with --filter-response or --filter-request accept human-readable case-insensitive number
  suffixes such as "k", "m", "g" and "t" referring to the metric units KB,
  MB, GB and TB respectively. Adding an "i" to these prefixes, uses the IEC
  units, so that "gi" refers to "gibibyte" or "GiB". A "b" at the end is
  also accepted. Without suffixes the unit is bytes.

EXAMPLES:
  1. Show verbose console trace for MinIO server
     {{.Prompt}} {{.HelpName}} -v -a myminio

  2. Show trace only for failed requests for MinIO server
    {{.Prompt}} {{.HelpName}} -v -e myminio

  3. Show verbose console trace for requests with '503' status code
    {{.Prompt}} {{.HelpName}} -v --status-code 503 myminio

  4. Show console trace for a specific path
    {{.Prompt}} {{.HelpName}} --path my-bucket/my-prefix/* myminio

  5. Show console trace for requests with '404' and '503' status code
    {{.Prompt}} {{.HelpName}} --status-code 404 --status-code 503 myminio
  
  6. Show trace only for requests bytes greater than 1MB
    {{.Prompt}} {{.HelpName}} --filter-request --filter-size 1MB myminio

  7. Show trace only for response bytes greater than 1MB
    {{.Prompt}} {{.HelpName}} --filter-response --filter-size 1MB myminio
  
  8. Show trace only for requests operations duration greater than 5ms
     {{.Prompt}} {{.HelpName}} --response-duration 5ms myminio
`,
}

const traceTimeFormat = "2006-01-02T15:04:05.000"

var colors = []color.Attribute{color.FgCyan, color.FgWhite, color.FgYellow, color.FgGreen}

func checkAdminTraceSyntax(ctx *cli.Context) {
	if len(ctx.Args()) != 1 && len(ctx.String("in")) == 0 {
		showCommandHelpAndExit(ctx, 1) // last argument is exit code
	}
	filterFlag := ctx.Bool("filter-request") || ctx.Bool("filter-response")
	if filterFlag && ctx.String("filter-size") == "" {
		// filter must use with filter-size flags
		showCommandHelpAndExit(ctx, 1)
	}

	if ctx.Bool("all") && len(ctx.StringSlice("call")) > 0 {
		fatalIf(errDummy().Trace(), "You cannot specify both --all and --call flags at the same time.")
	}
}

func printTrace(verbose bool, traceInfo madmin.ServiceTraceInfo) {
	if verbose {
		printMsg(traceMessage{ServiceTraceInfo: traceInfo})
	} else {
		printMsg(shortTrace(traceInfo))
	}
}

type matchString struct {
	val     string
	reverse bool
}

type matchOpts struct {
	statusCodes  []int
	methods      []string
	funcNames    []string
	apiPaths     []string
	nodes        []string
	reqHeaders   []matchString
	reqQueries   []matchString
	requestSize  uint64
	responseSize uint64
}

func (opts matchOpts) matches(traceInfo madmin.ServiceTraceInfo) bool {
	// Filter request path if passed by the user
	if len(opts.apiPaths) > 0 {
		matched := false
		for _, apiPath := range opts.apiPaths {
			if pathMatch(path.Join("/", apiPath), traceInfo.Trace.Path) {
				matched = true
				break
			}
		}
		if !matched {
			return false
		}
	}

	// Filter response status codes if passed by the user
	if len(opts.statusCodes) > 0 {
		matched := false
		for _, code := range opts.statusCodes {
			if traceInfo.Trace.HTTP != nil && traceInfo.Trace.HTTP.RespInfo.StatusCode == code {
				matched = true
				break
			}
		}
		if !matched {
			return false
		}

	}

	// Filter request method if passed by the user
	if len(opts.methods) > 0 {
		matched := false
		for _, method := range opts.methods {
			if traceInfo.Trace.HTTP != nil && traceInfo.Trace.HTTP.ReqInfo.Method == method {
				matched = true
				break
			}
		}
		if !matched {
			return false
		}

	}

	if len(opts.funcNames) > 0 {
		matched := false
		// Filter request function handler names if passed by the user.
		for _, funcName := range opts.funcNames {
			if nameMatch(funcName, traceInfo.Trace.FuncName) {
				matched = true
				break
			}
		}
		if !matched {
			return false
		}
	}

	if len(opts.nodes) > 0 {
		matched := false
		// Filter request by node if passed by the user.
		for _, node := range opts.nodes {
			if nameMatch(node, traceInfo.Trace.NodeName) {
				matched = true
				break
			}
		}
		if !matched {
			return false
		}
	}

	if len(opts.reqHeaders) > 0 && traceInfo.Trace.HTTP != nil {
		matched := false
		for _, hdr := range opts.reqHeaders {
			headerFound := false
			for traceHdr, traceVals := range traceInfo.Trace.HTTP.ReqInfo.Headers {
				for _, traceVal := range traceVals {
					if patternMatch(hdr.val, traceHdr+": "+traceVal) {
						headerFound = true
						goto exitFindingHeader
					}
				}
			}
		exitFindingHeader:
			if !hdr.reverse && headerFound || hdr.reverse && !headerFound {
				matched = true
				goto exitMatchingHeader
			}
		}
	exitMatchingHeader:
		if !matched {
			return false
		}
	}

	if len(opts.reqQueries) > 0 && traceInfo.Trace.HTTP != nil {
		matched := false
		for _, qry := range opts.reqQueries {
			queryFound := false
			v, err := url.ParseQuery(traceInfo.Trace.HTTP.ReqInfo.RawQuery)
			if err != nil {
				continue
			}
			for traceQuery, traceVals := range v {
				for _, traceVal := range traceVals {
					if patternMatch(qry.val, traceQuery+"="+traceVal) {
						queryFound = true
						goto exitFindingQuery
					}
				}
			}
		exitFindingQuery:
			if !qry.reverse && queryFound || qry.reverse && !queryFound {
				matched = true
				goto exitMatchingQuery
			}
		}
	exitMatchingQuery:
		if !matched {
			return false
		}
	}

	if opts.requestSize > 0 && traceInfo.Trace.HTTP.CallStats.InputBytes < int(opts.requestSize) {
		return false
	}

	if opts.responseSize > 0 && traceInfo.Trace.HTTP.CallStats.OutputBytes < int(opts.responseSize) {
		return false
	}

	return true
}

func matchingOpts(ctx *cli.Context) (opts matchOpts) {
	opts.statusCodes = ctx.IntSlice("status-code")
	opts.methods = ctx.StringSlice("method")
	opts.funcNames = ctx.StringSlice("funcname")
	opts.apiPaths = ctx.StringSlice("path")
	opts.nodes = ctx.StringSlice("node")
	for _, s := range ctx.StringSlice("request-header") {
		opts.reqHeaders = append(opts.reqHeaders, matchString{
			reverse: strings.HasPrefix(s, "!"),
			val:     strings.TrimPrefix(s, "!"),
		})
	}
	for _, s := range ctx.StringSlice("request-query") {
		opts.reqQueries = append(opts.reqQueries, matchString{
			reverse: strings.HasPrefix(s, "!"),
			val:     strings.TrimPrefix(s, "!"),
		})
	}

	var e error
	var requestSize, responseSize uint64
	if ctx.Bool("filter-request") && ctx.String("filter-size") != "" {
		requestSize, e = humanize.ParseBytes(ctx.String("filter-size"))
		fatalIf(probe.NewError(e).Trace(ctx.String("filter-size")), "Unable to parse input bytes.")
	}

	if ctx.Bool("filter-response") && ctx.String("filter-size") != "" {
		responseSize, e = humanize.ParseBytes(ctx.String("filter-size"))
		fatalIf(probe.NewError(e).Trace(ctx.String("filter-size")), "Unable to parse input bytes.")
	}
	opts.requestSize = requestSize
	opts.responseSize = responseSize
	return
}

// Calculate tracing options for command line flags
func tracingOpts(ctx *cli.Context, apis []string) (opts madmin.ServiceTraceOpts, e error) {
	opts.Threshold = ctx.Duration("response-duration")
	opts.OnlyErrors = ctx.Bool("errors")

	if ctx.Bool("all") {
		for _, fn := range traceCallTypes {
			fn(&opts)
		}
	}

	if len(apis) == 0 {
		// If api flag is not specified, then we will
		// trace only S3 requests by default.
		opts.S3 = true
		return
	}

	for _, api := range apis {
		for _, api := range strings.Split(api, ",") {
			fn, ok := traceCallTypes[api]
			if !ok {
				fn, ok = traceCallTypeAliases[api]
			}
			if !ok {
				return madmin.ServiceTraceOpts{}, fmt.Errorf("unknown call name: `%s`", api)
			}
			fn(&opts)
		}
	}
	return
}

// mainAdminTrace - the entry function of trace command
func mainAdminTrace(ctx *cli.Context) error {
	// Check for command syntax
	checkAdminTraceSyntax(ctx)

	verbose := ctx.Bool("verbose")
	stats := ctx.Bool("stats")

	console.SetColor("Stat", color.New(color.FgYellow))

	console.SetColor("Request", color.New(color.FgCyan))
	console.SetColor("Method", color.New(color.Bold, color.FgWhite))
	console.SetColor("Host", color.New(color.Bold, color.FgGreen))
	console.SetColor("FuncName", color.New(color.Bold, color.FgGreen))

	console.SetColor("ReqHeaderKey", color.New(color.Bold, color.FgWhite))
	console.SetColor("RespHeaderKey", color.New(color.Bold, color.FgCyan))
	console.SetColor("HeaderValue", color.New(color.FgWhite))
	console.SetColor("RespStatus", color.New(color.Bold, color.FgYellow))
	console.SetColor("ErrStatus", color.New(color.Bold, color.FgRed))

	console.SetColor("Response", color.New(color.FgGreen))
	console.SetColor("Extra", color.New(color.FgBlue))
	console.SetColor("Body", color.New(color.FgYellow))
	for _, c := range colors {
		console.SetColor(fmt.Sprintf("Node%d", c), color.New(c))
	}

	var traceCh <-chan madmin.ServiceTraceInfo

	ctxt, cancel := context.WithCancel(globalContext)
	defer cancel()

	if inFile := ctx.String("in"); inFile != "" {
		stats = true
		ch := make(chan madmin.ServiceTraceInfo, 1000)
		traceCh = ch
		go func() {
			f, e := os.Open(inFile)
			fatalIf(probe.NewError(e), "Unable to open input")
			defer f.Close()
			in := io.Reader(f)
			if strings.HasSuffix(inFile, ".zst") {
				zr, e := zstd.NewReader(in)
				fatalIf(probe.NewError(e), "Unable to open input")
				defer zr.Close()
				in = zr
			}
			sc := bufio.NewReader(in)
			for ctxt.Err() == nil {
				b, e := sc.ReadBytes('\n')
				if e == io.EOF {
					break
				}
				var t shortTraceMsg
				e = json.Unmarshal(b, &t)
				if e != nil || t.Type == "Bootstrap" {
					// Ignore bootstrap, since their times skews averages.
					continue
				}
				ch <- madmin.ServiceTraceInfo{
					Trace: madmin.TraceInfo{
						TraceType:  t.trcType, // TODO: Grab from string, once we can.
						NodeName:   t.Host,
						FuncName:   t.FuncName,
						Time:       t.Time,
						Path:       t.Path,
						Duration:   t.Duration,
						Bytes:      t.Size,
						Message:    t.StatusMsg,
						Error:      t.Error,
						Custom:     t.Extra,
						HTTP:       nil,
						HealResult: nil,
					},
					Err: nil,
				}
			}
			close(ch)
			select {}
		}()
	} else {
		// Create a new MinIO Admin Client
		aliasedURL := ctx.Args().Get(0)

		client, err := newAdminClient(aliasedURL)
		if err != nil {
			fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.")
			return nil
		}

		opts, e := tracingOpts(ctx, ctx.StringSlice("call"))
		fatalIf(probe.NewError(e), "Unable to start tracing")

		// Start listening on all trace activity.
		traceCh = client.ServiceTrace(ctxt, opts)
	}

	mopts := matchingOpts(ctx)
	if stats {
		filteredTraces := make(chan madmin.ServiceTraceInfo, 1)
		ui := tea.NewProgram(initTraceStatsUI(ctx.Bool("all"), ctx.Int("stats-n"), filteredTraces))
		var te error
		go func() {
			for t := range traceCh {
				if t.Err != nil {
					te = t.Err
					ui.Kill()
					return
				}
				if mopts.matches(t) {
					filteredTraces <- t
				}
			}
			ui.Send(tea.Quit())
		}()
		if _, e := ui.Run(); e != nil {
			cancel()
			if te != nil {
				e = te
			}
			aliasedURL := ctx.Args().Get(0)
			fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch http trace statistics")
		}
		return nil
	}
	for traceInfo := range traceCh {
		if traceInfo.Err != nil {
			fatalIf(probe.NewError(traceInfo.Err), "Unable to listen to http trace")
		}
		if mopts.matches(traceInfo) {
			printTrace(verbose, traceInfo)
		}
	}

	return nil
}

// Short trace record
type shortTraceMsg struct {
	Status     string            `json:"status"`
	Host       string            `json:"host"`
	Time       time.Time         `json:"time"`
	Client     string            `json:"client"`
	CallStats  *callStats        `json:"callStats,omitempty"`
	Duration   time.Duration     `json:"duration"`
	TTFB       time.Duration     `json:"timeToFirstByte"`
	FuncName   string            `json:"api"`
	Path       string            `json:"path"`
	Query      string            `json:"query"`
	StatusCode int               `json:"statusCode"`
	StatusMsg  string            `json:"statusMsg"`
	Type       string            `json:"type"`
	Size       int64             `json:"size,omitempty"`
	Error      string            `json:"error"`
	Extra      map[string]string `json:"extra"`
	trcType    madmin.TraceType
}

type traceMessage struct {
	Status string `json:"status"`
	madmin.ServiceTraceInfo
}

type requestInfo struct {
	Time     time.Time         `json:"time"`
	Proto    string            `json:"proto"`
	Method   string            `json:"method"`
	Path     string            `json:"path,omitempty"`
	RawQuery string            `json:"rawQuery,omitempty"`
	Headers  map[string]string `json:"headers,omitempty"`
	Body     string            `json:"body,omitempty"`
}

type responseInfo struct {
	Time       time.Time         `json:"time"`
	Headers    map[string]string `json:"headers,omitempty"`
	Body       string            `json:"body,omitempty"`
	StatusCode int               `json:"statusCode,omitempty"`
}

type callStats struct {
	Rx       int           `json:"rx"`
	Tx       int           `json:"tx"`
	Duration time.Duration `json:"duration"`
	TTFB     time.Duration `json:"timeToFirstByte"`
}

type verboseTrace struct {
	Type string `json:"type"`

	NodeName     string                 `json:"host"`
	FuncName     string                 `json:"api"`
	Time         time.Time              `json:"time"`
	Duration     time.Duration          `json:"duration"`
	Path         string                 `json:"path"`
	Error        string                 `json:"error,omitempty"`
	Message      string                 `json:"message,omitempty"`
	RequestInfo  *requestInfo           `json:"request,omitempty"`
	ResponseInfo *responseInfo          `json:"response,omitempty"`
	CallStats    *callStats             `json:"callStats,omitempty"`
	HealResult   *madmin.HealResultItem `json:"healResult,omitempty"`
	Extra        map[string]string      `json:"extra,omitempty"`

	trcType madmin.TraceType
}

// return a struct with minimal trace info.
func shortTrace(ti madmin.ServiceTraceInfo) shortTraceMsg {
	s := shortTraceMsg{}
	t := ti.Trace

	s.trcType = t.TraceType
	s.Type = t.TraceType.String()
	s.FuncName = t.FuncName
	s.Time = t.Time
	s.Path = t.Path
	s.Error = t.Error
	s.Host = t.NodeName
	s.Duration = t.Duration
	s.StatusMsg = t.Message
	s.Extra = t.Custom
	s.Size = t.Bytes

	switch t.TraceType {
	case madmin.TraceS3, madmin.TraceInternal:
		s.Query = t.HTTP.ReqInfo.RawQuery
		s.StatusCode = t.HTTP.RespInfo.StatusCode
		s.StatusMsg = http.StatusText(t.HTTP.RespInfo.StatusCode)
		s.Client = t.HTTP.ReqInfo.Client
		s.CallStats = &callStats{}
		s.CallStats.Duration = t.Duration
		s.CallStats.Rx = t.HTTP.CallStats.InputBytes
		s.CallStats.Tx = t.HTTP.CallStats.OutputBytes
		s.CallStats.TTFB = t.HTTP.CallStats.TimeToFirstByte
	}
	return s
}

func (s shortTraceMsg) JSON() string {
	s.Status = "success"
	buf := &bytes.Buffer{}
	enc := json.NewEncoder(buf)
	enc.SetIndent("", " ")
	// Disable escaping special chars to display XML tags correctly
	enc.SetEscapeHTML(false)

	fatalIf(probe.NewError(enc.Encode(s)), "Unable to marshal into JSON.")
	return buf.String()
}

func (s shortTraceMsg) String() string {
	var hostStr string
	b := &strings.Builder{}

	if s.Host != "" {
		hostStr = colorizedNodeName(s.Host)
	}
	fmt.Fprintf(b, "%s ", s.Time.Local().Format(traceTimeFormat))

	switch s.trcType {
	case madmin.TraceS3, madmin.TraceInternal:
	case madmin.TraceBootstrap:
		fmt.Fprintf(b, "[%s] %s %s %s", console.Colorize("RespStatus", strings.ToUpper(s.trcType.String())), console.Colorize("FuncName", s.FuncName),
			hostStr,
			s.StatusMsg,
		)
		return b.String()
	default:
		if s.Error != "" {
			fmt.Fprintf(b, "[%s] %s %s %s err='%s' %2s", console.Colorize("RespStatus", strings.ToUpper(s.trcType.String())), console.Colorize("FuncName", s.FuncName),
				hostStr,
				s.Path,
				console.Colorize("ErrStatus", s.Error),
				console.Colorize("HeaderValue", s.Duration))
		} else {
			sz := ""
			if s.Size != 0 {
				sz = fmt.Sprintf(" %s", humanize.IBytes(uint64(s.Size)))
			}
			fmt.Fprintf(b, "[%s] %s %s %s %2s%s", console.Colorize("RespStatus", strings.ToUpper(s.trcType.String())), console.Colorize("FuncName", s.FuncName),
				hostStr,
				s.Path,
				console.Colorize("HeaderValue", s.Duration),
				sz)
		}
		return b.String()
	}

	statusStr := fmt.Sprintf("%d %s", s.StatusCode, s.StatusMsg)
	if s.StatusCode >= http.StatusBadRequest {
		statusStr = console.Colorize("ErrStatus", statusStr)
	} else {
		statusStr = console.Colorize("RespStatus", statusStr)
	}

	fmt.Fprintf(b, "[%s] %s ", statusStr, console.Colorize("FuncName", s.FuncName))
	fmt.Fprintf(b, "%s%s", hostStr, s.Path)

	if s.Query != "" {
		fmt.Fprintf(b, "?%s ", s.Query)
	}
	fmt.Fprintf(b, " %s ", s.Client)

	spaces := 15 - len(s.Client)
	fmt.Fprintf(b, "%*s", spaces, " ")
	fmt.Fprint(b, console.Colorize("HeaderValue", fmt.Sprintf(" %2s", s.CallStats.Duration.Round(time.Microsecond))))
	spaces = 12 - len(fmt.Sprintf("%2s", s.CallStats.Duration.Round(time.Microsecond)))
	fmt.Fprintf(b, "%*s", spaces, " ")
	fmt.Fprint(b, console.Colorize("Stat", " ⇣ "))
	fmt.Fprint(b, console.Colorize("HeaderValue", fmt.Sprintf(" %2s", s.CallStats.TTFB.Round(time.Nanosecond))))
	spaces = 10 - len(fmt.Sprintf("%2s", s.CallStats.TTFB.Round(time.Nanosecond)))
	fmt.Fprintf(b, "%*s", spaces, " ")
	fmt.Fprint(b, console.Colorize("Stat", " ↑ "))
	fmt.Fprint(b, console.Colorize("HeaderValue", humanize.IBytes(uint64(s.CallStats.Rx))))
	fmt.Fprint(b, console.Colorize("Stat", " ↓ "))
	fmt.Fprint(b, console.Colorize("HeaderValue", humanize.IBytes(uint64(s.CallStats.Tx))))

	return b.String()
}

// colorize node name
func colorizedNodeName(nodeName string) string {
	nodeHash := fnv.New32a()
	nodeHash.Write([]byte(nodeName))
	nHashSum := nodeHash.Sum32()
	idx := nHashSum % uint32(len(colors))
	return console.Colorize(fmt.Sprintf("Node%d", colors[idx]), nodeName)
}

func (t traceMessage) JSON() string {
	trc := verboseTrace{
		trcType:    t.Trace.TraceType,
		Type:       t.Trace.TraceType.String(),
		NodeName:   t.Trace.NodeName,
		FuncName:   t.Trace.FuncName,
		Time:       t.Trace.Time,
		Duration:   t.Trace.Duration,
		Path:       t.Trace.Path,
		Error:      t.Trace.Error,
		HealResult: t.Trace.HealResult,
		Message:    t.Trace.Message,
		Extra:      t.Trace.Custom,
	}

	if t.Trace.HTTP != nil {
		rq := t.Trace.HTTP.ReqInfo
		rs := t.Trace.HTTP.RespInfo

		var (
			rqHdrs  = make(map[string]string)
			rspHdrs = make(map[string]string)
		)
		for k, v := range rq.Headers {
			rqHdrs[k] = strings.Join(v, " ")
		}
		for k, v := range rs.Headers {
			rspHdrs[k] = strings.Join(v, " ")
		}

		trc.RequestInfo = &requestInfo{
			Time:     rq.Time,
			Proto:    rq.Proto,
			Method:   rq.Method,
			Path:     rq.Path,
			RawQuery: rq.RawQuery,
			Body:     string(rq.Body),
			Headers:  rqHdrs,
		}
		trc.ResponseInfo = &responseInfo{
			Time:       rs.Time,
			Body:       string(rs.Body),
			Headers:    rspHdrs,
			StatusCode: rs.StatusCode,
		}
		trc.CallStats = &callStats{
			Duration: t.Trace.Duration,
			Rx:       t.Trace.HTTP.CallStats.InputBytes,
			Tx:       t.Trace.HTTP.CallStats.OutputBytes,
			TTFB:     t.Trace.HTTP.CallStats.TimeToFirstByte,
		}
	}
	buf := &bytes.Buffer{}
	enc := json.NewEncoder(buf)
	enc.SetIndent("", " ")
	// Disable escaping special chars to display XML tags correctly
	enc.SetEscapeHTML(false)
	fatalIf(probe.NewError(enc.Encode(trc)), "Unable to marshal into JSON.")

	// strip off extra newline added by json encoder
	return strings.TrimSuffix(buf.String(), "\n")
}

func (t traceMessage) String() string {
	var nodeNameStr string
	b := &strings.Builder{}

	trc := t.Trace
	if trc.NodeName != "" {
		nodeNameStr = fmt.Sprintf("%s ", colorizedNodeName(trc.NodeName))
	}
	extra := ""
	if len(t.Trace.Custom) > 0 {
		for k, v := range t.Trace.Custom {
			extra = fmt.Sprintf("%s %s=%s", extra, k, v)
		}
		extra = console.Colorize("Extra", extra)
	}
	switch trc.TraceType {
	case madmin.TraceS3, madmin.TraceInternal:
		if trc.HTTP == nil {
			return ""
		}
	case madmin.TraceBootstrap:
		fmt.Fprintf(b, "%s %s [%s] %s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[%s %s]", strings.ToUpper(trc.TraceType.String()), trc.FuncName)), trc.Time.Local().Format(traceTimeFormat), trc.Message, extra)
		return b.String()
	default:
		sz := ""
		if trc.Bytes != 0 {
			sz = fmt.Sprintf(" %s", humanize.IBytes(uint64(trc.Bytes)))
		}
		if trc.Error != "" {
			fmt.Fprintf(b, "%s %s [%s] %s%s err='%s' %s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[%s %s]", strings.ToUpper(trc.TraceType.String()), trc.FuncName)), trc.Time.Local().Format(traceTimeFormat), trc.Path, extra, console.Colorize("ErrStatus", trc.Error), trc.Duration, sz)
		} else {
			fmt.Fprintf(b, "%s %s [%s] %s%s %s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[%s %s]", strings.ToUpper(trc.TraceType.String()), trc.FuncName)), trc.Time.Local().Format(traceTimeFormat), trc.Path, extra, trc.Duration, sz)
		}
		return b.String()
	}

	ri := trc.HTTP.ReqInfo
	rs := trc.HTTP.RespInfo
	fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Request", fmt.Sprintf("[REQUEST %s] ", trc.FuncName)))
	fmt.Fprintf(b, "[%s] %s\n", ri.Time.Local().Format(traceTimeFormat), console.Colorize("Host", fmt.Sprintf("[Client IP: %s]", ri.Client)))
	fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Method", fmt.Sprintf("%s %s", ri.Method, ri.Path)))
	if ri.RawQuery != "" {
		fmt.Fprintf(b, "?%s", ri.RawQuery)
	}
	fmt.Fprint(b, "\n")
	fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Method", fmt.Sprintf("Proto: %s\n", ri.Proto)))
	host, ok := ri.Headers["Host"]
	if ok {
		delete(ri.Headers, "Host")
	}
	hostStr := strings.Join(host, "")
	fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Host", fmt.Sprintf("Host: %s\n", hostStr)))
	for k, v := range ri.Headers {
		fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("ReqHeaderKey",
			fmt.Sprintf("%s: ", k))+console.Colorize("HeaderValue", fmt.Sprintf("%s\n", strings.Join(v, ""))))
	}

	fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Body", fmt.Sprintf("%s\n", string(ri.Body))))
	fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("Response", "[RESPONSE] "))
	fmt.Fprintf(b, "[%s] ", rs.Time.Local().Format(traceTimeFormat))
	fmt.Fprint(b, console.Colorize("Stat", fmt.Sprintf("[ Duration %2s TTFB %2s ↑ %s  ↓ %s ]\n", trc.Duration.Round(time.Microsecond), trc.HTTP.CallStats.TimeToFirstByte.Round(time.Nanosecond),
		humanize.IBytes(uint64(trc.HTTP.CallStats.InputBytes)), humanize.IBytes(uint64(trc.HTTP.CallStats.OutputBytes)))))

	statusStr := console.Colorize("RespStatus", fmt.Sprintf("%d %s", rs.StatusCode, http.StatusText(rs.StatusCode)))
	if rs.StatusCode != http.StatusOK {
		statusStr = console.Colorize("ErrStatus", fmt.Sprintf("%d %s", rs.StatusCode, http.StatusText(rs.StatusCode)))
	}
	fmt.Fprintf(b, "%s%s\n", nodeNameStr, statusStr)

	for k, v := range rs.Headers {
		fmt.Fprintf(b, "%s%s", nodeNameStr, console.Colorize("RespHeaderKey",
			fmt.Sprintf("%s: ", k))+console.Colorize("HeaderValue", fmt.Sprintf("%s\n", strings.Join(v, ","))))
	}
	if len(extra) > 0 {
		fmt.Fprintf(b, "%s%s\n", nodeNameStr, extra)
	}
	fmt.Fprintf(b, "%s%s\n", nodeNameStr, console.Colorize("Body", string(rs.Body)))
	fmt.Fprint(b, nodeNameStr)
	return b.String()
}

type statItem struct {
	Name           string
	Count          int           `json:"count"`
	Duration       time.Duration `json:"duration"`
	Errors         int           `json:"errors,omitempty"`
	CallStatsCount int           `json:"callStatsCount,omitempty"`
	CallStats      callStats     `json:"callStats,omitempty"`
	TTFB           time.Duration `json:"ttfb,omitempty"`
	MaxTTFB        time.Duration `json:"maxTTFB,omitempty"`
	MaxDur         time.Duration `json:"maxDuration"`
	MinDur         time.Duration `json:"minDuration"`
	Size           int64         `json:"size"`
}

type statTrace struct {
	Calls  map[string]statItem `json:"calls"`
	Oldest time.Time
	Latest time.Time
	mu     sync.Mutex
}

func (s *statTrace) JSON() string {
	s.mu.Lock()
	defer s.mu.Unlock()
	buf := &bytes.Buffer{}
	enc := json.NewEncoder(buf)
	enc.SetIndent("", " ")
	// Disable escaping special chars to display XML tags correctly
	enc.SetEscapeHTML(false)
	fatalIf(probe.NewError(enc.Encode(s)), "Unable to marshal into JSON.")

	// strip off extra newline added by json encoder
	return strings.TrimSuffix(buf.String(), "\n")
}

func (s *statTrace) String() string {
	return ""
}

func (s *statTrace) add(t madmin.ServiceTraceInfo) {
	id := t.Trace.FuncName
	s.mu.Lock()
	defer s.mu.Unlock()
	if t.Trace.TraceType != madmin.TraceBootstrap {
		// We can't use bootstrap to find start/end
		ended := t.Trace.Time.Add(t.Trace.Duration)
		if s.Oldest.IsZero() {
			s.Oldest = ended
		}
		if ended.After(s.Latest) {
			s.Latest = ended
		}
	}
	got := s.Calls[id]
	if got.Name == "" {
		got.Name = id
	}
	if got.MaxDur < t.Trace.Duration {
		got.MaxDur = t.Trace.Duration
	}
	if got.MinDur <= 0 {
		got.MinDur = t.Trace.Duration
	}
	if got.MinDur > t.Trace.Duration {
		got.MinDur = t.Trace.Duration
	}
	got.Count++
	got.Duration += t.Trace.Duration
	if t.Trace.Error != "" {
		got.Errors++
	}
	got.Size += t.Trace.Bytes
	if t.Trace.HTTP != nil {
		got.CallStatsCount++
		got.CallStats.Rx += t.Trace.HTTP.CallStats.InputBytes
		got.CallStats.Tx += t.Trace.HTTP.CallStats.OutputBytes
		got.TTFB += t.Trace.HTTP.CallStats.TimeToFirstByte
		if got.MaxTTFB < t.Trace.HTTP.CallStats.TimeToFirstByte {
			got.MaxTTFB = t.Trace.HTTP.CallStats.TimeToFirstByte
		}
	}
	s.Calls[id] = got
}
