ginlogrus

package module
v2.0.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2020 License: BSD-3-Clause Imports: 13 Imported by: 1

README

go-gin-logrus

Go Report Card Release

Gin Web Framework Open Tracing middleware.

This middleware also support aggregate logging: the ability to aggregate all log entries into just one write. This aggregation is helpful when your logs are being sent to Kibana and you only want to index one log per request.

Installation

$ go get github.com/Bose/go-gin-logrus

If you want to use it with opentracing you could consider installing:

$ go get github.com/Bose/go-gin-opentracing

Dependencies - for local development

If you want to see your traces on your local system, you'll need to run a tracing backend like Jaeger. You'll find info about how-to in the Jaeger Tracing github repo docs Basically, you can run the Jaeger opentracing backend under docker via:

docker run -d -e \
  COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

Usage

# example aggregated log entry for a request with UseBanner == true
{
  "new-header-index-name": "this is how you set new header level data",
  "request-summary-info": {
    "comment": "",
    "ip": "::1",
    "latency": "     98.217µs",
    "method": "GET",
    "path": "/",
    "requestID": "4b4fb22ef51cc540:4b4fb22ef51cc540:0:1",
    "status": 200,
    "time": "2019-02-06T13:24:06Z",
    "user-agent": "curl/7.54.0"
  },
  "entries": [
    {
      "level": "info",
      "msg": "this will be aggregated into one write with the access log and will show up when the request is completed",
      "time": "2019-02-06T08:24:06-05:00"
    },
    {
      "comment": "this is an aggregated log entry with initial comment field",
      "level": "debug",
      "msg": "aggregated entry with new comment field",
      "time": "2019-02-06T08:24:06-05:00"
    },
    {
      "level": "error",
      "msg": "aggregated error entry with new-comment field",
      "new-comment": "this is an aggregated log entry with reset comment field",
      "time": "2019-02-06T08:24:06-05:00"
    }
  ],
  "banner": "[GIN] --------------------------------------------------------------- GinLogrusWithTracing ----------------------------------------------------------------"
}

package main

import (
	"fmt"
	"os"
	"runtime"
	"time"

	"github.com/opentracing/opentracing-go/ext"

	ginlogrus "github.com/Bose/go-gin-logrus"
	ginopentracing "github.com/Bose/go-gin-opentracing"
	"github.com/gin-gonic/gin"
	opentracing "github.com/opentracing/opentracing-go"
	"github.com/sirupsen/logrus"
)

func main() {
	// use the JSON formatter
	logrus.SetFormatter(&logrus.JSONFormatter{})

	r := gin.New() // don't use the Default(), since it comes with a logger

	// setup tracing...
	hostName, err := os.Hostname()
	if err != nil {
		hostName = "unknown"
	}

	tracer, reporter, closer, err := ginopentracing.InitTracing(
		fmt.Sprintf("go-gin-logrus-example::%s", hostName), // service name for the traces
		"localhost:5775",                        // where to send the spans
		ginopentracing.WithEnableInfoLog(false)) // WithEnableLogInfo(false) will not log info on every span sent... if set to true it will log and they won't be aggregated
	if err != nil {
		panic("unable to init tracing")
	}
	defer closer.Close()
	defer reporter.Close()
	opentracing.SetGlobalTracer(tracer)

	p := ginopentracing.OpenTracer([]byte("api-request-"))
	r.Use(p)

	r.Use(gin.Recovery()) // add Recovery middleware
	useBanner := true
	useUTC := true
	r.Use(ginlogrus.WithTracing(logrus.StandardLogger(),
		useBanner,
		time.RFC3339,
		useUTC,
		"requestID",
		[]byte("uber-trace-id"), // where jaeger might have put the trace id
		[]byte("RequestID"),     // where the trace ID might already be populated in the headers
		ginlogrus.WithAggregateLogging(true)))

	r.GET("/", func(c *gin.Context) {
		ginlogrus.SetCtxLoggerHeader(c, "new-header-index-name", "this is how you set new header level data")

		logger := ginlogrus.GetCtxLogger(c) // will get a logger with the aggregate Logger set if it's enabled - handy if you've already set fields for the request
		logger.Info("this will be aggregated into one write with the access log and will show up when the request is completed")

		// add some new fields to the existing logger
		logger = ginlogrus.SetCtxLogger(c, logger.WithFields(logrus.Fields{"comment": "this is an aggregated log entry with initial comment field"}))
		logger.Debug("aggregated entry with new comment field")

		// replace existing logger fields with new ones (notice it's logrus.WithFields())
		logger = ginlogrus.SetCtxLogger(c, logrus.WithFields(logrus.Fields{"new-comment": "this is an aggregated log entry with reset comment field"}))
		logger.Error("aggregated error entry with new-comment field")

		logrus.Info("this will NOT be aggregated and will be logged immediately")
		span := newSpanFromContext(c, "sleep-span")
		defer span.Finish() // this will get logged because tracing was setup with ginopentracing.WithEnableInfoLog(true)

		go func() {
			// need a NewBuffer for aggregate logging of this goroutine (since the req will be done long before this thing finishes)
			// it will inherit header info from the existing request
			buff := ginlogrus.NewBuffer(logger)
			time.Sleep(1 * time.Second)
			logger.Info("Hi from a goroutine completing after the request")
			fmt.Printf(buff.String())
		}()
		c.JSON(200, "Hello world!")
	})

	r.Run(":29090")
}

func newSpanFromContext(c *gin.Context, operationName string) opentracing.Span {
	parentSpan, _ := c.Get("tracing-context")
	options := []opentracing.StartSpanOption{
		opentracing.Tag{Key: ext.SpanKindRPCServer.Key, Value: ext.SpanKindRPCServer.Value},
		opentracing.Tag{Key: string(ext.HTTPMethod), Value: c.Request.Method},
		opentracing.Tag{Key: string(ext.HTTPUrl), Value: c.Request.URL.Path},
		opentracing.Tag{Key: "current-goroutines", Value: runtime.NumGoroutine()},
	}

	if parentSpan != nil {
		options = append(options, opentracing.ChildOf(parentSpan.(opentracing.Span).Context()))
	}

	return opentracing.StartSpan(operationName, options...)
}


See the example.go file

Reduced Logging Options

The Options.WithReducedLoggingFunc(c *gin.Context) allows users to specify a function for determining whether or not logs will be written. This function can be used with aggregate logging in situations where users want to maintain the details and fidelity of log messages but not necessarily log on every single request. The example below allows users to maintain aggregate logs at the DEBUG level but only write logs out on non-2xx response codes. Reduced Logging Function:

// This function will determine whether to write a log message or not.
// When the request is not a 2xx the function will return true indicating that a log message should be written.
func ProductionLogging(c *gin.Context) bool {
	statusCode := c.Writer.Status()
	if statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices {
		return true
	}
	return false
}
	r.Use(ginlogrus.WithTracing(logrus.StandardLogger(),
		useBanner,
		time.RFC3339,
		useUTC,
		"requestID",
		[]byte("uber-trace-id"), // where jaeger might have put the trace id
		[]byte("RequestID"),     // where the trace ID might already be populated in the headers
		ginlogrus.WithAggregateLogging(true),
		ginlogrus.WithReducedLoggingFunc(ProductionLogging)))

Documentation

Index

Constants

View Source
const DefaultBanner = "" /* 155-byte string literal not displayed */
View Source
const DefaultLogBufferMaxSize = 100000

DefaultLogBufferMaxSize - avg single spaced page contains 3k chars, so 100k == 33 pages which is a reasonable max

Variables

View Source
var ContextTraceIDField string

ContextTraceIDField - used to find the trace id in the gin.Context - optional

Functions

func CopyHeader

func CopyHeader(dst *LogBuffer, src *LogBuffer)

CopyHeader - copy a header

func CxtRequestID

func CxtRequestID(c *gin.Context) string

CxtRequestID - if not already set, then add logrus Field to the entry with the tracing ID for the request. then return the trace/request id

func GetCtxLogger

func GetCtxLogger(c *gin.Context) *logrus.Entry

GetCtxLogger - get the *logrus.Entry for this request from the gin.Context

func GetCxtRequestID

func GetCxtRequestID(c *gin.Context) string

GetCxtRequestID - dig the request ID out of the *logrus.Entry in the gin.Context

func SetCtxLogger

func SetCtxLogger(c *gin.Context, logger *logrus.Entry) *logrus.Entry

SetCtxLogger - used when you want to set the *logrus.Entry with new logrus.WithFields{} for this request in the gin.Context so it can be used going forward for the request

func SetCtxLoggerHeader

func SetCtxLoggerHeader(c *gin.Context, name string, data interface{})

SetCtxLoggerHeader - if aggregate logging, add header info... otherwise just info log the data passed

func WithTracing

func WithTracing(
	logger loggerEntryWithFields,
	useBanner bool,
	timeFormat string,
	utc bool,
	logrusFieldNameForTraceID string,
	traceIDHeader []byte,
	contextTraceIDField []byte,
	opt ...Option) gin.HandlerFunc

WithTracing returns a gin.HandlerFunc (middleware) that logs requests using logrus.

Requests with errors are logged using logrus.Error(). Requests without errors are logged using logrus.Info().

It receives:

  1. A logrus.Entry with fields
  2. A boolean stating whether to use a BANNER in the log entry
  3. A time package format string (e.g. time.RFC3339).
  4. A boolean stating whether to use UTC time zone or local.
  5. A string to use for Trace ID the Logrus log field.
  6. A []byte for the request header that contains the trace id
  7. A []byte for "getting" the requestID out of the gin.Context
  8. A list of possible ginlogrus.Options to apply

Types

type LogBuffer

type LogBuffer struct {
	Buff strings.Builder

	AddBanner bool

	MaxSize uint
	// contains filtered or unexported fields
}

LogBuffer - implement io.Writer inferface to append to a string

func NewBuffer

func NewBuffer(l *logrus.Entry) *LogBuffer

NewBuffer - create a new aggregate logging buffer for the *logrus.Entry , which can be flushed by the consumer how-to, when to use this:

		the request level log entry is written when the request is over, so you need this thing to
		write go routine logs that complete AFTER the request is completed.
     careful: the loggers will share a ref to the same Header (writes to one will affect the other)

example:

go func() {
		buff := NewBuffer(logger) // logger is an existing *logrus.Entry
		// do somem work here and write some logs via the logger.  Like logger.Info("hi mom! I'm a go routine that finished after the request")
		fmt.Printf(buff.String()) // this will write the aggregated buffered logs to stdout
}()

func NewLogBuffer

func NewLogBuffer(opt ...LogBufferOption) LogBuffer

NewLogBuffer - create a LogBuffer and initialize it

func (*LogBuffer) DeleteHeader

func (b *LogBuffer) DeleteHeader(k string)

DeleteHeader - delete a header

func (*LogBuffer) GetAllHeaders

func (b *LogBuffer) GetAllHeaders() (map[string]interface{}, error)

GetAllHeaders - return all the headers

func (*LogBuffer) GetHeader

func (b *LogBuffer) GetHeader(k string) (interface{}, bool)

GetHeader - get a header

func (*LogBuffer) Length added in v2.0.12

func (b *LogBuffer) Length() int

Length - return the length of the aggregate log buffer

func (*LogBuffer) SetCustomBanner

func (b *LogBuffer) SetCustomBanner(banner string)

SetCustomBanner allows a custom banner to be set after the NewLogBuffer() has been used

func (*LogBuffer) StoreHeader

func (b *LogBuffer) StoreHeader(k string, v interface{})

StoreHeader - store a header

func (*LogBuffer) String

func (b *LogBuffer) String() string

String - output the strings.Builder as one aggregated JSON object

func (*LogBuffer) Write

func (b *LogBuffer) Write(data []byte) (n int, err error)

Write - simply append to the strings.Buffer but add a comma too

type LogBufferOption

type LogBufferOption func(*logBufferOptions)

LogBufferOption - define options for LogBuffer

func WithBanner

func WithBanner(a bool) LogBufferOption

WithBanner - define an Option func for passing in an optional add Banner

func WithCustomBanner

func WithCustomBanner(b string) LogBufferOption

WithCustomBanner allows users to define their own custom banner

func WithHeader

func WithHeader(k string, v interface{}) LogBufferOption

WithHeader - define an Option func for passing in a set of optional header

func WithMaxSize

func WithMaxSize(s uint) LogBufferOption

WithMaxSize specifies the bounded max size the logBuffer can grow to

type Option

type Option func(*options)

Option - define options for WithTracing()

func WithAggregateLogging

func WithAggregateLogging(a bool) Option

WithAggregateLogging - define an Option func for passing in an optional aggregateLogging

func WithEmptyAggregateEntries added in v2.0.12

func WithEmptyAggregateEntries(a bool) Option

WithEmptyAggregateEntries - define an Option func for printing aggregate logs with empty entries

func WithLogCustomBanner

func WithLogCustomBanner(b string) Option

WithLogCustomBanner allows users to define their own custom banner. There is some overlap with this name and the LogBufferOption.CustomBanner and yes, they are related options, but I didn't want to make a breaking API change to support this new option... so we'll have to live with a bit of confusion/overlap in option names

func WithLogLevel

func WithLogLevel(logLevel logrus.Level) Option

WithLogLevel - define an Option func for passing in an optional logLevel

func WithReducedLoggingFunc added in v2.0.12

func WithReducedLoggingFunc(a ReducedLoggingFunc) Option

WithReducedLoggingFunc - define an Option func for reducing logs based on a custom function

func WithWriter

func WithWriter(w io.Writer) Option

WithWriter allows users to define the writer used for middlware aggregagte logging, the default writer is os.Stdout

type ReducedLoggingFunc added in v2.0.12

type ReducedLoggingFunc func(c *gin.Context) bool

Function definition for reduced logging. The return value of this function will be used to determine whether or not a log will be output.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL