app

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 27, 2024 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package app provides some tools useful for typical Go project.

Error

Custom Error type and set of standard errors:

  • ErrUnknown
  • ErrTimeout
  • ErrUnavailable
  • ErrSystem
  • ErrNotExists
  • ErrValidation
  • ErrInvalidArgument
  • ErrIncorrectOperation
  • ErrInsufficientPrivileges

Use Err function to create Error instance with custom code, message and optional reason.

Note, that Error instances matched by code:

errors.Is(app.Err("CODE", "msg1"), app.Err("CODE", "msg2"))  // true
errors.Is(app.Err(app.ErrCodeValidation, "msg1"), app.ErrValidation)  // true
errors.Is(app.Err(app.ErrCodeValidation, "msg1"), app.ErrUnavailable)  // false

Logger

Logger - use NewLogger to set up.

You can add some key-value pairs to operation context using WithLogAttrs and next, when you call any of Logger.DebugContext/Logger.InfoContext/Logger.WarnContext/Logger.ErrorContext methods, logger will add those key-value pairs into structured log record.

Logger provides methods to make error logging easier: Logger.Err and Logger.ErrContext.

Logger allows to change it level dynamically using Logger.SetLevel.

Index

Examples

Constants

View Source
const (
	ErrCodeTimeout     = "ERR_TIMEOUT"
	ErrCodeUnavailable = "ERR_UNAVAILABLE"
	ErrCodeSystem      = "ERR_SYSTEM"
)

The following errors represents temporal problems on the server side, we expect it will be resolved soon... so caller can repeat request a bit later and get success response

  • ErrCodeTimeout is given when the app cannot perform action (or get response from external system) in time
  • ErrCodeUnavailable means the app is currently unable to perform action (or external request), but we expect that it will be resolved soon
  • ErrCodeSystem - unclassified error on server side
View Source
const (
	ErrCodeNotExists              = "ERR_NOT_FOUND"
	ErrCodeValidation             = "ERR_VALIDATION"
	ErrCodeInvalidArgument        = "ERR_INVALID_ARGUMENT"
	ErrCodeIncorrectOperation     = "ERR_INCORRECT_OPERATION"
	ErrCodeInsufficientPrivileges = "ERR_INSUFFICIENT_PRIVILEGES"
)

The following errors represents problems on client side, so caller must change request to get success response

  • ErrCodeNotExists
  • ErrCodeValidation
  • ErrCodeInvalidArgument
  • ErrCodeIncorrectOperation
  • ErrCodeInsufficientPrivileges
View Source
const (
	CtxKeyLogAttrs = ctxKey(iota + 1)
	CtxKeyXRequestId
)
View Source
const (
	LogKeyRequestId   = "request_id"
	LogKeyComponent   = "component"
	LogKeyError       = "error"
	LogKeyErrorReason = "reason"
)
View Source
const (
	// ErrCodeUnknown is the worst case: app has not any suggestion, how to handle it.
	// Caller should decide itself, repeat request ot not
	ErrCodeUnknown = "ERR_UNKNOWN"
)

Variables

View Source
var (
	ErrUnknown                = Error{/* contains filtered or unexported fields */}
	ErrTimeout                = Error{/* contains filtered or unexported fields */}
	ErrUnavailable            = Error{/* contains filtered or unexported fields */}
	ErrSystem                 = Error{/* contains filtered or unexported fields */}
	ErrNotExists              = Error{/* contains filtered or unexported fields */}
	ErrValidation             = Error{/* contains filtered or unexported fields */}
	ErrInvalidArgument        = Error{/* contains filtered or unexported fields */}
	ErrIncorrectOperation     = Error{/* contains filtered or unexported fields */}
	ErrInsufficientPrivileges = Error{/* contains filtered or unexported fields */}
)

Sentinel errors to compare using errors.Is/As

Functions

func RequestID

func RequestID(ctx context.Context) string

func WithAddSource

func WithAddSource(addSource bool) func(logger *Logger)

WithAddSource enables/disables logging of source code position

func WithLogAttrs

func WithLogAttrs(ctx context.Context, attrs Attrs) context.Context
Example
package main

import (
	"context"
	"fmt"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	ctxFirst := app.WithLogAttrs(context.Background(), app.Attrs{"k0": 0})

	ctx := ctxFirst
	ctx = app.WithLogAttrs(ctx, app.Attrs{"k1": 1})
	ctx = app.WithLogAttrs(ctx, app.Attrs{"k2": 2})
	ctx = app.WithLogAttrs(ctx, app.Attrs{"k3": 3})

	ctxFinal := ctx

	fmt.Printf("first ctx: %#v\n", app.LogAttrs(ctxFirst))
	fmt.Printf("last ctx: %#v\n", app.LogAttrs(ctxFinal))
}
Output:

first ctx: app.Attrs{"k0":0}
last ctx: app.Attrs{"k0":0, "k1":1, "k2":2, "k3":3}

func WithLogLevel

func WithLogLevel(level slog.Level) func(*Logger)

WithLogLevel sets the log level

func WithRequestID

func WithRequestID(ctx context.Context, reqID string) context.Context

Types

type Attrs

type Attrs map[string]any

func LogAttrs

func LogAttrs(ctx context.Context) Attrs
Example
package main

import (
	"context"
	"fmt"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	ctx := app.WithLogAttrs(context.Background(), app.Attrs{"k1": 1})

	attrs := app.LogAttrs(ctx)

	fmt.Printf("%#v\n", attrs)
	// Output
	// app.Attrs{"k1":1}
}
Output:

type Error

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

Error is an error with additional data

func Err

func Err(code, msg string, reason ...error) Error

Err creates new instance of Error.

Params:

  • code - 'business' error code, see ErrCodeUnknown or other ErrCode* constants
  • msg - any error description
  • reason - wraps 'root cause' errors
Example
package main

import (
	"errors"
	"fmt"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	reason1 := errors.New("foo")
	reason2 := errors.New("bar")
	appErr := app.Err(app.ErrCodeValidation, "baz", reason1, reason2)

	fmt.Printf("appErr code: '%s'\n", appErr.Code())
	fmt.Printf("appErr is reason 1: %v\n", errors.Is(appErr, reason1))
	fmt.Printf("appErr is reason 2: %v\n", errors.Is(appErr, reason2))
}
Output:

appErr code: 'ERR_VALIDATION'
appErr is reason 1: true
appErr is reason 2: true

func (Error) Code

func (e Error) Code() string

Code returns error-code, see ErrCode* constants

Example
package main

import (
	"fmt"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	baseErr := app.Error{}
	appErr := app.Err(app.ErrCodeValidation, "foo")

	fmt.Printf("baseErr code: '%s'\n", baseErr.Code())
	fmt.Printf("appErr code: '%s'\n", appErr.Code())
}
Output:

baseErr code: ''
appErr code: 'ERR_VALIDATION'

func (Error) Error

func (e Error) Error() string

Error implements std error interface

func (Error) Is

func (e Error) Is(target error) bool

Is checks if target's code matches current error's code

func (Error) Message

func (e Error) Message() string

Message returns Error's message

Example
package main

import (
	"fmt"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	baseErr := app.Error{}
	appErr := app.Err(app.ErrCodeValidation, "bar")

	fmt.Printf("baseErr message: '%s'\n", baseErr.Message())
	fmt.Printf("appErr message: '%s'\n", appErr.Message())
}
Output:

baseErr message: ''
appErr message: 'bar'

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap returns the reason of current error

type LogHandlerMiddleware

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

LogHandlerMiddleware is a slog.Handler wrapper that enriches log record with context data

func NewLogHandlerMiddleware

func NewLogHandlerMiddleware(next slog.Handler) *LogHandlerMiddleware

func (LogHandlerMiddleware) Enabled

func (l LogHandlerMiddleware) Enabled(ctx context.Context, level slog.Level) bool

func (LogHandlerMiddleware) Handle

func (l LogHandlerMiddleware) Handle(ctx context.Context, record slog.Record) error

func (LogHandlerMiddleware) WithAttrs

func (l LogHandlerMiddleware) WithAttrs(attrs []slog.Attr) slog.Handler

func (LogHandlerMiddleware) WithGroup

func (l LogHandlerMiddleware) WithGroup(name string) slog.Handler

type Logger

type Logger struct {
	*slog.Logger
	// contains filtered or unexported fields
}

Logger is a slog.Logger wrapper with some shortcut methods

func NewLogger

func NewLogger(w io.Writer, options ...LoggerOption) *Logger

NewLogger returns ready to use instance of Logger.

If w is nil it creates 'noop' logger useful for tests

There are options:

  • WithLogLevel(slog.LogLevel) - sets level, slog.LevelError is default
  • WithAddSource - enables/disables logging of source code position
Example (Noop)
package main

import (
	"context"
	"log/slog"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	ctx := context.Background()
	log := app.NewLogger(nil, app.WithLogLevel(slog.LevelDebug))

	log.DebugContext(ctx, "hello", "key", "value")
	log.InfoContext(ctx, "hello", "key", "value")
	log.WarnContext(ctx, "hello", "key", "value")
	log.ErrorContext(ctx, "hello", "key", "value")

}
Output:

Example (Stdout)
package main

import (
	"context"
	"log/slog"
	"os"
	"regexp"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	ctx := context.Background()
	ctx = app.WithRequestID(ctx, "ZZZ")
	ctx = app.WithLogAttrs(ctx, app.Attrs{"XXX": "YYY"})

	log := app.NewLogger(stdout{}, app.WithLogLevel(slog.LevelDebug))

	log.DebugContext(ctx, "hello", "key", "value")
	log.InfoContext(ctx, "hello", "key", "value")
	log.WarnContext(ctx, "hello", "key", "value")
	log.ErrorContext(ctx, "hello", "key", "value")

}

var hideTimeRe = regexp.MustCompile(`"time":"[TZ0-9:+.-]+"`)

type stdout struct {
}

func (s stdout) Write(b []byte) (int, error) {
	b = hideTimeRe.ReplaceAll(b, []byte(`"time":"2006-01-02T15:05:06.000000000+07:00"`))
	return os.Stdout.Write(b)
}
Output:

{"time":"2006-01-02T15:05:06.000000000+07:00","level":"DEBUG","msg":"hello","key":"value","request_id":"ZZZ","XXX":"YYY"}
{"time":"2006-01-02T15:05:06.000000000+07:00","level":"INFO","msg":"hello","key":"value","request_id":"ZZZ","XXX":"YYY"}
{"time":"2006-01-02T15:05:06.000000000+07:00","level":"WARN","msg":"hello","key":"value","request_id":"ZZZ","XXX":"YYY"}
{"time":"2006-01-02T15:05:06.000000000+07:00","level":"ERROR","msg":"hello","key":"value","request_id":"ZZZ","XXX":"YYY"}

func (*Logger) Err

func (l *Logger) Err(err error, msg string, args ...any)

Err writes ERROR message

Example
package main

import (
	"errors"
	"log/slog"
	"os"
	"regexp"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	log := app.NewLogger(stdout{}, app.WithLogLevel(slog.LevelDebug))
	reason := errors.New("foo")
	err := app.Err(app.ErrCodeValidation, "baz", reason)

	log.Err(err, "hello", "key", "value")

}

var hideTimeRe = regexp.MustCompile(`"time":"[TZ0-9:+.-]+"`)

type stdout struct {
}

func (s stdout) Write(b []byte) (int, error) {
	b = hideTimeRe.ReplaceAll(b, []byte(`"time":"2006-01-02T15:05:06.000000000+07:00"`))
	return os.Stdout.Write(b)
}
Output:

{"time":"2006-01-02T15:05:06.000000000+07:00","level":"ERROR","msg":"hello","key":"value","error":"ERR_VALIDATION: baz","reason":"foo"}

func (*Logger) ErrContext

func (l *Logger) ErrContext(ctx context.Context, err error, msg string, args ...any)

ErrContext writes ERROR message with context

Example
package main

import (
	"context"
	"errors"
	"log/slog"
	"os"
	"regexp"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	log := app.NewLogger(stdout{}, app.WithLogLevel(slog.LevelDebug))
	ctx := app.WithRequestID(context.Background(), "ZZZ")
	reason := errors.New("foo")
	err := app.Err(app.ErrCodeValidation, "baz", reason)

	log.ErrContext(ctx, err, "hello", "key", "value")

}

var hideTimeRe = regexp.MustCompile(`"time":"[TZ0-9:+.-]+"`)

type stdout struct {
}

func (s stdout) Write(b []byte) (int, error) {
	b = hideTimeRe.ReplaceAll(b, []byte(`"time":"2006-01-02T15:05:06.000000000+07:00"`))
	return os.Stdout.Write(b)
}
Output:

{"time":"2006-01-02T15:05:06.000000000+07:00","level":"ERROR","msg":"hello","key":"value","error":"ERR_VALIDATION: baz","reason":"foo","request_id":"ZZZ"}

func (*Logger) LogLogger

func (l *Logger) LogLogger() *log.Logger

LogLogger returns std log.Logger that acts as bridge to structured handler.

For compatibility with libs that uses the older log API (http.Server.ErrorLog for example)

Example
package main

import (
	"log/slog"
	"os"
	"regexp"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	log := app.NewLogger(stdout{}, app.WithLogLevel(slog.LevelInfo))
	logLogger := log.LogLogger()

	logLogger.Printf("some key: %s", "val")
}

var hideTimeRe = regexp.MustCompile(`"time":"[TZ0-9:+.-]+"`)

type stdout struct {
}

func (s stdout) Write(b []byte) (int, error) {
	b = hideTimeRe.ReplaceAll(b, []byte(`"time":"2006-01-02T15:05:06.000000000+07:00"`))
	return os.Stdout.Write(b)
}
Output:

{"time":"2006-01-02T15:05:06.000000000+07:00","level":"INFO","msg":"some key: val"}

func (*Logger) SetLevel

func (l *Logger) SetLevel(level slog.Level)

SetLevel updates current logger level

Example
package main

import (
	"context"
	"log/slog"
	"os"
	"regexp"

	"github.com/ncotds/nco-qoordinator/pkg/app"
)

func main() {
	ctx := context.Background()

	log := app.NewLogger(stdout{}, app.WithLogLevel(slog.LevelInfo))
	// debug record will be skipped
	log.DebugContext(ctx, "hello first", "key", "value")

	log.SetLevel(slog.LevelDebug)
	// debug record will now be printed
	log.DebugContext(ctx, "hello second", "key", "value")

}

var hideTimeRe = regexp.MustCompile(`"time":"[TZ0-9:+.-]+"`)

type stdout struct {
}

func (s stdout) Write(b []byte) (int, error) {
	b = hideTimeRe.ReplaceAll(b, []byte(`"time":"2006-01-02T15:05:06.000000000+07:00"`))
	return os.Stdout.Write(b)
}
Output:

{"time":"2006-01-02T15:05:06.000000000+07:00","level":"DEBUG","msg":"hello second","key":"value"}

func (*Logger) With

func (l *Logger) With(args ...any) *Logger

With returns a Logger that includes the given attributes in each output operation. Arguments are converted to attributes as if by Logger.Log

func (*Logger) WithComponent

func (l *Logger) WithComponent(component string) *Logger

WithComponent returns a Logger that appends 'component' attribute. If component is empty, WithGroup returns the receiver.

func (*Logger) WithGroup

func (l *Logger) WithGroup(name string) *Logger

WithGroup returns a Logger that starts a group, if name is non-empty. The keys of all attributes added to the Logger will be qualified by the given name. (How that qualification happens depends on the Handler.WithGroup method of the Logger's Handler.) If name is empty, WithGroup returns the receiver.

type LoggerOption

type LoggerOption func(*Logger)

type NoopLogger

type NoopLogger struct {
}

NoopLogger is a slog.Handler implementation that do nothing, like >/dev/null

Useful for tests

func (NoopLogger) Enabled

func (n NoopLogger) Enabled(_ context.Context, _ slog.Level) bool

func (NoopLogger) Handle

func (n NoopLogger) Handle(_ context.Context, _ slog.Record) error

func (NoopLogger) WithAttrs

func (n NoopLogger) WithAttrs(_ []slog.Attr) slog.Handler

func (NoopLogger) WithGroup

func (n NoopLogger) WithGroup(_ string) slog.Handler

Directories

Path Synopsis
middleware
Package middleware contains common used HTTP-server middlewares:
Package middleware contains common used HTTP-server middlewares:

Jump to

Keyboard shortcuts

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