gromer

#golang#htmx#ssr

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

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


9ea90c2 — Peter John 2021-12-28T00:40:18+05:30
improve logging

Generated files not shown: go.sum

Files changed (4) hide show
  1. example/main.go +5 -14
  2. example/pages/api/recover/get.go +17 -0
  3. go.mod +2 -3
  4. 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
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
+ }))