eal

package module
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: May 3, 2024 License: MIT Imports: 12 Imported by: 0

README

eal

Extended Access Logging

Simplifies access and error logging of Labstack/echo HTTP servers

Setup echo access/error logging

To get started with access and error logging, it's enough to call eal.Init, eal.InitDefaultErrorLogging (both are optional), and then add the middleware returned by eal.CreateLoggerMiddleware() to the echo server.

package main

import (
  "github.com/labstack/echo/v4"
  "github.com/modfin/eal"
)

func main() {
  // Initialize logrus JSON logger.
  eal.Init(false)

  // Initialize eal default error logging for echo.HTTPError and jwt.ValidationError error types.
  eal.InitDefaultErrorLogging()

  // Create echo instance and set up the access logging middleware.
  e := echo.New()
  e.Use(eal.CreateLoggerMiddleware())

  // Setup endpoints and start echo server the usual way...
  // ...

Add information to access/error log entry

To extend the log entry that is going to be written when the endpoint is about to return, one can use the AddContextFields method.

  e.POST("/user", func(c echo.Context) error {
    userID := c.FormValue("user-id")

    // Add "user-id" field to context, that will be included in the log entry generated by the middleware when
    // handler have returned.
    eal.AddContextFields(c, Fields{"user-id": userID})

    // ...
  })

Add stacktrace information to logged errors

To generate a stacktrace, the Trace method can be used. Trace takes an error and wrap it in a new error that contain a stacktrace. It is possible to configure what errors and error types that shouldn't generate a stacktrace (see InhibitStacktraceForError for more information). If the error provided to Trace already is, or contain, a wrapped stacktrace-error, the original error will be returned unmodified.

There is a global parameter that can be set that affect when the stacktrace is first logged: LogCallStackDirectly. If it's true, Trace will write a new log entry directly after the new stacktrace error have been created. This can be useful if there is a chance that the error returned by Trace isn't wrapped and returned to the middleware logger.

  if err != nil {
    // Wrap the original error in a stacktrace, before wrapping it in a new error with more information (GO 1.13 and later)
    return fmt.Errorf("encode: %v: %w", data, eal.Trace(err))
  }

Add more error information to the log event

Some error types may have more information than what's shown in the Error() string, or if it's desirable to have some error information logged as a separate field in the log. The RegisterErrorLogFunc method can be used to extend the log entry with specific error information.

See InitDefaultErrorLogging() for an example of how to use RegisterErrorLogFunc.

Send Error information to caller

Normally echo will send back a HTTP status 500 when an error is returned from the echo handlerFunc, unless the error is a echo.HTTPError. When the eal.CreateLoggerMiddleware is used, it will look for the earliest echo.HTTPError if can find in the returned error, and return that to echo, if the returned error don't contain a wrapped echoHTTPError, the error will be passed on to echo unmodified.

var errNope error = echo.NewHTTPError(http.StatusNotFound, "Nope") // Returns 404 {"message":"Nope"}, to caller

...

  e.GET("/droids", func(c echo.Context) error {
    return errNope
  })

or if the error information that we want to send back is caused by an error, eal implement a NewHTTPError method that wrap an error in a echo.HTTPError

func errNope(err error) error {
  // Wrap the error in a stacktrace, and then wrap it in a echo.HTTPError
  return eal.NewHTTPError(eal.Trace(err), http.StatusNotFound, "Nope") // Return 404 {"message":"Nope"}, to caller
}

...

  e.GET("/droids", func(c echo.Context) error {
    d, err := getDroids()
    if err != nil {
      return errNope(err)
    }
    return c.JSON(http.StatusOK, d)
  })

it's also possible to send back a custom JSON message to the caller by using a struct as a parameter in the echo.HTTPError

type ErrorMessage struct {
  ErrorCode    int    `json:"error_code"`
  ErrorMessage string `json:"error_message"`
}

var ErrSomeMessage error = echo.NewHTTPError(http.StatusNotFound, &ErrorMessage{ErrorCode: 42, ErrorMessage: "common.error.some_message"})

Documentation

Overview

Package eal (Extended Access Logging) is used to simplify access and error logging of GO endpoints. It can also be used to help create a structured way of handling error codes to be sent to frontend.

A small example of how this package can be used:

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/modfin/eal"
)

type FrontendMessage struct {
	ErrorCode    int    `json:"error_code"`
	ErrorMessage string `json:"error_message"`
}

// Show two different echo.HTTPError examples, the first where the message parameter is set to a string.
// That will send JSON: {"message":"Nope"} to caller, and the second where te message parameter is set to an object
// (ErrSomeMessage), that will send back JSON: {"error_code:42,"error_message":"common.error.some_message"} to the caller.
var (
	ErrNope         error = echo.NewHTTPError(http.StatusForbidden, "Nope")          // Return 403 {"message":"Nope"}, to caller
	ErrUserDisabled error = echo.NewHTTPError(http.StatusForbidden, "User disabled") // Return 403 {"message":"User disabled"}, to caller
	ErrSomeMessage  error = echo.NewHTTPError(http.StatusNotFound, &FrontendMessage{ErrorCode: 42, ErrorMessage: "common.error.some_message"})
)

func ErrUser(err error) error {
	return eal.NewHTTPError(eal.Trace(err), http.StatusInternalServerError, "User error") // Return 500 User error, to caller
}

func getUser(c echo.Context) (User, error) {
	usr, err := dao.GetUser()
	if err != nil {
		// Failed to get user, send back generic user error message to caller
		return nil, ErrUser(err)
	}
	if usr.Disabled {
		// User is disabled, send back user disabled message to caller
		return ErrUserDisabled
	}
	return usr, nil
}

func main() {
	// Initialize logrus JSON logger.
	eal.Init(false)

	// Initialize eal default error logging for echo.HTTPError and jwt.ValidationError error types.
	eal.InitDefaultErrorLogging()

	// Create echo instance and set up the access logging middleware.
	e := echo.New()
	e.Use(eal.CreateLoggerMiddleware())

	e.GET("/ping", func(c echo.Context) error {
		usr, err := getUser(c)
		if err != nil {
			// When several echo.HTTPError exist in the error-chain, only the first/earliest will be sent to the caller.
			return ErrUser(err)
		}
		return c.String(200, "pong")
	})

	e.GET("/nope", func(c echo.Context) error {
		return ErrNope
	})
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultContextLogFunc = func(c echo.Context, fields Fields) {
	req := c.Request()
	res := c.Response()

	host := req.Header.Get("X-Host")
	if host == "" {
		alt := req.Header.Get("X-Forwarded-Host")
		if alt != "" {
			host = strings.Split(alt, ":")[0]
			req.Header.Set("X-Host", host)
		}
	}

	id := req.Header.Get("X-Request-Id")
	if id == "" {
		id = uuid.New().String()
		req.Header.Set("X-Request-Id", id)
		res.Header().Set("X-Request-Id", id)
	}

	// Attempt to get remote address of the client
	var remoteAddr string
	for _, h := range []string{"X-Forwarded-For", "X-Real-Ip", "X-Remote-Addr"} {
		remoteAddr = req.Header.Get(h)
		if remoteAddr != "" {
			break
		}
	}
	if remoteAddr == "" {
		remoteAddr = req.RemoteAddr
	}

	fields["request_id"] = id
	fields["remote_addr"] = remoteAddr
	fields["host"] = host
	fields["method"] = req.Method
	fields["uri"] = req.RequestURI
	fields["router_path"] = c.Path()
}
View Source
var LogCallStackDirectly bool

LogCallStackDirectly control if an error message should be logged immediately with the callstack when the Trace method is called. If there is a chance that the error that is returned by the Trace method is thrown away before it's logged, LogCallStackDirectly can be set to true to log the callstack immediately.

Functions

func AddContextFields

func AddContextFields(c echo.Context, fields Fields)

AddContextFields add the fields to the log context, fields added to the context is included in logging done by the CreateLoggerMiddleware. The fields added by this method can also be logged elsewhere by using Entry.WithCtx method.

Example
e := echo.New()

// Initialize the logging middleware
e.Use(CreateLoggerMiddleware())

e.GET("/ping", func(c echo.Context) error {
	userID := c.FormValue("user-id")

	// Add "user-id" field to context, that will be included in the log entry generated by the middleware when
	// handler have returned.
	AddContextFields(c, Fields{"user-id": userID})

	return c.String(200, "")
})

func CreateLoggerMiddleware

func CreateLoggerMiddleware(logFunctions ...ContextLogFunc) echo.MiddlewareFunc

CreateLoggerMiddleware return an echo middleware method that handle access and error logging of the call.

If an error is returned from the handlerFunc, the middleware will look at the complete error-chain to find the earliest echo.HTTPError, and return the status code and message from that to the frontend. If the error-chain don't contain an echo.HTTPError, a new echo.HTTPError will be created that wrap the returned error.

Example
e := echo.New()
e.Use(CreateLoggerMiddleware())

func GetInnerHTTPError

func GetInnerHTTPError(err error) *echo.HTTPError

GetInnerHTTPError check if the provided error is, or have a wrapped echo.HTTPError, and if there is one, it's returned. If the error chain contains more than one, the inner/earliest is returned.

func InhibitStacktraceForError

func InhibitStacktraceForError(err ...error)

InhibitStacktraceForError will add the error types/instances to a map that is checked when Trace is called. If Trace is called with an error type/instance that exist in the map, a callstack won't be generated and Trace will return the error unmodified.

Example (ErrorReference)
// Don't generate a stacktrace when Trace is called with a sql.ErrNoRows error.
InhibitStacktraceForError(sql.ErrNoRows)

func Init

func Init(devMode bool)

Init initialize the logrus logger. If devMode is true, a text based logger will be used, otherwise a JSON logger is used to output the log information to STDOUT.

func InitDefaultErrorLogging

func InitDefaultErrorLogging()

InitDefaultErrorLogging register a error logger that append more information to the log for echo.HTTPError.

func NewHTTPError

func NewHTTPError(err error, code int, msg ...interface{}) error

NewHTTPError complements echo.NewHTTPError, this also takes an error as a parameter.

func RegisterErrorLogFunc

func RegisterErrorLogFunc(errFmtFunc ErrLogFunc, errList ...error)

RegisterErrorLogFunc registers a function that is called when a specific error interface is seen by UnwrapError. If you have your own error types (structs) that you want to log, it is easier to implement a SetLogFields method to handle logging. RegisterErrorLogFunc should be used for other error types that you don't have any control over, that contains information that isn't exposed via the Error() method or if you want to use structured logging for data in the error type, for example:

eal.RegisterErrorLogFunc(func(err error, fields eal.Fields) {
  oe, ok := err.(*net.OpError)
  if !ok {
    return
  }
  fields["net_oper"] = oe.Op
  fields["net_addr"] = oe.Addr.String()
  fields["temporary"] = oe.Temporary()
  fields["timeout"] = oe.Timeout()
}, (*net.OpError)(nil))
Example (Multiple)
errFmt := func(err error, fields Fields) {
	var i interface{} = err
	switch e := i.(type) {
	case *net.OpError:
		fields["net_oper"] = e.Op
		fields["net_addr"] = e.Addr.String()
		fields["temporary"] = e.Temporary()
		fields["timeout"] = e.Timeout()
	case *net.ParseError:
		fields["net_type"] = e.Type
		fields["net_text"] = e.Text
	}
}
RegisterErrorLogFunc(errFmt, (*net.OpError)(nil), (*net.ParseError)(nil))
Example (Single)
RegisterErrorLogFunc(func(err error, fields Fields) {
	oe, ok := err.(*net.OpError)
	if !ok {
		return
	}
	fields["net_oper"] = oe.Op
	fields["net_addr"] = oe.Addr.String()
	fields["temporary"] = oe.Temporary()
	fields["timeout"] = oe.Timeout()
}, (*net.OpError)(nil))

func Trace

func Trace(err error) error

Trace can wrap the provided error in a ErrorStackTrace type that contain the callstack. If the provided error type/instance have been added to the inhibit-map by calling InhibitStacktraceForError, the error will be returned as-is and won't be wrapped in a ErrorStackTrace type. If the provided error already is, or contain a wrapped ErrorStackTrace error, the error is also returned as-is.

func UnwrapError

func UnwrapError(err error, fields map[string]interface{})

UnwrapError walks the error-chain and add information to the provided log-fields. For each error in the error-chain, it will check if the error either implements the SetLogFields(map[string]interface{}) interface or if the type have a registered log function that is used to populate the log-fields. This is used by Entry.WithError to add error information to a log event.

Types

type ContextLogFunc

type ContextLogFunc func(c echo.Context, fields Fields)

ContextLogFunc can be implemented to be able to add log fields from an echo context.

type CustomTextFormatter

type CustomTextFormatter struct{}

func (*CustomTextFormatter) Format

func (f *CustomTextFormatter) Format(entry *logrus.Entry) ([]byte, error)

type Entry

type Entry struct {
	logrus.Entry
}

Entry extend the logrus.Entry type, with additional convenience methods: WithCtx, WithError and WithFields, to simplify logging.

func NewEntry

func NewEntry() *Entry

NewEntry return an Entry instance to be used for creating a log entry. For example:

eal.NewEntry().Info("App started")

func (*Entry) WithCtx

func (e *Entry) WithCtx(c echo.Context) *Entry

WithCtx add fields from the context, to the log entry.

func (*Entry) WithError

func (e *Entry) WithError(err error) *Entry

WithError uses UnwrapError internally to extract more information from the error and add it to the log entry fields.

See UnwrapError and RegisterErrorLogFunc methods for more information about how to extend the log entry fields.

func (*Entry) WithFields

func (e *Entry) WithFields(f map[string]interface{}) *Entry

WithFields adds custom fields (key/value) to the log entry. For example:

eal.NewEntry().WithFields(eal.Fields{"time": time.Since(start)}).Info("Work completed")

type ErrLogFunc

type ErrLogFunc func(err error, fields Fields)

ErrLogFunc type can be implemented to be able to add log fields for a specific error.

See RegisterErrorLogFunc and UnwrapError regarding the SetLogFields interface for more information.

type ErrorStackTrace

type ErrorStackTrace struct {
	// contains filtered or unexported fields
}

ErrorStackTrace is created by the Trace function and hold a stacktrace to where Trace where first called. The error message returned by Error isn't changed from the original error message. To retrieve the recorded callstack, the Stack function can be used, the callstack is also logged so the only way to retrieve the callstack, is to either walk the chain of errors

func GetErrorStackTrace

func GetErrorStackTrace(err error) (st *ErrorStackTrace, ok bool)

GetErrorStackTrace check if the provided error is, or have a wrapped ErrorStackTrace, and if there is one, it's returned.

func (*ErrorStackTrace) Error

func (st *ErrorStackTrace) Error() string

Error return the wrapped errors message, the ErrorStackTrace type don't add the stacktrace information to the error string. The stacktrace can be accessed by calling Stack, or through SetLogFields.

func (*ErrorStackTrace) SetLogFields

func (st *ErrorStackTrace) SetLogFields(logFields map[string]interface{})

SetLogFields is used by Entry.WithError to populate log fields.

func (*ErrorStackTrace) Stack

func (st *ErrorStackTrace) Stack() string

Stack return the stacktrace to where the ErrorStackTrace first were inserted in the error chain.

func (*ErrorStackTrace) TypeName

func (st *ErrorStackTrace) TypeName() string

TypeName return the name of the wrapped error struct.

func (*ErrorStackTrace) Unwrap

func (st *ErrorStackTrace) Unwrap() error

Unwrap return the wrapped error.

type Fields

type Fields map[string]interface{}

Fields hold the map of key/value log fields that should be logged.

Jump to

Keyboard shortcuts

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