Skip to content

Commit 9f72f68

Browse files
committed
initial commit
1 parent 4809d3b commit 9f72f68

8 files changed

Lines changed: 355 additions & 0 deletions

File tree

.github/workflows/go.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Go Test
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
name: Go Unit Tests
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v5
14+
- uses: actions/setup-go@v6
15+
with:
16+
go-version: '1.25'
17+
18+
- run: go test -v ./...

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
test:
3+
@go test -v ./...
4+
5+
bench:
6+
@go test -bench=BenchmarkLogger_JSON -benchtime=10s -benchmem

attr.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package teapot
2+
3+
// Attr
4+
type Attr struct {
5+
key string
6+
kfield kField
7+
8+
intValue int
9+
int16Value int16
10+
int32Value int32
11+
int64Value int64
12+
stringValue string
13+
boolValue bool
14+
15+
anyValue any
16+
17+
errValue error
18+
}
19+
20+
// Int
21+
func Int(key string, value int) Attr {
22+
return Attr{key: key, kfield: kint, intValue: value}
23+
}
24+
25+
// Int16
26+
func Int16(key string, value int16) Attr {
27+
return Attr{key: key, kfield: kint16, int16Value: value}
28+
}
29+
30+
// Int32
31+
func Int32(key string, value int32) Attr {
32+
return Attr{key: key, kfield: kint32, int32Value: value}
33+
}
34+
35+
// Int64
36+
func Int64(key string, value int64) Attr {
37+
return Attr{key: key, kfield: kint64, int64Value: value}
38+
}
39+
40+
// Bool
41+
func Bool(key string, value bool) Attr {
42+
return Attr{key: key, kfield: kbool, boolValue: value}
43+
}
44+
45+
// String
46+
func String(key, value string) Attr {
47+
return Attr{key: key, kfield: kstring, stringValue: value}
48+
}
49+
50+
// Any
51+
func Any(key string, value any) Attr {
52+
return Attr{key: key, kfield: kany, anyValue: value}
53+
}
54+
55+
// Error
56+
func Error(err error) Attr {
57+
return Attr{key: "error", kfield: kerr, errValue: err}
58+
}

const.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package teapot
2+
3+
type Level int
4+
5+
const (
6+
DEBUG Level = iota
7+
INFO
8+
WARN
9+
ERROR
10+
FATAL
11+
)
12+
13+
var levelName = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
14+
15+
type Format int
16+
17+
const (
18+
TEXT Format = iota
19+
JSON
20+
)
21+
22+
type kField int
23+
24+
const (
25+
kint kField = iota
26+
kint16
27+
kint32
28+
kint64
29+
kbool
30+
kany
31+
kstring
32+
kerr
33+
)

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/structx/teapot
2+
3+
go 1.25.5

log.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package teapot
2+
3+
import (
4+
"io"
5+
"os"
6+
"runtime"
7+
"strconv"
8+
"sync"
9+
"time"
10+
)
11+
12+
// Option
13+
type Option func(*Logger)
14+
15+
// WithLevel
16+
func WithLevel(level Level) Option { return func(l *Logger) { l.lvl = level } }
17+
18+
// WithWriter
19+
func WithWriter(w io.Writer) Option { return func(l *Logger) { l.w = w } }
20+
21+
// Logger
22+
type Logger struct {
23+
mu sync.Mutex
24+
p *sync.Pool
25+
26+
w io.Writer
27+
28+
lvl Level
29+
f Format
30+
}
31+
32+
// New
33+
func New(opts ...Option) *Logger {
34+
l := &Logger{
35+
w: os.Stdout,
36+
lvl: DEBUG,
37+
f: JSON,
38+
mu: sync.Mutex{},
39+
p: &sync.Pool{
40+
New: func() any {
41+
buf := make([]byte, 0, 1024)
42+
return &buf
43+
},
44+
},
45+
}
46+
for _, o := range opts {
47+
o(l)
48+
}
49+
return l
50+
}
51+
52+
// Debug
53+
func (l *Logger) Debug(format string, attrs ...Attr) {
54+
l.printf(format, DEBUG, attrs...)
55+
}
56+
57+
// Debugf
58+
func (l *Logger) Debugf(format string, attrs ...Attr) {
59+
l.printf(format, INFO, attrs...)
60+
}
61+
62+
// Info
63+
func (l *Logger) Info(format string) {
64+
l.printf(format, INFO)
65+
}
66+
67+
// Infof
68+
func (l *Logger) Infof(format string, attrs ...Attr) {
69+
l.printf(format, INFO, attrs...)
70+
}
71+
72+
// Error
73+
func (l *Logger) Error(format string, attrs ...Attr) {
74+
l.printf(format, ERROR, attrs...)
75+
}
76+
77+
// Fatal
78+
func (l *Logger) Fatal(format string, attrs ...Attr) {
79+
l.printf(format, FATAL, attrs...)
80+
os.Exit(1)
81+
}
82+
83+
func (l *Logger) printf(format string, level Level, attrs ...Attr) {
84+
if level < l.lvl {
85+
return
86+
}
87+
88+
ptr := l.p.Get().(*[]byte)
89+
buf := (*ptr)[:0]
90+
91+
switch l.f {
92+
// TODO
93+
// implement text logger
94+
// case TEXT:
95+
case JSON:
96+
buf = append(buf, `{"time":"`...)
97+
buf = time.Now().UTC().AppendFormat(buf, time.RFC3339Nano)
98+
buf = append(buf, `", "level":"`...)
99+
buf = append(buf, levelName[l.lvl]...)
100+
buf = append(buf, `", "message":"`...)
101+
buf = append(buf, format...)
102+
buf = append(buf, `"`...)
103+
for _, attr := range attrs {
104+
buf = append(buf, `,"`...)
105+
buf = append(buf, attr.key...)
106+
buf = append(buf, `":`...)
107+
108+
switch attr.kfield {
109+
case kint:
110+
buf = strconv.AppendInt(buf, int64(attr.intValue), 10)
111+
case kint16:
112+
buf = strconv.AppendInt(buf, int64(attr.int16Value), 10)
113+
case kint32:
114+
buf = strconv.AppendInt(buf, int64(attr.int32Value), 10)
115+
case kint64:
116+
buf = strconv.AppendInt(buf, attr.int64Value, 10)
117+
case kbool:
118+
buf = strconv.AppendBool(buf, attr.boolValue)
119+
case kstring:
120+
buf = strconv.AppendQuote(buf, attr.stringValue)
121+
case kerr:
122+
buf = strconv.AppendQuote(buf, attr.errValue.Error())
123+
}
124+
}
125+
126+
if level >= ERROR {
127+
buf = append(buf, `, "stacktrace":`...)
128+
129+
var sb = make([]byte, 2048)
130+
n := runtime.Stack(sb, false)
131+
buf = strconv.AppendQuote(buf, string(sb[:n]))
132+
}
133+
134+
buf = append(buf, "}"...)
135+
default:
136+
return
137+
}
138+
139+
buf = append(buf, '\n')
140+
141+
l.mu.Lock()
142+
l.w.Write(buf)
143+
l.mu.Unlock()
144+
145+
*ptr = buf
146+
l.p.Put(ptr)
147+
}

log_benchmark_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package teapot
2+
3+
import (
4+
"io"
5+
"testing"
6+
)
7+
8+
func BenchmarkLogger_JSON(b *testing.B) {
9+
l := New(
10+
WithWriter(io.Discard), // does not measure os write performance
11+
)
12+
13+
attrs := []Attr{
14+
String("user", "alice"),
15+
Int("status", 200),
16+
Bool("success", true),
17+
}
18+
19+
b.ResetTimer()
20+
b.RunParallel(func(p *testing.PB) {
21+
for p.Next() {
22+
l.Debug("received request", attrs...)
23+
}
24+
})
25+
}

log_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package teapot
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"testing"
9+
)
10+
11+
func TestLogger_Levels(t *testing.T) {
12+
var buf bytes.Buffer
13+
14+
l := New(
15+
WithLevel(INFO),
16+
WithWriter(&buf),
17+
)
18+
19+
l.printf("hello", DEBUG)
20+
if buf.Len() > 0 {
21+
t.Fatal("expected empty buffer")
22+
}
23+
24+
l.printf("hello", INFO)
25+
if buf.Len() == 0 {
26+
t.Fatal("expected non empty buffer")
27+
}
28+
}
29+
30+
func TestLogger_JSON(t *testing.T) {
31+
var buf bytes.Buffer
32+
33+
l := New(
34+
WithLevel(INFO),
35+
WithWriter(&buf),
36+
)
37+
38+
l.Infof("hello",
39+
String("user", "alice"),
40+
)
41+
42+
var result map[string]interface{}
43+
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
44+
t.Fatalf("json.Unmarshal: %v", err)
45+
}
46+
47+
if result["user"] != "alice" {
48+
t.Fatalf("unexpected user value %s expected alice", result["user"])
49+
}
50+
}
51+
52+
func TestLogger_Error(t *testing.T) {
53+
var buf bytes.Buffer
54+
55+
l := New(
56+
WithLevel(ERROR),
57+
WithWriter(&buf),
58+
)
59+
60+
e := errors.New("something happened")
61+
62+
l.Error("this is bad", Error(e))
63+
64+
fmt.Println(buf.String())
65+
}

0 commit comments

Comments
 (0)