You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

232 lines
6.2 KiB

  1. package handlers
  2. // Inspired by node's Connect library implementation of the logging middleware
  3. // https://github.com/senchalabs/connect
  4. import (
  5. "fmt"
  6. "net/http"
  7. "regexp"
  8. "strings"
  9. "time"
  10. "github.com/PuerkitoBio/ghost"
  11. )
  12. const (
  13. // Predefined logging formats that can be passed as format string.
  14. Ldefault = "_default_"
  15. Lshort = "_short_"
  16. Ltiny = "_tiny_"
  17. )
  18. var (
  19. // Token parser for request and response headers
  20. rxHeaders = regexp.MustCompile(`^(req|res)\[([^\]]+)\]$`)
  21. // Lookup table for predefined formats
  22. predefFormats = map[string]struct {
  23. fmt string
  24. toks []string
  25. }{
  26. Ldefault: {
  27. `%s - - [%s] "%s %s HTTP/%s" %d %s "%s" "%s"`,
  28. []string{"remote-addr", "date", "method", "url", "http-version", "status", "res[Content-Length]", "referrer", "user-agent"},
  29. },
  30. Lshort: {
  31. `%s - %s %s HTTP/%s %d %s - %.3f s`,
  32. []string{"remote-addr", "method", "url", "http-version", "status", "res[Content-Length]", "response-time"},
  33. },
  34. Ltiny: {
  35. `%s %s %d %s - %.3f s`,
  36. []string{"method", "url", "status", "res[Content-Length]", "response-time"},
  37. },
  38. }
  39. )
  40. // Augmented ResponseWriter implementation that captures the status code for the logger.
  41. type statusResponseWriter struct {
  42. http.ResponseWriter
  43. code int
  44. oriURL string
  45. }
  46. // Intercept the WriteHeader call to save the status code.
  47. func (this *statusResponseWriter) WriteHeader(code int) {
  48. this.code = code
  49. this.ResponseWriter.WriteHeader(code)
  50. }
  51. // Intercept the Write call to save the default status code.
  52. func (this *statusResponseWriter) Write(data []byte) (int, error) {
  53. if this.code == 0 {
  54. this.code = http.StatusOK
  55. }
  56. return this.ResponseWriter.Write(data)
  57. }
  58. // Implement the WrapWriter interface.
  59. func (this *statusResponseWriter) WrappedWriter() http.ResponseWriter {
  60. return this.ResponseWriter
  61. }
  62. // LogHandler options
  63. type LogOptions struct {
  64. LogFn func(string, ...interface{}) // Defaults to ghost.LogFn if nil
  65. Format string
  66. Tokens []string
  67. CustomTokens map[string]func(http.ResponseWriter, *http.Request) string
  68. Immediate bool
  69. DateFormat string
  70. }
  71. // Create a new LogOptions struct. The DateFormat defaults to time.RFC3339.
  72. func NewLogOptions(l func(string, ...interface{}), ft string, tok ...string) *LogOptions {
  73. return &LogOptions{
  74. LogFn: l,
  75. Format: ft,
  76. Tokens: tok,
  77. CustomTokens: make(map[string]func(http.ResponseWriter, *http.Request) string),
  78. DateFormat: time.RFC3339,
  79. }
  80. }
  81. // LogHandlerFunc is the same as LogHandler, it is just a convenience
  82. // signature that accepts a func(http.ResponseWriter, *http.Request) instead of
  83. // a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast.
  84. func LogHandlerFunc(h http.HandlerFunc, opts *LogOptions) http.HandlerFunc {
  85. return LogHandler(h, opts)
  86. }
  87. // Create a log handler for every request it receives.
  88. func LogHandler(h http.Handler, opts *LogOptions) http.HandlerFunc {
  89. return func(w http.ResponseWriter, r *http.Request) {
  90. if _, ok := getStatusWriter(w); ok {
  91. // Self-awareness, logging handler already set up
  92. h.ServeHTTP(w, r)
  93. return
  94. }
  95. // Save the response start time
  96. st := time.Now()
  97. // Call the wrapped handler, with the augmented ResponseWriter to handle the status code
  98. stw := &statusResponseWriter{w, 0, ""}
  99. // Log immediately if requested, otherwise on exit
  100. if opts.Immediate {
  101. logRequest(stw, r, st, opts)
  102. } else {
  103. // Store original URL, may get modified by handlers (i.e. StripPrefix)
  104. stw.oriURL = r.URL.String()
  105. defer logRequest(stw, r, st, opts)
  106. }
  107. h.ServeHTTP(stw, r)
  108. }
  109. }
  110. func getIpAddress(r *http.Request) string {
  111. hdr := r.Header
  112. hdrRealIp := hdr.Get("X-Real-Ip")
  113. hdrForwardedFor := hdr.Get("X-Forwarded-For")
  114. if hdrRealIp == "" && hdrForwardedFor == "" {
  115. return r.RemoteAddr
  116. }
  117. if hdrForwardedFor != "" {
  118. // X-Forwarded-For is potentially a list of addresses separated with ","
  119. part := strings.Split(hdrForwardedFor, ",")[0]
  120. return strings.TrimSpace(part) + ":0"
  121. }
  122. return hdrRealIp
  123. }
  124. // Check if the specified token is a predefined one, and if so return its current value.
  125. func getPredefinedTokenValue(t string, w *statusResponseWriter, r *http.Request,
  126. st time.Time, opts *LogOptions) (interface{}, bool) {
  127. switch t {
  128. case "http-version":
  129. return fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor), true
  130. case "response-time":
  131. return time.Now().Sub(st).Seconds(), true
  132. case "remote-addr":
  133. return getIpAddress(r), true
  134. case "date":
  135. return time.Now().Format(opts.DateFormat), true
  136. case "method":
  137. return r.Method, true
  138. case "url":
  139. if w.oriURL != "" {
  140. return w.oriURL, true
  141. }
  142. return r.URL.String(), true
  143. case "referrer", "referer":
  144. return r.Referer(), true
  145. case "user-agent":
  146. return r.UserAgent(), true
  147. case "status":
  148. return w.code, true
  149. }
  150. // Handle special cases for header
  151. mtch := rxHeaders.FindStringSubmatch(t)
  152. if len(mtch) > 2 {
  153. if mtch[1] == "req" {
  154. return r.Header.Get(mtch[2]), true
  155. } else {
  156. // This only works for headers explicitly set via the Header() map of
  157. // the writer, not those added by the http package under the covers.
  158. return w.Header().Get(mtch[2]), true
  159. }
  160. }
  161. return nil, false
  162. }
  163. // Do the actual logging.
  164. func logRequest(w *statusResponseWriter, r *http.Request, st time.Time, opts *LogOptions) {
  165. var (
  166. fn func(string, ...interface{})
  167. ok bool
  168. format string
  169. toks []string
  170. )
  171. // If no specific log function, use the default one from the ghost package
  172. if opts.LogFn == nil {
  173. fn = ghost.LogFn
  174. } else {
  175. fn = opts.LogFn
  176. }
  177. // If this is a predefined format, use it instead
  178. if v, ok := predefFormats[opts.Format]; ok {
  179. format = v.fmt
  180. toks = v.toks
  181. } else {
  182. format = opts.Format
  183. toks = opts.Tokens
  184. }
  185. args := make([]interface{}, len(toks))
  186. for i, t := range toks {
  187. if args[i], ok = getPredefinedTokenValue(t, w, r, st, opts); !ok {
  188. if f, ok := opts.CustomTokens[t]; ok && f != nil {
  189. args[i] = f(w, r)
  190. } else {
  191. args[i] = "?"
  192. }
  193. }
  194. }
  195. fn(format, args...)
  196. }
  197. // Helper function to retrieve the status writer.
  198. func getStatusWriter(w http.ResponseWriter) (*statusResponseWriter, bool) {
  199. st, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
  200. _, ok := tst.(*statusResponseWriter)
  201. return ok
  202. })
  203. if ok {
  204. return st.(*statusResponseWriter), true
  205. }
  206. return nil, false
  207. }