slogmw

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Apr 3, 2023 License: MIT Imports: 7 Imported by: 0

README

Slogmw (Slog Multi-Writer)

This library provides a way for you to intercept the Golang Slog library output and write to multiple destinations. You are also able to change formats and modify the log line differently per destination. For example, you can write JSON to a log collector and LOGFMT to a file. In the future, we may add additional output types and feel free to write your own and submit a PR.

How to use it

go get gitlab.com/djarbz/golang/slogmw

Setup Slog as you normally would with only 2 requirements.

  1. Use the Slog JSON handler slog.NewJSONHandler().
  2. Use an io.Multiwriter and pass our writers to it as the output. If you don't use our constructors, you will need to manually set the writer.

Documentation

Overview

Example
package main

import (
	"fmt"
	"io"
	"log"
	"os"
	"path/filepath"
	"runtime"

	"golang.org/x/exp/slog"
	"gopkg.in/natefinch/lumberjack.v2"
	"slogmw"
)

type config struct {
	ConsoleDisable bool
	MinimumLevel   slog.Level
	ConsoleFormat  string
	AddSource      bool
	AddFullSource  bool
	FileEnable     bool
	FilePath       string
	FileFormat     string
	RotateEnable   bool
	RotateSize     int
	RotateKeep     int
	RotateAge      int
}

const (
	FormatText = "TEXT"
	FormatJSON = "JSON"
)

func main() {
	appConfig := config{
		ConsoleDisable: false,
		MinimumLevel:   slog.LevelInfo,
		ConsoleFormat:  FormatJSON,
		AddSource:      true,
		AddFullSource:  false,
		FileEnable:     true,
		FilePath:       "./slog.log",
		FileFormat:     FormatText,
		RotateEnable:   false,
		RotateSize:     0,
		RotateKeep:     0,
		RotateAge:      0,
	}

	logger, err := newMultiSlogger(appConfig, "SlogTest")
	if err != nil {
		panic(fmt.Errorf("setup logging: %w", err))
	}

	logFunc(logger, "Logging initialized...")
	logFunc(logger.With(slog.Int("With", lineNumber())), "1st With Call")

	for i := 0; i <= 10; i++ {
		logFunc(logger.With(slog.Int("Iteration", i)), "Looping")
	}

	logFunc(logger.WithGroup("Grouping"), "Group", slog.String("SubKey", "SubValue"))

}

func lineNumber() int {
	// notice that we're using 1, so it will actually log the where
	// the error happened, 0 = this function, we don't want that.
	_, _, line, _ := runtime.Caller(1)
	return line
}

func logFunc(log *slog.Logger, msg string, args ...any) {
	log.Info(msg, args...)
}

func removeTime(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		a.Key = ""
	}
	return a
}

func newMultiSlogger(conf config, appName string) (*slog.Logger, error) {
	handler := slog.HandlerOptions{
		AddSource:   conf.AddSource,
		ReplaceAttr: removeTime,
	}

	if !conf.AddFullSource {
		handler.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr {
			// Remove the directory from the source's filename.
			if a.Key == slog.SourceKey {
				a.Value = slog.StringValue(filepath.Base(a.Value.String()))
			}

			// Remove Timestamp for testing
			if a.Key == slog.TimeKey && len(groups) == 0 {
				a.Key = ""
			}
			return a
		}
	}

	logWriter, err := slogSetupMultiWriter(conf)
	if err != nil {
		return nil, fmt.Errorf("setup Slog MultiWriter: %w", err)
	}

	return slog.New(
		handler.NewJSONHandler(logWriter).
			WithAttrs([]slog.Attr{
				slog.String("name", appName),
			}),
	), nil
}

func outputFormatWriter(format string, writer io.Writer) io.Writer {
	switch format {
	case FormatJSON:
		log.Println("Utilizing JSON")
		return slogmw.NewJSON(writer)
	default:
		log.Println("Utilizing LOGFMT")
		return slogmw.NewLOGFMT(writer)
	}
}

func slogSetupMultiWriter(conf config) (io.Writer, error) {
	var writers []io.Writer

	if !conf.ConsoleDisable {
		log.Println("Enabling Console Logger")
		writers = append(writers, outputFormatWriter(conf.ConsoleFormat, os.Stdout))
	}

	if conf.FileEnable {
		log.Println("Enabling File Logger")
		var output io.Writer
		if conf.RotateEnable {
			output = &lumberjack.Logger{
				Filename:   conf.FilePath,
				MaxSize:    conf.RotateSize, // megabytes
				MaxBackups: conf.RotateKeep,
				MaxAge:     conf.RotateAge, // days
			}
		} else {
			logFile, err := os.OpenFile(
				conf.FilePath,
				os.O_APPEND|os.O_CREATE|os.O_WRONLY,
				0o644, //nolint:gomnd
			)
			if err != nil {
				return nil, fmt.Errorf("could not open log file [%s] Error: %w", conf.FilePath, err)
			}

			output = logFile
		}

		writers = append(writers, outputFormatWriter(conf.FileFormat, output))
	}

	return io.MultiWriter(writers...), nil
}
Output:

{"level":"INFO","source":"example_test.go:91","msg":"Logging initialized...","name":"SlogTest"}
{"level":"INFO","source":"example_test.go:91","msg":"1st With Call","name":"SlogTest","With":58}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":0}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":1}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":2}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":3}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":4}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":5}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":6}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":7}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":8}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":9}
{"level":"INFO","source":"example_test.go:91","msg":"Looping","name":"SlogTest","Iteration":10}
{"level":"INFO","source":"example_test.go:91","msg":"Group","name":"SlogTest","Grouping":{"SubKey":"SubValue"}}

Index

Examples

Constants

This section is empty.

Variables

View Source
var VerboseErrors bool

VerboseErrors will indicate that we should print to console any errors as they are normally swallowed by Slog.

Functions

This section is empty.

Types

type JSON

type JSON struct {
	Writer io.Writer
	// contains filtered or unexported fields
}

JSON stores the destination writer and passes through the Slog JSON. In the future, we may perform additional logic on the given JSON.

func NewJSON

func NewJSON(writer io.Writer) *JSON

NewJSON will pass through the raw JSON to the given writer. This function is a stub in case we want to parse the JSON and add additional logic later.

func (*JSON) Write

func (j *JSON) Write(data []byte) (int, error)

Write implements the io.writer interface to wrap the destination writer for Slog.

type LOGFMT

type LOGFMT struct {
	Writer io.Writer
	// contains filtered or unexported fields
}

LOGFMT stores the destination writer and converts the Slog JSON to the LOGFMT format.

func NewLOGFMT

func NewLOGFMT(writer io.Writer) *LOGFMT

NewLOGFMT will wrap a writer to convert logs to LOGFMT

func (*LOGFMT) Write

func (lf *LOGFMT) Write(data []byte) (int, error)

Write implements the io.writer interface to wrap the destination writer for Slog.

Jump to

Keyboard shortcuts

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