Skip to content

Commit 7961e17

Browse files
committed
Add JSON logger, update middleware to all follow same pattern
1 parent b9f9f61 commit 7961e17

File tree

9 files changed

+240
-53
lines changed

9 files changed

+240
-53
lines changed

main.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@ import (
55
"github.com/sbecker/gin-api-demo/dao"
66
"github.com/sbecker/gin-api-demo/middleware"
77
"github.com/sbecker/gin-api-demo/resources"
8+
"github.com/sbecker/gin-api-demo/util"
89
)
910

1011
func main() {
1112
dao.InitDb()
1213

13-
r := gin.Default()
14-
r.Use(middleware.RequestID)
15-
r.Use(middleware.Auth)
16-
r.Use(middleware.CORS)
14+
util.UseJSONLogFormat()
15+
gin.SetMode(gin.ReleaseMode)
1716

18-
r.GET("/users", resources.GetAllUsers)
19-
r.GET("/users/:id", resources.GetUserByID)
17+
r := gin.New()
18+
19+
r.Use(middleware.JSONLogMiddleware())
20+
r.Use(gin.Recovery())
21+
r.Use(middleware.RequestID(middleware.RequestIDOptions{AllowSetting: false}))
22+
r.Use(middleware.Auth())
23+
r.Use(middleware.CORS(middleware.CORSOptions{}))
24+
25+
resources.NewUserResource(r)
2026

2127
r.Run(":8080") // listen and serve on 0.0.0.0:8080
2228
}

middleware/auth.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,32 @@ import (
77
"github.com/sbecker/gin-api-demo/dao"
88
)
99

10-
func Auth(c *gin.Context) {
11-
authHeader := c.Request.Header.Get("Authorization")
12-
r, _ := regexp.Compile("^Bearer (.+)$")
13-
14-
match := r.FindStringSubmatch(authHeader)
15-
16-
if len(match) == 0 {
17-
c.AbortWithStatus(401)
18-
return
19-
}
20-
tokenString := match[1]
21-
22-
if len(tokenString) == 0 {
23-
c.AbortWithStatus(401)
24-
return
10+
func Auth() gin.HandlerFunc {
11+
return func(c *gin.Context) {
12+
authHeader := c.Request.Header.Get("Authorization")
13+
r, _ := regexp.Compile("^Bearer (.+)$")
14+
15+
match := r.FindStringSubmatch(authHeader)
16+
17+
if len(match) == 0 {
18+
c.AbortWithStatus(401)
19+
return
20+
}
21+
tokenString := match[1]
22+
23+
if len(tokenString) == 0 {
24+
c.AbortWithStatus(401)
25+
return
26+
}
27+
28+
user, err := dao.GetUserByAuthToken(tokenString)
29+
if err != nil {
30+
c.AbortWithStatus(401)
31+
return
32+
}
33+
34+
c.Set("user", user)
35+
c.Set("userID", user.ID)
36+
c.Next()
2537
}
26-
27-
user, err := dao.GetUserByAuthToken(tokenString)
28-
if err != nil {
29-
c.AbortWithStatus(401)
30-
return
31-
}
32-
33-
c.Set("currentUser", user)
34-
c.Next()
3538
}

middleware/cors.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@ package middleware
22

33
import "github.com/gin-gonic/gin"
44

5+
type CORSOptions struct {
6+
Origin string
7+
}
8+
59
// CORS middleware from https://github.com/gin-gonic/gin/issues/29#issuecomment-89132826
6-
func CORS(c *gin.Context) {
7-
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // allow any origin domain
8-
// c.Writer.Header().Set("Access-Control-Allow-Origin", "http://domain.com") // uncomment to restrict to single domain
9-
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
10-
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
11-
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
12-
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
13-
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
10+
func CORS(options CORSOptions) gin.HandlerFunc {
11+
return func(c *gin.Context) {
12+
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // allow any origin domain
13+
if options.Origin != "" {
14+
c.Writer.Header().Set("Access-Control-Allow-Origin", options.Origin)
15+
}
16+
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
17+
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
18+
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
19+
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
20+
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
1421

15-
if c.Request.Method == "OPTIONS" {
16-
c.AbortWithStatus(200)
17-
} else {
18-
c.Next()
22+
if c.Request.Method == "OPTIONS" {
23+
c.AbortWithStatus(200)
24+
} else {
25+
c.Next()
26+
}
1927
}
2028
}

middleware/json_logger.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package middleware
2+
3+
import (
4+
"time"
5+
6+
log "github.com/Sirupsen/logrus"
7+
"github.com/gin-gonic/gin"
8+
"github.com/sbecker/gin-api-demo/util"
9+
)
10+
11+
// JSONLogMiddleware logs a gin HTTP request in JSON format, with some additional custom key/values
12+
func JSONLogMiddleware() gin.HandlerFunc {
13+
return func(c *gin.Context) {
14+
// Start timer
15+
start := time.Now()
16+
17+
// Process Request
18+
c.Next()
19+
20+
// Stop timer
21+
duration := util.GetDurationInMillseconds(start)
22+
23+
entry := log.WithFields(log.Fields{
24+
"client_ip": util.GetClientIP(c),
25+
"duration": duration,
26+
"method": c.Request.Method,
27+
"path": c.Request.RequestURI,
28+
"status": c.Writer.Status(),
29+
"user_id": util.GetUserID(c),
30+
"referrer": c.Request.Referer(),
31+
"request_id": c.Writer.Header().Get("Request-Id"),
32+
// "api_version": util.ApiVersion,
33+
})
34+
35+
if c.Writer.Status() >= 500 {
36+
entry.Error(c.Errors.String())
37+
} else {
38+
entry.Info("")
39+
}
40+
}
41+
}

middleware/request_id.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,25 @@ import (
55
"github.com/pborman/uuid"
66
)
77

8-
func RequestID(c *gin.Context) {
9-
// If Set-Request-Id header is set on request, use that for
10-
// Request-Id response header. Otherwise, generate a new one.
11-
requestID := c.Request.Header.Get("Set-Request-Id")
12-
if requestID == "" {
13-
requestID = uuid.New()
8+
type RequestIDOptions struct {
9+
AllowSetting bool
10+
}
11+
12+
func RequestID(options RequestIDOptions) gin.HandlerFunc {
13+
return func(c *gin.Context) {
14+
var requestID string
15+
16+
if options.AllowSetting {
17+
// If Set-Request-Id header is set on request, use that for
18+
// Request-Id response header. Otherwise, generate a new one.
19+
requestID = c.Request.Header.Get("Set-Request-Id")
20+
}
21+
22+
if requestID == "" {
23+
requestID = uuid.New()
24+
}
25+
26+
c.Writer.Header().Set("Request-Id", requestID)
27+
c.Next()
1428
}
15-
c.Writer.Header().Set("Request-Id", requestID)
16-
c.Next()
1729
}

resources/helper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ func getStringParam(c *gin.Context, name string) (string, error) {
2323
}
2424

2525
func getCurrentUser(c *gin.Context) models.User {
26-
return c.MustGet("currentUser").(models.User)
26+
return c.MustGet("user").(models.User)
2727
}

resources/user_resource.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,24 @@ import (
66
"github.com/sbecker/gin-api-demo/serializers"
77
)
88

9-
func GetAllUsers(c *gin.Context) {
9+
type UserResource struct {
10+
}
11+
12+
func NewUserResource(e *gin.Engine) {
13+
u := UserResource{}
14+
15+
// Setup Routes
16+
e.GET("/users", u.getAllUsers)
17+
e.GET("/users/:id", u.getUserByID)
18+
}
19+
20+
func (r *UserResource) getAllUsers(c *gin.Context) {
1021
currentUser := getCurrentUser(c)
1122
users := dao.GetAllUsers(currentUser)
1223
c.JSON(200, serializers.SerializeUsers(users, currentUser, "/users"))
1324
}
1425

15-
func GetUserByID(c *gin.Context) {
26+
func (r *UserResource) getUserByID(c *gin.Context) {
1627
currentUser := getCurrentUser(c)
1728

1829
id, err := getStringParam(c, "id")

util/env.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package util
2+
3+
import "os"
4+
5+
func GetEnv(key, defaultValue string) string {
6+
value := os.Getenv(key)
7+
if value != "" {
8+
return value
9+
}
10+
11+
return defaultValue
12+
}

util/log.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package util
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
log "github.com/Sirupsen/logrus"
10+
"github.com/gin-gonic/gin"
11+
"github.com/thedataguild/faer/util"
12+
)
13+
14+
// UseJSONLogFormat sets up the JSON log formatter
15+
func UseJSONLogFormat() {
16+
env := util.GetEnv("GIN_ENV", "development")
17+
program := util.GetEnv("SERVICE_NAME", "gin-api-demo")
18+
19+
log.SetFormatter(&JSONFormatter{
20+
Program: program,
21+
Env: env,
22+
})
23+
24+
// so our debug entries appear!
25+
log.SetLevel(log.DebugLevel)
26+
}
27+
28+
// Timestamps in microsecond resolution (like time.RFC3339Nano but microseconds)
29+
var timeStampFormat = "2006-01-02T15:04:05.000000Z07:00"
30+
31+
// JSONFormatter is a logger for use with Logrus
32+
type JSONFormatter struct {
33+
Program string
34+
Env string
35+
}
36+
37+
// Format includes the program, environment, and a custom time format: microsecond resolution
38+
func (f *JSONFormatter) Format(entry *log.Entry) ([]byte, error) {
39+
data := make(log.Fields, len(entry.Data)+3)
40+
for k, v := range entry.Data {
41+
data[k] = v
42+
}
43+
data["time"] = entry.Time.UTC().Format(timeStampFormat)
44+
data["msg"] = entry.Message
45+
data["level"] = strings.ToUpper(entry.Level.String())
46+
data["program"] = f.Program
47+
data["env"] = f.Env
48+
49+
serialized, err := json.Marshal(data)
50+
if err != nil {
51+
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
52+
}
53+
return append(serialized, '\n'), nil
54+
}
55+
56+
// clientIP gets the correct IP for the end client instead of the proxy
57+
func GetClientIP(c *gin.Context) string {
58+
// first check the X-Forwarded-For header
59+
requester := c.Request.Header.Get("X-Forwarded-For")
60+
// if empty, check the Real-IP header
61+
if len(requester) == 0 {
62+
requester = c.Request.Header.Get("X-Real-IP")
63+
}
64+
// if the requester is still empty, use the hard-coded address from the socket
65+
if len(requester) == 0 {
66+
requester = c.Request.RemoteAddr
67+
}
68+
69+
// if requester is a comma delimited list, take the first one
70+
// (this happens when proxied via elastic load balancer then again through nginx)
71+
if strings.Contains(requester, ",") {
72+
requester = strings.Split(requester, ",")[0]
73+
}
74+
75+
return requester
76+
}
77+
78+
// getUserID gets the current_user ID as a string
79+
func GetUserID(c *gin.Context) string {
80+
userID, exists := c.Get("userID")
81+
if exists {
82+
return userID.(string)
83+
}
84+
return ""
85+
}
86+
87+
// gets the duration of time since "start", in milliseconds
88+
func GetDurationInMillseconds(start time.Time) float64 {
89+
end := time.Now()
90+
duration := end.Sub(start)
91+
milliseconds := float64(duration) / float64(time.Millisecond)
92+
rounded := float64(int(milliseconds*100+.5)) / 100
93+
return rounded
94+
}

0 commit comments

Comments
 (0)