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 }