~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


d611f2d1 Peter John

tag: v0.9.5

v0.9.5

3 years ago
improve logging
Files changed (5) hide show
  1. example/main.go +5 -14
  2. example/pages/api/recover/get.go +17 -0
  3. go.mod +2 -3
  4. go.sum +4 -8
  5. http.go +98 -28
example/main.go CHANGED
@@ -17,8 +17,8 @@ import (
17
17
  "github.com/pyros2097/gromer/example/pages/api/todos"
18
18
  "github.com/pyros2097/gromer/example/pages"
19
19
  "github.com/pyros2097/gromer/example/pages/about"
20
+ "github.com/pyros2097/gromer/example/pages/api/recover"
20
21
  "github.com/pyros2097/gromer/example/pages/api/todos/_todoId_"
21
-
22
22
  )
23
23
 
24
24
  //go:embed assets/*
@@ -27,7 +27,8 @@ var assetsFS embed.FS
27
27
  func main() {
28
28
  isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
29
29
  r := mux.NewRouter()
30
+ r.Use(gromer.LogMiddleware)
30
- r.NotFoundHandler = http.HandlerFunc(notFound)
31
+ r.NotFoundHandler = gromer.NotFoundHandler
31
32
  r.PathPrefix("/assets/").Handler(wrapCache(http.FileServer(http.FS(assetsFS))))
32
33
  handle(r, "GET", "/api", gromer.ApiExplorer(apiDefinitions()))
33
34
  handle(r, "GET", "/about", about.GET)
@@ -36,6 +37,7 @@ func main() {
36
37
  handle(r, "PUT", "/api/todos/{todoId}", todos_todoId_.PUT)
37
38
  handle(r, "GET", "/api/todos", todos.GET)
38
39
  handle(r, "POST", "/api/todos", todos.POST)
40
+ handle(r, "GET", "/api/recover", recover.GET)
39
41
  handle(r, "GET", "/", pages.GET)
40
42
 
41
43
  if !isLambda {
@@ -59,16 +61,8 @@ func wrapCache(h http.Handler) http.Handler {
59
61
  })
60
62
  }
61
63
 
62
- func notFound(w http.ResponseWriter, r *http.Request) {
63
- gromer.LogReq(404, r)
64
- }
65
-
66
64
  func handle(router *mux.Router, method, route string, h interface{}) {
67
65
  router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
68
- var status int
69
- defer func() {
70
- gromer.LogReq(status, r)
71
- }()
72
66
  ctx, err := context.WithContext(c.WithValue(
73
67
  c.WithValue(
74
68
  c.WithValue(r.Context(), "assetsFS", assetsFS),
@@ -78,10 +72,7 @@ func handle(router *mux.Router, method, route string, h interface{}) {
78
72
  gromer.RespondError(w, 500, err)
79
73
  return
80
74
  }
81
- status, err = gromer.PerformRequest(route, h, ctx, w, r)
75
+ gromer.PerformRequest(route, h, ctx, w, r)
82
- if err != nil {
83
- log.Error().Stack().Err(err).Msg("")
84
- }
85
76
  }).Methods(method)
86
77
  }
87
78
 
example/pages/api/recover/get.go ADDED
@@ -0,0 +1,17 @@
1
+ package recover
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ )
7
+
8
+ type Params struct {
9
+ Limit int `json:"limit"`
10
+ }
11
+
12
+ func GET(ctx context.Context, params Params) (*Params, int, error) {
13
+ arr := []string{}
14
+ v := arr[55]
15
+ fmt.Printf("%s", v)
16
+ return &params, 200, nil
17
+ }
go.mod CHANGED
@@ -6,13 +6,12 @@ require (
6
6
  github.com/apex/gateway/v2 v2.0.0
7
7
  github.com/aymerick/raymond v2.0.2+incompatible
8
8
  github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
9
- github.com/fatih/color v1.13.0
10
9
  github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
11
- github.com/go-playground/validator/v10 v10.9.0 // indirect
10
+ github.com/go-playground/validator/v10 v10.9.0
12
11
  github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
13
12
  github.com/google/uuid v1.3.0
14
13
  github.com/gorilla/mux v1.8.0
15
- github.com/iancoleman/strcase v0.2.0 // indirect
14
+ github.com/iancoleman/strcase v0.2.0
16
15
  github.com/lib/pq v1.10.4
17
16
  github.com/markbates/inflect v1.0.4
18
17
  github.com/microcosm-cc/bluemonday v1.0.15 // indirect
go.sum CHANGED
@@ -153,8 +153,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
153
153
  github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
154
154
  github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
155
155
  github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
156
- github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
157
- github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
158
156
  github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
159
157
  github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
160
158
  github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc=
@@ -167,6 +165,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
167
165
  github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
168
166
  github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
169
167
  github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
168
+ github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
170
169
  github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
171
170
  github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
172
171
  github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@@ -296,12 +295,12 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
296
295
  github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
297
296
  github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
298
297
  github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
299
- github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
300
298
  github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
299
+ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
301
300
  github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
302
301
  github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
303
- github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
304
302
  github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
303
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
305
304
  github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
306
305
  github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
307
306
  github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
@@ -311,12 +310,8 @@ github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
311
310
  github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
312
311
  github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g=
313
312
  github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs=
314
- github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
315
- github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
316
313
  github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
317
314
  github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
318
- github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
319
- github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
320
315
  github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSOZZLjYpbeY=
321
316
  github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30=
322
317
  github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -334,6 +329,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
334
329
  github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
335
330
  github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
336
331
  github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
332
+ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
337
333
  github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
338
334
  github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
339
335
  github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
http.go CHANGED
@@ -2,25 +2,38 @@ package gromer
2
2
 
3
3
  import (
4
4
  "encoding/json"
5
+ "fmt"
6
+ "net"
5
7
  "net/http"
6
8
  "os"
7
9
  "reflect"
8
10
  "regexp"
11
+ "runtime/debug"
9
12
  "strconv"
10
13
  "strings"
11
14
  "time"
12
15
 
13
- "github.com/fatih/color"
14
16
  "github.com/go-playground/validator/v10"
15
17
  "github.com/gorilla/mux"
16
18
  "github.com/iancoleman/strcase"
17
19
  "github.com/rs/zerolog"
18
20
  "github.com/rs/zerolog/log"
21
+ "github.com/rs/zerolog/pkgerrors"
19
22
  )
20
23
 
24
+ var info *debug.BuildInfo
25
+ var IsLambda bool
26
+
21
27
  func init() {
28
+ IsLambda = os.Getenv("_LAMBDA_SERVER_PORT") != ""
29
+ info, _ = debug.ReadBuildInfo()
22
- zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
30
+ zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
31
+ if !IsLambda {
23
- log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
32
+ log.Logger = log.Output(zerolog.ConsoleWriter{
33
+ Out: os.Stdout,
34
+ TimeFormat: zerolog.TimeFormatUnix,
35
+ })
36
+ }
24
37
  }
25
38
 
26
39
  func RespondError(w http.ResponseWriter, status int, err error) {
@@ -29,6 +42,10 @@ func RespondError(w http.ResponseWriter, status int, err error) {
29
42
  merror := map[string]interface{}{
30
43
  "error": err.Error(),
31
44
  }
45
+ if status >= 500 {
46
+ log.Error().Str("type", "panic").Msg(err.Error())
47
+ merror["error"] = "Internal Server Error"
48
+ }
32
49
  validationErrors, ok := err.(validator.ValidationErrors)
33
50
  if ok {
34
51
  emap := map[string]string{}
@@ -63,7 +80,7 @@ func GetRouteParams(route string) []string {
63
80
  return params
64
81
  }
65
82
 
66
- func PerformRequest(route string, h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) (int, error) {
83
+ func PerformRequest(route string, h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) {
67
84
  params := GetRouteParams(route)
68
85
  args := []reflect.Value{reflect.ValueOf(ctx)}
69
86
  funcType := reflect.TypeOf(h)
@@ -76,13 +93,13 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
76
93
  structType := funcType.In(icount - 1)
77
94
  instance := reflect.New(structType)
78
95
  if structType.Kind() != reflect.Struct {
79
- panic(route + " func final param should be a struct")
96
+ log.Fatal().Msgf("router '%s' func final param should be a struct", route)
80
97
  }
81
98
  if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
82
99
  err := json.NewDecoder(r.Body).Decode(instance.Interface())
83
100
  if err != nil {
84
101
  RespondError(w, 400, err)
85
- return 400, err
102
+ return
86
103
  }
87
104
  } else if r.Method == "GET" {
88
105
  rv := instance.Elem()
@@ -103,7 +120,7 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
103
120
  v, err := strconv.ParseInt(jsonValue, 10, base)
104
121
  if err != nil {
105
122
  RespondError(w, 400, err)
106
- return 400, err
123
+ return
107
124
  }
108
125
  f.SetInt(v)
109
126
  }
@@ -114,12 +131,12 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
114
131
  v, err := time.Parse(time.RFC3339, jsonValue)
115
132
  if err != nil {
116
133
  RespondError(w, 400, err)
117
- return 400, err
134
+ return
118
135
  }
119
136
  f.Set(reflect.ValueOf(v))
120
137
  }
121
138
  } else {
122
- panic("Uknown query param: " + jsonName + " " + jsonValue)
139
+ log.Fatal().Msgf("Uknown query param: '%s' '%s'", jsonName, jsonValue)
123
140
  }
124
141
  }
125
142
  }
@@ -132,37 +149,90 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
132
149
  responseError := values[2].Interface()
133
150
  if responseError != nil {
134
151
  RespondError(w, responseStatus, responseError.(error))
135
- return responseStatus, responseError.(error)
152
+ return
136
153
  }
137
154
  if v, ok := response.(HtmlPage); ok {
138
155
  w.Header().Set("Content-Type", "text/html")
139
156
  // This has to be at end always
140
157
  w.WriteHeader(responseStatus)
141
158
  v.WriteHtml(w)
142
- return 200, nil
159
+ return
143
160
  }
144
161
  w.Header().Set("Content-Type", "application/json")
145
162
  // This has to be at end always
146
163
  w.WriteHeader(responseStatus)
147
164
  data, _ := json.Marshal(response)
148
165
  w.Write(data)
149
- return 200, nil
150
166
  }
151
167
 
168
+ type writeCounter int64
169
+
152
- func LogReq(status int, r *http.Request) {
170
+ func (wc *writeCounter) Write(p []byte) (n int, err error) {
153
- a := color.FgGreen
171
+ *wc += writeCounter(len(p))
154
- if status >= 500 {
172
+ return len(p), nil
155
- a = color.FgRed
156
- } else if status >= 400 {
157
- a = color.FgYellow
158
- }
173
+ }
159
- m := color.FgCyan
160
- if r.Method == "POST" {
161
- m = color.FgYellow
162
- } else if r.Method == "PUT" {
174
+ func headerSize(h http.Header) int64 {
163
- m = color.FgMagenta
175
+ var wc writeCounter
176
+ h.Write(&wc)
164
- } else if r.Method == "DELETE" {
177
+ return int64(wc) + 2 // for CRLF
165
- m = color.FgRed
166
- }
178
+ }
179
+
167
- log.Info().Msgf("%3s %s %s", color.New(a).Sprint(status), color.New(m).Sprintf("%-4s", r.Method), color.WhiteString(r.URL.String()))
180
+ type LogResponseWriter struct {
181
+ http.ResponseWriter
182
+ responseStatusCode int
183
+ responseContentLength int
184
+ responseHeaderSize int
168
185
  }
186
+
187
+ func NewLogResponseWriter(w http.ResponseWriter) *LogResponseWriter {
188
+ return &LogResponseWriter{ResponseWriter: w}
189
+ }
190
+
191
+ func (w *LogResponseWriter) WriteHeader(code int) {
192
+ w.ResponseWriter.WriteHeader(code)
193
+ w.responseStatusCode = code
194
+ w.responseHeaderSize = int(headerSize(w.Header()))
195
+
196
+ }
197
+
198
+ func (w *LogResponseWriter) Write(body []byte) (int, error) {
199
+ w.responseContentLength += len(body)
200
+ return w.ResponseWriter.Write(body)
201
+ }
202
+
203
+ var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
204
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
205
+ defer func() {
206
+ if err := recover(); err != nil {
207
+ stack := string(debug.Stack())
208
+ RespondError(w, 500, fmt.Errorf("panic: %+v\n %s", err, stack))
209
+ }
210
+ }()
211
+ startTime := time.Now()
212
+ logRespWriter := NewLogResponseWriter(w)
213
+ next.ServeHTTP(logRespWriter, r)
214
+ ip, _, _ := net.SplitHostPort(r.RemoteAddr)
215
+ if len(ip) > 0 && ip[0] == '[' {
216
+ ip = ip[1 : len(ip)-1]
217
+ }
218
+ log.Info().
219
+ Str("method", r.Method).
220
+ Str("url", r.URL.String()).
221
+ Int("header_size", int(headerSize(r.Header))).
222
+ Int64("body_size", r.ContentLength).
223
+ Str("host", r.Host).
224
+ // Str("agent", r.UserAgent()).
225
+ Str("referer", r.Referer()).
226
+ Str("proto", r.Proto).
227
+ Str("remote_ip", ip).
228
+ Int("status", logRespWriter.responseStatusCode).
229
+ Int("resp_header_size", logRespWriter.responseHeaderSize).
230
+ Int("resp_body_size", logRespWriter.responseContentLength).
231
+ Str("latency", time.Since(startTime).String()).
232
+ Msg("")
233
+ })
234
+ })
235
+
236
+ var NotFoundHandler = LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
237
+ RespondError(w, 404, fmt.Errorf("path '%s' not found", r.URL.String()))
238
+ }))