github.com/pachyderm/pachyderm@v1.13.4/src/server/pps/pretty/pretty.go (about) 1 package pretty 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "strings" 9 "text/template" 10 11 units "github.com/docker/go-units" 12 "github.com/fatih/color" 13 "github.com/gogo/protobuf/types" 14 "github.com/juju/ansiterm" 15 "github.com/pachyderm/pachyderm/src/client" 16 pfsclient "github.com/pachyderm/pachyderm/src/client/pfs" 17 "github.com/pachyderm/pachyderm/src/client/pkg/errors" 18 ppsclient "github.com/pachyderm/pachyderm/src/client/pps" 19 "github.com/pachyderm/pachyderm/src/server/pkg/pretty" 20 ) 21 22 const ( 23 // PipelineHeader is the header for pipelines. 24 PipelineHeader = "NAME\tVERSION\tINPUT\tCREATED\tSTATE / LAST JOB\tDESCRIPTION\t\n" 25 // JobHeader is the header for jobs 26 JobHeader = "ID\tPIPELINE\tSTARTED\tDURATION\tRESTART\tPROGRESS\tDL\tUL\tSTATE\t\n" 27 // DatumHeader is the header for datums 28 DatumHeader = "ID\tFILES\tSTATUS\tTIME\t\n" 29 // SecretHeader is the header for secrets 30 SecretHeader = "NAME\tTYPE\tCREATED\t\n" 31 // jobReasonLen is the amount of the job reason that we print 32 jobReasonLen = 25 33 ) 34 35 func safeTrim(s string, l int) string { 36 if len(s) < l { 37 return s 38 } 39 return strings.TrimSpace(s[:l]) + "..." 40 } 41 42 // PrintJobInfo pretty-prints job info. 43 func PrintJobInfo(w io.Writer, jobInfo *ppsclient.JobInfo, fullTimestamps bool) { 44 fmt.Fprintf(w, "%s\t", jobInfo.Job.ID) 45 fmt.Fprintf(w, "%s\t", jobInfo.Pipeline.Name) 46 if fullTimestamps { 47 fmt.Fprintf(w, "%s\t", jobInfo.Started.String()) 48 } else { 49 fmt.Fprintf(w, "%s\t", pretty.Ago(jobInfo.Started)) 50 } 51 if jobInfo.Finished != nil { 52 fmt.Fprintf(w, "%s\t", pretty.TimeDifference(jobInfo.Started, jobInfo.Finished)) 53 } else { 54 fmt.Fprintf(w, "-\t") 55 } 56 fmt.Fprintf(w, "%d\t", jobInfo.Restart) 57 fmt.Fprintf(w, "%s\t", Progress(jobInfo)) 58 fmt.Fprintf(w, "%s\t", pretty.Size(jobInfo.Stats.DownloadBytes)) 59 fmt.Fprintf(w, "%s\t", pretty.Size(jobInfo.Stats.UploadBytes)) 60 if jobInfo.State == ppsclient.JobState_JOB_FAILURE { 61 fmt.Fprintf(w, "%s: %s\t", JobState(jobInfo.State), safeTrim(jobInfo.Reason, jobReasonLen)) 62 } else { 63 fmt.Fprintf(w, "%s\t", JobState(jobInfo.State)) 64 } 65 fmt.Fprintln(w) 66 } 67 68 // PrintPipelineInfo pretty-prints pipeline info. 69 func PrintPipelineInfo(w io.Writer, pipelineInfo *ppsclient.PipelineInfo, fullTimestamps bool) { 70 if pipelineInfo.Transform == nil { 71 fmt.Fprintf(w, "%s\t", pipelineInfo.Pipeline.Name) 72 fmt.Fprint(w, "-\t") 73 fmt.Fprint(w, "-\t") 74 fmt.Fprint(w, "-\t") 75 fmt.Fprintf(w, "%s / %s\t", pipelineState(pipelineInfo.State), JobState(pipelineInfo.LastJobState)) 76 fmt.Fprint(w, "could not retrieve pipeline spec\t") 77 } else { 78 fmt.Fprintf(w, "%s\t", pipelineInfo.Pipeline.Name) 79 fmt.Fprintf(w, "%d\t", pipelineInfo.Version) 80 fmt.Fprintf(w, "%s\t", ShorthandInput(pipelineInfo.Input)) 81 if fullTimestamps { 82 fmt.Fprintf(w, "%s\t", pipelineInfo.CreatedAt.String()) 83 } else { 84 fmt.Fprintf(w, "%s\t", pretty.Ago(pipelineInfo.CreatedAt)) 85 } 86 fmt.Fprintf(w, "%s / %s\t", pipelineState(pipelineInfo.State), JobState(pipelineInfo.LastJobState)) 87 fmt.Fprintf(w, "%s\t", pipelineInfo.Description) 88 } 89 fmt.Fprintln(w) 90 } 91 92 // PrintWorkerStatusHeader pretty prints a worker status header. 93 func PrintWorkerStatusHeader(w io.Writer) { 94 fmt.Fprint(w, "WORKER\tJOB\tDATUM\tSTARTED\tQUEUE\t\n") 95 } 96 97 // PrintWorkerStatus pretty prints a worker status. 98 func PrintWorkerStatus(w io.Writer, workerStatus *ppsclient.WorkerStatus, fullTimestamps bool) { 99 fmt.Fprintf(w, "%s\t", workerStatus.WorkerID) 100 fmt.Fprintf(w, "%s\t", workerStatus.JobID) 101 for _, datum := range workerStatus.Data { 102 fmt.Fprintf(w, datum.Path) 103 } 104 fmt.Fprintf(w, "\t") 105 if fullTimestamps { 106 fmt.Fprintf(w, "%s\t", workerStatus.Started.String()) 107 } else { 108 fmt.Fprintf(w, "%s\t", pretty.Ago(workerStatus.Started)) 109 } 110 fmt.Fprintf(w, "%d\t", workerStatus.QueueSize) 111 fmt.Fprintln(w) 112 } 113 114 // PrintableJobInfo is a wrapper around JobInfo containing any formatting options 115 // used within the template to conditionally print information. 116 type PrintableJobInfo struct { 117 *ppsclient.JobInfo 118 FullTimestamps bool 119 } 120 121 // NewPrintableJobInfo constructs a PrintableJobInfo from just a JobInfo. 122 func NewPrintableJobInfo(ji *ppsclient.JobInfo) *PrintableJobInfo { 123 return &PrintableJobInfo{ 124 JobInfo: ji, 125 } 126 } 127 128 // PrintDetailedJobInfo pretty-prints detailed job info. 129 func PrintDetailedJobInfo(w io.Writer, jobInfo *PrintableJobInfo) error { 130 template, err := template.New("JobInfo").Funcs(funcMap).Parse( 131 `ID: {{.Job.ID}} {{if .Pipeline}} 132 Pipeline: {{.Pipeline.Name}} {{end}} {{if .ParentJob}} 133 Parent: {{.ParentJob.ID}} {{end}}{{if .FullTimestamps}} 134 Started: {{.Started}}{{else}} 135 Started: {{prettyAgo .Started}} {{end}}{{if .Finished}} 136 Duration: {{prettyTimeDifference .Started .Finished}} {{end}} 137 State: {{jobState .State}} 138 Reason: {{.Reason}} 139 Processed: {{.DataProcessed}} 140 Failed: {{.DataFailed}} 141 Skipped: {{.DataSkipped}} 142 Recovered: {{.DataRecovered}} 143 Total: {{.DataTotal}} 144 Data Downloaded: {{prettySize .Stats.DownloadBytes}} 145 Data Uploaded: {{prettySize .Stats.UploadBytes}} 146 Download Time: {{prettyDuration .Stats.DownloadTime}} 147 Process Time: {{prettyDuration .Stats.ProcessTime}} 148 Upload Time: {{prettyDuration .Stats.UploadTime}} 149 Datum Timeout: {{.DatumTimeout}} 150 Job Timeout: {{.JobTimeout}} 151 Worker Status: 152 {{workerStatus .}}Restarts: {{.Restart}} 153 ParallelismSpec: {{.ParallelismSpec}} 154 {{ if .ResourceRequests }}ResourceRequests: 155 CPU: {{ .ResourceRequests.Cpu }} 156 Memory: {{ .ResourceRequests.Memory }} {{end}} 157 {{ if .ResourceLimits }}ResourceLimits: 158 CPU: {{ .ResourceLimits.Cpu }} 159 Memory: {{ .ResourceLimits.Memory }} 160 {{ if .ResourceLimits.Gpu }}GPU: 161 Type: {{ .ResourceLimits.Gpu.Type }} 162 Number: {{ .ResourceLimits.Gpu.Number }} {{end}} {{end}} 163 {{ if .SidecarResourceLimits }}SidecarResourceLimits: 164 CPU: {{ .SidecarResourceLimits.Cpu }} 165 Memory: {{ .SidecarResourceLimits.Memory }} {{end}} 166 {{ if .Service }}Service: 167 {{ if .Service.InternalPort }}InternalPort: {{ .Service.InternalPort }} {{end}} 168 {{ if .Service.ExternalPort }}ExternalPort: {{ .Service.ExternalPort }} {{end}} {{end}}Input: 169 {{jobInput .}} 170 Transform: 171 {{prettyTransform .Transform}} {{if .OutputCommit}} 172 Output Commit: {{.OutputCommit.ID}} {{end}} {{ if .StatsCommit }} 173 Stats Commit: {{.StatsCommit.ID}} {{end}} {{ if .Egress }} 174 Egress: {{.Egress.URL}} {{end}} 175 `) 176 if err != nil { 177 return err 178 } 179 return template.Execute(w, jobInfo) 180 } 181 182 // PrintablePipelineInfo is a wrapper around PipelinInfo containing any formatting options 183 // used within the template to conditionally print information. 184 type PrintablePipelineInfo struct { 185 *ppsclient.PipelineInfo 186 FullTimestamps bool 187 } 188 189 // NewPrintablePipelineInfo constructs a PrintablePipelineInfo from just a PipelineInfo. 190 func NewPrintablePipelineInfo(pi *ppsclient.PipelineInfo) *PrintablePipelineInfo { 191 return &PrintablePipelineInfo{ 192 PipelineInfo: pi, 193 } 194 } 195 196 // PrintDetailedPipelineInfo pretty-prints detailed pipeline info. 197 func PrintDetailedPipelineInfo(w io.Writer, pipelineInfo *PrintablePipelineInfo) error { 198 template, err := template.New("PipelineInfo").Funcs(funcMap).Parse( 199 `Name: {{.Pipeline.Name}}{{if .Description}} 200 Description: {{.Description}}{{end}}{{if .FullTimestamps }} 201 Created: {{.CreatedAt}}{{ else }} 202 Created: {{prettyAgo .CreatedAt}} {{end}} 203 State: {{pipelineState .State}} 204 Reason: {{.Reason}} 205 Workers Available: {{.WorkersAvailable}}/{{.WorkersRequested}} 206 Stopped: {{ .Stopped }} 207 Parallelism Spec: {{.ParallelismSpec}} 208 {{ if .ResourceRequests }}ResourceRequests: 209 CPU: {{ .ResourceRequests.Cpu }} 210 Memory: {{ .ResourceRequests.Memory }} {{end}} 211 {{ if .ResourceLimits }}ResourceLimits: 212 CPU: {{ .ResourceLimits.Cpu }} 213 Memory: {{ .ResourceLimits.Memory }} 214 {{ if .ResourceLimits.Gpu }}GPU: 215 Type: {{ .ResourceLimits.Gpu.Type }} 216 Number: {{ .ResourceLimits.Gpu.Number }} {{end}} {{end}} 217 Datum Timeout: {{.DatumTimeout}} 218 Job Timeout: {{.JobTimeout}} 219 Input: 220 {{pipelineInput .PipelineInfo}} 221 {{ if .GithookURL }}Githook URL: {{.GithookURL}} {{end}} 222 Output Branch: {{.OutputBranch}} 223 Transform: 224 {{prettyTransform .Transform}} 225 {{ if .Egress }}Egress: {{.Egress.URL}} {{end}} 226 {{if .RecentError}} Recent Error: {{.RecentError}} {{end}} 227 Job Counts: 228 {{jobCounts .JobCounts}} 229 `) 230 if err != nil { 231 return err 232 } 233 err = template.Execute(w, pipelineInfo) 234 if err != nil { 235 return err 236 } 237 return nil 238 } 239 240 // PrintDatumInfo pretty-prints file info. 241 // If recurse is false and directory size is 0, display "-" instead 242 // If fast is true and file size is 0, display "-" instead 243 func PrintDatumInfo(w io.Writer, datumInfo *ppsclient.DatumInfo) { 244 totalTime := "-" 245 if datumInfo.Stats != nil { 246 totalTime = units.HumanDuration(client.GetDatumTotalTime(datumInfo.Stats)) 247 } 248 if datumInfo.Datum.ID == "" { 249 datumInfo.Datum.ID = "-" 250 } 251 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t", datumInfo.Datum.ID, datumFiles(datumInfo), datumState(datumInfo.State), totalTime) 252 fmt.Fprintln(w) 253 } 254 255 func datumFiles(datumInfo *ppsclient.DatumInfo) string { 256 builder := &strings.Builder{} 257 for i, fi := range datumInfo.Data { 258 if i != 0 { 259 builder.WriteString(", ") 260 } 261 fmt.Fprintf(builder, "%s@%s:%s", fi.File.Commit.Repo.Name, fi.File.Commit.ID, fi.File.Path) 262 } 263 return builder.String() 264 } 265 266 // PrintDetailedDatumInfo pretty-prints detailed info about a datum 267 func PrintDetailedDatumInfo(w io.Writer, datumInfo *ppsclient.DatumInfo) { 268 fmt.Fprintf(w, "ID\t%s\n", datumInfo.Datum.ID) 269 fmt.Fprintf(w, "Job ID\t%s\n", datumInfo.Datum.Job.ID) 270 fmt.Fprintf(w, "State\t%s\n", datumInfo.State) 271 fmt.Fprintf(w, "Data Downloaded\t%s\n", pretty.Size(datumInfo.Stats.DownloadBytes)) 272 fmt.Fprintf(w, "Data Uploaded\t%s\n", pretty.Size(datumInfo.Stats.UploadBytes)) 273 274 totalTime := client.GetDatumTotalTime(datumInfo.Stats).String() 275 fmt.Fprintf(w, "Total Time\t%s\n", totalTime) 276 277 var downloadTime string 278 dl, err := types.DurationFromProto(datumInfo.Stats.DownloadTime) 279 if err != nil { 280 downloadTime = err.Error() 281 } else { 282 downloadTime = dl.String() 283 } 284 fmt.Fprintf(w, "Download Time\t%s\n", downloadTime) 285 286 var procTime string 287 proc, err := types.DurationFromProto(datumInfo.Stats.ProcessTime) 288 if err != nil { 289 procTime = err.Error() 290 } else { 291 procTime = proc.String() 292 } 293 fmt.Fprintf(w, "Process Time\t%s\n", procTime) 294 295 var uploadTime string 296 ul, err := types.DurationFromProto(datumInfo.Stats.UploadTime) 297 if err != nil { 298 uploadTime = err.Error() 299 } else { 300 uploadTime = ul.String() 301 } 302 fmt.Fprintf(w, "Upload Time\t%s\n", uploadTime) 303 304 fmt.Fprintf(w, "PFS State:\n") 305 tw := ansiterm.NewTabWriter(w, 10, 1, 3, ' ', 0) 306 PrintFileHeader(tw) 307 PrintFile(tw, datumInfo.PfsState) 308 tw.Flush() 309 fmt.Fprintf(w, "Inputs:\n") 310 tw = ansiterm.NewTabWriter(w, 10, 1, 3, ' ', 0) 311 PrintFileHeader(tw) 312 for _, d := range datumInfo.Data { 313 PrintFile(tw, d.File) 314 } 315 tw.Flush() 316 } 317 318 // PrintSecretInfo pretty-prints secret info. 319 func PrintSecretInfo(w io.Writer, secretInfo *ppsclient.SecretInfo) { 320 fmt.Fprintf(w, "%s\t%s\t%s\t\n", secretInfo.Secret.Name, secretInfo.Type, pretty.Ago(secretInfo.CreationTimestamp)) 321 } 322 323 // PrintFileHeader prints the header for a pfs file. 324 func PrintFileHeader(w io.Writer) { 325 fmt.Fprintf(w, " REPO\tCOMMIT\tPATH\t\n") 326 } 327 328 // PrintFile values for a pfs file. 329 func PrintFile(w io.Writer, file *pfsclient.File) { 330 fmt.Fprintf(w, " %s\t%s\t%s\t\n", file.Commit.Repo.Name, file.Commit.ID, file.Path) 331 } 332 333 func datumState(datumState ppsclient.DatumState) string { 334 switch datumState { 335 case ppsclient.DatumState_SKIPPED: 336 return color.New(color.FgYellow).SprintFunc()("skipped") 337 case ppsclient.DatumState_FAILED: 338 return color.New(color.FgRed).SprintFunc()("failed") 339 case ppsclient.DatumState_RECOVERED: 340 return color.New(color.FgYellow).SprintFunc()("recovered") 341 case ppsclient.DatumState_SUCCESS: 342 return color.New(color.FgGreen).SprintFunc()("success") 343 } 344 return "-" 345 } 346 347 // JobState returns the state of a job as a pretty printed string. 348 func JobState(jobState ppsclient.JobState) string { 349 switch jobState { 350 case ppsclient.JobState_JOB_STARTING: 351 return color.New(color.FgYellow).SprintFunc()("starting") 352 case ppsclient.JobState_JOB_RUNNING: 353 return color.New(color.FgYellow).SprintFunc()("running") 354 case ppsclient.JobState_JOB_MERGING: 355 return color.New(color.FgYellow).SprintFunc()("merging") 356 case ppsclient.JobState_JOB_FAILURE: 357 return color.New(color.FgRed).SprintFunc()("failure") 358 case ppsclient.JobState_JOB_SUCCESS: 359 return color.New(color.FgGreen).SprintFunc()("success") 360 case ppsclient.JobState_JOB_KILLED: 361 return color.New(color.FgRed).SprintFunc()("killed") 362 case ppsclient.JobState_JOB_EGRESSING: 363 return color.New(color.FgYellow).SprintFunc()("egressing") 364 365 } 366 return "-" 367 } 368 369 // Progress pretty prints the datum progress of a job. 370 func Progress(ji *ppsclient.JobInfo) string { 371 if ji.DataRecovered != 0 { 372 return fmt.Sprintf("%d + %d + %d / %d", ji.DataProcessed, ji.DataSkipped, ji.DataRecovered, ji.DataTotal) 373 } 374 return fmt.Sprintf("%d + %d / %d", ji.DataProcessed, ji.DataSkipped, ji.DataTotal) 375 } 376 377 func pipelineState(pipelineState ppsclient.PipelineState) string { 378 switch pipelineState { 379 case ppsclient.PipelineState_PIPELINE_STARTING: 380 return color.New(color.FgYellow).SprintFunc()("starting") 381 case ppsclient.PipelineState_PIPELINE_RUNNING: 382 return color.New(color.FgGreen).SprintFunc()("running") 383 case ppsclient.PipelineState_PIPELINE_RESTARTING: 384 return color.New(color.FgYellow).SprintFunc()("restarting") 385 case ppsclient.PipelineState_PIPELINE_FAILURE: 386 return color.New(color.FgRed).SprintFunc()("failure") 387 case ppsclient.PipelineState_PIPELINE_PAUSED: 388 return color.New(color.FgYellow).SprintFunc()("paused") 389 case ppsclient.PipelineState_PIPELINE_STANDBY: 390 return color.New(color.FgYellow).SprintFunc()("standby") 391 case ppsclient.PipelineState_PIPELINE_CRASHING: 392 return color.New(color.FgRed).SprintFunc()("crashing") 393 } 394 return "-" 395 } 396 397 func jobInput(jobInfo PrintableJobInfo) string { 398 if jobInfo.Input == nil { 399 return "" 400 } 401 input, err := json.MarshalIndent(jobInfo.Input, "", " ") 402 if err != nil { 403 panic(errors.Wrapf(err, "error marshalling input")) 404 } 405 return string(input) + "\n" 406 } 407 408 func workerStatus(jobInfo PrintableJobInfo) string { 409 var buffer bytes.Buffer 410 writer := ansiterm.NewTabWriter(&buffer, 20, 1, 3, ' ', 0) 411 PrintWorkerStatusHeader(writer) 412 for _, workerStatus := range jobInfo.WorkerStatus { 413 PrintWorkerStatus(writer, workerStatus, jobInfo.FullTimestamps) 414 } 415 // can't error because buffer can't error on Write 416 writer.Flush() 417 return buffer.String() 418 } 419 420 func pipelineInput(pipelineInfo *ppsclient.PipelineInfo) string { 421 if pipelineInfo.Input == nil { 422 return "" 423 } 424 input, err := json.MarshalIndent(pipelineInfo.Input, "", " ") 425 if err != nil { 426 panic(errors.Wrapf(err, "error marshalling input")) 427 } 428 return string(input) + "\n" 429 } 430 431 func jobCounts(counts map[int32]int32) string { 432 var buffer bytes.Buffer 433 for i := int32(ppsclient.JobState_JOB_STARTING); i <= int32(ppsclient.JobState_JOB_SUCCESS); i++ { 434 fmt.Fprintf(&buffer, "%s: %d\t", JobState(ppsclient.JobState(i)), counts[i]) 435 } 436 return buffer.String() 437 } 438 439 func prettyTransform(transform *ppsclient.Transform) (string, error) { 440 result, err := json.MarshalIndent(transform, "", " ") 441 if err != nil { 442 return "", err 443 } 444 return pretty.UnescapeHTML(string(result)), nil 445 } 446 447 // ShorthandInput renders a pps.Input as a short, readable string 448 func ShorthandInput(input *ppsclient.Input) string { 449 switch { 450 case input == nil: 451 return "none" 452 case input.Pfs != nil: 453 return fmt.Sprintf("%s:%s", input.Pfs.Repo, input.Pfs.Glob) 454 case input.Cross != nil: 455 var subInput []string 456 for _, input := range input.Cross { 457 subInput = append(subInput, ShorthandInput(input)) 458 } 459 return "(" + strings.Join(subInput, " ⨯ ") + ")" 460 case input.Join != nil: 461 var subInput []string 462 for _, input := range input.Join { 463 subInput = append(subInput, ShorthandInput(input)) 464 } 465 return "(" + strings.Join(subInput, " ⋈ ") + ")" 466 case input.Group != nil: 467 var subInput []string 468 for _, input := range input.Group { 469 subInput = append(subInput, ShorthandInput(input)) 470 } 471 return "(Group: " + strings.Join(subInput, ", ") + ")" 472 case input.Union != nil: 473 var subInput []string 474 for _, input := range input.Union { 475 subInput = append(subInput, ShorthandInput(input)) 476 } 477 return "(" + strings.Join(subInput, " ∪ ") + ")" 478 case input.Cron != nil: 479 return fmt.Sprintf("%s:%s", input.Cron.Name, input.Cron.Spec) 480 } 481 return "" 482 } 483 484 var funcMap = template.FuncMap{ 485 "pipelineState": pipelineState, 486 "jobState": JobState, 487 "datumState": datumState, 488 "workerStatus": workerStatus, 489 "pipelineInput": pipelineInput, 490 "jobInput": jobInput, 491 "prettyAgo": pretty.Ago, 492 "prettyTimeDifference": pretty.TimeDifference, 493 "prettyDuration": pretty.Duration, 494 "prettySize": pretty.Size, 495 "jobCounts": jobCounts, 496 "prettyTransform": prettyTransform, 497 }