carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// The Logs tab's fold: applog.Records in, terminal lines out.
4//
5// The console's Logs tab and `carlos logs` are two windows onto the same
6// records, so they print the same line — logs.go's printLogRecords is the
7// format this mirrors: UTC time to the second, the stream in brackets, the
8// route host, two spaces, the message, then any structured fields as sorted
9// k=v pairs. The one thing the pane adds is COLUMN ALIGNMENT: stream and
10// host are padded to the widest in the window (the way `docker compose
11// logs` does it), which costs nothing when every line comes from the same
12// instance and keeps the messages in one column when they don't.
13//
14// Level never becomes text — the CLI doesn't print the word "error" either,
15// it colours the message. Tone is that colour, decided here so the template
16// carries no severity logic of its own.
17
18import (
19 "sort"
20 "strings"
21 "unicode/utf8"
22
23 "github.com/carlosframework/platform/internal/applog"
24)
25
26// logLineTimeFormat is printLogRecords' stamp, to the second: enough to
27// order a busy window by eye, short enough that it never crowds the message.
28const logLineTimeFormat = "2006-01-02 15:04:05"
29
30// logLine is one rendered line of the pane. Every field is finished text —
31// padding included — so the template is spans and nothing else.
32type logLine struct {
33 Time string // UTC, to the second
34 Stream string // "[app]", padded to the window's widest tag
35 Host string // padded to the window's widest host; "" when no row has one
36 Msg string // the record's own message, verbatim
37 Fields string // " k=v k=v", sorted; "" when the record carried none
38 Tone string // "", "warn" or "error" — the class the message wears
39}
40
41// logLines folds a window of records into the pane's lines. Order is the
42// caller's (oldest first, newest at the foot, as a terminal scrolls).
43func logLines(recs []applog.Record) []logLine {
44 streamW, hostW := 0, 0
45 for _, r := range recs {
46 if n := utf8.RuneCountInString(r.Stream) + 2; n > streamW { // + the brackets
47 streamW = n
48 }
49 if n := utf8.RuneCountInString(r.Host); n > hostW {
50 hostW = n
51 }
52 }
53
54 out := make([]logLine, 0, len(recs))
55 for _, r := range recs {
56 l := logLine{
57 Time: r.TS.UTC().Format(logLineTimeFormat),
58 Stream: pad("["+r.Stream+"]", streamW),
59 Msg: r.Msg,
60 }
61 // A window with no hosts at all renders no host column, rather
62 // than a run of padding spaces in front of every message.
63 if hostW > 0 {
64 l.Host = pad(r.Host, hostW)
65 }
66 switch r.Level {
67 case applog.LevelError:
68 l.Tone = "error"
69 case applog.LevelWarn:
70 l.Tone = "warn"
71 }
72 if len(r.Fields) > 0 {
73 keys := make([]string, 0, len(r.Fields))
74 for k := range r.Fields {
75 keys = append(keys, k)
76 }
77 sort.Strings(keys)
78 var b strings.Builder
79 for _, k := range keys {
80 b.WriteString(" " + k + "=" + r.Fields[k])
81 }
82 l.Fields = b.String()
83 }
84 out = append(out, l)
85 }
86 return out
87}
88
89// pad right-pads s with spaces to w runes. The pane is monospaced, so a
90// space is a column.
91func pad(s string, w int) string {
92 if n := w - utf8.RuneCountInString(s); n > 0 {
93 return s + strings.Repeat(" ", n)
94 }
95 return s
96}
97