| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "time" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/applog" |
| 8 | "github.com/carlosframework/platform/internal/provision" |
| 9 | "github.com/carlosframework/platform/internal/runs" |
| 10 | ) |
| 11 | |
| 12 | // A run's two lines in `carlos logs` (amadan #325): when it started and who |
| 13 | // asked for it, then how it ended. The output stays in the run's chunks and |
| 14 | // the argv in the audit log; neither is ever written here, since a log |
| 15 | // reader is not necessarily someone the command's arguments were meant for. |
| 16 | // |
| 17 | // Both lines are info. The Errors and Availability views read the platform |
| 18 | // stream at warn and above, and a command that failed says nothing about |
| 19 | // whether the instance could serve. |
| 20 | |
| 21 | const runEventLevel = applog.LevelInfo |
| 22 | |
| 23 | // runStartedLine is the line for a run whose unit has started. A service's |
| 24 | // run names the service, and the person it named only as its statement |
| 25 | // (service runs spec §2). |
| 26 | func runStartedLine(req runs.Request) string { |
| 27 | if by := req.ServiceBy(); by != "" { |
| 28 | return fmt.Sprintf("run %s started %s", req.ID, by) |
| 29 | } |
| 30 | return fmt.Sprintf("run %s started by %s", req.ID, req.By) |
| 31 | } |
| 32 | |
| 33 | // runEndedLine is the line for a run's result, whatever ended it. started |
| 34 | // says whether RunCmd handed back a running unit: a result written before |
| 35 | // that (a refusal, a cancel or a timeout during the wake or the staging, a |
| 36 | // stop that overlapped the launch) is a run that did not start, whatever |
| 37 | // its fields say. A failed start is the exception: StartCmd can answer an |
| 38 | // error for a unit the control service did start (a lost response), and |
| 39 | // RunCmd then stops it and hands back nothing, so the line hedges as the |
| 40 | // CLI's does rather than say it never ran. A cancel or a deadline that |
| 41 | // lands during that same start is written as cancelled or timed out with |
| 42 | // no handle, and its line says "before it started", as the CLI says of the |
| 43 | // same result: the result does not record whether the start was reached, |
| 44 | // and the line restates the result, never more. |
| 45 | func runEndedLine(id string, res runs.Result, started bool) string { |
| 46 | took := res.Ended.Sub(res.Started).Round(time.Second) |
| 47 | switch { |
| 48 | case res.Refused != "": |
| 49 | return fmt.Sprintf("run %s did not start: %s", id, runRefusedWords(res)) |
| 50 | case res.Interrupted != "" && !started && res.Interrupted != runs.InterruptedStartFailed: |
| 51 | return fmt.Sprintf("run %s did not start: %s", id, res.Interrupted) |
| 52 | case res.Cancelled && !started: |
| 53 | return fmt.Sprintf("run %s was cancelled before it started", id) |
| 54 | case res.Cancelled: |
| 55 | return fmt.Sprintf("run %s was cancelled after %s", id, took) |
| 56 | case res.TimedOut && !started: |
| 57 | return fmt.Sprintf("run %s timed out before it started", id) |
| 58 | case res.TimedOut: |
| 59 | return fmt.Sprintf("run %s timed out after %s", id, took) |
| 60 | case res.Interrupted != "": |
| 61 | return fmt.Sprintf("run %s was interrupted: %s. it may have done part of its work", id, res.Interrupted) |
| 62 | case res.Signal != "": |
| 63 | return fmt.Sprintf("run %s was ended by signal %s after %s", id, res.Signal, took) |
| 64 | } |
| 65 | return fmt.Sprintf("run %s ended with exit %d after %s", id, res.Exit, took) |
| 66 | } |
| 67 | |
| 68 | // runRefusedWords is why the box did not start the run. An unknown code |
| 69 | // renders as itself, as the CLI's does. |
| 70 | func runRefusedWords(res runs.Result) string { |
| 71 | switch res.Refused { |
| 72 | case runs.RefusedFlat: |
| 73 | return "the box is not on the nested layout yet" |
| 74 | case runs.RefusedClaim: |
| 75 | return "the box has host claims armed" |
| 76 | case runs.RefusedWake: |
| 77 | return fmt.Sprintf("the instance did not wake. %s", res.Detail) |
| 78 | case runs.RefusedIncarnation: |
| 79 | return "the instance was deleted and created again after the run was requested" |
| 80 | case runs.RefusedImport: |
| 81 | return "the instance is an import that is not committed yet" |
| 82 | } |
| 83 | return res.Refused |
| 84 | } |
| 85 | |
| 86 | // runEvent publishes one of a run's lines against the record's instance. |
| 87 | // A box with no log store has no sink, and emits nothing. |
| 88 | func (w *runsWorker) runEvent(rec provision.Record, line string) { |
| 89 | if w.events == nil { |
| 90 | return |
| 91 | } |
| 92 | w.events.Event(rec.Account, rec.App, rec.Host, runEventLevel, line) |
| 93 | } |
| 94 | |