github.com/aldelo/common@v1.5.1/wrapper/gin/ginzap.go (about)

     1  package gin
     2  
     3  /*
     4   * Copyright 2020-2023 Aldelo, LP
     5   *
     6   * Licensed under the Apache License, Version 2.0 (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at
     9   *
    10   *     http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  import (
    20  	"fmt"
    21  	util "github.com/aldelo/common"
    22  	"go.uber.org/zap"
    23  	"net"
    24  	"net/http"
    25  	"net/http/httputil"
    26  	"os"
    27  	"runtime/debug"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/aldelo/common/wrapper/zap"
    32  	"github.com/gin-gonic/gin"
    33  )
    34  
    35  // NewGinZapMiddleware returns a newly created GinZap struct object
    36  func NewGinZapMiddleware(logName string, outputToConsole bool) *GinZap {
    37  	return &GinZap{
    38  		LogName:         logName,
    39  		OutputToConsole: outputToConsole,
    40  	}
    41  }
    42  
    43  // GinZap struct defines logger middleware for use with Gin, using Zap logging package,
    44  // CREDIT:this code is based and modified from github.com/gin-contrib/zap
    45  //
    46  // LogName = (required) specifies the log name being written to
    47  // OutputToConsole = (required) specifies if logger writes to console or disk
    48  // TimeFormat = (optional) default = time.RFC3339
    49  // TimeUtc = (optional) default = false
    50  // PanicStack = (optional) when panic, log to include stack
    51  type GinZap struct {
    52  	LogName         string
    53  	OutputToConsole bool
    54  
    55  	TimeFormat string // default = time.RFC3339
    56  	TimeUtc    bool   // default = false
    57  	PanicStack bool   // default = false
    58  
    59  	_zapLog *data.ZapLog
    60  }
    61  
    62  // Init will initialize zap logger and prep ginzap struct object for middleware use
    63  func (z *GinZap) Init() error {
    64  	if util.LenTrim(z.LogName) == 0 {
    65  		return fmt.Errorf("Log Name is Required")
    66  	}
    67  
    68  	z._zapLog = &data.ZapLog{
    69  		DisableLogger:   false,
    70  		OutputToConsole: z.OutputToConsole,
    71  		AppName:         z.LogName,
    72  	}
    73  
    74  	if err := z._zapLog.Init(); err != nil {
    75  		z._zapLog = nil
    76  		return err
    77  	} else {
    78  		if util.LenTrim(z.TimeFormat) == 0 {
    79  			z.TimeFormat = time.RFC3339
    80  		}
    81  		return nil
    82  	}
    83  }
    84  
    85  // NormalLogger returns a gin.HandlerFunc (middleware) that logs requests using uber-go/zap.
    86  //
    87  // Requests with errors are logged using zap.Error().
    88  // Requests without errors are logged using zap.Info().
    89  func (z *GinZap) NormalLogger() gin.HandlerFunc {
    90  	return func(c *gin.Context) {
    91  		if z._zapLog == nil {
    92  			c.Next()
    93  			return
    94  		}
    95  
    96  		start := time.Now()
    97  
    98  		path := c.Request.URL.Path
    99  		query := c.Request.URL.RawQuery
   100  
   101  		path = strings.ReplaceAll(path, "\n", "")
   102  		path = strings.ReplaceAll(path, "\r", "")
   103  
   104  		query = strings.ReplaceAll(query, "\n", "")
   105  		query = strings.ReplaceAll(query, "\r", "")
   106  
   107  		c.Next()
   108  
   109  		end := time.Now()
   110  		latency := end.Sub(start)
   111  
   112  		if z.TimeUtc {
   113  			end = end.UTC()
   114  		}
   115  
   116  		hdr := ""
   117  
   118  		if c.Request.Header != nil {
   119  			for k, v := range c.Request.Header {
   120  				if len(v) >= 1 {
   121  					hdr += fmt.Sprintf("%s=%s, ", k, strings.ReplaceAll(strings.ReplaceAll(v[0], "\n", ""), "\r", ""))
   122  				}
   123  			}
   124  		}
   125  
   126  		if len(c.Errors) > 0 {
   127  			for _, e := range c.Errors.Errors() {
   128  				z._zapLog.Error(e,
   129  					zap.String("method", strings.ReplaceAll(strings.ReplaceAll(c.Request.Method, "\n", ""), "\r", "")),
   130  					zap.String("path", path),
   131  					zap.String("header", hdr),
   132  					zap.String("query", query),
   133  					zap.String("ip", strings.ReplaceAll(strings.ReplaceAll(c.ClientIP(), "\n", ""), "\r", "")),
   134  					zap.String("user-agent", strings.ReplaceAll(strings.ReplaceAll(c.Request.UserAgent(), "\n", ""), "\r", "")),
   135  					zap.String("time", end.Format(z.TimeFormat)),
   136  					zap.Duration("latency", latency))
   137  			}
   138  		} else {
   139  			z._zapLog.Info(path,
   140  				zap.Int("status", c.Writer.Status()),
   141  				zap.String("method", strings.ReplaceAll(strings.ReplaceAll(c.Request.Method, "\n", ""), "\r", "")),
   142  				zap.String("path", path),
   143  				zap.String("header", hdr),
   144  				zap.String("query", query),
   145  				zap.String("ip", strings.ReplaceAll(strings.ReplaceAll(c.ClientIP(), "\n", ""), "\r", "")),
   146  				zap.String("user-agent", strings.ReplaceAll(strings.ReplaceAll(c.Request.UserAgent(), "\n", ""), "\r", "")),
   147  				zap.String("time", end.Format(z.TimeFormat)),
   148  				zap.Duration("latency", latency))
   149  		}
   150  	}
   151  }
   152  
   153  // PanicLogger returns a gin.HandlerFunc (middleware)
   154  //
   155  // this logger recovers from any panics and logs requests using uber-go/zap
   156  //
   157  // All errors are logged using zap.Error()
   158  func (z *GinZap) PanicLogger() gin.HandlerFunc {
   159  	return func(c *gin.Context) {
   160  		defer func() {
   161  			if z._zapLog == nil {
   162  				return
   163  			}
   164  
   165  			if err := recover(); err != nil {
   166  				// Check for a broken connection, as it is not really a
   167  				// condition that warrants a panic stack trace.
   168  				var brokenPipe bool
   169  				if ne, ok := err.(*net.OpError); ok {
   170  					if se, ok := ne.Err.(*os.SyscallError); ok {
   171  						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
   172  							brokenPipe = true
   173  						}
   174  					}
   175  				}
   176  
   177  				httpRequest, _ := httputil.DumpRequest(c.Request, false)
   178  				if brokenPipe {
   179  					z._zapLog.Error(c.Request.URL.Path,
   180  						zap.Any("error", err),
   181  						zap.String("request", string(httpRequest)),
   182  					)
   183  
   184  					// If the connection is dead, we can't write a status to it.
   185  					_ = c.Error(err.(error)) // nolint: errcheck
   186  					c.Abort()
   187  					return
   188  				}
   189  
   190  				t := time.Now()
   191  
   192  				if z.TimeUtc {
   193  					t = t.UTC()
   194  				}
   195  
   196  				if z.PanicStack {
   197  					z._zapLog.Error("[Recovery From Panic]",
   198  						zap.Time("time", t),
   199  						zap.Any("error", err),
   200  						zap.String("request", string(httpRequest)),
   201  						zap.String("stack", string(debug.Stack())),
   202  					)
   203  				} else {
   204  					z._zapLog.Error("[Recovery From Panic]",
   205  						zap.Time("time", t),
   206  						zap.Any("error", err),
   207  						zap.String("request", string(httpRequest)),
   208  					)
   209  				}
   210  
   211  				c.AbortWithStatus(http.StatusInternalServerError)
   212  			}
   213  		}()
   214  
   215  		c.Next()
   216  	}
   217  }