Skip to content

Commit ce6b1e2

Browse files
authored
Logger middleware interface (#820)
* Modified logger format Signed-off-by: Vishal Rana <vr@labstack.com> * Logger middleware via struct Signed-off-by: Vishal Rana <vr@labstack.com> * Fixed time format Signed-off-by: Vishal Rana <vr@labstack.com>
1 parent a09afe2 commit ce6b1e2

File tree

4 files changed

+145
-101
lines changed

4 files changed

+145
-101
lines changed

db/db.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package db
2+
3+
type (
4+
// DB defines the interface for general database operations.
5+
DB interface {
6+
Logger
7+
}
8+
9+
// Logger defines the interface for logger middleware.
10+
Logger interface {
11+
Log(*Request) error
12+
}
13+
14+
// Mongo implements `DB`
15+
Mongo struct{}
16+
)

db/model.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package db
2+
3+
import "time"
4+
5+
type (
6+
// Request defines the data to be logged by logger middleware.
7+
Request struct {
8+
// ID string `json:"id,omitempty"` (Request ID - Not implemented)
9+
Time time.Time `json:"time,omitempty"`
10+
RemoteIP string `json:"remote_ip,omitempty"`
11+
URI string `json:"uri,omitempty"`
12+
Host string `json:"host,omitempty"`
13+
Method string `json:"method,omitempty"`
14+
Path string `json:"path,omitempty"`
15+
Referer string `json:"referer,omitempty"`
16+
UserAgent string `json:"user_agent,omitempty"`
17+
Status int `json:"status,omitempty"`
18+
Latency time.Duration `json:"latency,omitempty"`
19+
LatencyHuman string `json:"latency_human,omitempty"`
20+
BytesIn int64 `json:"bytes_in"`
21+
BytesOut int64 `json:"bytes_out"`
22+
Header map[string]string `json:"header,omitempty"`
23+
Form map[string]string `json:"form,omitempty"`
24+
Query map[string]string `json:"query,omitempty"`
25+
}
26+
)

middleware/logger.go

Lines changed: 75 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
package middleware
22

33
import (
4-
"bytes"
54
"io"
65
"os"
76
"strconv"
87
"strings"
9-
"sync"
108
"time"
119

10+
"encoding/json"
11+
1212
"github.com/labstack/echo"
13-
"github.com/labstack/gommon/color"
14-
isatty "github.com/mattn/go-isatty"
15-
"github.com/valyala/fasttemplate"
13+
"github.com/labstack/echo/db"
1614
)
1715

1816
type (
@@ -21,8 +19,9 @@ type (
2119
// Skipper defines a function to skip middleware.
2220
Skipper Skipper
2321

24-
// Log format which can be constructed using the following tags:
22+
// Availabe logger fields:
2523
//
24+
// - time_unix
2625
// - time_rfc3339
2726
// - id (Request ID - Not implemented)
2827
// - remote_ip
@@ -33,39 +32,51 @@ type (
3332
// - referer
3433
// - user_agent
3534
// - status
36-
// - latency (In microseconds)
35+
// - latency (In nanosecond)
3736
// - latency_human (Human readable)
3837
// - bytes_in (Bytes received)
3938
// - bytes_out (Bytes sent)
4039
// - header:<name>
4140
// - query:<name>
4241
// - form:<name>
43-
//
44-
// Example "${remote_ip} ${status}"
45-
//
46-
// Optional. Default value DefaultLoggerConfig.Format.
47-
Format string `json:"format"`
4842

49-
// Output is a writer where logs are written.
50-
// Optional. Default value os.Stdout.
51-
Output io.Writer
43+
// Optional. Default value DefaultLoggerConfig.Fields.
44+
Fields []string `json:"fields"`
45+
46+
// Output is where logs are written.
47+
// Optional. Default value &Stream{os.Stdout}.
48+
Output db.Logger
49+
}
5250

53-
template *fasttemplate.Template
54-
color *color.Color
55-
pool sync.Pool
51+
// Stream implements `db.Logger`.
52+
Stream struct {
53+
io.Writer
5654
}
5755
)
5856

57+
// LogRequest encodes `db.Request` into a stream.
58+
func (s *Stream) Log(r *db.Request) error {
59+
enc := json.NewEncoder(s)
60+
return enc.Encode(r)
61+
}
62+
5963
var (
6064
// DefaultLoggerConfig is the default Logger middleware config.
6165
DefaultLoggerConfig = LoggerConfig{
6266
Skipper: defaultSkipper,
63-
Format: `{"time":"${time_rfc3339}","remote_ip":"${remote_ip}","host":"${host}",` +
64-
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
65-
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
66-
`"bytes_out":${bytes_out}}` + "\n",
67-
Output: os.Stdout,
68-
color: color.New(),
67+
Fields: []string{
68+
"time",
69+
"remote_ip",
70+
"host",
71+
"method",
72+
"uri",
73+
"status",
74+
"latency",
75+
"latency_human",
76+
"bytes_in",
77+
"bytes_out",
78+
},
79+
Output: &Stream{os.Stdout},
6980
}
7081
)
7182

@@ -81,24 +92,13 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
8192
if config.Skipper == nil {
8293
config.Skipper = DefaultLoggerConfig.Skipper
8394
}
84-
if config.Format == "" {
85-
config.Format = DefaultLoggerConfig.Format
95+
if len(config.Fields) == 0 {
96+
config.Fields = DefaultLoggerConfig.Fields
8697
}
8798
if config.Output == nil {
8899
config.Output = DefaultLoggerConfig.Output
89100
}
90101

91-
config.template = fasttemplate.New(config.Format, "${", "}")
92-
config.color = color.New()
93-
if w, ok := config.Output.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) {
94-
config.color.Disable()
95-
}
96-
config.pool = sync.Pool{
97-
New: func() interface{} {
98-
return bytes.NewBuffer(make([]byte, 256))
99-
},
100-
}
101-
102102
return func(next echo.HandlerFunc) echo.HandlerFunc {
103103
return func(c echo.Context) (err error) {
104104
if config.Skipper(c) {
@@ -112,74 +112,66 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
112112
c.Error(err)
113113
}
114114
stop := time.Now()
115-
buf := config.pool.Get().(*bytes.Buffer)
116-
buf.Reset()
117-
defer config.pool.Put(buf)
118-
119-
_, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
120-
switch tag {
121-
case "time_rfc3339":
122-
return w.Write([]byte(time.Now().Format(time.RFC3339)))
115+
request := &db.Request{
116+
Header: make(map[string]string),
117+
Query: make(map[string]string),
118+
Form: make(map[string]string),
119+
}
120+
121+
for _, f := range config.Fields {
122+
switch f {
123+
case "time":
124+
request.Time = time.Now()
123125
case "remote_ip":
124-
ra := c.RealIP()
125-
return w.Write([]byte(ra))
126+
request.RemoteIP = c.RealIP()
126127
case "host":
127-
return w.Write([]byte(req.Host))
128+
request.Host = req.Host
128129
case "uri":
129-
return w.Write([]byte(req.RequestURI))
130+
request.URI = req.RequestURI
130131
case "method":
131-
return w.Write([]byte(req.Method))
132+
request.Method = req.Method
132133
case "path":
133134
p := req.URL.Path
134135
if p == "" {
135136
p = "/"
136137
}
137-
return w.Write([]byte(p))
138+
request.Path = p
138139
case "referer":
139-
return w.Write([]byte(req.Referer()))
140+
request.Referer = req.Referer()
140141
case "user_agent":
141-
return w.Write([]byte(req.UserAgent()))
142+
request.UserAgent = req.UserAgent()
142143
case "status":
143-
n := res.Status
144-
s := config.color.Green(n)
145-
switch {
146-
case n >= 500:
147-
s = config.color.Red(n)
148-
case n >= 400:
149-
s = config.color.Yellow(n)
150-
case n >= 300:
151-
s = config.color.Cyan(n)
152-
}
153-
return w.Write([]byte(s))
144+
request.Status = res.Status
154145
case "latency":
155-
l := stop.Sub(start).Nanoseconds() / 1000
156-
return w.Write([]byte(strconv.FormatInt(l, 10)))
146+
request.Latency = stop.Sub(start)
157147
case "latency_human":
158-
return w.Write([]byte(stop.Sub(start).String()))
148+
request.LatencyHuman = stop.Sub(start).String()
159149
case "bytes_in":
160-
b := req.Header.Get(echo.HeaderContentLength)
161-
if b == "" {
162-
b = "0"
150+
cl := req.Header.Get(echo.HeaderContentLength)
151+
if cl == "" {
152+
cl = "0"
163153
}
164-
return w.Write([]byte(b))
154+
l, _ := strconv.ParseInt(cl, 10, 64)
155+
request.BytesIn = l
165156
case "bytes_out":
166-
return w.Write([]byte(strconv.FormatInt(res.Size, 10)))
157+
request.BytesOut = res.Size
167158
default:
168159
switch {
169-
case strings.HasPrefix(tag, "header:"):
170-
return buf.Write([]byte(c.Request().Header.Get(tag[7:])))
171-
case strings.HasPrefix(tag, "query:"):
172-
return buf.Write([]byte(c.QueryParam(tag[6:])))
173-
case strings.HasPrefix(tag, "form:"):
174-
return buf.Write([]byte(c.FormValue(tag[5:])))
160+
case strings.HasPrefix(f, "header:"):
161+
k := f[7:]
162+
request.Header[k] = c.Request().Header.Get(k)
163+
case strings.HasPrefix(f, "query:"):
164+
k := f[6:]
165+
request.Query[k] = c.QueryParam(k)
166+
case strings.HasPrefix(f, "form:"):
167+
k := f[5:]
168+
request.Form[k] = c.FormValue(k)
175169
}
176170
}
177-
return 0, nil
178-
})
179-
if err == nil {
180-
config.Output.Write(buf.Bytes())
181171
}
182-
return
172+
173+
// Write
174+
return config.Output.Log(request)
183175
}
184176
}
185177
}

middleware/logger_test.go

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,52 +81,62 @@ func TestLoggerIPAddress(t *testing.T) {
8181
assert.Contains(t, ip, buf.String())
8282
}
8383

84-
func TestLoggerTemplate(t *testing.T) {
84+
func TestLoggerFields(t *testing.T) {
8585
buf := new(bytes.Buffer)
8686

8787
e := echo.New()
8888
e.Use(LoggerWithConfig(LoggerConfig{
89-
Format: `{"time":"${time_rfc3339}","remote_ip":"${remote_ip}","host":"${host}","user_agent":"${user_agent}",` +
90-
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
91-
`"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "referer":"${referer}",` +
92-
`"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}",` +
93-
`"us":"${query:username}", "cf":"${form:username}"}` + "\n",
94-
Output: buf,
89+
Fields: []string{
90+
"time",
91+
"remote_ip",
92+
"host",
93+
"user_agent",
94+
"method",
95+
"uri",
96+
"path",
97+
"referer",
98+
"status",
99+
"latency",
100+
"latency_human",
101+
"bytes_in",
102+
"bytes_out",
103+
"header:X-Custom-Header",
104+
"query:user",
105+
"form:user",
106+
},
107+
Output: &Stream{buf},
95108
}))
96109

97110
e.GET("/", func(c echo.Context) error {
98-
return c.String(http.StatusOK, "Header Logged")
111+
return c.String(http.StatusOK, "OK")
99112
})
100113

101-
req, _ := http.NewRequest(echo.GET, "/?username=apagano-param&password=secret", nil)
114+
req, _ := http.NewRequest(echo.GET, "/?user=joe", nil)
102115
req.RequestURI = "/"
103116
req.Header.Add(echo.HeaderXRealIP, "127.0.0.1")
104117
req.Header.Add("Referer", "google.com")
105-
req.Header.Add("User-Agent", "echo-tests-agent")
118+
req.Header.Add("User-Agent", "test-agent")
106119
req.Header.Add("X-Custom-Header", "AAA-CUSTOM-VALUE")
107120
req.Form = url.Values{
108-
"username": []string{"apagano-form"},
109-
"password": []string{"secret-form"},
121+
"user": []string{"jon"},
110122
}
111123

112124
rec := httptest.NewRecorder()
113125
e.ServeHTTP(rec, req)
114126

115127
cases := map[string]bool{
116-
"apagano-param": true,
117-
"apagano-form": true,
128+
"time": true,
129+
"joe": true,
130+
"jon": true,
118131
"AAA-CUSTOM-VALUE": true,
119-
"BBB-CUSTOM-VALUE": false,
120-
"secret-form": false,
121-
"hexvalue": false,
122132
"GET": true,
123133
"127.0.0.1": true,
124134
"\"path\":\"/\"": true,
125135
"\"uri\":\"/\"": true,
126136
"\"status\":200": true,
127137
"\"bytes_in\":0": true,
128138
"google.com": true,
129-
"echo-tests-agent": true,
139+
"test-agent": true,
130140
}
131141

132142
for token, present := range cases {

0 commit comments

Comments
 (0)