1 // Package lol (log of location) is a simple logging library that prints a high
2 // precision unix timestamp and the source location of a log print to make
3 // tracing errors simpler. Includes a set of logging levels and the ability to
4 // filter out higher log levels for a more quiet output.
5 package lol
6 7 import (
8 "fmt"
9 "io"
10 "os"
11 "runtime"
12 "sync/atomic"
13 "time"
14 15 "github.com/davecgh/go-spew/spew"
16 )
17 18 const (
19 Off = iota
20 Fatal
21 Error
22 Warn
23 Info
24 Debug
25 Trace
26 )
27 28 var LevelNames = []string{
29 "off",
30 "fatal",
31 "error",
32 "warn",
33 "info",
34 "debug",
35 "trace",
36 }
37 38 type (
39 // LevelPrinter defines a set of terminal printing primitives that output
40 // with extra data, time, log logLevelList, and code location
41 42 // Ln prints lists of server with spaces in between
43 Ln func(a ...interface{})
44 // F prints like fmt.Println surrounded []byte log details
45 F func(format string, a ...interface{})
46 // S prints a spew.Sdump for an enveloper slice
47 S func(a ...interface{})
48 // C accepts a function so that the extra computation can be avoided if it is not being
49 // viewed
50 C func(closure func() string)
51 // Chk is a shortcut for printing if there is an error, or returning true
52 Chk func(e error) bool
53 // Err is a pass-through function that uses fmt.Errorf to construct an error and returns the
54 // error after printing it to the log
55 Err func(format string, a ...any) error
56 57 // LevelPrinter is the set of log printers on each log level.
58 LevelPrinter struct {
59 Ln
60 F
61 S
62 C
63 Chk
64 Err
65 }
66 67 // LevelSpec is the name, ID and Colorizer for a log level.
68 LevelSpec struct {
69 ID int
70 Name string
71 Colorizer func(a ...any) string
72 }
73 74 // Entry is a log entry to be printed as json to the log file
75 Entry struct {
76 Time time.Time
77 Level string
78 Package string
79 CodeLocation string
80 Text string
81 }
82 )
83 84 var (
85 // Writer can be swapped out for any io.*Writer* that you want to use instead of stdout.
86 Writer io.Writer = os.Stderr
87 88 // LevelSpecs specifies the id, string name and color-printing function
89 LevelSpecs = []LevelSpec{
90 {Off, " ", NoSprint},
91 {Fatal, "â ī¸ ", fmt.Sprint},
92 {Error, "đ¨ ", fmt.Sprint},
93 {Warn, "â ī¸ ", fmt.Sprint},
94 {Info, "âšī¸ ", fmt.Sprint},
95 {Debug, "đ ", fmt.Sprint},
96 {Trace, "đģ ", fmt.Sprint},
97 }
98 )
99 100 // NoSprint is a noop for sprint (it returns nothing no matter what is given to it).
101 func NoSprint(_ ...any) string { return "" }
102 103 // Log is a set of log printers for the various Level items.
104 type Log struct {
105 F, E, W, I, D, T LevelPrinter
106 }
107 108 // Check is the set of log levels for a Check operation (prints an error if the error is not
109 // nil).
110 type Check struct {
111 F, E, W, I, D, T Chk
112 }
113 114 // Errorf prints an error that is also returned as an error, so the error is logged at the site.
115 type Errorf struct {
116 F, E, W, I, D, T Err
117 }
118 119 // Logger is a collection of things that creates a logger, including levels.
120 type Logger struct {
121 *Log
122 *Check
123 *Errorf
124 }
125 126 // Level is the level that the logger is printing at.
127 var Level atomic.Int32
128 129 // Main is the main logger.
130 var Main = &Logger{}
131 132 func init() {
133 // Main = &Logger{}
134 Main.Log, Main.Check, Main.Errorf = New(os.Stderr, 2)
135 ll := os.Getenv("LOG_LEVEL")
136 if ll == "" {
137 SetLogLevel("info")
138 } else {
139 for i := range LevelNames {
140 if ll == LevelNames[i] {
141 SetLoggers(i)
142 return
143 }
144 }
145 SetLoggers(Info)
146 }
147 }
148 149 // SetLoggers configures a log level.
150 func SetLoggers(level int) {
151 Main.Log.T.F("log level %s", LevelSpecs[level].Colorizer(LevelNames[level]))
152 Level.Store(int32(level))
153 }
154 155 // GetLogLevel returns the log level number of a string log level.
156 func GetLogLevel(level string) (i int) {
157 for i = range LevelNames {
158 if level == LevelNames[i] {
159 return i
160 }
161 }
162 return Info
163 }
164 165 // SetLogLevel sets the log level of the logger.
166 func SetLogLevel(level string) {
167 for i := range LevelNames {
168 if level == LevelNames[i] {
169 SetLoggers(i)
170 return
171 }
172 }
173 SetLoggers(Trace)
174 }
175 176 // JoinStrings joins together anything into a set of strings with space separating the items.
177 func JoinStrings(a ...any) (s string) {
178 for i := range a {
179 s += fmt.Sprint(a[i])
180 if i < len(a)-1 {
181 s += " "
182 }
183 }
184 return
185 }
186 187 var msgCol = fmt.Sprint
188 189 // GetPrinter returns a full logger that writes to the provided io.Writer.
190 func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
191 return LevelPrinter{
192 Ln: func(a ...interface{}) {
193 if Level.Load() < l {
194 return
195 }
196 _, _ = fmt.Fprintf(
197 writer,
198 "%s%s%s %s\n",
199 msgCol(TimeStamper()),
200 LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
201 JoinStrings(a...),
202 msgCol(GetLoc(skip)),
203 )
204 },
205 F: func(format string, a ...interface{}) {
206 if Level.Load() < l {
207 return
208 }
209 _, _ = fmt.Fprintf(
210 writer,
211 "%s%s%s %s\n",
212 msgCol(TimeStamper()),
213 LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
214 fmt.Sprintf(format, a...),
215 msgCol(GetLoc(skip)),
216 )
217 },
218 S: func(a ...interface{}) {
219 if Level.Load() < l {
220 return
221 }
222 _, _ = fmt.Fprintf(
223 writer,
224 "%s%s%s %s\n",
225 msgCol(TimeStamper()),
226 LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
227 spew.Sdump(a...),
228 msgCol(GetLoc(skip)),
229 )
230 },
231 C: func(closure func() string) {
232 if Level.Load() < l {
233 return
234 }
235 _, _ = fmt.Fprintf(
236 writer,
237 "%s%s%s %s\n",
238 msgCol(TimeStamper()),
239 LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
240 closure(),
241 msgCol(GetLoc(skip)),
242 )
243 },
244 Chk: func(e error) bool {
245 if Level.Load() < l {
246 return e != nil
247 }
248 if e != nil {
249 _, _ = fmt.Fprintf(
250 writer,
251 "%s%s%s %s\n",
252 msgCol(TimeStamper()),
253 LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
254 e.Error(),
255 msgCol(GetLoc(skip)),
256 )
257 return true
258 }
259 return false
260 },
261 Err: func(format string, a ...interface{}) error {
262 if Level.Load() >= l {
263 _, _ = fmt.Fprintf(
264 writer,
265 "%s%s%s %s\n",
266 msgCol(TimeStamper()),
267 LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
268 fmt.Sprintf(format, a...),
269 msgCol(GetLoc(skip)),
270 )
271 }
272 return fmt.Errorf(format, a...)
273 },
274 }
275 }
276 277 // GetNullPrinter is a logger that doesn't log.
278 func GetNullPrinter() LevelPrinter {
279 return LevelPrinter{
280 Ln: func(a ...interface{}) {},
281 F: func(format string, a ...interface{}) {},
282 S: func(a ...interface{}) {},
283 C: func(closure func() string) {},
284 Chk: func(e error) bool { return e != nil },
285 Err: func(
286 format string, a ...interface{},
287 ) error {
288 return fmt.Errorf(format, a...)
289 },
290 }
291 }
292 293 // New creates a new logger with all the levels and things.
294 func New(writer io.Writer, skip int) (l *Log, c *Check, errorf *Errorf) {
295 if writer == nil {
296 writer = Writer
297 }
298 l = &Log{
299 T: GetPrinter(Trace, writer, skip),
300 D: GetPrinter(Debug, writer, skip),
301 I: GetPrinter(Info, writer, skip),
302 W: GetPrinter(Warn, writer, skip),
303 E: GetPrinter(Error, writer, skip),
304 F: GetPrinter(Fatal, writer, skip),
305 }
306 c = &Check{
307 F: l.F.Chk,
308 E: l.E.Chk,
309 W: l.W.Chk,
310 I: l.I.Chk,
311 D: l.D.Chk,
312 T: l.T.Chk,
313 }
314 errorf = &Errorf{
315 F: l.F.Err,
316 E: l.E.Err,
317 W: l.W.Err,
318 I: l.I.Err,
319 D: l.D.Err,
320 T: l.T.Err,
321 }
322 return
323 }
324 325 // TimeStamper generates the timestamp for logs.
326 func TimeStamper() (s string) {
327 s = fmt.Sprint(time.Now().UnixMicro())
328 return
329 }
330 331 // GetLoc returns the code location of the caller.
332 func GetLoc(skip int) (output string) {
333 _, file, line, _ := runtime.Caller(skip)
334 output = fmt.Sprintf("%s:%d", file, line)
335 return
336 }
337