output

package module
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2024 License: MIT Imports: 8 Imported by: 1

README

output

GoDoc Reference go.mod LICENSE

Release Code Coverage Report Go Report Card Build Status

output is a Go library that provides an easy way for command-line oriented programs to handle console writing, error writing, and logging (but agnostic as to the choice of logging framework). It also provides a simple way to verify what is written to those channels.

Installing

Execute this:

go get github.com/majohn-r/output

Basic Usage

In main, create a Bus implementation and a Logger implementation. Here is an example that uses the https://github.com/sirupsen/logrus library to implement logging:

func main() {
    // the Bus created by output.NewDefaultBus() neither knows nor cares about
    // how logging actually works - that's the purview of the Logger
    // implementation it uses.
    o := output.NewDefaultBus(ProductionLogger{})
    runProgramLogic(o, os.Args)
}

func runProgramLogic(o output.Bus, args []string) {
    // any functions called should have the Bus passed in if they, or any
    // function they call, needs to write output or do any logging
    o.WriteConsole("hello world: %v\n", args)
}

type ProductionLogger struct {}

// Trace outputs a trace log message
func (ProductionLogger) Trace(msg string, fields map[string]any) {
    logrus.WithFields(fields).Trace(msg)
}

// Debug outputs a debug log message
func (ProductionLogger) Debug(msg string, fields map[string]any) {
    logrus.WithFields(fields).Debug(msg)
}

// Info outputs an info log message
func (ProductionLogger) Info(msg string, fields map[string]any) {
    logrus.WithFields(fields).Info(msg)
}

// Warning outputs a warning log message
func (ProductionLogger) Warning(msg string, fields map[string]any) {
    logrus.WithFields(fields).Warning(msg)
}

// Error outputs an error log message
func (ProductionLogger) Error(msg string, fields map[string]any) {
    logrus.WithFields(fields).Error(msg)
}

// Panic outputs a panic log message and calls panic()
func (ProductionLogger) Panic(msg string, fields map[string]any) {
    logrus.WithFields(fields).Panic(msg)
}

// Fatal outputs a fatal log message and terminates the program
func (ProductionLogger) Fatal(msg string, fields map[string]any) {
    logrus.WithFields(fields).Fatal(msg)
}

In the test code, the output can be checked like this:

func Test_runProgramLogic {
    tests := []struct {
        name string
        args []string
        output.WantedRecording
    }{
        {
            name: "test case",
            args: []string{"hi" "12" "true"},
            WantedRecording: output.WantedRecording{
                Console: "hello world: [hi 12 true]",
            },
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            o := NewRecorder()
            runProgramLogic(o, tt.args)
            if issues, ok := o.Verify(tt.WantedRecording); !ok {
                for _, issue := range issues {
                    t.Errorf("runProgramLogic() %s", issue)
                }
            }
        })
    }
}

Canonical Output

Long ago, I was taught that messages intended to be read by users should have a number of features, among them clarity. One way to achieve clarity is to output messages as properly written sentences. In that vein, then, the Bus interfaces includes these functions:

  • WriteCanonicalConsole(string, ...any)
  • WriteConsole(string, ...any)
  • WriteCanonicalError(string, ...any)
  • WriteError(string, ...any)

All use fmt.Printf to process the format string and arguments, but the Canonical variants do a little extra processing on the result:

  1. Remove all trailing newlines.
  2. Remove redundant end-of-sentence punctuation characters (period, exclamation point, and question mark), leaving only the last occuring such character. Append a period if there was no end-of-sentence punctuation character in the first place. This alleviates problems where the last value in the field of arguments ends with an end-of-sentence punctuation character, and so does the format string; this phase also ensures that the message ends with appropriate punctuation.
  3. Capitalize the first character in the message.
  4. Append a newline.

The result is, one hopes, a well-formed English sentence, starting with a capital letter and ending in exactly one end-of-sentence punctuation character and a newline. The content between the first character and the final punctuation is the caller's problem. If English grammar is not your strong suit, enlist a code reviewer who has the requisite skill set.

Depending on context, I use a mix of WriteConsole and WriteCanonicalConsole - but I only use WriteCanonicalError.

Documentation

Documentation beyond this file can be obtained by running ./build.sh doc, or go here: https://pkg.go.dev/github.com/majohn-r/output

Contributing

Git
  1. Fork the repository (https://github.com/majohn-r/output/fork).
  2. Create a feature branch (git checkout -b my-new-feature).
  3. Commit your changes (git commit -am 'Add some feature').
  4. Push to the branch (git push origin my-new-feature).
  5. Create a new Pull Request.
Code Quality

These are the minimum standards:

  1. There must be no lint issues - run [./build.sh lint] to verify.
  2. All unit tests must pass - run [./build.sh tests] to verify.
  3. Code coverage must be at 100% - run [./build.sh coverage] to verify.
  4. The code must be correctly formatted - run [./build.sh format] to verify.
Commit message

Reference an issue in the first line of the commit message:

[#1234] fix that nagging problem

In the example above, 1234 is the issue number this commit reference.

After the first line in the commit message, add a blank line, followed by relevant details.

This library adheres to Semantic Versioning standards, so it will be very helpful if the details in the commit message make clear whether the changes require a minor or major release bump.

Documentation

Overview

Package output provides an easy way for command-line oriented programs to handle console and error writing and logging, as well as a simple way to verify what is written to those channels.

Installation

go get github.com/majohn-r/output

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Bus

type Bus interface {
	Log(Level, string, map[string]any)
	WriteCanonicalConsole(string, ...any)
	WriteConsole(string, ...any)
	WriteCanonicalError(string, ...any)
	WriteError(string, ...any)
	ConsoleWriter() io.Writer
	ErrorWriter() io.Writer
	IsConsoleTTY() bool
	IsErrorTTY() bool
}

Bus defines a set of functions for writing console messages and error messages, and for providing access to the console writer, the error writer, and a Logger instance; its primary use is to simplify how application code handles console, error, and logged output, and its secondary use is to make it easy to test output writing.

func NewCustomBus

func NewCustomBus(c, e io.Writer, l Logger) Bus

NewCustomBus returns an implementation of Bus that lets the caller specify the console and error writers and the Logger.

func NewDefaultBus

func NewDefaultBus(l Logger) Bus

NewDefaultBus returns an implementation of Bus that writes console messages to stdout and error messages to stderr.

func NewNilBus

func NewNilBus() Bus

NewNilBus returns an implementation of Bus that does nothing.

type Level

type Level uint32

Level is used to specify log levels for Bus.Log().

const (
	Fatal Level = iota
	Panic
	Error
	Warning
	Info
	Debug
	Trace
)

These are the different logging levels.

type Logger

type Logger interface {
	Trace(msg string, fields map[string]any)
	Debug(msg string, fields map[string]any)
	Info(msg string, fields map[string]any)
	Warning(msg string, fields map[string]any)
	Error(msg string, fields map[string]any)
	Panic(msg string, fields map[string]any)
	Fatal(msg string, fields map[string]any)
}

Logger defines a set of functions for writing to a log at various log levels

type NilLogger

type NilLogger struct{}

NilLogger is a logger that does nothing at all; its intended use is for test code where the side effect of logging is of no interest whatsoever.

func (NilLogger) Debug

func (nl NilLogger) Debug(msg string, fields map[string]any)

Debug does nothing.

func (NilLogger) Error

func (nl NilLogger) Error(msg string, fields map[string]any)

Error does nothing.

func (NilLogger) Fatal

func (nl NilLogger) Fatal(msg string, fields map[string]any)

Fatal does nothing.

func (NilLogger) Info

func (nl NilLogger) Info(msg string, fields map[string]any)

Info does nothing.

func (NilLogger) Panic

func (nl NilLogger) Panic(msg string, fields map[string]any)

Panic does nothing.

func (NilLogger) Trace

func (nl NilLogger) Trace(msg string, fields map[string]any)

Trace does nothing.

func (NilLogger) Warning

func (nl NilLogger) Warning(msg string, fields map[string]any)

Warning does nothing.

type NilWriter

type NilWriter struct{}

NilWriter is a writer that does nothing at all; its intended use is for test code where the side effect of writing to the console or writing error output is of no interest whatsoever.

func (NilWriter) Write

func (nw NilWriter) Write(p []byte) (n int, err error)

Write does nothing except return the expected values

type Recorder

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

Recorder is an implementation of Bus that simply records its inputs; it's intended for unit tests, where you can provide the code under test (that needs a Bus) with an instance of Recorder and then verify that the code produces the expected console, error, and log output.

func NewRecorder

func NewRecorder() *Recorder

NewRecorder returns a recording implementation of Bus.

func (*Recorder) ConsoleOutput

func (r *Recorder) ConsoleOutput() string

ConsoleOutput returns the data written as console output.

func (*Recorder) ConsoleWriter

func (r *Recorder) ConsoleWriter() io.Writer

ConsoleWriter returns the internal console writer.

func (*Recorder) ErrorOutput

func (r *Recorder) ErrorOutput() string

ErrorOutput returns the data written as error output.

func (*Recorder) ErrorWriter

func (r *Recorder) ErrorWriter() io.Writer

ErrorWriter returns the internal error writer.

func (*Recorder) IsConsoleTTY added in v0.3.0

func (r *Recorder) IsConsoleTTY() bool

IsConsoleTTY returns whether the console writer is a TTY

func (*Recorder) IsErrorTTY added in v0.3.0

func (r *Recorder) IsErrorTTY() bool

IsErrorTTY returns whether the error writer is a TTY

func (*Recorder) Log

func (r *Recorder) Log(l Level, msg string, fields map[string]any)

Log records a message and map of fields at a specified log level.

func (*Recorder) LogOutput

func (r *Recorder) LogOutput() string

LogOutput returns the data written to a log.

func (*Recorder) Verify

func (r *Recorder) Verify(w WantedRecording) (issues []string, ok bool)

Verify verifies the recorded output against the expected output and returns any differences found.

func (*Recorder) WriteCanonicalConsole

func (r *Recorder) WriteCanonicalConsole(format string, a ...any)

WriteCanonicalConsole records data written to the console.

func (*Recorder) WriteCanonicalError

func (r *Recorder) WriteCanonicalError(format string, a ...any)

WriteCanonicalError records data written as an error.

func (*Recorder) WriteConsole

func (r *Recorder) WriteConsole(format string, a ...any)

WriteConsole records data written to the console.

func (*Recorder) WriteError

func (r *Recorder) WriteError(format string, a ...any)

WriteError records un-edited data written as an error.

type RecordingLogger

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

RecordingLogger is a simple logger intended for use in unit tests; it records the output given to it.

Caveats:

Your production log may not actually do anything with some calls into it - for instance, many logging frameworks allow you to limit the severity of what is logged, e.g., only warnings or worse; RecordingLogger will record every call made into it.

The output recorded cannot be guaranteed to match exactly what your logging code records - but it will include the log level, the message, and all field-value pairs.

The RecordingLogger will probably behave differently to a logging mechanism that supports panic and fatal logs, in that a production logger will probably call panic in processing a panic log, and will probably exit the program on a fatal log. RecordingLogger does neither of those.

func NewRecordingLogger

func NewRecordingLogger() *RecordingLogger

NewRecordingLogger returns a recording implementation of Logger.

func (*RecordingLogger) Debug

func (rl *RecordingLogger) Debug(msg string, fields map[string]any)

Debug records a debug log message.

func (*RecordingLogger) Error

func (rl *RecordingLogger) Error(msg string, fields map[string]any)

Error records an error log message.

func (*RecordingLogger) Fatal

func (rl *RecordingLogger) Fatal(msg string, fields map[string]any)

Fatal records a fatal log message and does not terminate the program.

func (*RecordingLogger) Info

func (rl *RecordingLogger) Info(msg string, fields map[string]any)

Info records an info log message.

func (*RecordingLogger) Panic

func (rl *RecordingLogger) Panic(msg string, fields map[string]any)

Panic records a panic log message and does not call panic().

func (*RecordingLogger) Trace

func (rl *RecordingLogger) Trace(msg string, fields map[string]any)

Trace records a trace log message.

func (*RecordingLogger) Warning

func (rl *RecordingLogger) Warning(msg string, fields map[string]any)

Warning records a warning log message.

type WantedRecording

type WantedRecording struct {
	Console string
	Error   string
	Log     string
}

WantedRecording is intended to be used in unit tests as part of the test structure; it allows the test writer to capture what the test wants the console, error, and log output to contain.

Directories

Path Synopsis
build module

Jump to

Keyboard shortcuts

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