github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/support-profile.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/minio/cli"
    28  	"github.com/minio/madmin-go/v3"
    29  	"github.com/minio/mc/pkg/probe"
    30  	"github.com/minio/minio-go/v7/pkg/set"
    31  	"github.com/minio/pkg/v2/console"
    32  )
    33  
    34  // profile command flags.
    35  var (
    36  	profileFlags = append([]cli.Flag{
    37  		cli.IntFlag{
    38  			Name:  "duration",
    39  			Usage: "profile for the specified duration in seconds",
    40  			Value: 10,
    41  		},
    42  		cli.StringFlag{
    43  			Name:  "type",
    44  			Usage: "profiler type, possible values are 'cpu', 'cpuio', 'mem', 'block', 'mutex', 'trace', 'threads' and 'goroutines'",
    45  			Value: "cpu,mem,block,mutex,goroutines",
    46  		},
    47  	}, subnetCommonFlags...)
    48  )
    49  
    50  const profileFile = "profile.zip"
    51  
    52  type supportProfileMessage struct {
    53  	Status string `json:"status"`
    54  	File   string `json:"file,omitempty"`
    55  	Error  string `json:"error,omitempty"`
    56  }
    57  
    58  // Colorized message for console printing.
    59  func (s supportProfileMessage) String() string {
    60  	var msg string
    61  	if s.Error != "" {
    62  		errMsg := fmt.Sprintln("Unable to upload profile file to SUBNET: ", s.Error)
    63  		msg := console.Colorize(supportErrorMsgTag, errMsg)
    64  		infoMsg := fmt.Sprintf("Profiling data saved locally at '%s'", profileFile)
    65  		msg += console.Colorize(supportSuccessMsgTag, infoMsg)
    66  		return msg
    67  	}
    68  
    69  	if globalAirgapped {
    70  		msg = fmt.Sprintf("Profiling data saved successfully at %s", s.File)
    71  	} else {
    72  		msg = "Profiling data uploaded to SUBNET successfully"
    73  	}
    74  	return console.Colorize(supportSuccessMsgTag, msg)
    75  }
    76  
    77  // JSON jsonified proxy remove message
    78  func (s supportProfileMessage) JSON() string {
    79  	return toJSON(s)
    80  }
    81  
    82  var supportProfileCmd = cli.Command{
    83  	Name:            "profile",
    84  	Usage:           "upload profile data for debugging",
    85  	Action:          mainSupportProfile,
    86  	OnUsageError:    onUsageError,
    87  	Before:          setGlobalsFromContext,
    88  	Flags:           profileFlags,
    89  	HideHelpCommand: true,
    90  	CustomHelpTemplate: `NAME:
    91    {{.HelpName}} - {{.Usage}}
    92  
    93  USAGE:
    94    {{.HelpName}} [FLAGS] TARGET
    95  
    96  FLAGS:
    97    {{range .VisibleFlags}}{{.}}
    98    {{end}}
    99  EXAMPLES:
   100    1. Profile CPU for 10 seconds on cluster with alias 'myminio' and upload results to SUBNET
   101       {{.Prompt}} {{.HelpName}} --type cpu myminio
   102  
   103    2. Profile CPU, Memory, Goroutines for 10 seconds on cluster with alias 'myminio' and upload results to SUBNET
   104       {{.Prompt}} {{.HelpName}} --type cpu,mem,goroutines myminio
   105  
   106    3. Profile CPU, Memory, Goroutines for 10 minutes on cluster with alias 'myminio' and upload results to SUBNET
   107       {{.Prompt}} {{.HelpName}} --type cpu,mem,goroutines --duration 600 myminio
   108  
   109    4. Profile CPU for 10 seconds on cluster with alias 'myminio', save and upload to SUBNET manually
   110       {{.Prompt}} {{.HelpName}} --type cpu --airgap myminio
   111  `,
   112  }
   113  
   114  func checkAdminProfileSyntax(ctx *cli.Context) {
   115  	s := set.CreateStringSet(string(madmin.ProfilerCPU),
   116  		string(madmin.ProfilerMEM),
   117  		string(madmin.ProfilerBlock),
   118  		string(madmin.ProfilerMutex),
   119  		string(madmin.ProfilerTrace),
   120  		string(madmin.ProfilerThreads),
   121  		string(madmin.ProfilerGoroutines),
   122  		string(madmin.ProfilerCPUIO))
   123  	// Check if the provided profiler type is known and supported
   124  	profilers := strings.Split(strings.ToLower(ctx.String("type")), ",")
   125  	for _, profiler := range profilers {
   126  		if profiler != "" {
   127  			if !s.Contains(profiler) {
   128  				fatalIf(errDummy().Trace(ctx.String("type")),
   129  					"Profiler type %s unrecognized. Possible values are: %v.", profiler, s)
   130  			}
   131  		}
   132  	}
   133  	if len(ctx.Args()) != 1 {
   134  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   135  	}
   136  
   137  	if ctx.Int("duration") < 10 {
   138  		fatal(errDummy().Trace(), "for any useful profiling one must run it for atleast 10 seconds")
   139  	}
   140  }
   141  
   142  // moveFile - os.Rename cannot handle cross device renames, in our situation
   143  // it is possible that /tmp is mounted from a separate partition and current
   144  // working directory is a different partition. To allow all situations to
   145  // be handled appropriately use this function instead of os.Rename()
   146  func moveFile(sourcePath, destPath string) error {
   147  	inputFile, e := os.Open(sourcePath)
   148  	if e != nil {
   149  		return e
   150  	}
   151  
   152  	outputFile, e := os.Create(destPath)
   153  	if e != nil {
   154  		inputFile.Close()
   155  		return e
   156  	}
   157  	defer outputFile.Close()
   158  
   159  	if _, e = io.Copy(outputFile, inputFile); e != nil {
   160  		inputFile.Close()
   161  		return e
   162  	}
   163  
   164  	// The copy was successful, so now delete the original file
   165  	inputFile.Close()
   166  	return os.Remove(sourcePath)
   167  }
   168  
   169  func saveProfileFile(data io.ReadCloser) {
   170  	// Create profile zip file
   171  	tmpFile, e := os.CreateTemp("", "mc-profile-")
   172  	fatalIf(probe.NewError(e), "Unable to download profile data.")
   173  
   174  	// Copy zip content to target download file
   175  	_, e = io.Copy(tmpFile, data)
   176  	fatalIf(probe.NewError(e), "Unable to download profile data.")
   177  
   178  	// Close everything
   179  	data.Close()
   180  	tmpFile.Close()
   181  
   182  	downloadedFile := profileFile + "." + time.Now().Format(dateTimeFormatFilename)
   183  
   184  	fi, e := os.Stat(profileFile)
   185  	if e == nil && !fi.IsDir() {
   186  		e = moveFile(profileFile, downloadedFile)
   187  		fatalIf(probe.NewError(e), "Unable to create a backup of profile.zip")
   188  	} else {
   189  		if !os.IsNotExist(e) {
   190  			fatal(probe.NewError(e), "Unable to save profile data")
   191  		}
   192  	}
   193  	fatalIf(probe.NewError(moveFile(tmpFile.Name(), profileFile)), "Unable to save profile data")
   194  }
   195  
   196  // mainSupportProfile is the handle for "mc support profile" command.
   197  func mainSupportProfile(ctx *cli.Context) error {
   198  	// Check for command syntax
   199  	checkAdminProfileSyntax(ctx)
   200  
   201  	setSuccessMessageColor()
   202  	setErrorMessageColor()
   203  
   204  	// Get the alias parameter from cli
   205  	aliasedURL := ctx.Args().Get(0)
   206  	alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true)
   207  	if len(apiKey) == 0 {
   208  		// api key not passed as flag. Check that the cluster is registered.
   209  		apiKey = validateClusterRegistered(alias, true)
   210  	}
   211  
   212  	// Create a new MinIO Admin Client
   213  	client := getClient(aliasedURL)
   214  
   215  	// Main execution
   216  	execSupportProfile(ctx, client, alias, apiKey)
   217  	return nil
   218  }
   219  
   220  func execSupportProfile(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey string) {
   221  	var reqURL string
   222  	var headers map[string]string
   223  	profilers := ctx.String("type")
   224  	duration := ctx.Int("duration")
   225  
   226  	if !globalAirgapped {
   227  		// Retrieve subnet credentials (login/license) beforehand as
   228  		// it can take a long time to fetch the profile data
   229  		uploadURL := SubnetUploadURL("profile")
   230  		reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey)
   231  	}
   232  
   233  	if !globalJSON {
   234  		console.Infof("Profiling '%s' for %d seconds... \n", alias, duration)
   235  	}
   236  	data, e := client.Profile(globalContext, madmin.ProfilerType(profilers), time.Second*time.Duration(duration))
   237  	fatalIf(probe.NewError(e), "Unable to save profile data")
   238  
   239  	saveProfileFile(data)
   240  
   241  	if !globalAirgapped {
   242  		_, e = (&SubnetFileUploader{
   243  			alias:             alias,
   244  			FilePath:          profileFile,
   245  			ReqURL:            reqURL,
   246  			Headers:           headers,
   247  			DeleteAfterUpload: true,
   248  		}).UploadFileToSubnet()
   249  		if e != nil {
   250  			printMsg(supportProfileMessage{
   251  				Status: "error",
   252  				Error:  e.Error(),
   253  				File:   profileFile,
   254  			})
   255  			return
   256  		}
   257  		printMsg(supportProfileMessage{
   258  			Status: "success",
   259  		})
   260  	} else {
   261  		printMsg(supportProfileMessage{
   262  			Status: "success",
   263  			File:   profileFile,
   264  		})
   265  	}
   266  }